From f71a5c59af9c88bc7904ebeae4e63dceed729ad4 Mon Sep 17 00:00:00 2001 From: ray zhou Date: Fri, 29 May 2026 11:21:40 +0800 Subject: [PATCH] ok --- .gitignore | 94 +- plans/console返水过期兜底_1de18924.plan.md | 39 + .../create_cursor_workspace_e8a5f935.plan.md | 59 + plans/c端_packages_响应精简_b2ac407a.plan.md | 122 + plans/c端_status_响应补全_b1190a3a.plan.md | 156 + plans/daily-rebate-info调整_9c1cb5e8.plan.md | 37 + plans/dailyrebatelogic重构_50d62b71.plan.md | 132 + .../docker_环境_cursor_规则_ae19913d.plan.md | 121 + ...first_cashout审核类型对齐_6c579ae2.plan.md | 47 + ...firstcashout_合并代码评审_b7afdfc6.plan.md | 181 + plans/firstcashout_合并提现_837e2d44.plan.md | 174 + plans/firstcashout_合并评估_df95dacf.plan.md | 135 + ...ree_credits_activity_edit_454064f6.plan.md | 247 ++ plans/free_credits_release_902566c9.plan.md | 182 + .../free_credits_后台统计页_9d6edc45.plan.md | 317 ++ ...ee_credits_老用户隐藏评审_1c0ab623.plan.md | 141 + plans/free_credits_验收核查_c1eade6d.plan.md | 162 + plans/is_end派奖结算改造_14ce4dc5.plan.md | 78 + plans/is_end结算回退方案_180cfb46.plan.md | 57 + plans/phpdoc_cursor_规则_1ee55114.plan.md | 141 + plans/phpunit_钱包链路测试_003ec505.plan.md | 84 + plans/phpunit_集成测试落地_3c120775.plan.md | 11 + plans/php用例写法规则_01c4ae1f.plan.md | 227 ++ plans/pwa每日返水流水展示_0e7ccf31.plan.md | 11 + ...charge-config规则说明字段_344d86ff.plan.md | 49 + plans/sdk_taskprogress_封装_5c8676e2.plan.md | 81 + plans/slot_workspace_82c92cd4.plan.md | 16 + plans/slot_硬规则落地_2bb799e2.plan.md | 176 + plans/status_接口字段精简_8ba244ad.plan.md | 165 + plans/status_返回_help_e344e40a.plan.md | 70 + .../status_透出后续最小充值_47cf9e62.plan.md | 129 + plans/status_配置与未充值_f37679fd.plan.md | 106 + plans/type11_活动初始金额_ee0e1a75.plan.md | 131 + plans/update_返回值_intval_3a7f24c6.plan.md | 58 + .../wallet-bet-win-api-split_8d6f7dea.plan.md | 53 + plans/walletuserstat_model_c0f7d053.plan.md | 132 + plans/win返回余额叠加pending_151a7ce9.plan.md | 45 + plans/中间派奖余额可见化_175ab934.plan.md | 91 + plans/任务进度筛选对接_acbb7213.plan.md | 228 ++ plans/修复第一档绑卡类型_1c86ae8f.plan.md | 115 + plans/充值用户_redis_集合_a31b1665.plan.md | 202 ++ plans/定格冻结用户流水_68d9c5d1.plan.md | 123 + plans/定格列表盈利阈值_5a7a1ed2.plan.md | 95 + plans/定格统计迁入_admin_6bbf8d93.plan.md | 108 + plans/异步事件单元测试_0dbff6a0.plan.md | 227 ++ plans/异步用户统计_mq_300efb8e.plan.md | 184 + plans/拆分win派奖文档_adbf09a1.plan.md | 56 + plans/支持_webp_上传_1fe8ff80.plan.md | 80 + ...旧提现接口兼容_package_id_9ba88779.plan.md | 94 + plans/每日返水活动化改造_dd1575a1.plan.md | 225 ++ plans/每日返水需求文档_20a03147.plan.md | 408 +++ plans/注册奖励随机游戏_9f41e4af.plan.md | 160 + plans/注册活动列表_game_id_2136b4fb.plan.md | 127 + plans/注册活动推荐游戏配置_0766f9ed.plan.md | 183 + plans/测试_register_bet_win_6f8e8ea1.plan.md | 73 + plans/测试报告可追踪输出_4fd907ad.plan.md | 82 + plans/玩家任务进度查询_bf651c60.plan.md | 59 + plans/移除_releasing_状态_fd4423e0.plan.md | 154 + plans/第一档提现审核拒绝_843062ca.plan.md | 174 + plans/第一档提现或保留余额_181f2d15.plan.md | 176 + plans/第一档提现独立接口_6b3aaf7e.plan.md | 162 + plans/第一档提现统计入账_b97ec4a2.plan.md | 120 + plans/跨服务_slot_sdk_约束_4e4faf5a.plan.md | 148 + plans/首充定格资金修复_ac2c16c6.plan.md | 223 ++ plans/首充第一档金额下限_43e84a69.plan.md | 106 + .../a742f0a700a7772ff5ed85f7c9fc1dad5afa9fcc | 1 + projects/1776824844777/terminals/1.txt | 6 + projects/1776824844777/terminals/2.txt | 6 + .../86df46b5-316e-4f6f-9dc9-5466d566dd14.txt | 2 + ...3b8482cf-c825-4bbf-95d0-9fcb2267a57a.jsonl | 10 + ...91517e7f-750e-4c75-86eb-023b786dafdf.jsonl | 97 + ...cafdbaa3-1037-430e-9070-90fed1ea1e3b.jsonl | 16 + .../mcps/cursor-app-control/INSTRUCTIONS.md | 12 + .../cursor-app-control/SERVER_METADATA.json | 4 + .../tools/create_project.json | 18 + .../tools/move_agent_to_cloned_root.json | 41 + .../tools/move_agent_to_root.json | 41 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + .../terminals/353840.txt | 55 + .../terminals/358521.txt | 14 + .../terminals/570972.txt | 14 + .../terminals/986794.txt | 14 + .../terminals/987394.txt | 14 + ...7598369b-ef5a-478a-bbe4-1a4d5c349056.jsonl | 42 + ...88d56417-a0f1-46cf-b0b5-d69a3dee8cdb.jsonl | 2 + ...baed1aae-85f9-4cae-ba82-9a2328df4b40.jsonl | 133 + ...d90007f8-41fe-4f42-ae51-f97a88bfe33f.jsonl | 107 + ...3359bc66-46c1-44ce-950c-6120cb65d712.jsonl | 18 + ...8d2a73dc-57c6-4c5e-8b55-ad2fb45cc725.jsonl | 14 + ...edbca34d-fad1-4b76-85b1-890ea42aae11.jsonl | 11 + ...df9c006c-99b4-469b-830e-8a367cd4e05d.jsonl | 85 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../terminals/2.txt | 6 + .../0f5ad70a-6d4a-4cf9-b9fe-bfb88f0e1a2d.txt | 725 ++++ .../1bd078d3-67ea-40f9-b187-204955d9f574.txt | 189 + .../23a4f8cc-affc-46d4-b23d-f75d2236d1d4.txt | 415 +++ .../3021218e-e66d-4714-8991-686e7109b6ce.txt | 514 +++ .../59498339-7bed-4ac3-bf7f-4c151dafbb35.txt | 252 ++ .../6027a469-6d9b-489e-be3b-65ff539ae345.txt | 586 ++++ .../7534eea9-0a02-4cb9-8770-6b184ebde367.txt | 321 ++ .../f01b30ff-7a55-4c56-8ec4-be9752349a0e.txt | 498 +++ ...235ae52b-02a6-4731-a437-bb819cf69254.jsonl | 9 + ...30221066-a3a9-45aa-952b-c3a4c84ae2dd.jsonl | 35 + ...dc2e7107-b379-44cf-a931-28a199f7b43d.jsonl | 62 + .../mcps/cursor-app-control/INSTRUCTIONS.md | 12 + .../cursor-app-control/SERVER_METADATA.json | 4 + .../tools/create_project.json | 18 + .../tools/move_agent_to_cloned_root.json | 41 + .../tools/move_agent_to_root.json | 41 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + ...0081e6f1-269e-4bcb-bcbd-c5a33c1dc251.jsonl | 84 + ...06407b93-44bf-41be-bcbd-481752efa5a6.jsonl | 39 + ...1ef4b54b-61cb-4b29-b8f3-acbd82571aab.jsonl | 3 + ...21e21244-14fe-4407-aad2-cb760860fb5b.jsonl | 105 + ...2c7463c2-8199-443e-bcbd-8884dc7d15b6.jsonl | 1 + ...456c43f9-a1b6-47d1-9d6b-9d35271c848a.jsonl | 1 + ...6ffe4fe3-d96a-4bbd-a326-5be1da6059fc.jsonl | 1 + ...72c6cce6-48d8-4b14-90b3-58713eee92e5.jsonl | 71 + ...0a092a3a-354b-4f18-9b58-82f0eabf0af3.jsonl | 14 + ...21e71787-95ae-4381-a0e1-815888198050.jsonl | 15 + ...223fe721-1469-45f2-a997-f19c6ceb4cae.jsonl | 25 + ...2b3ebdc2-14b1-448f-8442-5f591e4f497b.jsonl | 19 + ...62bf63fa-aeb9-4222-addd-15dcc40a1203.jsonl | 15 + ...9fed36da-6a07-4c81-a238-0555d0a71d6f.jsonl | 22 + ...abafb133-7da2-4805-a54b-a2d56c3e6015.jsonl | 20 + ...bf37e8a4-3dd9-4e0c-a239-f72eaf9e0a7b.jsonl | 22 + ...c0ca2aac-4945-44b6-8f3d-851a0d313571.jsonl | 20 + ...d165d2b6-c186-4b14-8d83-6055e2db10e0.jsonl | 21 + ...b0b5f038-3181-4df2-b3c0-7f78c40f9c29.jsonl | 17 + ...ccfbd29f-425a-4c61-a2e5-a90f914f265a.jsonl | 124 + ...d4300101-e310-4d43-8085-6abbc31b7a17.jsonl | 10 + ...e79a7143-1356-4119-a49d-372439527351.jsonl | 50 + ...fd7fdcb2-5cda-4659-974d-e88674ae16ed.jsonl | 46 + ...fed4f1f6-353a-452a-8b7f-2d2e49a5c59f.jsonl | 64 + ...2e28b09c-70e7-4cb7-a37a-c4ec0cc0cb9e.jsonl | 2 + ...3f917b3e-9dff-4d6f-8e82-d9e6e4e3d9b3.jsonl | 26 + ...47b6bb84-7a0a-472f-bf7d-7de5025614bb.jsonl | 7 + ...48595fe5-14de-4ddf-a0b1-a400d626f717.jsonl | 6 + ...a8ec2c49-027e-48de-ada2-acdf3cd09d5c.jsonl | 26 + ...bb6aebd5-2ccb-437d-ab54-65f0acf87c77.jsonl | 11 + ...ccdd9b1e-b19b-4e16-af6a-27a1f47bd9eb.jsonl | 6 + ...d2fd49e1-9372-4820-b099-1f17bc3dce96.jsonl | 4 + ...eb369b0f-ffd5-4862-8417-4fa00411317c.jsonl | 8 + ...f2ca1d6f-2a5f-4dc4-803b-0c32574bd0a0.jsonl | 3 + ...f52feb71-c388-4fa3-ba59-e55bc7586129.jsonl | 18 + .../mcps/cursor-ide-browser/INSTRUCTIONS.md | 51 + .../cursor-ide-browser/SERVER_METADATA.json | 4 + .../tools/browser_click.json | 64 + .../tools/browser_console_messages.json | 14 + .../tools/browser_drag.json | 32 + .../tools/browser_fill.json | 30 + .../tools/browser_fill_form.json | 49 + .../tools/browser_get_bounding_box.json | 25 + .../tools/browser_handle_dialog.json | 24 + .../tools/browser_highlight.json | 29 + .../tools/browser_hover.json | 25 + .../tools/browser_lock.json | 24 + .../tools/browser_mouse_click_xy.json | 34 + .../tools/browser_navigate.json | 36 + .../tools/browser_navigate_back.json | 14 + .../tools/browser_network_requests.json | 14 + .../tools/browser_press_key.json | 20 + .../tools/browser_profile_start.json | 14 + .../tools/browser_profile_stop.json | 14 + .../tools/browser_resize.json | 25 + .../tools/browser_scroll.json | 44 + .../tools/browser_search.json | 30 + .../tools/browser_select_option.json | 33 + .../tools/browser_snapshot.json | 38 + .../tools/browser_tabs.json | 34 + .../tools/browser_take_screenshot.json | 34 + .../tools/browser_type.json | 42 + .../tools/browser_wait_for.json | 30 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + ...fae8a528-7cba-436b-bff2-dcb96b0a8115.jsonl | 56 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../245f97c0-4e46-4a80-8b6d-493d0fe33219.txt | 3048 +++++++++++++++++ .../51f64f35-dbc2-4344-bd19-c093579af1cf.txt | 327 ++ .../9c2cf97f-f215-45ae-9ee5-1064cda5523a.txt | 563 +++ .../a100d50f-d5a9-478c-a4bc-0741087d261b.txt | 317 ++ .../a57ceca7-0cca-46c4-8f76-354c1fe871d2.txt | 884 +++++ .../fd89156b-559e-4c56-87e7-959775193a17.txt | 494 +++ ...02932ace-a7e8-4fc3-ae92-e638aeb718f6.jsonl | 70 + ...0ec18d0c-e3a5-465f-b006-ef8dd4777f42.jsonl | 13 + ...149bd03a-1920-4ef2-bd92-7d386f52a31e.jsonl | 122 + ...24245260-3f42-46b6-8b26-de4050e217be.jsonl | 5 + ...2950ae75-8ea1-4450-ac89-16322294b92c.jsonl | 41 + ...3734ba2f-fc11-4cc6-bc19-1cc3229efd37.jsonl | 25 + ...3835cba0-7f58-46d4-b10e-ed98bbc17e40.jsonl | 7 + ...39d5a866-43f9-42e8-8b19-ce6aeeb68d2b.jsonl | 53 + ...28088517-b298-460b-8aec-792a445dc782.jsonl | 18 + ...3a163b5d-2e32-4015-a102-ca08a50dfd10.jsonl | 2 + ...3b90f0c5-4e23-433f-90e4-d0e783481f3b.jsonl | 33 + ...3e27a66b-5548-4089-8558-b95e2b583ab0.jsonl | 72 + ...504a03c1-382e-45cc-8f0a-34630ba34e4a.jsonl | 20 + ...ed5907ca-da34-4442-a8a3-a60e6ad978c0.jsonl | 21 + ...5afaf419-0ead-4b53-b34f-2b104062a7aa.jsonl | 8 + ...6052680e-576d-48e2-bed4-2a0bd520c8da.jsonl | 83 + ...60e2438a-8ed3-4059-a59f-b94155ff33df.jsonl | 11 + ...3bc00cd3-4ee1-4ba6-bdd4-ade6816b33f0.jsonl | 12 + ...66a9fb8d-aa24-47d6-84fa-ced6f949d29a.jsonl | 39 + ...76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6.jsonl | 11 + ...3c5817ae-b042-411b-acbc-2059be1fa97f.jsonl | 8 + ...7d55aed7-a1c3-4287-928b-e379f4391494.jsonl | 23 + ...9d6cf304-dd79-4034-b7ca-43f6a2c7b273.jsonl | 14 + ...9f8a7b15-c4cd-417e-904b-a4961450646f.jsonl | 21 + ...aaec02c4-d467-43b3-a04e-c86e0878631e.jsonl | 4 + ...e9b2b1ea-4347-434c-91f6-4d81788521a6.jsonl | 19 + ...f7bc8476-6f24-4159-8ef7-fa4564987159.jsonl | 14 + ...8f7f6581-b616-4b64-b4fa-b4c38f5e048a.jsonl | 89 + ...91415af2-11a5-4746-ac2e-618f4c2cb9e6.jsonl | 25 + ...98e0f3a2-78eb-4ced-b954-d0f64020321b.jsonl | 67 + ...a3679178-e59a-4770-b10f-4afb2959c54a.jsonl | 77 + ...a5bdb268-2c7e-40c9-938f-d6167292b521.jsonl | 15 + ...aa6e970f-51e4-455f-9a38-a21a2d78131f.jsonl | 144 + ...b7e2249e-047b-4141-a2ee-61aada27cb8c.jsonl | 74 + ...b88722e7-0f1f-4e04-957c-65c2221ea00f.jsonl | 3 + ...b919f3a2-082e-468e-ac59-85445eecdbcc.jsonl | 57 + ...5c34933c-a7be-4dcb-89e2-4ad1bc8c4d77.jsonl | 28 + ...864ff1d5-4fe9-4681-a6b2-db0f9d7a1198.jsonl | 23 + ...bc11d387-fb1f-4959-9553-303dfec0cc74.jsonl | 172 + ...bdeef2a5-ff83-46ac-8baa-9293c612ae54.jsonl | 33 + ...08451b67-19f2-4f21-8bdf-17be1dbd1eac.jsonl | 5 + ...a2c90d53-327e-4da5-8856-ad890bfd114d.jsonl | 21 + ...be905171-75da-494c-8ca6-8f7c7785ffcc.jsonl | 32 + ...43b54fac-351e-4a42-a0b4-2c2f48c4c5cc.jsonl | 19 + ...7a2edafa-5d52-4fc7-8a39-b61df1b3bc3f.jsonl | 18 + ...9c46215a-f4b1-4749-9991-be64c2e13342.jsonl | 12 + ...a92aa6d1-82e3-4244-ab3c-430edf7fe73f.jsonl | 1 + ...c756e128-cafe-4b5a-b66c-e5625581754b.jsonl | 5 + ...cdb5898a-5b06-4598-99c2-3da46d001c8d.jsonl | 20 + ...a7bb2282-e1af-46c2-ab04-48966bb58ba0.jsonl | 18 + ...dcb9d98b-ffec-40e8-8163-ba8364c8a0c4.jsonl | 16 + ...d34c4e06-3f8a-4059-bff1-68b6f70008d4.jsonl | 32 + ...d79c5d7b-b2c6-458d-8371-fd4bc6a24f06.jsonl | 64 + ...d9c4c3be-02dc-4f58-9464-7c8298ce14cd.jsonl | 108 + ...d9e7939a-7f22-4ce3-a53b-f55e24ab93b1.jsonl | 17 + ...db1ae2eb-9966-48b8-a5f5-698f6fd51116.jsonl | 46 + ...dca40ef6-9179-4973-8f8f-1bc9b42fc24c.jsonl | 16 + ...e439d86b-8b66-49d3-ba59-8b1b33d7d207.jsonl | 49 + ...26706711-647a-4094-9173-78596d3f7814.jsonl | 18 + ...7a3fd195-968e-4780-ad4f-687cdea346ee.jsonl | 16 + ...eeefd5b6-11f2-4dae-bdb6-357f92aa9ffa.jsonl | 29 + ...f1b4e078-e0f9-4fb7-b53f-1182dbc449b2.jsonl | 11 + ...f47844ad-501f-4e08-816d-05fd5fa43369.jsonl | 200 ++ ...68634438-46d0-4623-90ca-e61f29e49396.jsonl | 1 + ...68756931-a7bc-49c8-bef6-0fa8b1c4a27f.jsonl | 20 + ...6a239e93-9b7e-4016-bafc-8ef80a54a19f.jsonl | 14 + ...f52f1fe1-ea39-4916-a987-5139f7404aa5.jsonl | 38 + ...f9deb13d-05eb-4295-b5b9-2ba898d8a893.jsonl | 7 + ...fa0a65c5-3a8f-4f09-9807-06988fe10ea4.jsonl | 155 + ...429148dd-cc28-4443-884b-383af64485a7.jsonl | 6 + ...aecb797c-c886-462f-81c2-bef35245cf7c.jsonl | 19 + ...ffe379ed-c47b-421c-9950-710583f9de44.jsonl | 2 + ...fb6b5008-c623-42c3-a490-2c11af3dfd30.jsonl | 424 +++ ...09f1969a-56ec-4dc0-b4d3-1c5083fade84.jsonl | 12 + ...5d3c5800-86eb-4a5d-a87b-b03849a9d6e4.jsonl | 19 + ...8fd54d30-fc42-4730-9333-7d6d670757b7.jsonl | 20 + ...fc84e69f-30d7-4c1c-b0dc-a43312a6a7df.jsonl | 151 + ...58444349-6fd3-4d23-a650-ef5ca8de1f4c.jsonl | 21 + ...ba34d82e-541f-4c17-aee0-0e1c0d18d03e.jsonl | 15 + ...eb962cb9-7523-4a74-a5a3-e55c153a7344.jsonl | 21 + .../mcps/cursor-ide-browser/INSTRUCTIONS.md | 46 + .../cursor-ide-browser/SERVER_METADATA.json | 4 + .../cursor-ide-browser/tools/browser_cdp.json | 28 + .../tools/browser_click.json | 67 + .../tools/browser_drag.json | 36 + .../tools/browser_fill.json | 33 + .../tools/browser_get_bounding_box.json | 24 + .../tools/browser_highlight.json | 28 + .../tools/browser_lock.json | 24 + .../tools/browser_mouse_click_xy.json | 38 + .../tools/browser_navigate.json | 36 + .../tools/browser_press_key.json | 24 + .../tools/browser_scroll.json | 48 + .../tools/browser_select_option.json | 36 + .../tools/browser_snapshot.json | 38 + .../tools/browser_tabs.json | 34 + .../tools/browser_take_screenshot.json | 34 + .../tools/browser_type.json | 45 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + .../terminals/1.txt | 141 + ...d76c9f01-f52d-4ac3-9c5e-ae44397c8114.jsonl | 214 ++ .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../terminals/476873.txt | 40 + .../terminals/569505.txt | 55 + ...a107aa6a-fbde-4fcf-a381-52a84c968f4b.jsonl | 23 + ...7d6538fb-5024-4fbf-888b-814014e07ed9.jsonl | 8 + ...8df7e19c-5f81-47d4-8a96-e1d84af44342.jsonl | 11 + ...c265019c-0a39-48fc-82ea-1b0fb8a383af.jsonl | 11 + ...e986ea99-31ef-43cf-b348-cf781823e734.jsonl | 17 + ...cde4689f-7de9-40a7-b6cb-d15d09b792e1.jsonl | 46 + ...d7dbe1fd-a305-4b9c-b14a-1c6bd51396c0.jsonl | 189 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../mcps/cursor-app-control/INSTRUCTIONS.md | 12 + .../cursor-app-control/SERVER_METADATA.json | 4 + .../tools/create_project.json | 18 + .../tools/move_agent_to_cloned_root.json | 41 + .../tools/move_agent_to_root.json | 41 + .../mcps/cursor-ide-browser/INSTRUCTIONS.md | 51 + .../cursor-ide-browser/SERVER_METADATA.json | 4 + .../tools/browser_click.json | 64 + .../tools/browser_console_messages.json | 14 + .../tools/browser_drag.json | 32 + .../tools/browser_fill.json | 30 + .../tools/browser_fill_form.json | 49 + .../tools/browser_get_bounding_box.json | 25 + .../tools/browser_handle_dialog.json | 24 + .../tools/browser_highlight.json | 29 + .../tools/browser_hover.json | 25 + .../tools/browser_lock.json | 24 + .../tools/browser_mouse_click_xy.json | 34 + .../tools/browser_navigate.json | 36 + .../tools/browser_navigate_back.json | 14 + .../tools/browser_network_requests.json | 14 + .../tools/browser_press_key.json | 20 + .../tools/browser_profile_start.json | 14 + .../tools/browser_profile_stop.json | 14 + .../tools/browser_resize.json | 25 + .../tools/browser_scroll.json | 44 + .../tools/browser_search.json | 30 + .../tools/browser_select_option.json | 33 + .../tools/browser_snapshot.json | 38 + .../tools/browser_tabs.json | 34 + .../tools/browser_take_screenshot.json | 34 + .../tools/browser_type.json | 42 + .../tools/browser_wait_for.json | 30 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 13 + .../mcps/cursor-ide-browser/INSTRUCTIONS.md | 51 + .../cursor-ide-browser/SERVER_METADATA.json | 4 + .../tools/browser_click.json | 64 + .../tools/browser_console_messages.json | 14 + .../tools/browser_drag.json | 32 + .../tools/browser_fill.json | 30 + .../tools/browser_fill_form.json | 49 + .../tools/browser_get_bounding_box.json | 25 + .../tools/browser_handle_dialog.json | 24 + .../tools/browser_highlight.json | 29 + .../tools/browser_hover.json | 25 + .../tools/browser_lock.json | 24 + .../tools/browser_mouse_click_xy.json | 34 + .../tools/browser_navigate.json | 36 + .../tools/browser_navigate_back.json | 14 + .../tools/browser_network_requests.json | 14 + .../tools/browser_press_key.json | 20 + .../tools/browser_profile_start.json | 14 + .../tools/browser_profile_stop.json | 14 + .../tools/browser_resize.json | 25 + .../tools/browser_scroll.json | 44 + .../tools/browser_search.json | 30 + .../tools/browser_select_option.json | 33 + .../tools/browser_snapshot.json | 38 + .../tools/browser_tabs.json | 34 + .../tools/browser_take_screenshot.json | 34 + .../tools/browser_type.json | 42 + .../tools/browser_wait_for.json | 30 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + .../mcps/cursor-ide-browser/INSTRUCTIONS.md | 51 + .../cursor-ide-browser/SERVER_METADATA.json | 4 + .../tools/browser_click.json | 64 + .../tools/browser_console_messages.json | 14 + .../tools/browser_drag.json | 32 + .../tools/browser_fill.json | 30 + .../tools/browser_fill_form.json | 49 + .../tools/browser_get_bounding_box.json | 25 + .../tools/browser_handle_dialog.json | 24 + .../tools/browser_highlight.json | 29 + .../tools/browser_hover.json | 25 + .../tools/browser_lock.json | 24 + .../tools/browser_mouse_click_xy.json | 34 + .../tools/browser_navigate.json | 36 + .../tools/browser_navigate_back.json | 14 + .../tools/browser_network_requests.json | 14 + .../tools/browser_press_key.json | 20 + .../tools/browser_profile_start.json | 14 + .../tools/browser_profile_stop.json | 14 + .../tools/browser_resize.json | 25 + .../tools/browser_scroll.json | 44 + .../tools/browser_search.json | 30 + .../tools/browser_select_option.json | 33 + .../tools/browser_snapshot.json | 38 + .../tools/browser_tabs.json | 34 + .../tools/browser_take_screenshot.json | 34 + .../tools/browser_type.json | 42 + .../tools/browser_wait_for.json | 30 + .../plugin-figma-figma/SERVER_METADATA.json | 4 + .../mcps/plugin-figma-figma/STATUS.md | 1 + .../plugin-figma-figma/tools/mcp_auth.json | 9 + rules/agent-completion-gate.mdc | 26 + rules/backend-layering.mdc | 7 + rules/php-clean-code.mdc | 218 ++ rules/php-doc.mdc | 60 - skills-cursor/.sync-manifest.json | 48 + skills-cursor/babysit/SKILL.md | 14 + skills-cursor/canvas/SKILL.md | 106 + skills-cursor/canvas/sdk/canvas-tokens.d.ts | 264 ++ .../canvas/sdk/chart-primitives.d.ts | 200 ++ .../canvas/sdk/collapsible-section.d.ts | 65 + skills-cursor/canvas/sdk/dag-layout.d.ts | 102 + skills-cursor/canvas/sdk/diff-view.d.ts | 130 + skills-cursor/canvas/sdk/form-primitives.d.ts | 194 ++ skills-cursor/canvas/sdk/hooks.d.ts | 141 + skills-cursor/canvas/sdk/index.d.ts | 59 + skills-cursor/canvas/sdk/swatch.d.ts | 40 + skills-cursor/canvas/sdk/theme.d.ts | 61 + skills-cursor/canvas/sdk/todo-list.d.ts | 49 + skills-cursor/canvas/sdk/ui-primitives.d.ts | 549 +++ .../canvas/sdk/ui-primitives.test.d.ts | 2 + skills-cursor/canvas/sdk/usage-bar.d.ts | 56 + skills-cursor/create-hook/SKILL.md | 239 ++ skills-cursor/create-rule/SKILL.md | 164 + skills-cursor/create-skill/SKILL.md | 504 +++ skills-cursor/create-subagent/SKILL.md | 225 ++ skills-cursor/loop/SKILL.md | 75 + skills-cursor/migrate-to-skills/SKILL.md | 134 + skills-cursor/sdk/SKILL.md | 380 ++ skills-cursor/shell/SKILL.md | 24 + skills-cursor/split-to-prs/SKILL.md | 49 + skills-cursor/statusline/SKILL.md | 196 ++ skills-cursor/update-cli-config/SKILL.md | 87 + skills-cursor/update-cursor-settings/SKILL.md | 122 + 447 files changed, 32245 insertions(+), 116 deletions(-) create mode 100644 plans/console返水过期兜底_1de18924.plan.md create mode 100644 plans/create_cursor_workspace_e8a5f935.plan.md create mode 100644 plans/c端_packages_响应精简_b2ac407a.plan.md create mode 100644 plans/c端_status_响应补全_b1190a3a.plan.md create mode 100644 plans/daily-rebate-info调整_9c1cb5e8.plan.md create mode 100644 plans/dailyrebatelogic重构_50d62b71.plan.md create mode 100644 plans/docker_环境_cursor_规则_ae19913d.plan.md create mode 100644 plans/first_cashout审核类型对齐_6c579ae2.plan.md create mode 100644 plans/firstcashout_合并代码评审_b7afdfc6.plan.md create mode 100644 plans/firstcashout_合并提现_837e2d44.plan.md create mode 100644 plans/firstcashout_合并评估_df95dacf.plan.md create mode 100644 plans/free_credits_activity_edit_454064f6.plan.md create mode 100644 plans/free_credits_release_902566c9.plan.md create mode 100644 plans/free_credits_后台统计页_9d6edc45.plan.md create mode 100644 plans/free_credits_老用户隐藏评审_1c0ab623.plan.md create mode 100644 plans/free_credits_验收核查_c1eade6d.plan.md create mode 100644 plans/is_end派奖结算改造_14ce4dc5.plan.md create mode 100644 plans/is_end结算回退方案_180cfb46.plan.md create mode 100644 plans/phpdoc_cursor_规则_1ee55114.plan.md create mode 100644 plans/phpunit_钱包链路测试_003ec505.plan.md create mode 100644 plans/phpunit_集成测试落地_3c120775.plan.md create mode 100644 plans/php用例写法规则_01c4ae1f.plan.md create mode 100644 plans/pwa每日返水流水展示_0e7ccf31.plan.md create mode 100644 plans/recharge-config规则说明字段_344d86ff.plan.md create mode 100644 plans/sdk_taskprogress_封装_5c8676e2.plan.md create mode 100644 plans/slot_workspace_82c92cd4.plan.md create mode 100644 plans/slot_硬规则落地_2bb799e2.plan.md create mode 100644 plans/status_接口字段精简_8ba244ad.plan.md create mode 100644 plans/status_返回_help_e344e40a.plan.md create mode 100644 plans/status_透出后续最小充值_47cf9e62.plan.md create mode 100644 plans/status_配置与未充值_f37679fd.plan.md create mode 100644 plans/type11_活动初始金额_ee0e1a75.plan.md create mode 100644 plans/update_返回值_intval_3a7f24c6.plan.md create mode 100644 plans/wallet-bet-win-api-split_8d6f7dea.plan.md create mode 100644 plans/walletuserstat_model_c0f7d053.plan.md create mode 100644 plans/win返回余额叠加pending_151a7ce9.plan.md create mode 100644 plans/中间派奖余额可见化_175ab934.plan.md create mode 100644 plans/任务进度筛选对接_acbb7213.plan.md create mode 100644 plans/修复第一档绑卡类型_1c86ae8f.plan.md create mode 100644 plans/充值用户_redis_集合_a31b1665.plan.md create mode 100644 plans/定格冻结用户流水_68d9c5d1.plan.md create mode 100644 plans/定格列表盈利阈值_5a7a1ed2.plan.md create mode 100644 plans/定格统计迁入_admin_6bbf8d93.plan.md create mode 100644 plans/异步事件单元测试_0dbff6a0.plan.md create mode 100644 plans/异步用户统计_mq_300efb8e.plan.md create mode 100644 plans/拆分win派奖文档_adbf09a1.plan.md create mode 100644 plans/支持_webp_上传_1fe8ff80.plan.md create mode 100644 plans/旧提现接口兼容_package_id_9ba88779.plan.md create mode 100644 plans/每日返水活动化改造_dd1575a1.plan.md create mode 100644 plans/每日返水需求文档_20a03147.plan.md create mode 100644 plans/注册奖励随机游戏_9f41e4af.plan.md create mode 100644 plans/注册活动列表_game_id_2136b4fb.plan.md create mode 100644 plans/注册活动推荐游戏配置_0766f9ed.plan.md create mode 100644 plans/测试_register_bet_win_6f8e8ea1.plan.md create mode 100644 plans/测试报告可追踪输出_4fd907ad.plan.md create mode 100644 plans/玩家任务进度查询_bf651c60.plan.md create mode 100644 plans/移除_releasing_状态_fd4423e0.plan.md create mode 100644 plans/第一档提现审核拒绝_843062ca.plan.md create mode 100644 plans/第一档提现或保留余额_181f2d15.plan.md create mode 100644 plans/第一档提现独立接口_6b3aaf7e.plan.md create mode 100644 plans/第一档提现统计入账_b97ec4a2.plan.md create mode 100644 plans/跨服务_slot_sdk_约束_4e4faf5a.plan.md create mode 100644 plans/首充定格资金修复_ac2c16c6.plan.md create mode 100644 plans/首充第一档金额下限_43e84a69.plan.md create mode 160000 plugins/cache/cursor-public/figma/a742f0a700a7772ff5ed85f7c9fc1dad5afa9fcc create mode 100644 projects/1776824844777/terminals/1.txt create mode 100644 projects/1776824844777/terminals/2.txt create mode 100644 projects/Users-ray-Documents-project-pop/agent-tools/86df46b5-316e-4f6f-9dc9-5466d566dd14.txt create mode 100644 projects/Users-ray-Documents-project-pop/agent-transcripts/3b8482cf-c825-4bbf-95d0-9fcb2267a57a/3b8482cf-c825-4bbf-95d0-9fcb2267a57a.jsonl create mode 100644 projects/Users-ray-Documents-project-pop/agent-transcripts/91517e7f-750e-4c75-86eb-023b786dafdf/91517e7f-750e-4c75-86eb-023b786dafdf.jsonl create mode 100644 projects/Users-ray-Documents-project-pop/agent-transcripts/cafdbaa3-1037-430e-9070-90fed1ea1e3b/cafdbaa3-1037-430e-9070-90fed1ea1e3b.jsonl create mode 100644 projects/Users-ray-Documents-project-pop/mcps/cursor-app-control/INSTRUCTIONS.md create mode 100644 projects/Users-ray-Documents-project-pop/mcps/cursor-app-control/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-pop/mcps/cursor-app-control/tools/create_project.json create mode 100644 projects/Users-ray-Documents-project-pop/mcps/cursor-app-control/tools/move_agent_to_cloned_root.json create mode 100644 projects/Users-ray-Documents-project-pop/mcps/cursor-app-control/tools/move_agent_to_root.json create mode 100644 projects/Users-ray-Documents-project-pop/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-pop/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-pop/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-pop/terminals/353840.txt create mode 100644 projects/Users-ray-Documents-project-pop/terminals/358521.txt create mode 100644 projects/Users-ray-Documents-project-pop/terminals/570972.txt create mode 100644 projects/Users-ray-Documents-project-pop/terminals/986794.txt create mode 100644 projects/Users-ray-Documents-project-pop/terminals/987394.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/7598369b-ef5a-478a-bbe4-1a4d5c349056/7598369b-ef5a-478a-bbe4-1a4d5c349056.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/88d56417-a0f1-46cf-b0b5-d69a3dee8cdb/88d56417-a0f1-46cf-b0b5-d69a3dee8cdb.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/baed1aae-85f9-4cae-ba82-9a2328df4b40/baed1aae-85f9-4cae-ba82-9a2328df4b40.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/d90007f8-41fe-4f42-ae51-f97a88bfe33f/d90007f8-41fe-4f42-ae51-f97a88bfe33f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/d90007f8-41fe-4f42-ae51-f97a88bfe33f/subagents/3359bc66-46c1-44ce-950c-6120cb65d712.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/d90007f8-41fe-4f42-ae51-f97a88bfe33f/subagents/8d2a73dc-57c6-4c5e-8b55-ad2fb45cc725.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/d90007f8-41fe-4f42-ae51-f97a88bfe33f/subagents/edbca34d-fad1-4b76-85b1-890ea42aae11.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/agent-transcripts/df9c006c-99b4-469b-830e-8a367cd4e05d/df9c006c-99b4-469b-830e-8a367cd4e05d.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-activity/terminals/2.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/0f5ad70a-6d4a-4cf9-b9fe-bfb88f0e1a2d.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/1bd078d3-67ea-40f9-b187-204955d9f574.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/23a4f8cc-affc-46d4-b23d-f75d2236d1d4.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/3021218e-e66d-4714-8991-686e7109b6ce.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/59498339-7bed-4ac3-bf7f-4c151dafbb35.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/6027a469-6d9b-489e-be3b-65ff539ae345.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/7534eea9-0a02-4cb9-8770-6b184ebde367.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-tools/f01b30ff-7a55-4c56-8ec4-be9752349a0e.txt create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-transcripts/235ae52b-02a6-4731-a437-bb819cf69254/235ae52b-02a6-4731-a437-bb819cf69254.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-transcripts/30221066-a3a9-45aa-952b-c3a4c84ae2dd/30221066-a3a9-45aa-952b-c3a4c84ae2dd.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/agent-transcripts/dc2e7107-b379-44cf-a931-28a199f7b43d/dc2e7107-b379-44cf-a931-28a199f7b43d.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/cursor-app-control/INSTRUCTIONS.md create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/cursor-app-control/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/cursor-app-control/tools/create_project.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/cursor-app-control/tools/move_agent_to_cloned_root.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/cursor-app-control/tools/move_agent_to_root.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-pwa/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/0081e6f1-269e-4bcb-bcbd-c5a33c1dc251/0081e6f1-269e-4bcb-bcbd-c5a33c1dc251.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/06407b93-44bf-41be-bcbd-481752efa5a6/06407b93-44bf-41be-bcbd-481752efa5a6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/1ef4b54b-61cb-4b29-b8f3-acbd82571aab/1ef4b54b-61cb-4b29-b8f3-acbd82571aab.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/21e21244-14fe-4407-aad2-cb760860fb5b/21e21244-14fe-4407-aad2-cb760860fb5b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/2c7463c2-8199-443e-bcbd-8884dc7d15b6/2c7463c2-8199-443e-bcbd-8884dc7d15b6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/2c7463c2-8199-443e-bcbd-8884dc7d15b6/subagents/456c43f9-a1b6-47d1-9d6b-9d35271c848a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/2c7463c2-8199-443e-bcbd-8884dc7d15b6/subagents/6ffe4fe3-d96a-4bbd-a326-5be1da6059fc.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/72c6cce6-48d8-4b14-90b3-58713eee92e5.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/0a092a3a-354b-4f18-9b58-82f0eabf0af3.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/21e71787-95ae-4381-a0e1-815888198050.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/223fe721-1469-45f2-a997-f19c6ceb4cae.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/2b3ebdc2-14b1-448f-8442-5f591e4f497b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/62bf63fa-aeb9-4222-addd-15dcc40a1203.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/9fed36da-6a07-4c81-a238-0555d0a71d6f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/abafb133-7da2-4805-a54b-a2d56c3e6015.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/bf37e8a4-3dd9-4e0c-a239-f72eaf9e0a7b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/c0ca2aac-4945-44b6-8f3d-851a0d313571.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/72c6cce6-48d8-4b14-90b3-58713eee92e5/subagents/d165d2b6-c186-4b14-8d83-6055e2db10e0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/b0b5f038-3181-4df2-b3c0-7f78c40f9c29/b0b5f038-3181-4df2-b3c0-7f78c40f9c29.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/ccfbd29f-425a-4c61-a2e5-a90f914f265a/ccfbd29f-425a-4c61-a2e5-a90f914f265a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/d4300101-e310-4d43-8085-6abbc31b7a17/d4300101-e310-4d43-8085-6abbc31b7a17.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/e79a7143-1356-4119-a49d-372439527351/e79a7143-1356-4119-a49d-372439527351.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fd7fdcb2-5cda-4659-974d-e88674ae16ed/fd7fdcb2-5cda-4659-974d-e88674ae16ed.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/2e28b09c-70e7-4cb7-a37a-c4ec0cc0cb9e.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/3f917b3e-9dff-4d6f-8e82-d9e6e4e3d9b3.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/47b6bb84-7a0a-472f-bf7d-7de5025614bb.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/48595fe5-14de-4ddf-a0b1-a400d626f717.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/a8ec2c49-027e-48de-ada2-acdf3cd09d5c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/bb6aebd5-2ccb-437d-ab54-65f0acf87c77.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/ccdd9b1e-b19b-4e16-af6a-27a1f47bd9eb.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/d2fd49e1-9372-4820-b099-1f17bc3dce96.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/eb369b0f-ffd5-4862-8417-4fa00411317c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/f2ca1d6f-2a5f-4dc4-803b-0c32574bd0a0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/agent-transcripts/fed4f1f6-353a-452a-8b7f-2d2e49a5c59f/subagents/f52feb71-c388-4fa3-ba59-e55bc7586129.jsonl create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/INSTRUCTIONS.md create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_click.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_console_messages.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_drag.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_fill.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_fill_form.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_get_bounding_box.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_handle_dialog.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_highlight.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_hover.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_lock.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_mouse_click_xy.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_navigate.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_navigate_back.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_network_requests.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_press_key.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_profile_start.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_profile_stop.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_resize.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_scroll.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_search.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_select_option.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_snapshot.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_tabs.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_take_screenshot.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_type.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/cursor-ide-browser/tools/browser_wait_for.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-ray-slot-wallet/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend-slot-admin-vue/agent-transcripts/fae8a528-7cba-436b-bff2-dcb96b0a8115/fae8a528-7cba-436b-bff2-dcb96b0a8115.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend-slot-admin-vue/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend-slot-admin-vue/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-slot-backend-slot-admin-vue/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/245f97c0-4e46-4a80-8b6d-493d0fe33219.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/51f64f35-dbc2-4344-bd19-c093579af1cf.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/9c2cf97f-f215-45ae-9ee5-1064cda5523a.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/a100d50f-d5a9-478c-a4bc-0741087d261b.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/a57ceca7-0cca-46c4-8f76-354c1fe871d2.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-tools/fd89156b-559e-4c56-87e7-959775193a17.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/02932ace-a7e8-4fc3-ae92-e638aeb718f6/02932ace-a7e8-4fc3-ae92-e638aeb718f6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/0ec18d0c-e3a5-465f-b006-ef8dd4777f42/0ec18d0c-e3a5-465f-b006-ef8dd4777f42.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/149bd03a-1920-4ef2-bd92-7d386f52a31e/149bd03a-1920-4ef2-bd92-7d386f52a31e.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/24245260-3f42-46b6-8b26-de4050e217be/24245260-3f42-46b6-8b26-de4050e217be.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/2950ae75-8ea1-4450-ac89-16322294b92c/2950ae75-8ea1-4450-ac89-16322294b92c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3734ba2f-fc11-4cc6-bc19-1cc3229efd37/3734ba2f-fc11-4cc6-bc19-1cc3229efd37.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3835cba0-7f58-46d4-b10e-ed98bbc17e40/3835cba0-7f58-46d4-b10e-ed98bbc17e40.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/39d5a866-43f9-42e8-8b19-ce6aeeb68d2b/39d5a866-43f9-42e8-8b19-ce6aeeb68d2b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/39d5a866-43f9-42e8-8b19-ce6aeeb68d2b/subagents/28088517-b298-460b-8aec-792a445dc782.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3a163b5d-2e32-4015-a102-ca08a50dfd10/3a163b5d-2e32-4015-a102-ca08a50dfd10.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3b90f0c5-4e23-433f-90e4-d0e783481f3b/3b90f0c5-4e23-433f-90e4-d0e783481f3b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3e27a66b-5548-4089-8558-b95e2b583ab0/3e27a66b-5548-4089-8558-b95e2b583ab0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3e27a66b-5548-4089-8558-b95e2b583ab0/subagents/504a03c1-382e-45cc-8f0a-34630ba34e4a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/3e27a66b-5548-4089-8558-b95e2b583ab0/subagents/ed5907ca-da34-4442-a8a3-a60e6ad978c0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/5afaf419-0ead-4b53-b34f-2b104062a7aa/5afaf419-0ead-4b53-b34f-2b104062a7aa.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/6052680e-576d-48e2-bed4-2a0bd520c8da/6052680e-576d-48e2-bed4-2a0bd520c8da.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/60e2438a-8ed3-4059-a59f-b94155ff33df/60e2438a-8ed3-4059-a59f-b94155ff33df.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/60e2438a-8ed3-4059-a59f-b94155ff33df/subagents/3bc00cd3-4ee1-4ba6-bdd4-ade6816b33f0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/66a9fb8d-aa24-47d6-84fa-ced6f949d29a/66a9fb8d-aa24-47d6-84fa-ced6f949d29a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/3c5817ae-b042-411b-acbc-2059be1fa97f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/7d55aed7-a1c3-4287-928b-e379f4391494.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/9d6cf304-dd79-4034-b7ca-43f6a2c7b273.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/9f8a7b15-c4cd-417e-904b-a4961450646f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/aaec02c4-d467-43b3-a04e-c86e0878631e.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/e9b2b1ea-4347-434c-91f6-4d81788521a6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/76a4b1e9-2bfd-4a5c-9ee7-61b02fbc08c6/subagents/f7bc8476-6f24-4159-8ef7-fa4564987159.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/8f7f6581-b616-4b64-b4fa-b4c38f5e048a/8f7f6581-b616-4b64-b4fa-b4c38f5e048a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/91415af2-11a5-4746-ac2e-618f4c2cb9e6/91415af2-11a5-4746-ac2e-618f4c2cb9e6.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/98e0f3a2-78eb-4ced-b954-d0f64020321b/98e0f3a2-78eb-4ced-b954-d0f64020321b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/a3679178-e59a-4770-b10f-4afb2959c54a/a3679178-e59a-4770-b10f-4afb2959c54a.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/a5bdb268-2c7e-40c9-938f-d6167292b521/a5bdb268-2c7e-40c9-938f-d6167292b521.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/aa6e970f-51e4-455f-9a38-a21a2d78131f/aa6e970f-51e4-455f-9a38-a21a2d78131f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/b7e2249e-047b-4141-a2ee-61aada27cb8c/b7e2249e-047b-4141-a2ee-61aada27cb8c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/b88722e7-0f1f-4e04-957c-65c2221ea00f/b88722e7-0f1f-4e04-957c-65c2221ea00f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/b919f3a2-082e-468e-ac59-85445eecdbcc/b919f3a2-082e-468e-ac59-85445eecdbcc.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/b919f3a2-082e-468e-ac59-85445eecdbcc/subagents/5c34933c-a7be-4dcb-89e2-4ad1bc8c4d77.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/b919f3a2-082e-468e-ac59-85445eecdbcc/subagents/864ff1d5-4fe9-4681-a6b2-db0f9d7a1198.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/bc11d387-fb1f-4959-9553-303dfec0cc74/bc11d387-fb1f-4959-9553-303dfec0cc74.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/bdeef2a5-ff83-46ac-8baa-9293c612ae54/bdeef2a5-ff83-46ac-8baa-9293c612ae54.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/bdeef2a5-ff83-46ac-8baa-9293c612ae54/subagents/08451b67-19f2-4f21-8bdf-17be1dbd1eac.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/bdeef2a5-ff83-46ac-8baa-9293c612ae54/subagents/a2c90d53-327e-4da5-8856-ad890bfd114d.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/be905171-75da-494c-8ca6-8f7c7785ffcc/be905171-75da-494c-8ca6-8f7c7785ffcc.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/be905171-75da-494c-8ca6-8f7c7785ffcc/subagents/43b54fac-351e-4a42-a0b4-2c2f48c4c5cc.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/be905171-75da-494c-8ca6-8f7c7785ffcc/subagents/7a2edafa-5d52-4fc7-8a39-b61df1b3bc3f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/be905171-75da-494c-8ca6-8f7c7785ffcc/subagents/9c46215a-f4b1-4749-9991-be64c2e13342.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/be905171-75da-494c-8ca6-8f7c7785ffcc/subagents/a92aa6d1-82e3-4244-ab3c-430edf7fe73f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/c756e128-cafe-4b5a-b66c-e5625581754b/c756e128-cafe-4b5a-b66c-e5625581754b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/cdb5898a-5b06-4598-99c2-3da46d001c8d/cdb5898a-5b06-4598-99c2-3da46d001c8d.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/cdb5898a-5b06-4598-99c2-3da46d001c8d/subagents/a7bb2282-e1af-46c2-ab04-48966bb58ba0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/cdb5898a-5b06-4598-99c2-3da46d001c8d/subagents/dcb9d98b-ffec-40e8-8163-ba8364c8a0c4.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/d34c4e06-3f8a-4059-bff1-68b6f70008d4/d34c4e06-3f8a-4059-bff1-68b6f70008d4.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/d79c5d7b-b2c6-458d-8371-fd4bc6a24f06/d79c5d7b-b2c6-458d-8371-fd4bc6a24f06.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/d9c4c3be-02dc-4f58-9464-7c8298ce14cd/d9c4c3be-02dc-4f58-9464-7c8298ce14cd.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/d9e7939a-7f22-4ce3-a53b-f55e24ab93b1/d9e7939a-7f22-4ce3-a53b-f55e24ab93b1.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/db1ae2eb-9966-48b8-a5f5-698f6fd51116/db1ae2eb-9966-48b8-a5f5-698f6fd51116.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/dca40ef6-9179-4973-8f8f-1bc9b42fc24c/dca40ef6-9179-4973-8f8f-1bc9b42fc24c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/e439d86b-8b66-49d3-ba59-8b1b33d7d207/e439d86b-8b66-49d3-ba59-8b1b33d7d207.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/e439d86b-8b66-49d3-ba59-8b1b33d7d207/subagents/26706711-647a-4094-9173-78596d3f7814.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/e439d86b-8b66-49d3-ba59-8b1b33d7d207/subagents/7a3fd195-968e-4780-ad4f-687cdea346ee.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/eeefd5b6-11f2-4dae-bdb6-357f92aa9ffa/eeefd5b6-11f2-4dae-bdb6-357f92aa9ffa.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f1b4e078-e0f9-4fb7-b53f-1182dbc449b2/f1b4e078-e0f9-4fb7-b53f-1182dbc449b2.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f47844ad-501f-4e08-816d-05fd5fa43369/f47844ad-501f-4e08-816d-05fd5fa43369.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f47844ad-501f-4e08-816d-05fd5fa43369/subagents/68634438-46d0-4623-90ca-e61f29e49396.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f47844ad-501f-4e08-816d-05fd5fa43369/subagents/68756931-a7bc-49c8-bef6-0fa8b1c4a27f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f47844ad-501f-4e08-816d-05fd5fa43369/subagents/6a239e93-9b7e-4016-bafc-8ef80a54a19f.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f52f1fe1-ea39-4916-a987-5139f7404aa5/f52f1fe1-ea39-4916-a987-5139f7404aa5.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/f9deb13d-05eb-4295-b5b9-2ba898d8a893/f9deb13d-05eb-4295-b5b9-2ba898d8a893.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fa0a65c5-3a8f-4f09-9807-06988fe10ea4/fa0a65c5-3a8f-4f09-9807-06988fe10ea4.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fa0a65c5-3a8f-4f09-9807-06988fe10ea4/subagents/429148dd-cc28-4443-884b-383af64485a7.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fa0a65c5-3a8f-4f09-9807-06988fe10ea4/subagents/aecb797c-c886-462f-81c2-bef35245cf7c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fa0a65c5-3a8f-4f09-9807-06988fe10ea4/subagents/ffe379ed-c47b-421c-9950-710583f9de44.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fb6b5008-c623-42c3-a490-2c11af3dfd30/fb6b5008-c623-42c3-a490-2c11af3dfd30.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fb6b5008-c623-42c3-a490-2c11af3dfd30/subagents/09f1969a-56ec-4dc0-b4d3-1c5083fade84.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fb6b5008-c623-42c3-a490-2c11af3dfd30/subagents/5d3c5800-86eb-4a5d-a87b-b03849a9d6e4.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fb6b5008-c623-42c3-a490-2c11af3dfd30/subagents/8fd54d30-fc42-4730-9333-7d6d670757b7.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fc84e69f-30d7-4c1c-b0dc-a43312a6a7df/fc84e69f-30d7-4c1c-b0dc-a43312a6a7df.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fc84e69f-30d7-4c1c-b0dc-a43312a6a7df/subagents/58444349-6fd3-4d23-a650-ef5ca8de1f4c.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fc84e69f-30d7-4c1c-b0dc-a43312a6a7df/subagents/ba34d82e-541f-4c17-aee0-0e1c0d18d03e.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/agent-transcripts/fc84e69f-30d7-4c1c-b0dc-a43312a6a7df/subagents/eb962cb9-7523-4a74-a5a3-e55c153a7344.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/INSTRUCTIONS.md create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_cdp.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_click.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_drag.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_fill.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_get_bounding_box.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_highlight.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_lock.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_mouse_click_xy.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_navigate.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_press_key.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_scroll.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_select_option.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_snapshot.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_tabs.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_take_screenshot.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/cursor-ide-browser/tools/browser_type.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-slot-backend/terminals/1.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/agent-transcripts/d76c9f01-f52d-4ac3-9c5e-ae44397c8114/d76c9f01-f52d-4ac3-9c5e-ae44397c8114.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/terminals/476873.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-monitor/terminals/569505.txt create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/a107aa6a-fbde-4fcf-a381-52a84c968f4b/a107aa6a-fbde-4fcf-a381-52a84c968f4b.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/a107aa6a-fbde-4fcf-a381-52a84c968f4b/subagents/7d6538fb-5024-4fbf-888b-814014e07ed9.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/a107aa6a-fbde-4fcf-a381-52a84c968f4b/subagents/8df7e19c-5f81-47d4-8a96-e1d84af44342.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/a107aa6a-fbde-4fcf-a381-52a84c968f4b/subagents/c265019c-0a39-48fc-82ea-1b0fb8a383af.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/a107aa6a-fbde-4fcf-a381-52a84c968f4b/subagents/e986ea99-31ef-43cf-b348-cf781823e734.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/cde4689f-7de9-40a7-b6cb-d15d09b792e1/cde4689f-7de9-40a7-b6cb-d15d09b792e1.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot-slot-wallet/agent-transcripts/d7dbe1fd-a305-4b9c-b14a-1c6bd51396c0/d7dbe1fd-a305-4b9c-b14a-1c6bd51396c0.jsonl create mode 100644 projects/Users-ray-Documents-project-www-slot/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/Users-ray-Documents-project-www-slot/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/Users-ray-Documents-project-www-slot/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/empty-window/mcps/cursor-app-control/INSTRUCTIONS.md create mode 100644 projects/empty-window/mcps/cursor-app-control/SERVER_METADATA.json create mode 100644 projects/empty-window/mcps/cursor-app-control/tools/create_project.json create mode 100644 projects/empty-window/mcps/cursor-app-control/tools/move_agent_to_cloned_root.json create mode 100644 projects/empty-window/mcps/cursor-app-control/tools/move_agent_to_root.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/INSTRUCTIONS.md create mode 100644 projects/empty-window/mcps/cursor-ide-browser/SERVER_METADATA.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_click.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_console_messages.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_drag.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_fill.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_fill_form.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_get_bounding_box.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_handle_dialog.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_highlight.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_hover.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_lock.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_mouse_click_xy.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_navigate.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_navigate_back.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_network_requests.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_press_key.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_profile_start.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_profile_stop.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_resize.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_scroll.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_search.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_select_option.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_snapshot.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_tabs.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_take_screenshot.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_type.json create mode 100644 projects/empty-window/mcps/cursor-ide-browser/tools/browser_wait_for.json create mode 100644 projects/empty-window/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/empty-window/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/empty-window/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-3ee74edc-a9d9-4294-9af2-eb34042f1991/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-3ee74edc-a9d9-4294-9af2-eb34042f1991/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-3ee74edc-a9d9-4294-9af2-eb34042f1991/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-4601ed89-9ab9-41e1-87af-4432ce0912a7/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-4601ed89-9ab9-41e1-87af-4432ce0912a7/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-4601ed89-9ab9-41e1-87af-4432ce0912a7/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/INSTRUCTIONS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_click.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_console_messages.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_drag.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_fill.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_fill_form.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_get_bounding_box.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_handle_dialog.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_highlight.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_hover.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_lock.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_mouse_click_xy.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_navigate.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_navigate_back.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_network_requests.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_press_key.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_profile_start.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_profile_stop.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_resize.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_scroll.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_search.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_select_option.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_snapshot.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_tabs.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_take_screenshot.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_type.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/cursor-ide-browser/tools/browser_wait_for.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-eaef842c-3386-4a87-bfd9-84b016882aed/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/INSTRUCTIONS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_click.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_console_messages.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_drag.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_fill.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_fill_form.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_get_bounding_box.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_handle_dialog.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_highlight.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_hover.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_lock.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_mouse_click_xy.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_navigate.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_navigate_back.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_network_requests.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_press_key.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_profile_start.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_profile_stop.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_resize.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_scroll.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_search.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_select_option.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_snapshot.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_tabs.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_take_screenshot.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_type.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/cursor-ide-browser/tools/browser_wait_for.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/plugin-figma-figma/SERVER_METADATA.json create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/plugin-figma-figma/STATUS.md create mode 100644 projects/var-folders-vp-v0rvf5pn4xlgr4b0c-v0mrbh0000gn-T-ed3169e3-6499-4758-b1b0-56424e0be988/mcps/plugin-figma-figma/tools/mcp_auth.json create mode 100644 rules/agent-completion-gate.mdc create mode 100644 rules/php-clean-code.mdc delete mode 100644 rules/php-doc.mdc create mode 100644 skills-cursor/.sync-manifest.json create mode 100644 skills-cursor/babysit/SKILL.md create mode 100644 skills-cursor/canvas/SKILL.md create mode 100644 skills-cursor/canvas/sdk/canvas-tokens.d.ts create mode 100644 skills-cursor/canvas/sdk/chart-primitives.d.ts create mode 100644 skills-cursor/canvas/sdk/collapsible-section.d.ts create mode 100644 skills-cursor/canvas/sdk/dag-layout.d.ts create mode 100644 skills-cursor/canvas/sdk/diff-view.d.ts create mode 100644 skills-cursor/canvas/sdk/form-primitives.d.ts create mode 100644 skills-cursor/canvas/sdk/hooks.d.ts create mode 100644 skills-cursor/canvas/sdk/index.d.ts create mode 100644 skills-cursor/canvas/sdk/swatch.d.ts create mode 100644 skills-cursor/canvas/sdk/theme.d.ts create mode 100644 skills-cursor/canvas/sdk/todo-list.d.ts create mode 100644 skills-cursor/canvas/sdk/ui-primitives.d.ts create mode 100644 skills-cursor/canvas/sdk/ui-primitives.test.d.ts create mode 100644 skills-cursor/canvas/sdk/usage-bar.d.ts create mode 100644 skills-cursor/create-hook/SKILL.md create mode 100644 skills-cursor/create-rule/SKILL.md create mode 100644 skills-cursor/create-skill/SKILL.md create mode 100644 skills-cursor/create-subagent/SKILL.md create mode 100644 skills-cursor/loop/SKILL.md create mode 100644 skills-cursor/migrate-to-skills/SKILL.md create mode 100644 skills-cursor/sdk/SKILL.md create mode 100644 skills-cursor/shell/SKILL.md create mode 100644 skills-cursor/split-to-prs/SKILL.md create mode 100644 skills-cursor/statusline/SKILL.md create mode 100644 skills-cursor/update-cli-config/SKILL.md create mode 100644 skills-cursor/update-cursor-settings/SKILL.md diff --git a/.gitignore b/.gitignore index 448dbea..e14ebe0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,63 +1,45 @@ # >>> CURSOR MANAGED BLOCK >>> -# Ignore everything in .cursor by default +# Ignore everything in .cursor * - -# Keep gitignore itself -!.gitignore - -# ========================================================= -# Team shared Cursor rules -# ========================================================= -!rules/ -!rules/** - -# ========================================================= -# Team shared Cursor agents -# ========================================================= -!agents/ -!agents/** - -# ========================================================= -# Team shared Cursor skills -# ========================================================= +# Un-ignore projects so we can descend to allowlisted subdirs +!projects/ +projects/* +!projects/*/ +projects/*/* +# MCP tool descriptors, resources, prompts +!projects/*/mcps/ +!projects/*/mcps/** +# Agent transcripts for citation +!projects/*/agent-transcripts/ +!projects/*/agent-transcripts/** +# Terminal output files +!projects/*/terminals/ +!projects/*/terminals/** +# Conversation notes (shared scratchpad) +!projects/*/agent-notes/ +!projects/*/agent-notes/** +# Large tool output files +!projects/*/agent-tools/ +!projects/*/agent-tools/** +# Plugin cache (rules, skills, agents) +!plugins/ +!plugins/** +# Built-in Cursor skills +!skills-cursor/ +!skills-cursor/** +# User's personal skills !skills/ !skills/** - -# ========================================================= -# Team shared slash commands -# ========================================================= +# User's personal slash commands !commands/ !commands/** - -# ========================================================= -# Team shared plans, optional -# ========================================================= -plans/ -plans/** - -# ========================================================= -# Do NOT track Cursor projects runtime files -# ========================================================= -projects/ -projects/** - -# ========================================================= -# Do NOT track Cursor plugin cache -# ========================================================= -plugins/ -plugins/** - -# ========================================================= -# Do NOT track built-in Cursor skills -# Usually auto-generated / synced by Cursor -# ========================================================= -skills-cursor/ -skills-cursor/** - -# ========================================================= -# Do NOT track runtime state / transcripts / terminal logs -# ========================================================= -subagents/ -subagents/** - +# User's plan files +!plans/ +!plans/** +# Subagent state/transcripts +!subagents/ +!subagents/** +# User-level cursor rules +!rules/ +!rules/** # <<< CURSOR MANAGED BLOCK <<< diff --git a/plans/console返水过期兜底_1de18924.plan.md b/plans/console返水过期兜底_1de18924.plan.md new file mode 100644 index 0000000..33aca72 --- /dev/null +++ b/plans/console返水过期兜底_1de18924.plan.md @@ -0,0 +1,39 @@ +--- +name: console返水过期兜底 +overview: 在 C 端返水信息接口增加“近7天过期兜底”逻辑:若存在未领取且已过期记录,进入接口时自动标记为 expired,避免依赖定时脚本。 +todos: + - id: locate-info-hook + content: 在 DailyRebateLogic::info() 增加近7天过期兜底调用点(查询记录前) + status: pending + - id: add-scope-expire-method + content: 新增用户+近7天范围的过期更新方法,仅更新 claimable 且已过期记录 + status: pending + - id: verify-behavior + content: 按过期/未过期/超范围三类样例验证 info 返回状态符合预期 + status: pending +isProject: false +--- + +# Console返水信息过期兜底改造 + +## 目标 +在用户访问返水信息接口时,对该用户“近7天”记录执行一次轻量过期兜底:把 `status=claimable` 且 `expire_at<=当前时间` 的记录更新为 `expired`,避免脚本漏跑导致前端状态不一致。 + +## 改动方案 +- 在 [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php) 的 `info()` 开头(`$unlocked` 判定后、组装 records 前)新增一步: + - 仅针对当前 `uid`、统计日期范围“今天往前 6 天到今天”(即接口展示的近7天)执行过期更新。 + - 更新条件:`status = STATUS_CLAIMABLE` 且 `expire_at <= now()`。 + - 更新结果无需抛错,作为兜底处理(可选记录 debug/info 日志)。 +- 在同文件新增受保护方法(例如 `expireRecentClaimableForInfo(int $uid): int`)承载该逻辑,保持 `info()` 主流程清晰,符合 Logic 分层。 +- 保留现有 `expireDueRecords()`(全量定时任务用),新方法仅用于 C 端接口的“用户级小范围补偿”,两者职责互补,不互相替代。 + +## 细节约束 +- 范围严格限定为近7天,避免接口触发全表更新。 +- 仅改 `claimable -> expired`,不触碰 `claimed` / `pending`。 +- 执行顺序放在读取记录前,保证本次 `records` 返回的状态已是最新。 + +## 验证计划 +- 构造 1 条近7天内 `claimable + expire_at 已过期` 数据,调用 `info` 后应返回 `expired`。 +- 构造 1 条近7天内 `claimable + expire_at 未到期` 数据,调用 `info` 后仍为 `claimable`。 +- 构造 1 条 7 天外过期 `claimable` 数据,调用 `info` 后不应被本接口更新(仍由定时任务处理)。 +- 回归 `claim` 接口:已过期记录仍不可领取(现有保护逻辑保持不变)。 \ No newline at end of file diff --git a/plans/create_cursor_workspace_e8a5f935.plan.md b/plans/create_cursor_workspace_e8a5f935.plan.md new file mode 100644 index 0000000..8f18437 --- /dev/null +++ b/plans/create_cursor_workspace_e8a5f935.plan.md @@ -0,0 +1,59 @@ +--- +name: Create Cursor Workspace +overview: 在 `/Users/ray/Documents/project/www/slot` 创建一个 Cursor/VS Code workspace 文件,并把该目录下的一级项目文件夹加入 workspace。默认排除 `.vscode` 和普通文件。 +todos: + - id: check-existing + content: 检查 `/Users/ray/Documents/project/www/slot/slot.code-workspace` 是否已存在 + status: completed + - id: write-workspace + content: 创建或合并 workspace folders 列表 + status: completed + - id: validate-json + content: 校验 `.code-workspace` JSON 格式 + status: completed +isProject: false +--- + +# Create Slot Workspace + +将在 [`/Users/ray/Documents/project/www/slot/slot.code-workspace`](/Users/ray/Documents/project/www/slot/slot.code-workspace) 创建 workspace 文件,内容使用标准 `.code-workspace` JSON 格式。 + +计划加入这些一级非隐藏文件夹: + +- [`backend`](/Users/ray/Documents/project/www/slot/backend) +- [`monitor`](/Users/ray/Documents/project/www/slot/monitor) +- [`slot-foundation`](/Users/ray/Documents/project/www/slot/slot-foundation) +- [`slot_agent`](/Users/ray/Documents/project/www/slot/slot_agent) +- [`slot_agent_vue`](/Users/ray/Documents/project/www/slot/slot_agent_vue) +- [`slot_center`](/Users/ray/Documents/project/www/slot/slot_center) +- [`slot_console`](/Users/ray/Documents/project/www/slot/slot_console) +- [`slot_gateway`](/Users/ray/Documents/project/www/slot/slot_gateway) +- [`slot_hub`](/Users/ray/Documents/project/www/slot/slot_hub) +- [`slot_lib`](/Users/ray/Documents/project/www/slot/slot_lib) +- [`slot_notification`](/Users/ray/Documents/project/www/slot/slot_notification) +- [`slot_pay`](/Users/ray/Documents/project/www/slot/slot_pay) +- [`slot_pwa`](/Users/ray/Documents/project/www/slot/slot_pwa) +- [`slot_risk`](/Users/ray/Documents/project/www/slot/slot_risk) +- [`slot_sdk`](/Users/ray/Documents/project/www/slot/slot_sdk) +- [`slot_user`](/Users/ray/Documents/project/www/slot/slot_user) +- [`slot_wallet`](/Users/ray/Documents/project/www/slot/slot_wallet) + +Workspace 文件结构会类似: + +```json +{ + "folders": [ + { "path": "backend" }, + { "path": "monitor" }, + { "path": "slot-foundation" } + ], + "settings": {} +} +``` + +实施步骤: + +1. 检查目标文件是否已存在,避免覆盖已有 workspace 配置。 +2. 如果不存在,创建 [`slot.code-workspace`](/Users/ray/Documents/project/www/slot/slot.code-workspace)。 +3. 如果已存在,先读取现有内容,再合并缺失的文件夹,保留已有设置。 +4. 校验生成的 JSON 格式可被 Cursor 打开。 \ No newline at end of file diff --git a/plans/c端_packages_响应精简_b2ac407a.plan.md b/plans/c端_packages_响应精简_b2ac407a.plan.md new file mode 100644 index 0000000..f967a56 --- /dev/null +++ b/plans/c端_packages_响应精简_b2ac407a.plan.md @@ -0,0 +1,122 @@ +--- +name: C端 packages 响应精简 +overview: 在 slot_console 的 Free Credits 定格状态接口(`/api/free-credits/status` 与 `claim` 共用 `buildStatus`)中,精简 `packages[]` 每项字段,并将档位 status 4/5 映射为 3,使 C 端仅见 0–3。后台 innerapi 与库表语义不变。 +todos: + - id: model-list-for-client + content: 在 FreeCreditsPackageModel 实现 mapStatusForClient,并精简 listForClient 返回字段 + status: in_progress + - id: update-phpdoc + content: 更新 FreeCreditsLogic、FreeCreditsController 的 packages/status PHPDoc + status: pending + - id: update-unit-tests + content: 调整 FreeCreditsClientStatusTest / Harness,并补充 status 4/5→3 用例 + status: pending + - id: run-phpunit + content: 在 php82 容器内跑 FreeCreditsClientStatusTest 验证 + status: pending +isProject: false +--- + +# C 端定格状态 packages 响应调整 + +## 背景与范围 + +目标接口在 [slot_console/app/api/controller/FreeCreditsController.php](slot_console/app/api/controller/FreeCreditsController.php): + +- `POST /api/free-credits/status` → `FreeCreditsLogic::status()` → `buildStatus()` +- `POST /api/free-credits/claim` 成功后的 `data` 结构相同 + +`packages[]` 当前由 [slot_console/app/model/common/FreeCreditsPackageModel.php](slot_console/app/model/common/FreeCreditsPackageModel.php) 的 `listForClient()` 组装,经 `FreeCreditsLogic::clientPackagesForPlayer()` 注入响应: + +```72:86:slot_console/app/model/common/FreeCreditsPackageModel.php + public static function listForClient(int $playerId, callable $formatAmount): array + { + // ... + $packages[] = [ + 'id' => intval($row->id), + 'package_no' => intval($row->package_no), + 'package_type' => intval($row->package_type), + 'amount' => $formatAmount(intval($row->amount_qf)), + 'status' => intval($row->status), + ]; +``` + +库表档位 status([install.sql](slot_console/db/install.sql)):`0` 锁定、`1` 可操作、`2` 处理中、`3` 已完成、`4` 失败、`5` 风控拒绝。 + +**不在本次范围**:`innerapi/free-credits/*`(后台仍用 `package_no` / `package_type` 聚合)、库表与 Logic 内按 `package_no` / `package_type` 的业务判断。 + +## 目标契约 + +`packages[]` 每项仅保留: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | int | 档位主键;claim / 第一档提现仍传 `package_id` | +| `amount` | float | 展示大单位,逻辑不变 | +| `status` | int | **仅可能为 0、1、2、3**;DB 为 4 或 5 时对外返回 3 | + +列表仍按 `package_no` 升序(查询不变,仅响应不暴露 `package_no`)。C 端可用数组下标区分第一档(`[0]`)与后续档。 + +Status 映射(仅 C 端展示层): + +```mermaid +flowchart LR + db0[DB 0 locked] --> c0[C 0] + db1[DB 1 ready] --> c1[C 1] + db2[DB 2 processing] --> c2[C 2] + db3[DB 3 completed] --> c3[C 3] + db4[DB 4 failed] --> c3 + db5[DB 5 rejected] --> c3 +``` + +## 实现步骤 + +### 1. 修改 `FreeCreditsPackageModel::listForClient()` + +文件:[FreeCreditsPackageModel.php](slot_console/app/model/common/FreeCreditsPackageModel.php) + +- 新增私有/公有静态方法(建议 `mapStatusForClient(int $status): int`),将 `STATUS_FAILED(4)`、`STATUS_REJECTED(5)` 转为 `STATUS_COMPLETED(3)`,其余原样返回。 +- `listForClient()` 返回项改为 `{ id, amount, status }`,`status` 走映射方法。 +- 更新 `@return` PHPDoc:`list`,并注明 C 端 status 取值 0–3。 + +### 2. 同步 Logic / Controller 文档 + +- [FreeCreditsLogic.php](slot_console/app/api/logic/FreeCreditsLogic.php):`clientPackagesForPlayer()` 的 `@return` 与注释。 +- [FreeCreditsController.php](slot_console/app/api/controller/FreeCreditsController.php):`status()` / `claim()` 中 `packages` 字段说明——去掉 `package_no`、`package_type`;`status` 改为 `0 locked ~ 3 completed(失败/拒绝对外亦为 3)`。 + +### 3. 单测 + +- [FreeCreditsClientStatusTest.php](slot_console/tests/Unit/FreeCreditsClientStatusTest.php):`PACKAGE_KEYS` 改为 `['id','amount','status']`;注入样例去掉 `package_no` / `package_type`。 +- 新增用例(二选一或都做): + - 对 `mapStatusForClient`:断言 `4→3`、`5→3`、`0–3` 不变; + - 或在 harness 注入 `status=4/5` 的 package,断言 `buildStatus` 输出为 `3`。 +- [FreeCreditsLogicHarness.php](slot_console/tests/Support/FreeCreditsLogicHarness.php):更新 `injectClientPackages` 相关 PHPDoc 类型。 + +运行(Docker): + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/FreeCreditsClientStatusTest.php +``` + +## 数据流(变更后) + +```mermaid +sequenceDiagram + participant C as C端 + participant API as FreeCreditsController + participant Logic as FreeCreditsLogic + participant Model as FreeCreditsPackageModel + + C->>API: status / claim + API->>Logic: buildStatus + Logic->>Model: listForClient + Model->>Model: mapStatusForClient + Model-->>Logic: id, amount, status + Logic-->>C: packages[] +``` + +## 风险与说明 + +- **C 端若已依赖 `package_no` / `package_type` 或区分 4/5**:需同步改 UI;服务端 claim 仍用 `package_id`,顺序校验仍在 Logic/DB,不依赖响应里的 `package_no`。 +- **失败/拒绝与成功完成在 UI 上同为 status=3**:符合当前需求;若以后要区分展示,需另加字段或改映射规则。 +- 单测 [FreeCreditsClientStatusTest](slot_console/tests/Unit/FreeCreditsClientStatusTest.php) 期望顶层含 `first_cash_amount`,但 [buildStatus](slot_console/app/api/logic/FreeCreditsLogic.php) 当前未返回该字段——与本次改动无关,不纳入本 PR,除非你希望一并补齐。 diff --git a/plans/c端_status_响应补全_b1190a3a.plan.md b/plans/c端_status_响应补全_b1190a3a.plan.md new file mode 100644 index 0000000..6d4c769 --- /dev/null +++ b/plans/c端_status_响应补全_b1190a3a.plan.md @@ -0,0 +1,156 @@ +--- +name: C端 status 响应补全 +overview: 完成 Free Credits 定格状态接口 C 端响应:packages 精简与 status 映射(Model 已改)、banner_image、首笔提现成功广播 broadcast(真实 Top10 + 不足补假数据),并同步单测与文档。 +todos: + - id: add-banner-image + content: 在 buildConfigClientFields / buildStatus / buildPreEnrollmentStatus 返回 banner_image + status: completed + - id: add-broadcast-list + content: 实现 broadcast 列表(真实首档 completed Top10 + 假数据补齐至 10 条) + status: completed + - id: finish-packages-docs + content: 更新 FreeCreditsController PHPDoc(packages、banner_image、broadcast) + status: completed + - id: update-unit-tests + content: 调整 FreeCreditsClientStatusTest / Harness;补充 banner_image、status 映射、broadcast 用例 + status: completed + - id: run-phpunit + content: php82 容器跑 FreeCreditsClientStatusTest 验证 + status: completed +isProject: false +--- + +# C 端定格 status 响应补全 + +## 当前进度 + +[FreeCreditsPackageModel.php](slot_console/app/model/common/FreeCreditsPackageModel.php) **已完成**: +- `mapStatusForClient()`:DB status 4/5 → 3 +- `listForClient()`:仅返回 `id`、`amount`、`status` + +**待完成**:`banner_image`、`broadcast`、Logic/Controller 文档、单测。 + +## 1. `banner_image`(活动 Banner) + +配置:`ext_config.banner_image`([activity/edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue)) + +在 [FreeCreditsLogic.php](slot_console/app/api/logic/FreeCreditsLogic.php) 的 `buildConfigClientFields()` 增加 `banner_image`,`buildStatus()` / `buildPreEnrollmentStatus()` 透传;`status=-1` 仍仅 `{ status: -1 }`。 + +## 2. `broadcast`(首笔提现成功最近 10 人) + +对齐需求文档 [§13 广播模块](docs/requirements/首充前免费余额定格与分档释放需求文档.md):弹窗展示最近 10 条;用户本次明确要求 **仅首笔提现成功**(非后续 claim)。 + +### 2.1 顶层字段 + +- 字段名:**`broadcast`**(`array`,固定长度 **10**) +- 每项结构: + +```json +{ "username": "U1***3", "amount": 20.0 } +``` + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `username` | string | 脱敏账号,展示用 | +| `amount` | float | 该用户首笔免打码提现金额(展示大单位) | + +C 端文案示例:`🎉 U1***3 just cashed out $20.00`(前端拼接,接口只给结构化数据)。 + +### 2.2 真实数据查询 + +在 [FreeCreditsPackageModel.php](slot_console/app/model/common/FreeCreditsPackageModel.php) 新增查询方法(仅查库,不做脱敏): + +```php +public static function listRecentFirstCashoutCompleted(int $activityId, int $limit = 10): array +``` + +条件: +- `activity_id = ?` +- `package_type = TYPE_FIRST_CASH(1)` +- `status = STATUS_COMPLETED(3)` +- `ORDER BY completed_time DESC`(`completed_time` 为空则 fallback `update_time`) +- `LIMIT 10` + +返回行至少含:`uid`、`amount_qf`。 + +### 2.3 组装与脱敏(Logic) + +在 [FreeCreditsLogic.php](slot_console/app/api/logic/FreeCreditsLogic.php) 新增 `buildBroadcastList(?Model $config): array`: + +1. 有 `activity_id` 时查真实记录,逐条: + - `amount` = `formatClientAmount(amount_qf)` + - `username` = `maskDisplayName(account)`:`UserService::getUserInfoEntity($uid)->account`,规则与 [GameLatestLogic::username()](slot_console/app/napi/logic/GameLatestLogic.php) 一致:`substr(0,2) + '***' + substr(-1)`;account 为空时用 `strval(uid)` 再脱敏 +2. 若真实条数 `< 10`,用 **`buildFakeBroadcastItems($need, $config)`** 补齐: + - `amount`:取当前活动 `first_cash_amount`(`configAmount` + `formatClientAmount`),可在 ±10% 内随机浮动(整数分位),避免 10 条完全相同 + - `username`:随机生成 `U` + 5–8 位数字再脱敏(勿与真实 uid 重复) +3. 合并后 **截断/保证恰好 10 条**(真实在前,假数据在后) + +在 `buildStatus()` / `buildPreEnrollmentStatus()` 增加: + +```php +'broadcast' => $this->buildBroadcastList($config), +``` + +`status=-1` 不返回 `broadcast`。 + +### 2.4 分层说明 + +- **Model**:只负责按条件查最近 N 条首档 completed +- **Logic**:脱敏、金额格式化、假数据补齐(业务展示规则,不抽到 Service) + +```mermaid +flowchart LR + pkgTable[free_credits_package] + modelQuery[listRecentFirstCashoutCompleted] + logicBuild[buildBroadcastList] + fakePad[buildFakeBroadcastItems] + statusAPI["status data.broadcast"] + pkgTable --> modelQuery --> logicBuild + logicBuild --> fakePad --> statusAPI +``` + +## 3. packages 文档收尾 + +[FreeCreditsController.php](slot_console/app/api/controller/FreeCreditsController.php):`packages` 项为 `id`、`amount`、`status(0–3)`;补充 `banner_image`、`broadcast`。 + +## 4. 单测 + +[FreeCreditsClientStatusTest.php](slot_console/tests/Unit/FreeCreditsClientStatusTest.php): + +| 项 | 调整 | +|---|---| +| `PLAYER_TOP_KEYS` | 增加 `banner_image`、`broadcast`;**移除** `first_cash_amount`(与当前 `buildStatus` 一致) | +| `PACKAGE_KEYS` | `['id','amount','status']` | +| 新增 | `testMapStatusForClientMapsFailedAndRejected` | +| 新增 | `testBuildStatusIncludesBannerImageFromExtConfig` | +| 新增 | `testBuildBroadcastAlwaysReturnsTenItems`:harness 注入空真实列表,断言 `count(broadcast)==10`、每项含 `username`/`amount` | + +[FreeCreditsLogicHarness.php](slot_console/tests/Support/FreeCreditsLogicHarness.php):可覆写 `buildBroadcastList` 或注入 Model 查询结果,避免单测连库。 + +## 5. 验证 + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/FreeCreditsClientStatusTest.php +``` + +## 响应示例(status≥0) + +```json +{ + "status": 2, + "frozen_amount": 78.5, + "win_threshold": 50.0, + "recharge_unlock_amount": 50.0, + "help": "...", + "banner_image": "https://cdn.example.com/xxx.png", + "broadcast": [ + { "username": "U1***3", "amount": 20.0 }, + { "username": "U9***2", "amount": 20.0 } + ], + "packages": [ + { "id": 101, "amount": 20.0, "status": 1 } + ] +} +``` + +注:`broadcast` 数组长度恒为 10;示例仅展示 2 条。 diff --git a/plans/daily-rebate-info调整_9c1cb5e8.plan.md b/plans/daily-rebate-info调整_9c1cb5e8.plan.md new file mode 100644 index 0000000..636874c --- /dev/null +++ b/plans/daily-rebate-info调整_9c1cb5e8.plan.md @@ -0,0 +1,37 @@ +--- +name: daily-rebate-info调整 +overview: 恢复 `info` 的 `tiers` 输出,并明确继续使用共享 Redis 实时计算当天返水,补齐稳定性与返回一致性。 +todos: + - id: restore-tiers + content: 在 DailyRebateLogic::info 恢复 tiers 返回并保持现有档位格式 + status: completed + - id: harden-today-redis + content: 加固 readTodayBetFromRedis 容错,确保实时计算稳定 + status: completed + - id: verify-info-contract + content: 核对 info 字段契约并运行 verify-slot-backend 脚本 + status: completed +isProject: false +--- + +# DailyRebate Info 返回修正计划 + +## 目标 +- 在 `DailyRebate info` 中恢复 `tiers` 字段。 +- 当天数据继续采用“共享 Redis 读取下注 + 本地按档位计算返水”的方案。 +- 保持近 7 天固定返回与展示金额口径不变。 + +## 变更点 +- 更新 [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php) + - 在 `info()` 返回结构中重新加入 `tiers`(复用现有档位格式化逻辑)。 + - 保持 `records` 固定 7 天(无数据补 0 展示金额)。 + - 明确当天逻辑:从 `readTodayBetFromRedis()` 读取共享 Redis 的当日下注,调用 `DailyRebateCalcService::calcRebateLi()` 计算当日 `rebate_display`。 + - 为 Redis 空值/脏值场景补轻量容错(反序列化失败时按 0 处理,避免接口抖动)。 + +- 只读一致性确认(不改跨服务架构) + - 依据 [`/Users/ray/Documents/project/www/slot/slot_pwa/app/service/user/UserProfitService.php`](/Users/ray/Documents/project/www/slot/slot_pwa/app/service/user/UserProfitService.php) 与 [`/Users/ray/Documents/project/www/slot/slot_pwa/app/service/RedisKeyManagerService.php`](/Users/ray/Documents/project/www/slot/slot_pwa/app/service/RedisKeyManagerService.php),继续对齐 `Redis::connection('share') + getUserProfitKey(date('Ymd'))` 键模型。 + +## 验证 +- 检查 `info` 返回字段:包含 `tiers`、`title/subtitle/help`、`records(7条)`、`claimable`。 +- 抽样验证当天记录:有 Redis 下注时 `rebate_display` 非 0;无下注时为 0。 +- 执行门禁脚本:`~/.cursor/hooks/verify-slot-backend.sh`。 \ No newline at end of file diff --git a/plans/dailyrebatelogic重构_50d62b71.plan.md b/plans/dailyrebatelogic重构_50d62b71.plan.md new file mode 100644 index 0000000..8d88400 --- /dev/null +++ b/plans/dailyrebatelogic重构_50d62b71.plan.md @@ -0,0 +1,132 @@ +--- +name: DailyRebateLogic重构 +overview: 按 `php-code` 规范重构每日返水领取链路:`claim` 编排化、BusinessException、Model 条件更新;同步调整 Controller 异常映射。`settleDate`/`info` 仅做必要小抽取,不改业务语义。 +todos: + - id: model-mark-claimed + content: DailyRebateRecordModel 增加 markClaimedIfClaimable + PHPDoc + status: pending + - id: logic-claim-refactor + content: DailyRebateLogic:claim 拆 assert/perform/format,BusinessException + status: pending + - id: logic-info-helper + content: 可选:isClaimableRecord 供 info 与 assert 复用 + status: pending + - id: controller-exception + content: DailyRebateController:catch BusinessException → FAILED + status: pending + - id: verify-smoke + content: 跑 verify-slot-backend.sh;确认无 RuntimeException 新增 + status: pending +isProject: false +--- + +# DailyRebateLogic 规范重构 + +## 范围(AskQuestion 中断,按推荐默认) + +- **必做**:[`claim`](slot_console/app/api/logic/DailyRebateLogic.php)、[`DailyRebateController::claim`](slot_console/app/api/controller/DailyRebateController.php)、[`DailyRebateRecordModel`](slot_console/app/model/common/DailyRebateRecordModel.php) +- **轻量**:`info()` 抽取「是否可领」判断为 private,与 `assertClaimableRecord` 复用逻辑 +- **不改语义**:`settleDate` / `expireDueRecords` / Redis 结算逻辑保持行为一致,仅在有重复代码时抽 1 个 private(可选) + +## 现状问题(对照 php-code) + +[`claim`](slot_console/app/api/logic/DailyRebateLogic.php) L92–157: + +- 6 段 `RuntimeException` if 墙 +- CAS 写在 Logic 内联,未沉淀 Model +- Controller `catch RuntimeException` + `PARAMS_ERROR`(verify 会拦新增行) + +参照:[`FreeCreditsLogic::claim`](slot_console/app/api/logic/FreeCreditsLogic.php)(`assert*` + `BusinessException` + 状态更新) + +## 目标结构 + +```mermaid +sequenceDiagram + participant C as DailyRebateController + participant L as DailyRebateLogic + participant M as DailyRebateRecordModel + participant W as WalletService + + C->>L: claim(uid, source, statDate) + L->>L: assertActivityEnabled + L->>L: assertUserDeposited + L->>M: findByUidAndDate + L->>L: assertClaimableRecord + L->>M: markClaimedIfClaimable + L->>W: gift(rebate, DAILY_REBATE, remark) + L-->>C: formatClaimResult +``` + +### 1. Model:条件更新 + +在 [`DailyRebateRecordModel`](slot_console/app/model/common/DailyRebateRecordModel.php) 新增: + +```php +/** + * 待领取状态下标记为已领取(CAS)。 + * + * @return int 影响行数,1 表示成功 + */ +public static function markClaimedIfClaimable(int $id): int +``` + +实现:`where id` + `where status = STATUS_CLAIMABLE` → `STATUS_CLAIMED` + `claimed_at`。 + +### 2. Logic:`claim` 拆分为编排 + assert + perform + +| 方法 | 职责 | +|------|------| +| `claim()` public | 编排 ≤20 行:`resolveSource` → 默认 `statDate` → assert → `performClaim` → 返回 | +| `assertActivityEnabled(string $source)` | 活动未开 → `BusinessException` | +| `assertUserDeposited(int $uid)` | 未充值 → `BusinessException` | +| `assertClaimableRecord(?DailyRebateRecordModel $record)` | 不存在/状态/金额/过期 → 各一条 `BusinessException`(合并原 4 条 if) | +| `performClaim(int $uid, DailyRebateRecordModel $record, string $statDate)` | 事务:`markClaimedIfClaimable` → `WalletService::gift` → commit;`affected !== 1` 或 wallet 空 → `BusinessException` | +| `formatClaimResult(...)` | 返回 `stat_date` / `rebate_amount` / `display` / `balance` | + +- 使用 `support\exception\BusinessException`(与 FreeCredits 一致) +- `@throws` 改为 `BusinessException`、`\Throwable`(钱包失败) +- **钱包 biz_id**:`WalletService::gift()` 内部已 `generateOrderId`([`slot_lib/src/services/WalletService.php`](slot_lib/src/services/WalletService.php) L262–265);幂等依赖 **记录 CAS**,remark 保持 `每日返水 {statDate}` 便于对账 + +### 3. Logic:`info` 轻量复用(可选) + +抽取 `isClaimableRecord(DailyRebateRecordModel $record): bool`(status + amount + expire),供 `info()` 的 `claimable` 块与 `assertClaimableRecord` 共用,避免两套判断漂移。 + +### 4. Controller:异常映射 + +[`DailyRebateController::claim`](slot_console/app/api/controller/DailyRebateController.php): + +```php +} catch (BusinessException $e) { + return $this->errorCode(ErrorCode::FAILED, $e->getMessage()); +} +``` + +- 对齐 [`SignController`](slot_console/app/api/controller/SignController.php)(业务失败 `FAILED` + 文案) +- **API 变更**:`code` 由 `40003`(PARAMS_ERROR)变为 `1`(FAILED);文案仍为中文业务提示。若 PWA 强依赖 `40003`,可在计划中改为保留 `PARAMS_ERROR`(实现时二选一,默认 `FAILED`) + +不采用 FreeCredits「无 catch、走全局 Handler」方式,避免未捕获时变成 `SYSTEM_ERROR`(50001)。 + +### 5. 不动 / 谨慎 + +- **事务边界**:维持「先 CAS 再 gift 再 commit」;不在此 PR 改为「先 wallet 后 DB」或 Outbox +- **`settleDate`**:逻辑不变;可选抽 `settleOneUserFromRedis(...)` 降低 foreach 嵌套(非必须) + +## 文件清单 + +| 文件 | 变更 | +|------|------| +| `slot_console/app/model/common/DailyRebateRecordModel.php` | +`markClaimedIfClaimable` | +| `slot_console/app/api/logic/DailyRebateLogic.php` | 重构 `claim`;可选 `isClaimableRecord` | +| `slot_console/app/api/controller/DailyRebateController.php` | `BusinessException` + `FAILED` | + +## 验收 + +1. `docker exec -w /app/www/slot/slot_console php82 php webman dailyRebateSettle --date=...`(如有环境)结算后,已充值用户可领昨日返水 +2. 重复领取 → 业务错误文案,DB 仍为 `CLAIMED`,不重复入账 +3. `~/.cursor/hooks/verify-slot-backend.sh` → PASS(无新增 `RuntimeException` 业务态) +4. 最终回复含 `PHPDoc: checked`(触及符号补全 `@throws BusinessException`) + +## 风险与回滚 + +- **PWA 错误码**:若前端按 `code===40003` 分支,需同步前端或 Controller 保留 `PARAMS_ERROR` +- 回滚:还原 3 个文件即可 diff --git a/plans/docker_环境_cursor_规则_ae19913d.plan.md b/plans/docker_环境_cursor_规则_ae19913d.plan.md new file mode 100644 index 0000000..f88f25a --- /dev/null +++ b/plans/docker_环境_cursor_规则_ae19913d.plan.md @@ -0,0 +1,121 @@ +--- +name: Docker 环境 Cursor 规则 +overview: 建议把 Docker 开发环境信息写成 Cursor 规则,但单独一条、尽量简短;优先放在 slot 项目级规则,只有跨多个仓库共用同一套 Docker 时才放到用户级规则。 +todos: + - id: decide-scope + content: 确认 Docker 规则放项目级 (slot) 还是用户级 (~/.cursor/rules) + status: completed + - id: confirm-php-container + content: 确认 slot 后端默认容器 php82 与各服务 working_dir + status: completed + - id: create-dev-rule + content: 新建 dev-environment.mdc(15–30 行,含 compose 路径、容器名、端口、exec 示例) + status: completed +isProject: false +--- + +# Docker 环境是否写入 Cursor 规则 + +## 结论 + +**值得写进 Cursor 规则**,但**不必**和 [`backend-layering.mdc`](/Users/ray/.cursor/rules/backend-layering.mdc) 混在同一条里,也**不必**默认全部塞进「用户级 + alwaysApply」。 + +原因:Agent 在帮你跑 `php`、`composer`、`artisan`、迁移、单测、连 Redis/MySQL 时,若不知道服务在容器里,常会错误地在宿主机执行,或连错端口(例如 MySQL 映射是 `3309:3306`)。 + +--- + +## 用户级 vs 项目级:怎么选 + +| 放置位置 | 路径 | 适用场景 | +| --- | --- | --- | +| **项目级(推荐)** | 例如在 slot 多根工作区根目录建 [`.cursor/rules/dev-environment.mdc`](file:///Users/ray/Documents/project/www/slot/.cursor/rules/dev-environment.mdc) | 只有 slot / `www/slot` 相关仓库用这套 Docker | +| **用户级** | [`~/.cursor/rules/dev-environment.mdc`](/Users/ray/.cursor/rules/dev-environment.mdc) | 多个不相关项目都共用 [`/Users/ray/Documents/project/docker/docker-compose.yml`](file:///Users/ray/Documents/project/docker/docker-compose.yml) | + +你当前用户级只有分层规范一条,且 `alwaysApply: true`。Docker 信息属于**运行环境**,和编码规范是不同关注点: + +- **分层规则**:继续 `alwaysApply: true`(跨项目仍有用) +- **Docker 规则**:建议 `alwaysApply: false`,或仅在 slot 工作区用项目级规则;避免在写前端、文档、Figma 时也占用上下文 + +当前 slot 工作区下**没有** [`.cursor/rules/`](file:///Users/ray/Documents/project/www/slot/.cursor/rules),更适合为 slot 单独加一条 `dev-environment.mdc`。 + +--- + +## 规则里应写什么(可操作、短) + +根据你的 [`docker-compose.yml`](file:///Users/ray/Documents/project/docker/docker-compose.yml),建议只写 Agent **执行命令时必需** 的信息: + +1. **Compose 位置**:`/Users/ray/Documents/project/docker/docker-compose.yml` +2. **容器名 → 用途**(执行命令用 `docker exec`,不要用宿主机 PHP/CLI): + - `php82` — PHP 8.2(slot 后端主环境,按你实际版本确认) + - `php72` — PHP 7.2(若有老项目) + - `goMysql` — MySQL 8(容器内 `3306`,宿主机 **`3309`**) + - `redis` — `6379`;`goredis` — 宿主机 `6378` +3. **挂载路径**:宿主机 `/Users/ray/Documents/project` → 容器内 `/app`(项目在容器内路径如 `/app/www/slot/backend`) +4. **命令约定**(示例,按你项目真实入口改): + - PHP:`docker exec -w /app/www/slot/backend php82 php ...` + - Composer / artisan:同样在 `php82` 内、对应 `working_dir` 执行 + - MySQL CLI:`docker exec -it goMysql mysql -uroot -proot ...`(或注明用宿主机 `127.0.0.1:3309`) +5. **明确禁止/避免**:不要在 macOS 宿主机直接跑 `php`/`composer`(除非已确认本机也有同版本环境) + +**不建议写进规则的内容**: + +- 完整 `docker-compose.yml` 复制(冗长、易过期) +- 所有服务密码细节(compose 里已有;规则里写「以 compose 为准」即可) +- RabbitMQ、Milvus 等与当前 slot 任务无关的服务(除非经常用到) + +--- + +## 与现有文档的关系 + +- **Cursor 规则**:给 Agent 的「默认假设」,每次对话自动带上(按 `alwaysApply` / `globs`) +- **仓库 README / `docs/dev-setup.md`**:给人看的完整说明;规则可写一句「详细步骤见 xxx」 + +两者可并存:规则 15–30 行,文档可更长。 + +--- + +## 建议的规则骨架(项目级示例) + +```markdown +--- +description: Slot 本地 Docker 开发环境(PHP / MySQL / Redis) +alwaysApply: true +--- + +# Local Dev (Docker) + +- Compose: `/Users/ray/Documents/project/docker/docker-compose.yml` +- Host project root: `/Users/ray/Documents/project` → container `/app` +- Run PHP/Composer/Artisan inside container `php82`, not on macOS host. +- MySQL: container `goMysql`; from host use `127.0.0.1:3309`. +- Redis: container `redis`, host port `6379`. +- Example: `docker exec -w /app/www/slot/backend php82 php artisan ...` +``` + +若放在**用户级**,把 `alwaysApply` 改为 `false`,或标题改成「Project docker (ray)」以免污染非 slot 项目。 + +--- + +## 推荐决策 + +```mermaid +flowchart TD + Q[多个仓库共用同一套 Docker?] + Q -->|是| UserRule["~/.cursor/rules/dev-environment.mdc\nalwaysApply: false"] + Q -->|否 仅 slot| ProjRule["www/slot/.cursor/rules/dev-environment.mdc\nalwaysApply: true 在该工作区"] + Both[保留 backend-layering 在用户级 alwaysApply] + UserRule --> Both + ProjRule --> Both +``` + +**对你当前情况**:slot 多仓库工作区 + Docker 在 `project/docker`,**优先项目级规则**;仅当你打开的其他 Cursor 工作区(非 slot)也依赖同一 compose 时,再复制一份到用户级。 + +--- + +## 下一步(你确认后可执行) + +1. 在 [`www/slot/.cursor/rules/`](file:///Users/ray/Documents/project/www/slot/.cursor/rules) 新建 `dev-environment.mdc`(约 20 行) +2. 确认 slot 后端默认用 `php82` 还是 `php72`,以及各子服务在容器内的 `working_dir` +3. 可选:在 [`docs/`](file:///Users/ray/Documents/project/www/slot/docs) 增加 `dev-setup.md` 供人查阅,规则里链过去 + +无需修改现有的 `backend-layering.mdc`。 diff --git a/plans/first_cashout审核类型对齐_6c579ae2.plan.md b/plans/first_cashout审核类型对齐_6c579ae2.plan.md new file mode 100644 index 0000000..318548f --- /dev/null +++ b/plans/first_cashout审核类型对齐_6c579ae2.plan.md @@ -0,0 +1,47 @@ +--- +name: first_cashout审核类型对齐 +overview: 将 `FreeCreditsLogic::buildFirstCashoutWithdrawalInfo` 中写死的 `auditType=1` 改为与正常提现一致的判定流程:金额阈值 + 黑名单/倍率风控强制人工。 +todos: + - id: inspect-normal-rule + content: 提炼 WithdrawService 中正常提现 auditType 判定口径(金额+风控) + status: completed + - id: add-first-cash-audit-evaluator + content: 在 FreeCreditsLogic 增加第一档提现 auditType 判定方法 + status: completed + - id: wire-build-method + content: 替换 buildFirstCashoutWithdrawalInfo 中写死 auditType 为动态判定 + status: completed + - id: run-verification-gate + content: 执行 verify-slot-backend.sh 并检查结果 + status: completed +isProject: false +--- + +# First Cashout 审核类型对齐计划 + +## 目标 +把 `slot_console` 中第一档独立提现的审核类型判定,从固定自动审核改为与正常提现流程一致,避免风控场景下误走自动审核。 + +## 现状结论 +- 在 [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php) 的 `buildFirstCashoutWithdrawalInfo()` 里目前写死:`'auditType' => 1`。 +- 正常提现在 [`/Users/ray/Documents/project/www/slot/slot_console/app/service/WithdrawService.php`](/Users/ray/Documents/project/www/slot/slot_console/app/service/WithdrawService.php) 的 `apply()` 中处理逻辑是: + - 先通过 `getAuditType($amount)` 按 `auto_audit_max` 决定自动/人工。 + - 再结合 `BlackApiService::status()` 与提现倍率(`totalW / wallet->r` 对比 `recharge_times`)在命中时强制人工(`auditType=2`)。 + +## 实施方案 +1. 在 [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php) 新增一个“第一档提现审核类型判定”私有方法(复用正常提现判定口径)。 +2. 判定方法内对齐正常流程: + - 读取提现配置 `auto_audit_max`,按第一档固定金额(`package->amount_qf` 转展示金额后)先算基础 `auditType`。 + - 查询黑名单状态,非白名单直接人工审核。 + - 白名单时按当前钱包数据计算倍率并与 `recharge_times` 比较,命中则人工审核。 +3. 在 `buildFirstCashoutWithdrawalInfo()` 中移除写死值,改为调用新判定方法赋值 `auditType`。 +4. 保持第一档提现已有业务语义不变(固定金额、`fee=0`、`bizType` 不改),只修正审核类型来源。 +5. 变更后执行门禁校验脚本:`~/.cursor/hooks/verify-slot-backend.sh`,并在结果中确认通过。 + +## 影响文件 +- [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php) + +## 验证要点 +- 第一档提现在低金额且白名单情况下:`auditType=1`。 +- 超过 `auto_audit_max` 或命中黑名单/倍率规则时:`auditType=2`。 +- 订单申请链路仍可正常调用 `PayService::apply()`,不影响原有字段结构。 \ No newline at end of file diff --git a/plans/firstcashout_合并代码评审_b7afdfc6.plan.md b/plans/firstcashout_合并代码评审_b7afdfc6.plan.md new file mode 100644 index 0000000..22bb19b --- /dev/null +++ b/plans/firstcashout_合并代码评审_b7afdfc6.plan.md @@ -0,0 +1,181 @@ +--- +name: firstCashout 合并代码评审 +overview: 你对「merge post + 统一 WithdrawService::apply」的改法方向正确,但当前 WithdrawService 在 package_id>0 时仍执行 checkInfo/手续费/黑规则,且 Pay 失败无回滚,会导致第一档提现不可用或卡在 processing。 +todos: + - id: fc-early-return + content: WithdrawService::apply 中 package_id>0 走独立 applyFreeCreditsFirstCashout 并 early return + status: completed + - id: skip-checkinfo-fc + content: FC 分支跳过 checkInfo、getAmountAndFee、黑规则(或产品确认保留项) + status: completed + - id: pay-fail-rollback + content: Pay apply 失败时 handleFirstCashoutResult 回滚 processing + status: completed + - id: fc-withdrawal-info + content: FC 使用 package amount_qf、fee=0、auditType=2 组 WithdrawalInfo + status: completed +isProject: false +--- + +# firstCashout 合并改动 — 代码评审 + +## 你的改动(理解正确) + +```mermaid +sequenceDiagram + participant Ctrl as WithdrawController + participant FC as FreeCreditsLogic + participant WS as WithdrawService + participant Pay as slot_pay + + Ctrl->>FC: mergeFirstCashoutIntoPost + Note over FC: amount + bizType + package_id + Ctrl->>WS: apply(DTO) + WS->>FC: firstCashout(uid, packageId, orderId) + Note over FC: markFirstCashoutProcessing + WS->>Pay: apply(WithdrawalInfo) +``` + +- [`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php):有 `package_id` 先 merge,再**同一套** `validate($type)` — 符合预期。 +- [`mergeFirstCashoutIntoPost`](slot_console/app/api/logic/FreeCreditsLogic.php):补 `amount` / `bizType` / `package_id` — 符合预期。 +- [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 收窄为只 `markFirstCashoutProcessing` + 外部统一 `PayService::apply` — 思路可行。 + +--- + +## P0:FC 仍会走 `checkInfo`,大概率直接失败 + +[`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序: + +```php +$bankInfo = $this->checkBankInfo($applyDTO); +$this->checkInfo($applyDTO->type, $applyDTO->amount); // 始终执行 +// ... +if ($applyDTO->package_id > 0) { + firstCashout(...); // 永远走不到(checkInfo 已抛错) +} +``` + +`checkInfo` 会校验**钱包可提现余额** ≥ amount([`WithdrawService.php` L333-335](slot_console/app/service/WithdrawService.php))。 +Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额里 → 典型报错 **`Insufficient balance`**。 + +**结论**:与需求「不进入普通钱包」冲突,第一档线上基本提不了现。 + +**建议**:`package_id > 0`(或 `bizType === free_credit_first_cashout`)时 **跳过** `checkInfo`(及与之绑定的首提 min/max 规则)。 + +--- + +## P0:Pay 失败时档位已置为 processing,无回滚 + +你现在在 **Pay 之前** 调用 `firstCashout` → `markFirstCashoutProcessing`(status=2)。 + +原实现是:mark → `PayService::apply` → **catch 时** `handleFirstCashoutResult($orderId, false)` 恢复 ready。 + +当前 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) L236 调 pay **没有** try/catch 回滚 → 申请失败时 package 会一直 **processing**,用户无法重试。 + +**建议**: + +```php +if ($applyDTO->package_id > 0) { + $orderId = CommonFn::generateOrderId(4, $userTag->uid); + (new FreeCreditsLogic())->firstCashout($userTag->uid, $applyDTO->package_id, $orderId); + $withdrawalInfo->orderId = $orderId; + $withdrawalInfo->bizType = FreeCreditsLogic::BIZ_TYPE_FIRST_CASHOUT; + try { + $res = PayService::getInstance()->apply($withdrawalInfo->toArray()); + return $res; + } catch (\Throwable $e) { + (new FreeCreditsLogic())->handleFirstCashoutResult($orderId, false); + throw $e; + } +} +``` + +(FC 分支应 **early return**,不要继续走下面黑规则 + `getAmountAndFee`。) + +--- + +## P1:FC 仍走普通手续费 / 黑规则 / auditType + +合并后 FC 路径仍执行: + +- `getAuditType($applyDTO->amount)`、`BlackApiService`、提现倍数检测 +- `getAmountAndFee($applyDTO->amount, $this->withdrawal)` — 可能扣手续费、按钱包余额改金额 + +原 [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 约定: + +- `fee = 0` +- `auditType = 2`(人工审核) +- `amount = package->amount_qf`(千分位,不经手续费逻辑) + +`merge` 写入的 `amount` 是 **展示大单位**(`getNumberFormat(qf)`),再经 `getAmountAndFee` 可能与 pay 侧期望的千分位不一致。 + +**建议**:FC 分支单独组 `WithdrawalInfo`: + +| 字段 | FC 取值 | +|------|---------| +| amount | 从 package 读 `amount_qf`(或 merge 时额外缓存 `_amount_qf`) | +| fee | 0 | +| auditType | 2 | +| bizType | `free_credit_first_cashout` | +| 黑规则 | 跳过(或产品确认是否要对 FC 也做) | + +--- + +## P1:`firstCashout` 与 `markFirstCashoutProcessing` 重复校验 + +[`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 内再次查 `STATUS_READY` 后调用 `markFirstCashoutProcessing`,而 `markFirstCashoutProcessing` ** again** 要求 `STATUS_READY`(L343)。 +逻辑重复但无害;可简化为只调 `markFirstCashoutProcessing`,或只保留一处校验。 + +注意:`mergeFirstCashoutIntoPost` 已校验 ready,到 `firstCashout` 时若并发重复提交,第二次会在 mark 阶段失败 — 符合「处理中不可重复提交」。 + +--- + +## P2:行为变化(需产品确认) + +| 项 | 原 FC | 现统一 apply | +|----|--------|----------------| +| `is_bind_name` | 不校验 | **校验**(L157) | +| Redis 5s 频控 | 无 | **有** | +| 返回值 | `{order_id, amount}` | pay `apply` 原始结构 | + +若 C 端依赖 `data.order_id` / `data.amount` 展示,需确认 pay 返回是否一致。 + +--- + +## 做得好的部分 + +- Controller 单入口 + merge post:**正确**。 +- 去掉独立 FC validator scene:**正确**。 +- [`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php) 兼容路径与主入口一致:**正确**。 +- pay 层仍靠 `bizType` 跳过 `withdrawFrozen`:**只要 bizType 确实传到 pay 就没问题**(当前 L184 已设)。 + +--- + +## 推荐修复结构(最小 diff) + +在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 开头(绑卡、频控之后): + +```php +if ($applyDTO->package_id > 0) { + return $this->applyFreeCreditsFirstCashout($applyDTO, $bankInfo); +} +// 原有普通提现逻辑不变 +``` + +`applyFreeCreditsFirstCashout` 内: + +1. 跳过 `checkInfo`、黑规则、`getAmountAndFee` +2. `orderId` → `firstCashout`(mark)→ 组 `WithdrawalInfo`(qf amount, fee=0, auditType=2, bizType) +3. try/catch pay + `handleFirstCashoutResult` 回滚 + +**不要**在普通流程中间用 `if ($package_id > 0) { firstCashout; }` 再接着跑普通逻辑 — 这是当前问题的根源。 + +--- + +## 验收清单 + +- [ ] `package_id>0` 且钱包 withdraw=0:能成功提交 pay(不报 Insufficient balance) +- [ ] pay 申请失败:package 回到 ready,可再次提现 +- [ ] pay 成功 + 回调:package completed,player `FIRST_CASH_DONE` +- [ ] 普通提现无 `package_id`:行为与改前一致 +- [ ] `remark`/bizType 在 pay 侧仍为 `free_credit_first_cashout`,不冻结钱包 diff --git a/plans/firstcashout_合并提现_837e2d44.plan.md b/plans/firstcashout_合并提现_837e2d44.plan.md new file mode 100644 index 0000000..c80fcec --- /dev/null +++ b/plans/firstcashout_合并提现_837e2d44.plan.md @@ -0,0 +1,174 @@ +--- +name: firstCashout 合并提现 +overview: 可以合并:推荐在 Withdraw 入口与 Pay 下单层统一,用 package_id 区分;活动档位状态机仍留在 FreeCreditsLogic,不能把 FC 逻辑硬塞进 WithdrawService::apply 主流程中间。 +todos: + - id: dto-package-id + content: WithdrawApplyDTO 增加 package_id;WithdrawValidator 增加 FC scene(package_id 替代 amount) + status: completed + - id: withdraw-fc-branch + content: WithdrawService::apply 增加 applyFreeCreditsFirstCashout early return(跳过 checkInfo/手续费/黑规则) + status: completed + - id: controller-unify + content: WithdrawController::apply 统一入口;FreeCreditsController::firstCashout 改为兼容 alias + status: completed + - id: bank-persist + content: FC 分支复用 checkBankInfo,与普通提现绑卡行为一致 + status: completed + - id: pay-rollback-test + content: 补单测/集成测:FC 无 withdraw 余额可提交、Pay 失败回滚 ready + status: completed +isProject: false +--- + +# firstCashout 能否合并进现有提现逻辑 + +## 结论(后端) + +**可以合并,且推荐合并「入口 + 校验 + 绑卡 + Pay 下单」;不应把 Free Credits 编排塞进 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 主流程中间。** + +当前仓库状态:**尚未合并**——仍是双入口: + +| 入口 | 现状 | +|------|------| +| [`POST /api/free-credits/first-cashout`](slot_console/app/api/controller/FreeCreditsController.php) | `FreeCreditsLogic::firstCashout` 完整编排 | +| [`POST /api/withdraw/apply`](slot_console/app/api/controller/WithdrawController.php) | `WithdrawService::apply`,无 `package_id` | + +Pay 层**已经统一**:两类提现最终都走 [`WithdrawalOrderEntity::apply`](slot_pay/app/entity/WithdrawalOrderEntity.php),靠 `bizType=free_credit_first_cashout` 跳过 `withdrawFrozen` 并走独立 MQ 回调。 + +--- + +## 为什么「可以」合并 + +C 端参数与需求 §11 一致:独立提现页 = 普通提现页精简版,收款字段相同,仅差: + +| 字段 | 普通提现 | Free Credits 第一档 | +|------|----------|----------------------| +| `amount` | 用户输入 | **不传**,服务端取 package | +| `package_id` | 无 | **必填** | + +因此用 **同一 `POST /api/withdraw/apply` + 可选 `package_id`** 区分业务是合理契约。 + +--- + +## 为什么不能「整段并入」WithdrawService::apply + +[`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序(L164–219): + +1. `checkBankInfo` +2. **`checkInfo`(校验钱包 withdraw 余额 ≥ amount)** +3. 黑规则、`getAmountAndFee`(手续费) +4. `PayService::apply` + +Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额。若在中间插入 `firstCashout` 而不 **early return**,会先被 `checkInfo` 打成 `Insufficient balance`(评审计划已记录为 P0)。 + +此外 FC 固定规则与普通提现不同: + +| 项 | 普通提现 | FC 第一档 | +|----|----------|-----------| +| 金额来源 | 用户 `amount` | `package->amount_qf` | +| 手续费 | `getAmountAndFee` | **0** | +| 审核 | 动态 `getAuditType` + 黑规则 | **auditType=2** | +| 钱包冻结 | 有 | **无**(pay 侧 bizType 分支) | +| 活动状态 | 无 | `markFirstCashoutProcessing` / 失败回滚 | +| Pay 失败回滚 | 钱包解冻 | **`handleFirstCashoutResult` 恢复 ready** | + +这些差异属于 **Logic 编排**,符合 [`backend-layering`](file:///Users/ray/.cursor/rules/backend-layering.mdc):不应把 `FreeCreditsLogic` 整段搬进 `WithdrawService` 当「又一层 Service」。 + +--- + +## 推荐合并结构 + +```mermaid +flowchart TB + Client["POST /api/withdraw/apply"] + Client --> Branch{package_id 存在?} + Branch -->|否| Normal["WithdrawService::apply 原逻辑"] + Branch -->|是| FC["WithdrawService::applyFreeCreditsFirstCashout"] + FC --> Mark["FreeCreditsLogic::markFirstCashoutProcessing"] + FC --> Pay["PayService::apply bizType=free_credit_first_cashout"] + Normal --> Pay2["PayService::apply 普通"] + Pay --> PayEntity["slot_pay WithdrawalOrderEntity"] + Pay2 --> PayEntity +``` + +### 合并层(推荐做) + +1. **[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php)** + - 有 `package_id` → 走 FC 分支(或 DTO 带 `package_id` 后交给 Service early return) + - 无 `package_id` → 现有普通提现 + +2. **[`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php)** + - 新增 FC scene:与普通 apply 对称,把 `amount` 换成 `package_id`(`SCENE_APPLY_CASH_FC` 等) + +3. **[`WithdrawApplyDTO`](slot_console/app/api/dto/request/WithdrawApplyDTO.php)** + - 增加可选 `package_id` + +4. **绑卡** + - FC 复用 `WithdrawService::checkBankInfo`(当前 `firstCashout` 只读绑卡、不写库,合并后应对齐普通提现) + +5. **[`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php)** + - 保留为 **兼容 alias**(内部转调 withdraw apply),或标记 deprecated + +### 保持分离(必须) + +- [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php):`markFirstCashoutProcessing`、`handleFirstCashoutResult`、package 状态机、`assertEligibleParticipant` +- [`WithdrawService::applyFreeCreditsFirstCashout`](slot_console/app/service/WithdrawService.php)(新建私有方法): + - 跳过 `checkInfo`、黑规则、`getAmountAndFee` + - `amount = package->amount_qf`,`fee = 0`,`auditType = 2` + - try/catch Pay,失败时 `handleFirstCashoutResult($orderId, false)` + +### 已统一、无需再改 + +- **slot_pay**:`bizType` / `remark` 分支(跳过冻结、Success/Fail/Rejected → Console MQ) +- **slot_console EventBus**:`FreeCreditsFirstCashoutSuccess/Fail/Rejected` → `handleFirstCashoutResult` + +--- + +## 不推荐的做法 + +在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 中间写: + +```php +$this->checkInfo(...); +// ... +if ($package_id > 0) { + (new FreeCreditsLogic())->firstCashout(...); +} +$amountInfo = $this->getAmountAndFee(...); +PayService::apply(...); +``` + +会导致:余额校验失败、手续费错误、Pay 失败不回滚档位(processing 卡死)。 + +--- + +## 与「单独提现」需求的关系 + +| 需求 | 合并后是否满足 | +|------|----------------| +| 不进入普通钱包(§15.3) | 是,仍靠 pay `bizType` | +| 固定第一档金额(§11.3) | 是,服务端取 package | +| 处理中不可重复提交(§5.6) | 是,仍由 package status 控制 | +| 成功/失败/拒绝状态(§20.2) | 是,回调逻辑不变 | + +合并的是 **HTTP 入口与收款参数校验**,不是改掉「独立提现」的账务语义。 + +--- + +## 实施要点(若执行) + +1. `WithdrawApplyDTO` 增加 `package_id` +2. `WithdrawService::apply` 在绑卡 + Redis 频控后 **early return** 到 `applyFreeCreditsFirstCashout` +3. FC 分支内:先 `markFirstCashoutProcessing`,再 Pay,catch 回滚 +4. 单测:FC 在 `withdraw=0` 时可提交;Pay 失败 package 回 ready;普通 apply 无回归 +5. 文档:`/api/free-credits/first-cashout` → 指向 `/api/withdraw/apply` + `package_id` + +--- + +## 验收清单 + +- `package_id>0` 且钱包可提现余额为 0:可成功提交 pay +- pay 申请失败:package 回到 `ready`,可重试 +- pay 回调成功:package `completed`,player `FIRST_CASH_DONE` +- 不传 `package_id`:普通提现与现网一致 +- pay 订单 `remark` 仍为 `free_credit_first_cashout`,不触发 `withdrawFrozen` diff --git a/plans/firstcashout_合并评估_df95dacf.plan.md b/plans/firstcashout_合并评估_df95dacf.plan.md new file mode 100644 index 0000000..b45836a --- /dev/null +++ b/plans/firstcashout_合并评估_df95dacf.plan.md @@ -0,0 +1,135 @@ +--- +name: firstCashout 合并评估 +overview: 客户端入参与普通提现几乎一致(仅多 package_id、不传 amount),推荐在 WithdrawController::apply 做薄入口合并并复用 WithdrawValidator;编排仍留 FreeCreditsLogic,不并入 WithdrawService::apply 内部。 +todos: + - id: merge-withdraw-entry + content: WithdrawController::apply 增加 package_id 分支,委托 FreeCreditsLogic::firstCashout;复用 WithdrawValidator 新增 scene(type 1/2/3/6 + package_id,无 amount) + status: completed + - id: align-bank-persist + content: firstCashout 改为复用 WithdrawService::checkBankInfo(或抽 helper),与普通提现一致写绑卡信息 + status: completed + - id: deprecate-fc-endpoint + content: /api/free-credits/first-cashout 保留作兼容 alias 或标记 deprecated,文档指向 /api/withdraw/apply?package_id= + status: completed + - id: keep-logic-split + content: FreeCreditsLogic 仍负责 package 状态机 + bizType;WithdrawService::apply 不增加 Free Credits 分支 + status: completed +isProject: false +--- + +# firstCashout 合并进现有提现接口 — 修订评估 + +## 用户反馈:客户端参数基本一致 + +对照 [`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php) 与 [`FreeCreditsValidator::SCENE_FIRST_CASHOUT`](slot_console/app/api/validator/FreeCreditsValidator.php): + +| 字段 | 普通提现 apply | Free Credits firstCashout | +|------|----------------|---------------------------| +| `type` | require (1/2/3/6) | require (1/2/3/6) | +| `pay_net` | require (1/2/3) | Logic 使用,Validator scene 未含(可补齐) | +| `user_name` / `cash_tag` | type=1 时 require | post 传入,Logic 读取 | +| `btc` / `usdt` | type=2/3 时 require | 同上 | +| `paypal_*` / `email` | type=6 时 require | 同上 | +| `amount` | **require,用户输入** | **不传,服务端取 package 金额** | +| `package_id` | 无 | **require,活动档位 id** | + +**结论**:C 端独立提现页(需求 §11)本来就是对普通提现页的精简——同一套收款字段,只是隐藏 amount 输入框。从接口契约看,**完全可以用同一个 apply 入口**,用 `package_id` 有无区分业务类型。 + +--- + +## 仍建议合并的范围:入口 + 校验 + 组单,不是 WithdrawService 内部 + +```mermaid +flowchart TB + Client["C 端 POST /api/withdraw/apply"] + Client --> Branch{package_id 存在?} + Branch -->|是| FC["FreeCreditsLogic::firstCashout"] + Branch -->|否| WS["WithdrawService::apply"] + FC --> Pay["PayService::apply bizType=free_credit_first_cashout"] + WS --> Pay2["PayService::apply 普通"] + Pay --> PayEntity["WithdrawalOrderEntity::apply 已统一"] + Pay2 --> PayEntity +``` + +### 可以合并(推荐) + +1. **统一入口**:[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php) 检测 `package_id`,有则委托 `FreeCreditsLogic::firstCashout`,无则走 `WithdrawService::apply`。 +2. **统一校验**:在 `WithdrawValidator` 新增 scene,与普通 apply 对称,仅把 `amount` 换成 `package_id`: + - `SCENE_APPLY_CASH_FC` => `['package_id', 'type', 'user_name', 'cash_tag']` + - `SCENE_APPLY_BTC_FC` => `['package_id', 'type', 'btc']` + - 等 +3. **统一绑卡写库**:`firstCashout` 目前只读 `UserBankCardModel`;普通提现通过 `WithdrawService::checkBankInfo` 会更新绑卡。合并入口后应 **复用同一绑卡逻辑**,避免两套行为。 +4. **统一 DTO**:`WithdrawApplyDTO` 增加可选 `package_id` 字段即可,不必维护两套 post 结构。 + +### 不应合并进 WithdrawService::apply + +编排差异仍在 Logic 层,不应塞进 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php): + +| 仍分离的逻辑 | 原因 | +|-------------|------| +| 金额 | 普通:用户 amount;FC:package->amount_qf | +| 余额/手续费/VIP/黑规则 | 普通:checkInfo + getAmountAndFee;FC:跳过 | +| 活动状态 | FC:markFirstCashoutProcessing / 失败回滚 | +| pay 回调 | 已通过 bizType 在 pay 层分支,无需 console 再分 | + +--- + +## 推荐实现(修订后方案 B) + +[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php): + +```php +public function apply(Request $request) +{ + $post = $request->post(); + $type = input('type', 1); + + if (!empty($post['package_id'])) { + // 复用 WithdrawValidator 的 FC scene(无 amount) + $error = $this->validate(WithdrawValidator::SCENE_APPLY_CASH_FC /* 按 type */, $post); + if ($error !== true) { + return $this->errorCode(ErrorCode::PARAMS_ERROR, $error); + } + return $this->success( + (new FreeCreditsLogic())->firstCashout( + $request->userEntity->uid, + (int) $post['package_id'], + $post + ), + 'Submitted successfully! ...' + ); + } + + // 原有普通提现 + $error = $this->validate($type, $post); + // ... +} +``` + +[`FreeCreditsLogic::firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 内部改动: + +- 绑卡:改为调用 `WithdrawService` 的 `checkBankInfo`(需将 `checkBankInfo` 改为 `protected` 公开方法,或抽到 helper)。 +- 其余不变:package 校验、固定金额、`bizType`、状态机。 + +[`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php): + +- 保留为 **兼容 alias**(内部同样调 Logic),或直接 deprecated 指向 withdraw apply。 + +--- + +## 与初版评估的差异 + +| 初版 | 修订 | +|------|------| +| 方案 A 推荐保持双入口 | **方案 B 升为推荐** — 参数一致,双入口无必要 | +| 强调「参数/校验不同故难合并」 | 参数 **高度重合**,差异仅 `package_id` vs `amount`;校验可共用 Validator scene | +| 合并障碍在入口层 | 合并障碍仅在 **WithdrawService 内部编排**,入口层应合并 | + +--- + +## 验收 + +- C 端独立提现页调用 `POST /api/withdraw/apply`,传 `package_id + type + 收款字段`,**不传 amount** +- 普通提现不传 `package_id`,行为与现网一致 +- Free Credits 仍不走 `withdrawFrozen`,回调仍更新 package/player +- 绑卡信息与走普通 apply 后一致落库 diff --git a/plans/free_credits_activity_edit_454064f6.plan.md b/plans/free_credits_activity_edit_454064f6.plan.md new file mode 100644 index 0000000..c8b3cf0 --- /dev/null +++ b/plans/free_credits_activity_edit_454064f6.plan.md @@ -0,0 +1,247 @@ +--- +name: free credits activity edit +overview: 在管理后台活动配置编辑表单中为活动类型 11(首充前免费余额定格与分档释放 / Free Credits)增加专属表单分支,并补齐字典与服务端校验;金额输入沿用 type==10 的「美元小数 → 千分位整数」模式,写入 ext_config 的 _qf 后缀键,与 slot_console `FreeCreditsLogic::configAmount()` 的读优先级一致。 +todos: + - id: dict-activity-type-11 + content: 字典 sm_system_dict_data 追加 code='activity_type' 的 value=11 / label='Free Credits(首充前免费余额)'(DB 或字典管理页) + status: completed + - id: edit-vue-type11-form + content: edit.vue 增加 v-if=type==11 的表单块(6 字段 + Banner) + status: completed + - id: edit-vue-skip-goods + content: edit.vue 让 type==11 跳过 goods 区块:扩展 onlyGift / 调整「添加赠送」与 goods 卡片的 v-if + status: completed + - id: edit-vue-submit-conv + content: edit.vue submit() 增加 type==11 的金额 ×1000 写入 _qf 键、清空 goods + status: completed + - id: edit-vue-setform-conv + content: edit.vue setFormData() 增加 type==11 的 _qf ÷1000 回填还原 + status: completed + - id: validate-free-credits-ext + content: ActivityValidate 增加 checkFreeCreditsExt(extConfig) 方法,含必填、非负、max_unlock_per_recharge≥1、first_cash ≤ recharge_unlock 约束 + status: completed + - id: controller-trigger-ext-check + content: ActivityController::save / update 在 checkData 后按 type==11 调用 checkFreeCreditsExt;update 兼容仅改状态请求 + status: completed + - id: verify-end-to-end + content: 本地验证:新建/编辑/必填/业务约束/类型切换/C 端 FreeCreditsLogic 读取 + status: completed +isProject: false +--- + +# Free Credits 活动后台编辑落地计划 + +## 目标 + +让运营在「活动管理」编辑弹窗里选活动类型 `11`(首充前免费余额)时,能直接编辑需求文档第 17 节列出的所有配置项;保存后落到 `s_recharge_gift_config.ext_config` JSON 字段,C 端 `FreeCreditsLogic` 立即生效。 + +## 现状要点 + +- `slot_console` 已实现 `RechargeGiftConfigModel::TYPE_FREE_CREDITS = 11`、`FreeCreditsLogic`、运行时配置读取(`ext_config.{key}_qf` 优先)。 +- `slot_admin` 后端 [ActivityController](backend/slot_admin/app/game/controller/ActivityController.php) 走 `slotLib\services\ActivityService` 透传到 `slot_console innerapi/activity/*`,`ext_config` 已原样落库,**不需要改动 slot_console**。 +- 管理前端 [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) 现仅对 6 / 9 / 10 做了 `v-if` 分支,`type==11` 无任何 UI;字典 `activity_type` 也无 value=11 条目。 + +## 字段映射(type=11 专属 `ext_config`) + +UI 输入用美元小数,提交时 ×1000 写入 `_qf` 键(与 type==10 模式一致;`FreeCreditsLogic::configAmount()` 优先读 `_qf`): + +- `win_threshold_qf` ← 首笔赢取门槛(默认 $50) +- `recharge_unlock_amount_qf` ← 累计充值解锁第一档(默认 $50) +- `first_cash_amount_qf` ← 第一档免打码金额(默认 $20) +- `package_amount_qf` ← 后续每档拆分金额(默认 $10) +- `subsequent_min_recharge_qf` ← 解锁下一档单笔充值下限(默认 $10) +- `max_unlock_per_recharge` ← 整数,每笔充值最多解锁档数(默认 1,**不走 _qf**) +- `banner_image` ← Free Play to Go 弹窗 Banner 图片 URL(可空) + +## 改动清单 + +### 1. 字典:追加 `activity_type` value=11 + +在系统管理「字典管理 → activity_type」以有,不需要再执行了 + +### 2. 前端:[backend/slot_admin_vue/src/views/game/activity/edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) + +#### 2.1 增加 type==11 表单块 + +参考 [edit.vue line 108-123](backend/slot_admin_vue/src/views/game/activity/edit.vue) type==10 的写法,新增: + +```vue + +``` + +#### 2.2 让 type==11 跳过 goods 区块 + +- [edit.vue line 60](backend/slot_admin_vue/src/views/game/activity/edit.vue) `onlyGift` computed 追加 `|| formData.type == 11`: + +```js +let onlyGift = computed(() => { + return formData.type == 7 || formData.type == 8 || formData.type == 9 || formData.type == 11; +}) +``` + +- [edit.vue line 125](backend/slot_admin_vue/src/views/game/activity/edit.vue) 把「添加赠送」按钮与下方 `` 的 goods 卡片包成一组条件,排除 type==10 和 type==11: + +```vue + + + + ...goods 卡片... + +``` + +(保留原 type==9 的 exchange 表格 v-if 不变。) + +#### 2.3 `submit()` 中追加 type==11 的金额 ×1000 转换 + +[edit.vue line 370](backend/slot_admin_vue/src/views/game/activity/edit.vue) 类似 type==10 的处理,把 6 个美元字段写到 `_qf` 键,整数字段原样保留: + +```js +if (formData.type === 11) { + const e = data.ext_config || {}; + const toQf = (v) => v === '' || v == null ? undefined : Math.round(Number(v) * 1000); + data.ext_config = { + win_threshold_qf: toQf(e.win_threshold), + recharge_unlock_amount_qf: toQf(e.recharge_unlock_amount), + first_cash_amount_qf: toQf(e.first_cash_amount), + package_amount_qf: toQf(e.package_amount), + subsequent_min_recharge_qf: toQf(e.subsequent_min_recharge), + max_unlock_per_recharge: e.max_unlock_per_recharge == null ? undefined : Math.round(Number(e.max_unlock_per_recharge)), + banner_image: e.banner_image || '', + }; + data.goods = []; +} +``` + +#### 2.4 `setFormData()` 中追加 type==11 的回填还原 + +[edit.vue line 344-351](backend/slot_admin_vue/src/views/game/activity/edit.vue) 仿照 type==10: + +```js +if (data.type === 11 && data.ext_config) { + const e = data.ext_config; + formData.ext_config = { + win_threshold: e.win_threshold_qf != null ? e.win_threshold_qf / 1000 : e.win_threshold, + recharge_unlock_amount: e.recharge_unlock_amount_qf != null ? e.recharge_unlock_amount_qf / 1000 : e.recharge_unlock_amount, + first_cash_amount: e.first_cash_amount_qf != null ? e.first_cash_amount_qf / 1000 : e.first_cash_amount, + package_amount: e.package_amount_qf != null ? e.package_amount_qf / 1000 : e.package_amount, + subsequent_min_recharge: e.subsequent_min_recharge_qf != null ? e.subsequent_min_recharge_qf / 1000 : e.subsequent_min_recharge, + max_unlock_per_recharge: e.max_unlock_per_recharge ?? 1, + banner_image: e.banner_image || '', + }; +} +``` + +### 3. 后端校验:[backend/slot_admin/app/game/validate/ActivityValidate.php](backend/slot_admin/app/game/validate/ActivityValidate.php) + +ThinkValidate 对嵌套 JSON 支持有限,采用「在 Controller 按 `type` 触发额外校验」+「Validate 提供专用方法」的模式,避免污染既有 scene。 + +#### 3.1 ActivityValidate 增加一个公开方法 + +```php +/** + * Free Credits(type=11)专用 ext_config 校验。 + * + * @param array $extConfig 前端提交的 ext_config,金额已 ×1000 写入 _qf 键 + * @throws \think\exception\ValidateException + */ +public function checkFreeCreditsExt(array $extConfig): void +{ + $required = [ + 'win_threshold_qf' => '首笔赢取门槛', + 'recharge_unlock_amount_qf' => '充值解锁门槛', + 'first_cash_amount_qf' => '免打码提现额', + 'package_amount_qf' => '解锁拆分金额', + 'subsequent_min_recharge_qf' => '后续解锁最小充值', + 'max_unlock_per_recharge' => '每笔最多解锁档数', + ]; + foreach ($required as $key => $label) { + if (!isset($extConfig[$key]) || $extConfig[$key] === '' || (int)$extConfig[$key] < 0) { + throw new \think\exception\ValidateException("{$label}必须填写且为非负数"); + } + } + if ((int)$extConfig['max_unlock_per_recharge'] < 1) { + throw new \think\exception\ValidateException('每笔最多解锁档数必须 ≥ 1'); + } + // 业务约束:免打码提现额 ≤ 累计充值解锁门槛 + if ((int)$extConfig['first_cash_amount_qf'] > (int)$extConfig['recharge_unlock_amount_qf']) { + throw new \think\exception\ValidateException('免打码提现额不能超过充值解锁门槛'); + } +} +``` + +#### 3.2 ActivityController save / update 触发 + +[ActivityController::save / update](backend/slot_admin/app/game/controller/ActivityController.php) 在 `checkData()` 之后追加: + +```php +if ((int)input('type') === 11) { + $this->validate->checkFreeCreditsExt((array)input('ext_config', [])); +} +``` + +`update` 里同样判断,且要兼容仅改状态的请求(无 `updateData` 时不校验 ext_config): + +```php +if (!empty(input('updateData')) && (int)input('type') === 11) { + $this->validate->checkFreeCreditsExt((array)input('ext_config', [])); +} +``` + +### 4. 不需要改的部分 + +- `slot_console` 侧 `ActivityConfigEntity::updateConfig` 已原样写入 `ext_config`,无变更。 +- `Consts::ACTIVITY_TYPE_*` 不必新增 11;C 端已通过 `RechargeGiftConfigModel::TYPE_FREE_CREDITS` 引用。 +- 不新增独立菜单页,复用通用「活动管理」编辑弹窗。 + +## 数据流 + +```mermaid +flowchart LR + edit[edit.vue type=11 form] -->|"美元×1000 → _qf"| save["POST /game/activity/save|update"] + save --> ctrl[ActivityController] + ctrl -->|"checkFreeCreditsExt"| validate[ActivityValidate] + ctrl --> svc[ActivityService HTTP] + svc --> console["slot_console innerapi/activity/update"] + console --> entity[ActivityConfigEntity::updateConfig] + entity --> db["s_recharge_gift_config.ext_config"] + db --> fc["FreeCreditsLogic::configAmount key_qf 优先"] +``` + + + +## 验收 + +- 新建 type=11 活动:填写 6 字段 + Banner,保存成功;DB `ext_config` 是 `*_qf` 整数 + `max_unlock_per_recharge` + `banner_image`。 +- 编辑回填:再次打开同一条活动,美元字段显示为原值(如 50 / 20 / 10),整数字段为 1,Banner 显示已上传图。 +- 必填校验:清空任一必填项保存,返回明确错误信息(如「首笔赢取门槛必须填写且为非负数」)。 +- 业务约束:免打码提现额填 60、充值解锁门槛填 50,保存被拒。 +- 类型切换:type 选 11→1,再切回 11,goods 区块不出现,formData 不污染;保存 type==1 时 `_qf` 字段不会被带到 ext_config。 +- C 端:调 `FreeCreditsLogic::status($uid)`,门槛 / 第一档金额 / 解锁拆分金额能读到刚配的值。 +- type 字典:管理后台编辑弹窗活动类型下拉出现「Free Credits(首充前免费余额)」选项。 + diff --git a/plans/free_credits_release_902566c9.plan.md b/plans/free_credits_release_902566c9.plan.md new file mode 100644 index 0000000..c817343 --- /dev/null +++ b/plans/free_credits_release_902566c9.plan.md @@ -0,0 +1,182 @@ +--- +name: free credits release +overview: 为“首充前免费余额定格与分档释放”准备开发方案,按钱包账务、充值提现事件、客户端 API、后台配置统计分阶段落地。重点遵循现有后端分层规则,避免把业务编排错误地下沉到 Service。 +todos: + - id: confirm-wallet-fields + content: 按已确认口径使用 deposit_balance + withdraw_balance 作为免费余额 + status: completed + - id: design-schema + content: 设计独立 Free Credits 活动主表、档位明细表、流水关联和旧活动 ID 关联 + status: completed + - id: implement-console-core + content: 在 slot_console 实现活动领域模型、定格、解锁、Claim 和状态查询 Logic + status: pending + - id: wire-recharge + content: 接入 slot_pay/slot_console 充值成功链路并保证首充定格幂等 + status: completed + - id: wire-cashout + content: 实现第一档独立提现订单和回调状态同步 + status: completed + - id: add-console-apis + content: 补充大厅状态、活动入口、后台配置统计相关 API + status: completed + - id: test-acceptance + content: 按需求文档核心规则和账务验收补测试/联调用例 + status: completed +isProject: false +--- + +# 首充前免费余额定格与分档释放开发准备 + +## 目标范围 + +本次需求核心实现应以后端账务和状态机为主,前端展示依赖新增/扩展 API。主要涉及: + +- [`/Users/ray/Documents/project/www/slot/slot_console`](slot_console):Free Credits 活动领域模型、Pool、档位、状态机、定格/释放/Claim 编排、大厅/提现页接口、活动入口、配置读取、后台统计入口。 +- [`/Users/ray/Documents/project/www/slot/slot_wallet`](slot_wallet):只提供钱包原子能力,例如余额扣减/入账、钱包流水、Deposit Balance 入账、Y1 流水任务创建;不放活动领域模型和活动状态机。 +- [`/Users/ray/Documents/project/www/slot/slot_pay`](slot_pay):充值成功异步通知、第一档独立提现订单。 +- [`/Users/ray/Documents/project/www/slot/backend/slot_admin`](backend/slot_admin):运营后台配置和统计页,如该项目负责管理端。 +- 客户端 UI 由前端人员在其它仓库实现,本仓库只提供后端接口和状态数据。 + +已有统一活动管理表可作为关联来源: + +- `s_common.s_recharge_gift_config`:现有充值赠送活动主表。 +- `recharge_gift_player`:现有充值赠送活动参与玩家表。 + +本需求不直接复用这两张表承载 Free Credits 账务状态,采用独立 Free Credits 表设计,并保留与旧活动 ID 的关联,避免把“首充前免费余额定格”与已有充值赠送活动规则混在同一张参与表里。 + +## 数据表 DDL 草案 + +金额字段建议沿用现有活动表中的 `_qf` 口径,按千分位整数保存,避免小数精度问题。 + +```sql +CREATE TABLE `free_credits_player` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `activity_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '关联 s_recharge_gift_config.id', + `uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', + `source` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道', + `model_id` int unsigned NOT NULL DEFAULT '0' COMMENT '游戏模型ID', + `home_withdraw_unlocked` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '首页Withdraw是否已解锁 0否 1是', + `frozen_amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '首充时定格金额,千分位', + `first_cash_amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '第一档免打码提现金额,千分位', + `first_recharge_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '触发定格的首笔充值订单号', + `first_recharge_time` datetime DEFAULT NULL COMMENT '首笔充值成功时间', + `first_cashout_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '第一档独立提现订单号', + `status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '主状态 0未开始 1首页已解锁 2已定格 3待充值解锁 4第一档可提现 5第一档提现中 6第一档已提现 7后续释放中 10全部完成', + `completed_time` datetime DEFAULT NULL COMMENT '全部完成时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_activity_uid` (`activity_id`,`uid`), + KEY `idx_uid` (`uid`), + KEY `idx_activity_status` (`activity_id`,`status`), + KEY `idx_first_recharge_order` (`first_recharge_order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Free Credits用户活动主表'; +``` + +```sql +CREATE TABLE `free_credits_package` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `player_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'free_credits_player.id', + `activity_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '关联 s_recharge_gift_config.id', + `uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', + `package_no` int unsigned NOT NULL DEFAULT '0' COMMENT '档位序号,从1开始', + `package_type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '档位类型 1第一档免打码提现 2后续释放档', + `amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '档位金额,千分位', + `status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '档位状态 0锁定 1可操作 2处理中 3已完成 4失败 5风控拒绝', + `withdraw_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '第一档提现订单号', + `claim_biz_id` varchar(64) NOT NULL DEFAULT '' COMMENT '后续档Claim入账幂等业务号', + `unlocked_time` datetime DEFAULT NULL COMMENT '解锁时间', + `claimed_time` datetime DEFAULT NULL COMMENT '领取时间', + `completed_time` datetime DEFAULT NULL COMMENT '完成时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_player_package` (`player_id`,`package_no`), + UNIQUE KEY `uniq_claim_biz` (`claim_biz_id`), + KEY `idx_uid_status` (`uid`,`status`), + KEY `idx_activity_status` (`activity_id`,`status`), + KEY `idx_withdraw_order` (`withdraw_order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Free Credits档位明细表'; +``` + +## 建议数据流 + +```mermaid +flowchart TD + preDepositUser["未首充用户"] --> winThreshold["免费余额曾达到门槛"] + winThreshold --> homeUnlocked["首页 Withdraw 解锁"] + preDepositUser --> firstRecharge["任意入口首笔真实充值成功"] + firstRecharge --> freezeFreeCredits["定格当前免费余额"] + freezeFreeCredits --> freeCreditsPool["slot_console 保存 Free Credits Pool 与档位"] + firstRecharge --> walletTotalRecharge["查询钱包累计充值总额"] + walletTotalRecharge --> firstReady["累计充值满门槛释放第一档"] + firstReady --> firstCashout["第一档独立提现"] + firstCashout --> laterPackages["后续档位按充值解锁"] + laterPackages --> claimToWallet["Claim 入 Deposit Balance"] + claimToWallet --> y1Task["创建 Y1 流水任务"] +``` + +## 后端落地方案 + +1. 在 `slot_console` 新增独立 Free Credits 活动领域模型。 + - 新增用户池主表,保存 `activity_id`、`uid`、定格金额、首充订单、主状态、首页解锁标记、完成时间等;主表不保存累计充值。 + - 新增档位明细表,一档一行,保存序号、类型、金额、状态、关联提现单/Claim 流水、解锁/完成时间。 + - `activity_id` 关联现有 `s_common.s_recharge_gift_config` 或后台活动配置 ID,但 Free Credits 的进度、档位和账务状态不写入 `recharge_gift_player`。 + - 表归属按 `slot_console` 现有业务库和活动模块规范处理;`slot_wallet` 不新增 Free Credits 活动表。 + +2. 在 `slot_console` 增加 Logic 编排定格、解锁、Claim。 + - Controller 只做请求接收、Validate、DTO、统一响应。 + - Validate 处理参数必填/类型/枚举。 + - DTO 只承载已校验字段。 + - Logic 负责首充定格、查询钱包累计充值总额、档位状态流转、事务和幂等。 + - Model 负责查询/写入和状态条件更新。 + - Service 仅用于已有公共能力,如调用钱包原子 API、配置读取、外部系统封装,不新增单纯转发 Service。 + - 调用 `slot_wallet` 时只请求余额变更、钱包流水、入 Deposit Balance、创建 Y1 任务等钱包能力,不把活动状态写入钱包服务。 + +3. 接入充值成功链路。 + - 现有充值成功链路在 [`/Users/ray/Documents/project/www/slot/slot_pay/app/command/EventRecharge.php`](slot_pay/app/command/EventRecharge.php) 调用钱包充值入账。 + - 充值成功后由 `slot_pay` 或事件消费者通知 `slot_console`,由 `slot_console` 判断是否首笔真实充值并触发定格。 + - 定格需要调用 `slot_wallet` 原子能力扣减当前免费余额并写钱包流水,再由 `slot_console` 落 Free Credits Pool 和档位。 + - 第一档门槛通过调用钱包查询用户累计充值总额判断;`slot_console` 主表不保存累计充值,也不新增充值事件明细表。 + +4. 实现第一档独立提现。 + - 第一档不进入普通钱包余额,创建独立提现订单类型 `free_credit_first_cashout`。 + - 需要在 `slot_pay` 的提现订单模型/实体中支持新订单类型,提现处理中防重复,成功/失败/拒绝回写档位状态。 + - 第一档提现成功后关闭首页状态条,但活动入口保留到所有档位完成。 + +5. 实现后续档位解锁和 Claim。 + - 后续每笔符合条件真实充值最多解锁下一档,按配置控制最小充值金额和每笔最多解锁档数。 + - Claim 由 `slot_console` 校验档位状态和顺序后,调用 `slot_wallet` 将档位金额入 `Deposit Balance`,创建 Deposit Lot 和 Y1 Wager Task。 + - `slot_console` 负责 Claim 幂等和档位状态流转;`slot_wallet` 负责钱包入账幂等和流水一致性。失败时档位保持 `ready`,重复点击不能重复入账。 + +6. 增加客户端查询与操作 API。 + - 查询用户活动状态:首页状态条、活动入口、Free Play to Go 弹窗需要同一份状态数据。 + - 操作 API:第一档提现、后续 Claim、后续 Unlock 跳充值。 + - `slot_console` 可在 [`/Users/ray/Documents/project/www/slot/slot_console/app/napi/controller/LobbyController.php`](slot_console/app/napi/controller/LobbyController.php) 或独立 API 暴露大厅所需数据。 + +7. 增加后台配置与统计。 + - 配置项优先绑定到现有统一活动管理的活动 ID;Free Credits 专属配置包括活动开关、首笔赢取门槛、充值解锁门槛、免打码提现额、拆分金额、后续最小充值、每笔最多解锁档数、Y1 倍数、Banner。 + - 统计项:定格人数、完成提现人数、全部完成人数、定格总金额、已提现金额、已领取金额、待释放金额。 + - 筛选排序按文档要求实现。 + +## 已确认口径 + +- 免费余额 = `deposit_balance + withdraw_balance`,即钱包余额。 +- 第一档不需要流水,完全绕开普通可提现余额计算。 +- 第一档解锁通过充值成功异步通知驱动,并调用钱包查询累计充值总额判断是否达到门槛。 +- Free Credits 定格逻辑以独立活动配置为准。 +- 客户端 UI 由前端人员处理,本仓库不包含对应页面代码。 + +## 关键风险与需确认点 + +- 文档中的 `Deposite Balance / Deposite Lot` 建议统一确认是否为历史命名还是拼写问题。 + +## 建议开发顺序 + +1. 先实现 `slot_console` 活动数据模型、配置读取、状态机和只读查询接口。 +2. 梳理并补齐 `slot_wallet` 需要暴露的钱包原子能力,包括查询累计充值总额、冻结/扣减免费余额、入 Deposit Balance、创建 Y1 任务和幂等流水。 +3. 接入充值成功链路,完成 `slot_pay` 到 `slot_console` 的事件通知、首充定格和调用钱包累计充值总额解锁第一档。 +4. 实现第一档独立提现链路和提现回调到 `slot_console` 的状态同步。 +5. 实现后续档位解锁、Claim 调用钱包入账和 Y1 流水任务创建。 +6. 补 `slot_console` 大厅/活动入口接口、后台统计、客户端 UI、文案、埋点和验收用例。 \ No newline at end of file diff --git a/plans/free_credits_后台统计页_9d6edc45.plan.md b/plans/free_credits_后台统计页_9d6edc45.plan.md new file mode 100644 index 0000000..ade2b63 --- /dev/null +++ b/plans/free_credits_后台统计页_9d6edc45.plan.md @@ -0,0 +1,317 @@ +--- +name: free credits 后台统计页 +overview: 实现需求 §18 的 Free Credits 后台统计与展示:在 slot_console 增强 innerapi(加筛选、加列表字段、富化累计充值与档位聚合),slot_lib 新增 FreeCreditsService 并给 WalletService 补一个 statistics 批量方法,slot_admin 新增 FreeCreditsStatsController 代理,slot_admin_vue 新增 views/game/freeCreditsStats/index.vue 一页搞定,顶部统计走 sa-table 的 otherData 模式。 +todos: + - id: slot-console-stats-logic + content: slot_console 新建 innerapi/logic/FreeCreditsStatsLogic.php,封装 statistics + list 的查询编排(含 status>=2 口径、批量 package 聚合 SQL) + status: completed + - id: slot-console-list-enrich + content: slot_console FreeCreditsController::list 加筛选 + 字段富化(first_cash_status / progress / claimed / first_cashout / remaining / total_recharge) + status: completed + - id: slot-console-stats-filter + content: slot_console FreeCreditsController::statistics 加 source / first_recharge_time / status 等筛选;frozen_user_count 改为 status>=2 + status: completed + - id: slot-lib-wallet-stats + content: slot_lib WalletService 新增 statistics(uids, currency) 方法 → 调 /api/wallet/statistics + status: completed + - id: slot-lib-free-credits-service + content: slot_lib 新建 FreeCreditsService(basePath=innerapi/free-credits,含 list + statistics) + status: completed + - id: slot-admin-controller + content: slot_admin 新建 game/controller/FreeCreditsStatsController.php,index 一次调两个 service 合并 otherData 返回 + status: completed + - id: slot-admin-vue-api + content: slot_admin_vue 新建 api/game/freeCreditsStats.js,导出 getPageList + status: completed + - id: slot-admin-vue-page + content: "slot_admin_vue 新建 views/game/freeCreditsStats/index.vue:sa-table + #tableAfterButtons 顶部 7 统计 + 12 列表 + 筛选 + 排序白名单" + status: completed + - id: menu-permission + content: 在 sm_system_menu 加 1 条 L 菜单 + 1 条 B 按钮码(/game/freeCreditsStats/index),并分配角色 + status: completed + - id: manual-verify + content: docker 内 A/B/C 三种玩家手测:默认排序、7 项统计、筛选联动、排序白名单、权限拦截 + status: completed +isProject: false +--- + +## 数据流 + +```mermaid +flowchart LR + vue[freeCreditsStats/index.vue sa-table] + vue -->|"POST /game/FreeCreditsStats/index"| ctrl[FreeCreditsStatsController] + ctrl -->|"FreeCreditsService::list"| svc[slot_lib FreeCreditsService] + ctrl -->|"FreeCreditsService::statistics"| svc + svc -->|"POST innerapi/free-credits/list"| console[slot_console FreeCreditsController] + svc -->|"POST innerapi/free-credits/statistics"| console + console -->|"WalletService statistics uids currency"| wallet["slot_wallet api/wallet/statistics"] + console -->|"sum on free_credits_package"| db[(s_common.free_credits_player + free_credits_package)] +``` + +slot_admin Controller 一次代理两个 innerapi,合并成 `{ data, total, otherData: }` 给 sa-table,避免前端两次请求。 + +## 字段契约(最终对外) + +### `/game/FreeCreditsStats/index` 请求 + +筛选(与 §18.3 对齐) + +- `uid` 精确 +- `source` 字符串(沿用 channel_game_model + source 二级选择) +- `activity_id` 可选(默认取当前生效的 type=11 活动) +- `first_recharge_time` 范围 `[start, end]` → `whereBetween` +- `status` 数组(player.status:0/1/2/3/4/5/6/7/10) +- `first_cash_status` 数组(package.status:0/1/2/3/4/5,限定 `package_no=1`) +- `is_completed` 0/1(status=10 与否) +- `page`, `limit`, `orderBy`, `orderType` + +排序 + +- DB 可排序:`frozen_amount_qf`、`first_recharge_time`(默认 `first_recharge_time desc`) +- 其它字段(进度、剩余、累计充值)派生,前端不开启排序 + +### 响应 + +```json +{ + "data": [{ + "uid": 123, + "source": "us_01", + "frozen_amount_qf": 78500, + "first_cash_amount_qf": 20000, + "total_recharge_amount_qf": 30000, + "first_cash_status": 3, + "progress_done": 3, + "progress_total": 7, + "status": 7, + "remaining_amount_qf": 28500, + "claimed_amount_qf": 30000, + "first_recharge_time": "2026-05-12 11:23:01", + "completed_time": null + }], + "total": 240, + "otherData": { + "frozen_user_count": 240, + "first_cashout_user_count": 180, + "completed_user_count": 35, + "frozen_amount_qf": 18650000, + "first_cashout_amount_qf": 3520000, + "claimed_amount_qf": 8800000, + "pending_amount_qf": 6330000 + } +} +``` + +金额字段一律以 `*_qf`(千分位整数)传到 Vue,Vue 用 `/ 1000` + `toFixed(2)` 显示(与 edit.vue 同口径)。 + +--- + +## 改动清单 + +### 1. slot_console — 增强 innerapi + +文件:[slot_console/app/innerapi/controller/FreeCreditsController.php](slot_console/app/innerapi/controller/FreeCreditsController.php) + +#### 1.1 `statistics()` 加筛选 + +参照需求 §18.1,新增可选参数并贯穿到所有子查询: + +- `activity_id`、`source`、`first_recharge_time_start`、`first_recharge_time_end`、`uid` +- 子查询 `frozenTotal / completedCount / firstCashoutCount / firstCashoutAmount / claimedAmount` 全部基于同一个 `playerIds` 数组(按筛选后取出) +- `frozen_user_count` 口径改为 `status >= STATUS_FROZEN(2)` 的玩家数(与需求 "已创建 Free Credits Pool" 一致;当前实现把 status=1 也计入,定义不准) + +#### 1.2 `list()` 加筛选 + 字段富化 + +- 新增筛选参数:`source`、`first_recharge_time` 范围、`status[]`、`first_cash_status[]`、`is_completed`、`orderBy/orderType`(白名单:`first_recharge_time`, `frozen_amount_qf`) +- 输出每行追加: + - `first_cash_status`:对应 `free_credits_package.status` where `player_id=? AND package_no=1` + - `progress_done` / `progress_total`:`SUM(CASE WHEN status=3 THEN 1 ELSE 0 END)` / `COUNT(*)`,一次 `GROUP BY player_id` 跑完 + - `claimed_amount_qf`:`SUM(amount_qf) WHERE package_type=2 AND status=3 GROUP BY player_id` + - `first_cashout_amount_qf`:`SUM(amount_qf) WHERE package_type=1 AND status=3 GROUP BY player_id` + - `remaining_amount_qf`:`frozen_amount_qf - first_cashout_amount_qf - claimed_amount_qf` + - `total_recharge_amount_qf`:调用 `WalletService::statistics(uids, currency='USD')`,把 `total_deposit` 映射进来。currency 默认 'USD'(活动只面向美国 RMG,currency 字段后续如多币种再扩展) +- 一次性聚合查询,避免 N+1: + +```text +SELECT player_id, + SUM(CASE WHEN status=3 THEN 1 ELSE 0 END) AS progress_done, + COUNT(*) AS progress_total, + SUM(CASE WHEN package_type=1 AND status=3 THEN amount_qf ELSE 0 END) AS first_cashout_amount_qf, + SUM(CASE WHEN package_type=2 AND status=3 THEN amount_qf ELSE 0 END) AS claimed_amount_qf, + MAX(CASE WHEN package_no=1 THEN status END) AS first_cash_status +FROM s_common.free_credits_package +WHERE player_id IN (...) +GROUP BY player_id +``` + +#### 1.3 控制器分层 + +按 backend-layering 规则,把 1.1 / 1.2 的查询编排沉到 Logic:新增 [slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php](slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php)(或直接挂在已有 `api/logic/FreeCreditsLogic.php`,建议新建避免膨胀),Controller 只负责接参 / 调 Logic / 返回。 + +--- + +### 2. slot_lib — 新增 FreeCreditsService + 补 WalletService + +#### 2.1 [slot_lib/src/services/FreeCreditsService.php](slot_lib/src/services/FreeCreditsService.php) 新建 + +```php +class FreeCreditsService extends BaseApiService +{ + use SingletonService; + protected $hostKey = 'consoleApiHost'; + protected $basePath = 'innerapi/free-credits'; + + public function statistics(array $params) { return $this->postAction('statistics', $params); } + public function list(array $params) { return $this->postAction('list', $params); } +} +``` + +理由:不与既有 `ActivityService::join/add/update` 混杂;basePath 完全独立,匹配 slot_console 的路由。 + +#### 2.2 [slot_lib/src/services/WalletService.php](slot_lib/src/services/WalletService.php) 补 `statistics` + +```php +public function statistics(array $uids, string $currency) +{ + return $this->postAction('statistics', ['uids' => $uids, 'currency' => $currency]); +} +``` + +action 名 `statistics`,URL `{walletApiHost}/api/wallet/statistics`,对应 slot_wallet `WalletController::statistics`。slot_console 侧 `FreeCreditsStatsLogic` 调用此方法。 + +--- + +### 3. slot_admin — 新增代理 Controller + +文件:[backend/slot_admin/app/game/controller/FreeCreditsStatsController.php](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php) + +只做接参 + Service 调用 + 合并: + +```php +class FreeCreditsStatsController extends AdminController +{ + public function index(Request $request): Response + { + $params = $request->all(); + $params['page'] = (int)$request->get('page', 1); + $params['limit'] = (int)$request->get('limit', 20); + + $list = FreeCreditsService::getInstance()->list($params); + $stats = FreeCreditsService::getInstance()->statistics($params); + + $list['otherData'] = $stats; + return $this->success($list); + } +} +``` + +- 不需要 Validate(参数全部可选;非法值 slot_console 侧拦) +- 自动路由路径 `/game/freeCreditsStats/index` +- 不新增 `export()` action(V1 不做导出) + +--- + +### 4. slot_admin_vue — 新页 + API + +#### 4.1 [backend/slot_admin_vue/src/api/game/freeCreditsStats.js](backend/slot_admin_vue/src/api/game/freeCreditsStats.js) 新建 + +```js +import request from '@/utils/request' +const url = '/game/freeCreditsStats' +export default { + getPageList: (params) => request.get(`${url}/index`, params), +} +``` + +#### 4.2 [backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue) 新建 + +骨架仿 `views/game/order/recharge/index.vue`: + +- `defineOptions({ name: 'game/freeCreditsStats/index' })` +- `` `options.api = api.getPageList`,`options.add` / `options.delete` 关闭 +- `searchForm`:`uid`、`source`(用 `commonStore.allSourcesOptionsNoAll`)、`first_recharge_time`(`a-range-picker`)、`status`、`first_cash_status`、`is_completed` +- 顶部统计走 `#tableAfterButtons`: + +```vue + +``` + +- `stats = computed(() => crudRef.value?.getTableOtherData() ?? {})` +- 列定义:12 列对齐 §18.2;`status`、`first_cash_status` 直接用前端 map 表(不新建字典): + +```js +const PLAYER_STATUS_MAP = { + 0: '未触发', 1: 'Home Withdraw 已解锁', 2: '已定格', 3: '充值进行中', + 4: '第一档可提现', 5: '第一档处理中', 6: '第一档已提现', + 7: '后续档释放中', 10: '全部完成', +} +const PACKAGE_STATUS_MAP = { + 0: '未解锁', 1: '可操作', 2: '处理中', 3: '已完成', 4: '失败', 5: '已拒绝', +} +``` + +- `qfToDollar = (v) => ((Number(v||0))/1000).toFixed(2)` +- 不再调单独的 stats 接口,`tableAfterButtons` 数据从 `otherData` 取 + +--- + +### 5. 字典与菜单 + +#### 5.1 字典:不新增 + +`status` / `first_cash_status` 直接走前端 map 表,避免与 `RechargeGiftConfigModel` 共用字典污染。 + +#### 5.2 菜单:DB 表 `sm_system_menu` 追加(运营在「系统管理 → 菜单管理」操作即可) + +需要的 2 条数据(指引性 SQL,实际可走 UI): + +```sql +INSERT INTO sm_system_menu (parent_id, type, title, name, path, component, sort) + VALUES (, 'L', 'Free Credits 统计', + 'game/freeCreditsStats/index', '/game/freeCreditsStats', + 'game/freeCreditsStats/index', 50); +INSERT INTO sm_system_menu (parent_id, type, title, code) + VALUES (, 'B', '列表', '/game/freeCreditsStats/index'); +``` + +写完后给「超级管理员 / 运营」角色分配该菜单与按钮码。 + +--- + +## 不动的地方 + +- `slot_console` 既有 `FreeCreditsLogic` C 端逻辑、`free_credits_player` / `free_credits_package` 表结构、`ext_config` 配置读取等不动 +- `s_recharge_gift_config` 不需要新增列 +- `slot_pay` 不新增 innerapi(不需要订单表精确口径,wallet `total_deposit` 已足够) +- 现有 `ActivityController` 编辑表单(type=11 分支)已落地,不动 +- `RechargeOrderController` / `withdrawal` 等无关模块不动 + +## 验收(手测) + +按 [dev-environment](.cursor/rules/dev-environment.mdc) 在 docker 内调试: + +1. 准备 3 个测试账号:A 未定格、B 已定格未提现首档、C 全部完成;各 source 至少一个 +2. 访问 `/game/freeCreditsStats` 列表: + - 不传参 → 默认按 `first_recharge_time desc`,A 不出现(status<2 被过滤),B / C 都在 + - 顶部 7 个统计数字与逐条手算结果一致 + - 列表里 `total_recharge_amount_qf` 与 `slot_wallet.api/wallet/statistics` 直接拉的 `total_deposit` 一致 + - `progress_done/total` 等于 DB 中 `free_credits_package` 实际行数 +3. 筛选: + - 选 `source=us_01` 列表与统计同步收敛 + - 选 `first_recharge_time` 范围,统计 7 项随之变化 + - 选 `is_completed=1`,列表只剩 C + - 选 `first_cash_status=3`,列表只剩第一档已成功的玩家 +4. 排序:分别点击「定格金额」「定格时间」列头,order 切换;其它字段不可排序 +5. 权限:用未授权账号访问 → 403;授权后正常 +6. 仅改活动状态时(已存在的活动编辑场景)回归一遍,确认未触发 type=11 ext 校验路径回归 \ No newline at end of file diff --git a/plans/free_credits_老用户隐藏评审_1c0ab623.plan.md b/plans/free_credits_老用户隐藏评审_1c0ab623.plan.md new file mode 100644 index 0000000..53482d9 --- /dev/null +++ b/plans/free_credits_老用户隐藏评审_1c0ab623.plan.md @@ -0,0 +1,141 @@ +--- +name: Free Credits 老用户隐藏评审 +overview: EventBus 写库确定参加、status 只读展示;在此基础上用环境变量 FREE_CREDITS_ENROLL_REG_AFTER 做注册时间门槛——regTime 晚于该时间的用户才有资格参加,否则一律 status=-1 且 EventBus 跳过写池。 +todos: + - id: env-reg-cutoff + content: 新增 FREE_CREDITS_ENROLL_REG_AFTER 环境变量与 FreeCreditsLogic::isEligibleByRegTime(uid) 统一门禁 + status: completed + - id: apply-gate-all-paths + content: 在 status、syncHomeWithdrawUnlocked、handleFreeCreditInit、advanceAfterRecharge、claim、firstCashout 入口应用注册时间门禁 + status: completed + - id: fix-docs-lobby + content: 修正 FreeCreditsController PHPDoc(含 status=-1 含老用户/未达注册门槛);LobbyController 清理无用 import + status: completed + - id: guard-claim-cashout + content: 无资格或无 player 时 claim/firstCashout 返回明确业务错误 + status: completed + - id: verify-reg-cutoff + content: 联调:regTime 早于门槛 status=-1 且 EventBus 不建池;晚于门槛走 win/首充完整流程 + status: completed +isProject: false +--- + +# Free Credits 老用户隔离:注册时间门槛(计划) + +## 目标 + +C 端全量更新后,**注册时间早于活动上线节点的用户视为老用户,不参加、不展示**;**注册时间晚于该节点的新用户**,在现有「EventBus 写库 = 参加」模型下正常走活动。 + +「默认参加」含义:**有资格参加**(EventBus 允许建池、status 可读),**不是**无需 EventBus 自动插入 `free_credits_player`。 + +--- + +## 分层架构(保持不变) + +```mermaid +flowchart TB + gate{regTime > FREE_CREDITS_ENROLL_REG_AFTER?} + gate -->|否| block[不参加: status=-1 EventBus return] + gate -->|是| eligible[有资格] + eligible --> win[EventBus syncHomeWithdrawUnlocked] + eligible --> init[EventBus free_credit_init] + eligible --> api[status 只读 player 行] + win --> pool[(free_credits_player)] + init --> pool + api --> pool +``` + +| 路径 | 行为 | +| --- | --- | +| **读** `status()` | 未达注册门槛 → `status=-1`;达门槛且无 player 行 → `-1`;有行 → `buildStatus` | +| **写** EventBus | 未达注册门槛 → 各 Logic 方法开头直接 return,不建池、不定格、不推进 | + +--- + +## 实现要点 + +### 1. 环境变量 + +在 [`.env`](slot_console/.env) / 部署说明中增加(示例): + +```env +# 用户注册时间(Unix 或 Y-m-d H:i:s,建议与 TIMEZONE 一致)晚于此值才可参加 Free Credits +FREE_CREDITS_ENROLL_REG_AFTER=2026-05-20 00:00:00 +``` + +- 在 [`config/app.php`](slot_console/config/app.php) 或新建 `config/free_credits.php` 读取:`getenv('FREE_CREDITS_ENROLL_REG_AFTER')`,`strtotime` 解析为 `enroll_reg_after_ts`(启动时或首次调用缓存)。 +- **未配置时的默认策略(已确认)**:`FREE_CREDITS_ENROLL_REG_AFTER` 为空或未配置 → **全员不参加**(`isEligibleByRegTime` 恒 false,`status=-1`,EventBus 不写池)。 + +### 2. 统一门禁方法(建议放在 [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php)) + +```php +/** + * 是否具备 Free Credits 参与资格(注册时间晚于环境变量门槛)。 + */ +protected function isEligibleByRegTime(int $uid): bool +{ + $cutoff = self::enrollRegAfterTimestamp(); // 0 表示未配置 + if ($cutoff <= 0) { + return false; + } + + $userInfo = \app\service\user\UserService::getUserInfoEntity($uid); + if ($userInfo === null || $userInfo->create_at === '') { + return false; + } + + $regTime = strtotime($userInfo->create_at); + return $regTime !== false && $regTime > $cutoff; // 严格「晚于」 +} +``` + +- **数据来源**:[`app\service\user\UserService::getUserInfoEntity`](slot_console/app/service/user/UserService.php)(调 user 服 `/innerapi/user/info`),与 [`EventBus::registerEvent`](slot_console/app/command/EventBus.php) 等同用法。 +- **注册时间字段**:[`app\entity\UserInfoEntity::$create_at`](slot_console/app/entity/UserInfoEntity.php)(`Y-m-d H:i:s`),比较前 `strtotime` 为 Unix 秒。 +- **不用** `UserTagService::tagInfo->regTime`(标签缓存,与账号创建时间可能不一致)。 +- 备选:若 Logic 内已大量使用 slotLib,可用 `\slotLib\services\UserService::getInstance()->setUid($uid)->getUserInfo()?->regTime`(构造函数内由 `create_at` 解析),但 console 侧优先统一 `app\service\user\UserService`。 + +### 3. 调用点(读写对称) + +| 方法 | 未达门槛时 | +| --- | --- | +| `status()` | 直接 `return ['status' => -1]` | +| `syncHomeWithdrawUnlocked()` | `return null` | +| `handleFreeCreditInit()` | `return` | +| `advanceAfterRecharge()` | `return` | +| `claim()` / `firstCashout()` | `throw BusinessException('...')` 或统一文案 | + +**说明**:已达门槛、但库中无 player 行 → 仍为 `status=-1`(尚未被 EventBus 纳入);达门槛且 win 后 → EventBus 建池 → status 非 `-1`。 + +### 4. 与「已首充」的关系 + +- 注册门槛解决:**老账号 / 老注册** 不进入活动。 +- 钱包侧 [`maybeSendFreeCreditInit`](slot_wallet/app/api/logic/WalletLogic.php) 仍仅 **首充** 发定格,二者叠加,互不替代。 + +### 5. 撤销项 + +- 不再单独做 `total_deposit == 0` 的 EventBus 门禁(由注册时间门槛覆盖「老用户」定义)。 +- 不再要求 `frozen_amount > 0` 才展示(保留 HOME_UNLOCKED 阶段)。 + +### 6. 文档与测试 + +- 更新 [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php):`status=-1` = 未参加 / 活动关 / **注册时间早于门槛**。 +- 单测:mock `regTime` 与 env cutoff,覆盖 eligible / ineligible 的 status 与 freeze 是否被调用。 +- 联调矩阵见下。 + +--- + +## 联调矩阵 + +| regTime vs 门槛 | win 达门槛 | 首充定格 | status | EventBus 建池 | +| --- | --- | --- | --- | --- | +| 早于 | - | - | `-1` | 否 | +| 晚于 | 否 | 否 | `-1` | 否 | +| 晚于 | 是 | 否 | `1` | sync 建池 | +| 晚于 | - | 是 | `3/4` + packages | freeze | + +--- + +## 新 C 端约定(不变) + +- **`status === -1`**:隐藏(含老用户、未达注册门槛、未参加)。 +- **`status !== -1`**:已参加,按 `FreeCreditsPlayerModel` 状态渲染。 diff --git a/plans/free_credits_验收核查_c1eade6d.plan.md b/plans/free_credits_验收核查_c1eade6d.plan.md new file mode 100644 index 0000000..9c98332 --- /dev/null +++ b/plans/free_credits_验收核查_c1eade6d.plan.md @@ -0,0 +1,162 @@ +--- +name: Free Credits 验收核查 +overview: 「首充前免费余额定格与分档释放」的后端核心链路(定格、分档、首档提现、后续 Claim、充值事件、钱包账务)已在 slot_console / slot_wallet / slot_pay / slot_lib 落地约 70–80%,但尚未达到需求文档 V1.0 全量验收标准;运营后台、客户端 UI、广播、异常风控、部分 API 字段与自动化测试仍缺失或未对齐。 +todos: + - id: p0-wallet-atomicity + content: 补齐 Claim+Y1 原子性/回滚;确认是否需要 Deposit Lot + status: pending + - id: p0-status-api-fields + content: status/buildStatus 增加 total_recharge、show_home_status_bar、show_activity_entry + status: pending + - id: p0-peak-balance + content: 首页解锁「曾达到门槛」:增加峰值记录或可靠触发点 + status: pending + - id: p1-admin-config-stats + content: slot_admin 增加 type=11 配置页与统计页(对接 innerapi) + status: pending + - id: p1-refund-risk + content: 实现充值退款/拒付暂停未释放档位(文档 20.1) + status: pending + - id: p2-frontend-broadcast + content: 前端仓实现 UI/广播;本仓可增加 broadcast API + status: pending + - id: p2-tests + content: 按文档 22.1/22.2 补充自动化或验收用例 + status: pending +isProject: false +--- + +# 首充前免费余额定格与分档释放 — 实现完成度核查 + +**结论:未实现完。** 后端主流程可联调,但对照 [需求文档](file:///Users/ray/Documents/project/www/slot/docs/requirements/首充前免费余额定格与分档释放需求文档.md) 第 22 节验收标准,仍有若干 **P0 账务/规则缺口** 与大量 **前端/运营/统计** 范围未覆盖。 + +--- + +## 实现分布(已落地) + +```mermaid +flowchart TB + subgraph console [slot_console] + RechargeEvent --> FreeCreditsLogic + EventBus --> FreeCreditsLogic + FreeCreditsLogic --> DB[(free_credits_player/package)] + ApiCtrl[FreeCreditsController] --> FreeCreditsLogic + Lobby[LobbyController.frontData] --> FreeCreditsLogic + Inner[innerapi FreeCreditsController] --> DB + end + subgraph wallet [slot_wallet] + FreeCreditsLogic --> WalletLogic + WalletLogic --> freeze[freeCreditsFreeze] + WalletLogic --> claim[freeCreditsClaim] + claim --> Y1[createTask SOURCE_TYPE_FREE] + end + subgraph pay [slot_pay] + firstCashout[firstCashout API] --> WithdrawalOrder + WithdrawalOrder --> MQ[FreeCreditsFirstCashout*] + MQ --> EventBus + end +``` + +| 模块 | 关键文件 | 覆盖的需求章节 | +|------|----------|----------------| +| 领域与状态机 | [FreeCreditsLogic.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php) | 5.3–5.9 核心规则 | +| 数据表 | [install.sql](file:///Users/ray/Documents/project/www/slot/slot_console/db/install.sql) L36–86 | 活动池 + 档位 | +| 活动配置 | [RechargeGiftConfigModel.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/model/common/RechargeGiftConfigModel.php) `TYPE_FREE_CREDITS=11` | 17 配置(读库) | +| 客户端 API | [FreeCreditsController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/controller/FreeCreditsController.php) | status / claim / firstCashout | +| 大厅聚合 | [LobbyController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/napi/controller/LobbyController.php) `free_credits` | 7 状态条数据源(部分) | +| 充值事件 | [RechargeEvent.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php) | 5.3 首充定格、5.5 累计充值解锁 | +| 首页解锁 | [EventBus.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php) win/bonus 等 → `syncHomeWithdrawUnlocked` | 5.2 | +| 钱包原子能力 | [WalletLogic.php](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) | 15.2–15.4 | +| 首档提现 | [WithdrawalOrderEntity.php](file:///Users/ray/Documents/project/www/slot/slot_pay/app/entity/WithdrawalOrderEntity.php) + [EventWithdrawal.php](file:///Users/ray/Documents/project/www/slot/slot_pay/app/command/EventWithdrawal.php) | 5.6、15.3、20.2 | +| 运营统计 API | [innerapi/FreeCreditsController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/innerapi/controller/FreeCreditsController.php) | 18.1–18.2(简化版) | + +--- + +## 按需求章节对照(22.1 核心规则) + +| 验收项(文档 22.1) | 实现情况 | 说明 | +|---------------------|----------|------| +| 未达 $50 点 Withdraw 提示 | **前端** | 后端 `status` 返回 `win_threshold` / `home_withdraw_unlocked`,无专用提示 API | +| 曾达 $50 后输回仍解锁 | **部分** | 已解锁用户靠 `home_withdraw_unlocked=1` 保持;**未持久化历史峰值**,仅在 MQ 事件时若 `balance >= threshold` 才写库,存在漏解锁边界(例如达峰后无 win/bonus 类事件即输掉) | +| 任意入口首充定格 | **是** | `RechargeEvent` + `handleRecharge`,`frozenAmount = balance_after - wallet_amount` | +| 首充失败不定格 | **是** | 仅充值成功 MQ 触发 | +| 定格后余额扣除、活动展示 | **是** | `freeCreditsFreeze` + player/package 记录 | +| 累计未满 $50 第一档不可提现 | **是** | 首包 `STATUS_LOCKED` 直至 `wallet.r >= recharge_unlock_amount` | +| 累计满 $50 第一档可提现 | **是** | `advanceByRecharge` 解锁 package_no=1 | +| 定格 < $20 时第一档=定格额 | **是** | `min(frozen, first_cash_amount)` | +| 第一档提现成功后关首页状态条 | **后端未显式字段** | 有 `STATUS_FIRST_CASH_DONE`,但 `buildStatus` **未返回** `show_home_status_bar` 等 UI 开关,需前端自行推断 | +| 后续充值只解锁下一档 | **是** | `max_unlock_per_recharge` + `nextLockedReleasePackage` | +| Claim 入 Deposit + Y1 | **部分** | 入 deposit_balance + `createTask(SOURCE_TYPE_FREE)`;**无 Deposit Lot**(文档 15.4、22.2) | +| 全部完成关活动入口 | **部分** | `STATUS_COMPLETED=10`;API 未返回 `show_activity_entry` 布尔字段 | + +--- + +## 按需求章节对照(22.2 账务) + +| 验收项 | 实现情况 | 风险 | +|--------|----------|------| +| 定格有钱包流水 | **是** | `BIZ_TYPE_FREE_CREDITS_FREEZE` | +| 定格前后余额正确 | **是** | `addLog` 记录 | +| 第一档不进普通钱包 | **是** | 独立 `free_credit_first_cashout`,跳过 `withdrawFrozen` | +| Claim 创建 Deposit Lot | **否** | 全仓库无 `DepositLot`/`deposit_lot` 实现 | +| Claim 创建 Y1 | **是** | 但 `createTask` 在钱包事务 **commit 之后**;失败仅打日志,**不满足** 文档 20.3「流水任务失败整体回滚」 | +| 重复回调不重复定格 | **是** | `frozen_amount_qf > 0` 跳过 | +| 重复 Claim 不重复入账 | **部分** | package 状态 + `claim_biz_id` 唯一;钱包层 **未见 biz_id 幂等查重** | + +--- + +## 明确未实现 / 未对齐项 + +### P0(影响验收或资金一致性) + +1. **Deposit Lot**:文档 15.4 / 22.2 要求 Claim 后创建;当前仅 `inc` deposit + Y1 task。 +2. **Claim 与 Y1 原子性**:`freeCreditsClaim` 先 commit 入账,再 `createTask`;与 20.3 冲突。 +3. **充值退款/拒付暂停档位**(文档 20.1):`FreeCreditsLogic` 无 refund/chargeback 处理。 +4. **status API 缺累计充值进度**:定格后 UI 需要 `$9.99 / $50`(文档 6.3、7.3),`buildStatus` 未返回 `total_recharge`(内部用 `wallet.r`,未透出)。 +5. **「曾经达到 $50」**:无 `peak_balance` 字段;依赖事件驱动 `syncHomeWithdrawUnlocked`,与 5.2 字面规则不完全一致。 + +### P1(运营与配置) + +6. **后台配置模块**(文档 17):`backend/slot_admin` / `slot_admin_vue` **无** `TYPE_FREE_CREDITS=11` 的 CRUD 页面;仅能手工维护 `s_recharge_gift_config.ext_config`。 +7. **后台统计页**(文档 18):仅有 [innerapi](file:///Users/ray/Documents/project/www/slot/slot_console/app/innerapi/controller/FreeCreditsController.php) 简化接口;列表缺 **渠道、时间范围、第一档状态、排序项、累计充值、完成进度** 等字段/筛选项。 +8. **可配置 Y1 倍数**(文档 17 `后续档打码倍数`):代码写死 `createTask(0, fee, SOURCE_TYPE_FREE)`,未读 `ext_config`。 +9. **状态机 `STATUS_FROZEN=2`**:模型定义了但 Logic **从未赋值**(定格后直接 `DEPOSIT_PENDING` / `FIRST_CASH_READY`)。 + +### P2(产品体验 / 文档其他章节) + +10. **客户端 UI**(文档 6–14):`slot_pwa` 等仓库 **零引用** `free_credits`;首页状态条、Free Play to Go、独立提现页、广播模块均未实现(原开发计划也标明前端另仓)。 +11. **广播模块**(文档 13):无最近 10 条提现/领取记录 API。 +12. **注册赠送改 $30**(文档 19):与本活动解耦,**未在本需求代码变更中验证**是否已改配置/落地页。 +13. **自动化测试**:无 FreeCredits 相关单测/集成测试。 + +--- + +## 与现有开发计划的一致性 + +[free_credits_release 计划](file:///Users/ray/.cursor/plans/free_credits_release_902566c9.plan.md) 中 `implement-console-core` 仍为 **pending**,与代码现状(Logic 已较完整)不一致;`test-acceptance` 标 completed 但仓库内 **无测试文件**,建议以文档 22 节手工/自动化用例重新验收。 + +--- + +## 建议验收顺序(若需补齐) + +1. **手工走通 P0 账务用例**:首充定格幂等、满 $50 解锁、首档提现回调、顺序 Claim、重复 Claim/回调。 +2. **补齐 status 字段**:`total_recharge`、`show_home_status_bar`、`show_activity_entry`、`current_balance`。 +3. **对齐钱包**:Deposit Lot(若钱包规范要求)、Claim+Y1 同事务或补偿回滚。 +4. **运营**:slot_admin 增加 type=11 配置页 + 统计页对接 innerapi。 +5. **前端仓**:按文档 6–14 接 API(本 monorepo 外)。 +6. **补测试**:覆盖 22.1 / 22.2 表格各行。 + +--- + +## 总览评分(仅供沟通) + +| 范围 | 完成度(估) | +|------|-------------| +| 后端核心状态机 + 充值/提现/Claim 链路 | ~75–85% | +| 钱包账务与文档完全一致 | ~60% | +| 客户端 API 字段/UI 支撑 | ~50% | +| 运营后台配置与统计 | ~25% | +| 前端 UI / 文案 / 广播 | ~0%(本仓库) | +| 自动化测试 | ~0% | + +**综合:需求文档 V1.0 不能判定为「已全部实现」;可判定为「后端 MVP 已具备,待补齐账务细节、运营与前端后全量验收」。** diff --git a/plans/is_end派奖结算改造_14ce4dc5.plan.md b/plans/is_end派奖结算改造_14ce4dc5.plan.md new file mode 100644 index 0000000..f2b3c7b --- /dev/null +++ b/plans/is_end派奖结算改造_14ce4dc5.plan.md @@ -0,0 +1,78 @@ +--- +name: is_end派奖结算改造 +overview: 基于现有 BetFunding/WinService 实现,新增 is_end 分支:中间派奖仅累计不入账,结束派奖统一按 Round Final Settlement 入账;并对中间派奖增加 biz_id 严格幂等。 +todos: + - id: dto-validator-is-end + content: 扩展 DTO 与校验器,支持 is_end 并强制 win 场景 round_id 校验 + status: pending + - id: redis-win-keys + content: 新增 pending/dedupe Redis key 生成方法与 TTL 约定 + status: pending + - id: logic-win-branching + content: 在 WalletLogic::win 中实现 is_end=0 累计与 is_end=1 最终结算分支 + status: pending + - id: idempotency-regression + content: 补充关键回归场景说明并验证与现有幂等不冲突 + status: pending +isProject: false +--- + +# is_end 驱动的派奖结算改造计划 + +## 目标 +让 `win` 接口支持第三方 `is_end` 语义: +- `is_end=0`:仅记录/累计本局派奖,不做钱包入账、不做 Lot 终态收敛 +- `is_end=1`:将本次派奖 + 已累计派奖合并后,执行一次 Final Settlement(现有 `WinService::execute`) + +## 现状结论(基于代码) +- 入口路由在 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `type=win -> win()`。 +- Final Settlement 主流程在 [app/service/wallet/WinService.php](app/service/wallet/WinService.php)。 +- 当前 `win` 默认即 Final Settlement,不区分中间派奖事件。 +- 下注资金事实表在 [app/model/multi/WalletBetFundingModel.php](app/model/multi/WalletBetFundingModel.php)。 + +## 实现方案(按你选择) +### 1) 入参扩展与校验 +- 在 [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加字段:`is_end`(默认 `1`,仅允许 `0|1`)。 +- 在 [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php): + - 增加 `is_end` 校验规则(整数且取值 `0/1`)。 + - 将 `win` 也纳入 `round_id` 必传校验(当前仅 bet 必传)。 + +### 2) Redis Key 设计 +- 在 [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) 新增两个 key 生成器: + - `wallet:win:pending:{uid}:{currency}:{round_id}`:累计未结算派奖金额 + - `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}`:中间派奖幂等标记 +- 过期策略建议:`pending` 48h,`dedupe` 72h(与重放窗口对齐)。 + +### 3) win 主流程分支 +- 修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`: + - `is_end=0`: + - 命中 dedupe key 则直接返回当前钱包(幂等) + - 未命中则将 `fee` 累加到 pending key,写 dedupe key,返回当前钱包(不写 `BIZ_TYPE_WIN` 流水) + - `is_end=1`: + - 读取 pending 累计并与本次 `fee` 合并为 `finalWinAmount` + - 用 `finalWinAmount` 调用现有 `WinService::execute` + - 事务提交后清理 pending key(失败不清) + +### 4) 幂等与一致性 +- `is_end=1` 继续沿用现有 `wallet_log(uid,biz_id,biz_type=win)` 幂等。 +- `is_end=0` 使用 Redis dedupe key 防重,避免重复累计。 +- 若 `is_end=1` 无待分配 funding,保持当前行为(报错),便于暴露上游时序问题。 + +### 5) 兼容与回归 +- 默认 `is_end=1`,兼容未传该字段的旧调用。 +- 回归场景: + - 单次结算(只发 `is_end=1`) + - 多次派奖(多条 `is_end=0` + 一条 `is_end=1`) + - `is_end=0` 重放同 biz_id 不重复累计 + - `is_end=1` 重放同 biz_id 不重复入账 + - `is_end=1` 失败后 pending 不丢失 + +## 关键改动文件 +- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) +- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php) +- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) +- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) + +## 风险与边界 +- 该方案不新增 `wallet_game_round` / `wallet_game_round_event` 持久化表;中间派奖仅 Redis 暂存,审计粒度弱于 DB 事件流。 +- 若某局长期无 `is_end=1`,pending 依赖 TTL 过期清理。后续可升级为 DB 事件表方案。 \ No newline at end of file diff --git a/plans/is_end结算回退方案_180cfb46.plan.md b/plans/is_end结算回退方案_180cfb46.plan.md new file mode 100644 index 0000000..e0b5f72 --- /dev/null +++ b/plans/is_end结算回退方案_180cfb46.plan.md @@ -0,0 +1,57 @@ +--- +name: is_end结算回退方案 +overview: 取消“中间派奖实时余额可见化”调整,回退到严格 Final Settlement 方案:is_end=0 仅累计,is_end=1 才入账与状态收敛。 +todos: + - id: dto-validator-is-end + content: 增加 is_end 字段及 win round_id 强校验 + status: completed + - id: pending-dedupe-keys + content: 新增 pending 与 dedupe Redis key 并接入 is_end=0 累计 + status: completed + - id: win-final-merge + content: is_end=1 合并 pending 后调用现有 WinService 结算并清理缓存 + status: completed + - id: regression-check + content: 回归验证多次派奖累计、最终结算与幂等行为 + status: completed +isProject: false +--- + +# is_end 结算回退方案(不做中间余额更新) + +## 变更决策 +取消这部分: +- 多次派奖期间(`is_end=0`)对第三方返回余额进行实时更新 +- 对 `wallet` 查询叠加 pending 金额 + +保留并执行: +- `is_end=0`:仅做中间派奖累计,不改钱包余额、不做 Lot 收敛 +- `is_end=1`:合并累计派奖后一次 Final Settlement(调用现有 win 主流程) + +## 实现边界 +- 严格遵循 [doc/win.md](doc/win.md) 的“Final Settlement 才做真实入账”原则 +- 不引入展示层余额叠加逻辑,避免展示余额与真实可下注余额不一致 + +## 具体改造 +1. DTO / 校验 +- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加 `is_end`(默认 `1`) +- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php) + - 增加 `is_end` 仅允许 `0/1` + - `win` 场景强制 `round_id` 必传 + +2. Redis 累计与幂等(仅中间派奖) +- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) 增加 key: + - `wallet:win:pending:{uid}:{currency}:{round_id}` + - `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}` +- `is_end=0` 命中 dedupe 则忽略,未命中则累计 pending + +3. win 结算分支 +- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) + - `is_end=0`:只累计并返回当前真实钱包余额 + - `is_end=1`:读取 pending 并与本次 `fee` 合并后走现有 `WinService::execute`,成功后清理 pending + +## 验收 +- 多条 `is_end=0` 不触发真实入账 +- 一条 `is_end=1` 触发一次最终结算,金额=中间累计+末笔 +- 中间派奖重复 `biz_id` 不重复累计 +- Final 失败时 pending 不丢失,可重试 \ No newline at end of file diff --git a/plans/phpdoc_cursor_规则_1ee55114.plan.md b/plans/phpdoc_cursor_规则_1ee55114.plan.md new file mode 100644 index 0000000..7916290 --- /dev/null +++ b/plans/phpdoc_cursor_规则_1ee55114.plan.md @@ -0,0 +1,141 @@ +--- +name: PHPDoc Cursor 规则 +overview: 在 Cursor 全局规则中新增 PHP PHPDoc 严格规范(类、方法、常量全覆盖),与现有 backend-layering 规则并列,并明确适用范围与模板,避免与「不写显而易见注释」的原则冲突。 +todos: + - id: create-php-doc-mdc + content: "新建 /Users/ray/.cursor/rules/php-doc.mdc(globs: **/*.php,严格 PHPDoc 正文 + 示例)" + status: completed + - id: verify-rule-active + content: 在 Cursor 中打开任意 .php 文件,确认规则被注入;用 WalletLogic 缺注释方法做一次试写验证 + status: completed + - id: optional-readme-sync + content: (可选)将 PHPDoc §3.4 同步到 slot_wallet 等 README,与 Cursor 规则保持一致 + status: completed +isProject: false +--- + +# 新增 PHPDoc 严格 Cursor 规则 + +## 结论:要加,但不要写成「所有符号一刀切」的空话 + +你选择了 **严格全覆盖**。建议在 Cursor 里加一条 **独立规则文件**,与现有的 [`backend-layering.mdc`](/Users/ray/.cursor/rules/backend-layering.mdc) 并列,而不是塞进 layering 里(职责不同:一个管分层,一个管文档)。 + +**不建议**写成模糊的「所有都要 PHPDoc」——应写清 **哪些符号、最少哪些 tag、何时可写短句**。否则 Agent 会在 trivial 代码上堆 `@param int $uid uid` 这类无意义注释,与 [`slot_agent/README.md`](/Users/ray/Documents/project/www/slot/slot_agent/README.md) 第 9.3 节「不解释显而易见语句」打架。 + +## 现状 + +| 来源 | PHPDoc 要求 | +|------|-------------| +| [`.cursor/rules/`](/Users/ray/.cursor/rules/) | 仅有 layering + dev-environment,**无 PHPDoc** | +| [`slot_agent/README.md`](/Users/ray/Documents/project/www/slot/slot_agent/README.md) §6.2 | 已要求:业务类说明、公开方法说明、参数/返回值说明 | +| 实际代码(如 [`WalletLogic.php`](/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php)) | **不一致**:`run`/`register` 有块注释,`initWalletWithoutMoney`、`getBalance` 无 | + +终端里曾出现给 `slot_wallet/README.md` 增加 §3.4 PHPDoc 的 diff,但当前 README **尚未落地**该节——规范应优先进 **Cursor rule**(Agent 每次都会读),README 可作为人类文档二次同步(可选)。 + +## 推荐规则文件 + +**路径**:[`/Users/ray/.cursor/rules/php-doc.mdc`](/Users/ray/.cursor/rules/php-doc.mdc) + +**Frontmatter 建议**: + +```yaml +--- +description: PHP PHPDoc requirements for all classes, methods, and constants +globs: "**/*.php" +alwaysApply: false +--- +``` + +- 用 `globs: **/*.php`:编辑 PHP 时自动注入,不污染 Vue/TS 会话 +- `alwaysApply: false`:与 layering 的 `true` 区分,减少非 PHP 任务 token + +## 规则正文(严格版,建议写入 mdc) + +### 1. 适用范围 + +- **新增或修改**的 PHP 文件中的:**class / interface / trait / enum**、**所有方法**(`public` / `protected` / `private`)、**所有类常量**(`const`) +- 适用目录:`slot_*`、`backend/**` 下 PHP 业务代码 +- **不追溯**改历史未动代码;但 **本次 diff 触及的符号** 若缺 PHPDoc,须一并补齐 + +### 2. 最低 PHPDoc 内容 + +| 符号 | 必须包含 | +|------|----------| +| 类 / 接口 / Trait | 一行职责说明;复杂类可加 `@package`(可选) | +| 方法 | 职责说明 + 每个参数的 `@param` + `@return`;有 `throw` 的须 `@throws` | +| 类常量 | 一行说明业务含义(单位、枚举语义、与配置/表字段对应关系) | +| 属性(若新增) | `@var` 或 typed property + 一行说明(仅当类型/语义不直观时) | + +已有 **PHP 8+ 标量/对象类型声明** 时,`@param`/`@return` 仍要保留(与你选的 strict 一致),但 **描述句可短**,禁止空块或只复制类型名。 + +### 3. 禁止项(与 slot_agent 注释原则对齐) + +- 禁止无 `@param` / `@return` 的空 `/** */` +- 禁止 `@param int $id id` 式同义反复;语义、单位、边界写进描述 +- 禁止用 PHPDoc 替代 Validate / Logic 里的业务校验说明 + +### 4. 分层补充(与 layering 一致) + +对以下层 **额外** 要求写清业务语义(不仅是类型): + +- **Controller**:接口用途、幂等/鉴权前提(若有) +- **Logic**:用例步骤、事务边界、失败时行为 +- **Service**:复用场景、调用方约束 +- **Model**:查询条件、分表键、金额字段单位 +- **DTO / Validate**:字段含义、与上游参数映射 + +### 5. 示例模板(写入规则供 Agent 照抄) + +```php +/** + * 首充前冻结免费余额。 + * + * @return WalletEntity|null 成功返回钱包实体;无需冻结时返回 null + * @throws WalletException 余额不足或钱包不存在 + */ +public function freeCreditsFreeze(): ?WalletEntity +``` + +```php +/** 释放档位:单位分,对应配置 free_credits.release_tiers */ +public const RELEASE_TIER_MIN = 100; +``` + +## 与工具链的关系(可选,本期可不做的) + +Cursor rule **不能**在 CI 里自动 fail。若以后要机器 enforce,再单独加: + +- PHPStan + `phpstan/phpdoc-parser` 或 +- PHPCS `Squiz.Commenting` / 自定义 sniff + +本期仅 Cursor 规则即可满足「Agent 写码时遵守」。 + +## 实施步骤 + +1. 新建 [`php-doc.mdc`](/Users/ray/.cursor/rules/php-doc.mdc),按上文写入 frontmatter + 正文 +2. 在 Cursor Settings → Rules 确认该规则对 PHP 文件生效(`globs` 匹配) +3. (可选)把相同 §3.4 同步进 [`slot_wallet/README.md`](/Users/ray/Documents/project/www/slot/slot_wallet/README.md) 与其它服务 README,供人工 review 对照 +4. 用一次小改动验证:例如在 `WalletLogic` 给无注释的 `getBalance` 补 PHPDoc,看 Agent 是否自动遵循 + +## 风险与预期 + +- **Diff 变大**:strict 下每个新方法多 5–15 行注释,属预期成本 +- **历史债**:全库补 doc 工作量巨大;规则应写明 **仅 touch 到的符号**,避免 Agent 一次性重构整文件 +- **类型重复**:strict 仍保留 `@param`/`@return` 类型,利于 IDE/静态分析;描述聚焦「为什么/单位/边界」 + +```mermaid +flowchart LR + subgraph rules [Cursor Rules] + layering[backend-layering.mdc] + phpdoc[php-doc.mdc] + devenv[dev-environment.mdc] + end + subgraph code [PHP 改动] + edit[编辑 PHP 文件] + agent[Agent 生成/修改代码] + end + edit --> phpdoc + edit --> layering + agent --> phpdoc + agent --> layering +``` diff --git a/plans/phpunit_钱包链路测试_003ec505.plan.md b/plans/phpunit_钱包链路测试_003ec505.plan.md new file mode 100644 index 0000000..9ea4404 --- /dev/null +++ b/plans/phpunit_钱包链路测试_003ec505.plan.md @@ -0,0 +1,84 @@ +--- +name: PHPUnit 钱包链路测试 +overview: 补齐本仓库的 PHPUnit 基建,并新增 register/bet/win 集成测试用例,默认在本地 php82 容器执行、连接真实依赖,以 HTTP 结果与余额变化作为核心断言。 +todos: + - id: setup-phpunit + content: 补齐 PHPUnit 基建(composer require-dev、phpunit.xml、tests/bootstrap.php) + status: completed + - id: implement-feature-test + content: 实现 register/bet/win 集成测试与幂等/异常断言 + status: completed + - id: run-in-container + content: 在 php82 容器执行测试并根据结果修正 + status: completed + - id: document-runbook + content: 补充测试执行说明与可选增强项 + status: completed +isProject: false +--- + +# PHPUnit Register/Bet/Win 测试落地计划 + +## 默认口径(基于当前信息) +- 测试框架:PHPUnit(当前仓库尚无 `phpunit.xml` 和 `tests/`)。 +- 执行环境:本地 `php82` 容器。 +- 依赖模式:默认连本地真实 MySQL/Redis(贴近联调)。 +- 断言范围:默认先做 API 返回与余额变化断言;DB 深断言作为第二阶段可选扩展。 + +## 现状结论 +- 缺少 PHPUnit 基建:未发现 `phpunit*.xml` 与 `tests/` 目录。 +- `register/bet/win` 入口与逻辑已具备: + - 接口入口:[app/api/controller/WalletController.php](app/api/controller/WalletController.php) + - 业务编排:[app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) + - 注册服务:[app/service/wallet/RegisterService.php](app/service/wallet/RegisterService.php) + +## 实施步骤 +1. **补测试基建** +- 在 `require-dev` 增加 `phpunit/phpunit`。 +- 新增 `phpunit.xml`(定义 `tests/` 目录、bootstrap、环境变量覆盖)。 +- 新增 `tests/bootstrap.php`(加载自动加载与测试环境初始化)。 + +2. **新增钱包链路集成测试** +- 新建测试文件:[tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php) +- 用例覆盖: + - `register` 成功(通过 `wallet/update` + `type=register`) + - `bet` 成功并校验扣款后余额变化 + - `win is_end=0` 中间派奖仅影响待结算展示 + - `win is_end=1` 最终结算落账 + - `bet` 重复 `biz_id` 幂等 + - `win` 重复 `biz_id` 幂等 + - `bet/win` 缺 `round_id` 参数错误 + - `win` 非法 `is_end` 参数错误 + +3. **测试数据与可重复执行设计** +- 每个测试生成独立 `uid/round_id/biz_id/trace_id`(时间戳+随机后缀),避免脏数据冲突。 +- 用例内封装统一请求方法(POST JSON)与响应断言助手,降低重复代码。 +- 优先按顺序单线程执行此特性测试,避免并发串扰。 + +4. **执行与验证** +- 在容器内执行:`docker compose exec -T php82 php vendor/bin/phpunit --filter WalletRegisterBetWinTest` +- 验证输出: + - 所有用例通过 + - 幂等场景无重复记账(通过返回余额不重复变化来断言) + +5. **第二阶段可选增强(不在首版强制)** +- 增加 DB 断言(`wallet_log`、`wallet_fund_lot`、`wallet_bet_funding`)与 Redis pending key 清理断言。 +- 将该测试纳入 CI job(需环境可用性与测试隔离策略先达成一致)。 + +## 目标结构图 +```mermaid +flowchart TD + testBootstrap[phpunitBootstrap] --> featureTest[WalletRegisterBetWinTest] + featureTest --> registerStep[register_update] + registerStep --> betStep[bet] + betStep --> winMidStep[win_is_end_0] + winMidStep --> winFinalStep[win_is_end_1] + winFinalStep --> idemCheck[idempotencyChecks] + idemCheck --> negativeCheck[invalidParamsChecks] +``` + +## 受影响文件(计划新增/修改) +- [composer.json](composer.json) +- [phpunit.xml](phpunit.xml) +- [tests/bootstrap.php](tests/bootstrap.php) +- [tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php) \ No newline at end of file diff --git a/plans/phpunit_集成测试落地_3c120775.plan.md b/plans/phpunit_集成测试落地_3c120775.plan.md new file mode 100644 index 0000000..0824bce --- /dev/null +++ b/plans/phpunit_集成测试落地_3c120775.plan.md @@ -0,0 +1,11 @@ +--- +name: PHPUnit 集成测试落地 +overview: 补齐本仓库 PHPUnit 基建,并新增 register/bet/win 集成测试(连接真实 MySQL/Redis)与关键 DB 断言,支持在本地 php82 容器执行。 +todos: [] +isProject: false +--- + +# Register/Bet/Win PHPUnit 实施计划 + +## 目标 +- 新增可 \ No newline at end of file diff --git a/plans/php用例写法规则_01c4ae1f.plan.md b/plans/php用例写法规则_01c4ae1f.plan.md new file mode 100644 index 0000000..8cd7a94 --- /dev/null +++ b/plans/php用例写法规则_01c4ae1f.plan.md @@ -0,0 +1,227 @@ +--- +name: PHP用例写法规则 +overview: 在用户级 Cursor 新增 `php-use-case-style.mdc`,约束 slot 后端所有 PHP 业务代码(Controller/Logic/Service/Model/Command 等)的表达力与可维护性;并扩展 verify-slot-backend.sh 对 diff 中所有 touched 的 app/**/*.php 做轻量检查。仅约束本次 diff,不强制全盘重构历史代码。 +todos: + - id: create-mdc + content: 创建 ~/.cursor/rules/php-use-case-style.mdc(全层 MUST/禁止、BAD/GOOD、按层对照表) + status: completed + - id: update-gate-rule + content: 在 agent-completion-gate.mdc 增加对 php-use-case-style 的引用(任意 PHP 改动) + status: completed + - id: extend-verify + content: 扩展 verify-slot-backend.sh:所有 app/**/*.php diff 检查 + @use-case-exempt + status: completed + - id: smoke-verify + content: 分别在 Logic/Controller/Service diff 触发自测 PASS/FAIL/豁免 + status: completed + - id: merge-php-code + content: 用例写法合并进 php-code.mdc,修 frontmatter/强制要求/命名 + status: completed + - id: fix-gate-refs + content: agent-completion-gate 引用改为 php-code + status: completed + - id: smoke-rules + content: 确认规则生效 + verify PASS + status: completed +isProject: false +--- + +# PHP 代码写法规则 + Verify 门禁(全层) + +## 目标 + +让 AI 在改 **任意 slot 后端 PHP 业务代码** 时默认产出:**编排清晰、职责单一、业务异常语义正确、状态变更有条件更新与幂等键**,并对齐仓库内参照实现。 + +适用范围(规则 + verify 一致): + +- `app/api/controller/**` +- `app/api/logic/**`、`app/innerapi/logic/**`、`app/napi/logic/**` +- `app/service/**` +- `app/model/**` +- `app/command/**` +- 其它 `app/**` 下业务 PHP(`validate`、`entity` 等按条适用) + +**不覆盖**:`vendor/`、`tests/`(除非 diff 触及且单独约定)、纯配置/SQL 文件。 + +## 默认范围 + +- **规则文件**:用户级 [`~/.cursor/rules/php-use-case-style.mdc`](~/.cursor/rules/php-use-case-style.mdc) +- **生效方式**:`alwaysApply: true`(任意对话均注入;与 `backend-layering` 同级,避免只开 Logic 文件时漏规则) +- **门禁脚本**:[`~/.cursor/hooks/verify-slot-backend.sh`](~/.cursor/hooks/verify-slot-backend.sh)(仅 `git diff` + 路径 `app/`) +- **不在本阶段**:强制重构历史代码(如 `DailyRebateLogic::claim`);可另开样板 PR + +## 1. 新增规则 `php-use-case-style.mdc` + +**Frontmatter**: + +```yaml +--- +description: Slot PHP 写法——表达力、可维护、全层通用(Controller/Logic/Service/Model/Command) +alwaysApply: true +--- +``` + +### 1.1 全层通用 MUST + +| 条目 | 要求 | +|------|------| +| 单一职责 | 一个 public 方法只做一件事;编排方法目标 ≤25 行,超出拆 private | +| if 墙 | 禁止同一方法内连续 ≥4 个 `if (...) throw/return error`;合并为 `assert*` / `ensure*` | +| 业务异常 | 可预期业务失败用 `support\exception\BusinessException`;禁止用 `RuntimeException` 表示业务态(未开启、不可领、已过期等) | +| 魔法值 | 状态/类型用 Model 常量或 enum;禁止裸 `1/2/3` 散落(diff 新增代码) | +| 命名 | 方法名表达意图(`assertClaimable`、`markClaimedIfClaimable`),禁止 `doClaim`、`handle` 等空泛名(新增代码) | +| 参照 | 领取/入账编排:[`FreeCreditsLogic::claim`](slot_console/app/api/logic/FreeCreditsLogic.php);Controller:[`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) | + +### 1.2 按层补充(与 backend-layering 互补,不重复分层表) + +| 分层 | 写法 MUST | 禁止 | +|------|-----------|------| +| **Controller** | 仅:Validate → 调 Logic/Service → `success/errorCode`;`catch BusinessException` 映射业务码 | 业务 if 墙、直接 `Db::`、直接 `gift()` | +| **Logic** | 用例编排:`buildContext` → `assert*` → `perform*` → `format*`;事务边界在此 | 纯转发 Model 无编排、跨服务裸 curl | +| **Service** | 可复用能力封装;外部系统(wallet/sdk)隔离;稳定 `biz_id` | Logic 型 Service(单用例整条流水线) | +| **Model** | 查询/写入 + `scope`/`mark*If*` 条件更新;金额字段注释单位(厘) | 业务编排、调 Wallet | +| **Command** | 薄入口:参数解析 → 调 Logic;日志 phase 结构化 | 复制 Logic 大段 if 墙 | +| **Validate** | 格式/必填/枚举 | 业务规则(「是否可领」应在 Logic assert) | + +### 1.3 领取 / 入账 / 状态变更(凡涉及处均适用) + +- MUST:条件更新(`where status = expected` 再 `update`),鼓励 Model 方法 `mark*If*` +- MUST:钱包/外部入账带稳定 `biz_id`(如 `daily_rebate:{uid}:{statDate}`) +- Controller:禁止 `catch (\RuntimeException)` 后一律 `PARAMS_ERROR` + +### 1.4 BAD / GOOD(规则内各一段,≤8 行) + +- BAD:Controller `catch RuntimeException` + Logic 5 连 throw + Service 仅 `return Model::find()` +- GOOD:Controller 捕 `BusinessException`;Logic `assert*` + 条件更新;Service 封装 wallet + biz_id + +### 1.5 与现有规则关系 + +- 分层职责:仍服从 [`backend-layering.mdc`](~/.cursor/rules/backend-layering.mdc) +- 文档:仍服从 [`php-doc.mdc`](~/.cursor/rules/php-doc.mdc) +- [`agent-completion-gate.mdc`](~/.cursor/rules/agent-completion-gate.mdc) 增加:**任意改动 `app/**/*.php` 须遵守 `php-use-case-style`** + +## 2. 扩展 `verify-slot-backend.sh` + +对 **`CHANGED_PHP` 且路径匹配 `*/app/*`** 的文件检查(不再限定 `/logic/`): + +```bash +# 1) diff 新增行含 throw new \RuntimeException(业务态误用) +# 豁免:文件含 @use-case-exempt + +# 2) Controller(*/app/**/controller/*)diff 新增行: +# catch (\RuntimeException 且同文件/邻近行 PARAMS_ERROR → fail + +# 3) 已有:BaseController、banlist、deleted class — 保持不变 +``` + +**原则**: + +- 只 FAIL **diff 新增行**(`git diff -U0` / `git diff --cached -U0`),不扫历史行 +- `@use-case-exempt` 在文件前 5 行内则跳过该文件全部 use-case 检查 +- 首期不 FAIL:方法行数、biz_id 参数名(写在规则 MUST,verify 二期) + +## 3. 数据流 + +```mermaid +flowchart TB + subgraph agent [Agent 改任意 app PHP] + rule[php-use-case-style alwaysApply] + layers[Controller Logic Service Model Command] + end + subgraph gate [完成前] + verify[verify-slot-backend app/** diff] + hook[stop hook followup] + end + rule --> layers + layers --> verify + verify -->|FAIL| hook + verify -->|PASS| done[可声称完成] +``` + +## 4. 验收 + +1. 打开任意 `app/service/*.php` 或 `DailyRebateController.php`,规则均应生效(alwaysApply) +2. 在 Controller diff 新增 `catch (\RuntimeException` + `PARAMS_ERROR` → verify FAIL +3. 在 Service diff 新增 `throw new \RuntimeException('活动未开')` → verify FAIL +4. 加 `@use-case-exempt` → 该文件跳过 +5. `~/.cursor/hooks/verify-slot-backend.sh` 输出 PASS + +## 5. 后续可选(本计划不含) + +- 样板 PR:重构 `DailyRebateLogic::claim` + `DailyRebateController::claim` +- verify 二期:`gift(` / wallet 调用邻近 biz_id 检测;public 方法行数 WARN + +--- + +## 6. 阶段二:合并进 `php-code.mdc`(待执行) + +用户已将 PHPDoc 并入 [`php-code.mdc`](~/.cursor/rules/php-code.mdc);`php-use-case-style.mdc` 已不存在。verify 已具备 use-case 检查,但规则与 gate 引用断裂。本阶段只做规则对齐,不改业务代码。 + +### 6.1 修改 `php-code.mdc` + +**Frontmatter**(删空 `globs:`): + +```yaml +--- +description: PHP 全局工程规范(PHPDoc + 用例写法 + AI 纪律) +alwaysApply: true +--- +``` + +**在「代码原则」后插入新章节「用例写法(app/**)」**(约 35 行): + +- MUST:业务失败 `support\exception\BusinessException`;禁止 `RuntimeException` 表业务态 +- MUST:编排 public 方法 ≤25 行;同一方法禁止连续 ≥4 个 `if (...) throw` → 抽 `assert*` +- MUST:状态变更条件更新;钱包入账稳定 `biz_id`(`daily_rebate:{uid}:{date}`) +- 按层表(与 backend-layering 互补):Controller 禁止业务 if 墙 / 直接 gift;Logic 编排结构;禁止 Logic 型 Service +- 参照:`slot_console/app/api/logic/FreeCreditsLogic.php` :: `claim()`;`FreeCreditsController.php` +- BAD/GOOD 各一段(RuntimeException vs BusinessException;Controller catch) +- 豁免:文件前 5 行 `@use-case-exempt`(与 verify 一致) + +**改写「强制要求」**(L84–92)为 diff 范围: + +```markdown +## 强制要求(本次 diff 新增/修改须符合) + +- PHP 8+;新增/修改的方法须有 typed parameter 与 return +- 新增类属性须 typed property +- 新建文件或本次 diff 触及的文件顶部可加 `declare(strict_types=1);`,禁止为达标改无关历史文件 +``` + +**微调「命名规范」**: + +- 禁止 `$tmp`、`$a`、`$b`、无业务含义的 `$data`/`$list` +- **删除** blanket 禁止 `$info`(与 `UserInfoEntity` 等冲突);改为禁止「无上下文的 `$info` 临时变量」 + +**文末增加「与 verify 对齐」**: + +- diff 新增 `throw new RuntimeException`(业务态)→ verify FAIL +- Controller diff 新增 `catch RuntimeException` + `PARAMS_ERROR` → verify FAIL + +### 6.2 修改 `agent-completion-gate.mdc` + +```diff +- 3. 修改 PHP 后,最终回复含 `PHPDoc: checked`(见 php-doc 规则)。 +- 4. 改动 `app/**/*.php` 须遵守 `php-use-case-style`(表达力、BusinessException、按层写法)。 ++ 3. 修改 PHP 后,最终回复含 `PHPDoc: checked`(见 php-code 规则 PHPDoc 章节)。 ++ 4. 改动 `app/**/*.php` 须遵守 `php-code` 用例写法章节(BusinessException、按层写法)。 +``` + +### 6.3 不改动 + +- `verify-slot-backend.sh`(已含 use-case 检查) +- `backend-layering.mdc`、`cross-service-sdk.mdc` + +### 6.4 验收 + +1. `~/.cursor/rules/` 仅一份 PHP 总规范 `php-code.mdc`,无悬空 `php-doc` / `php-use-case-style` 引用 +2. 新开 Agent 对话,改 `app/**` PHP 时应看到用例写法 + PHPDoc +3. `~/.cursor/hooks/verify-slot-backend.sh` → PASS + +### 6.5 执行 todos + +| id | 内容 | +|----|------| +| merge-php-code | 按 6.1 更新 php-code.mdc | +| fix-gate-refs | 按 6.2 更新 agent-completion-gate.mdc | +| smoke-rules | 打开 DailyRebateLogic + 跑 verify | diff --git a/plans/pwa每日返水流水展示_0e7ccf31.plan.md b/plans/pwa每日返水流水展示_0e7ccf31.plan.md new file mode 100644 index 0000000..24d19fa --- /dev/null +++ b/plans/pwa每日返水流水展示_0e7ccf31.plan.md @@ -0,0 +1,11 @@ +--- +name: pwa每日返水流水展示 +overview: 在 slot_pwa 的交易流水服务中补充每日返水(65)展示映射,确保钱包已入账的每日返水可在前端流水里看到。 +todos: [] +isProject: false +--- + +# PWA 每日返水流水展示计划 + +## 目标 +- \ No newline at end of file diff --git a/plans/recharge-config规则说明字段_344d86ff.plan.md b/plans/recharge-config规则说明字段_344d86ff.plan.md new file mode 100644 index 0000000..197906d --- /dev/null +++ b/plans/recharge-config规则说明字段_344d86ff.plan.md @@ -0,0 +1,49 @@ +--- +name: recharge-config规则说明字段 +overview: 为 `s_common.s_recharge_gift_config` 新增独立 `rule_desc` 字段,并在 `slot_admin` 与 `slot_console` 全链路打通,保持 `help` 兼容不破坏现有接口。 +todos: + - id: add-rule-desc-ddl + content: 在 slot_console/db 与 slot_admin/db 增加 s_recharge_gift_config.rule_desc 的 DDL + status: completed + - id: wire-admin-rule-desc + content: 打通 slot_admin 的 validate/logic/model 对 rule_desc 的读写 + status: completed + - id: wire-console-rule-desc + content: 在 slot_console 模型与 DailyRebate info 返回新增 rule_desc 并保留 help + status: completed + - id: verify-multi-repo + content: 分别校验变更仓库并运行 verify-slot-backend 脚本 + status: completed +isProject: false +--- + +# s_recharge_gift_config 增加 rule_desc 计划 + +## 目标 +- 在 `s_common.s_recharge_gift_config` 增加独立规则说明字段 `rule_desc`。 +- 后台管理(`slot_admin`)支持新增/编辑/列表读写该字段。 +- C 端(`slot_console`)返回新增 `rule_desc`,并继续保留现有 `help` 字段兼容。 + +## 变更范围 +- 数据库 SQL(双落点) + - [`/Users/ray/Documents/project/www/slot/slot_console/db/install.sql`](/Users/ray/Documents/project/www/slot/slot_console/db/install.sql) + - [`/Users/ray/Documents/project/www/slot/backend/slot_admin/db`](/Users/ray/Documents/project/www/slot/backend/slot_admin/db) + - 增加 `ALTER TABLE s_common.s_recharge_gift_config ADD COLUMN rule_desc ...`(含注释、默认值、可空策略)。 + +- `slot_admin`(配置录入链路) + - [`/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/validate/RechargeGiftConfigValidate.php`](/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/validate/RechargeGiftConfigValidate.php) + - [`/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/logic/RechargeGiftConfigLogic.php`](/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/logic/RechargeGiftConfigLogic.php) + - [`/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/model/RechargeGiftConfigModel.php`](/Users/ray/Documents/project/www/slot/backend/slot_admin/app/game/model/RechargeGiftConfigModel.php) + - 确保 `saveRedPacket/updateRedPacket` 等写入路径可接收并持久化 `rule_desc`,列表查询可读出该字段。 + +- `slot_console`(对外返回链路) + - [`/Users/ray/Documents/project/www/slot/slot_console/app/model/common/RechargeGiftConfigModel.php`](/Users/ray/Documents/project/www/slot/slot_console/app/model/common/RechargeGiftConfigModel.php) + - [`/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php`](/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/DailyRebateLogic.php) + - 在活动元数据中新增 `rule_desc`,并保留现有 `help` 字段。 + - 返回策略:`rule_desc` 优先读新字段;`help` 维持原语义不变。 + +## 验证 +- SQL 可执行性:检查字段存在与默认值。 +- 后台接口:新增/编辑后回读包含 `rule_desc`。 +- C 端接口:`DailyRebate info` 同时返回 `help` 与 `rule_desc`。 +- 执行门禁:在变更仓库分别运行 `~/.cursor/hooks/verify-slot-backend.sh`。 \ No newline at end of file diff --git a/plans/sdk_taskprogress_封装_5c8676e2.plan.md b/plans/sdk_taskprogress_封装_5c8676e2.plan.md new file mode 100644 index 0000000..d97bc91 --- /dev/null +++ b/plans/sdk_taskprogress_封装_5c8676e2.plan.md @@ -0,0 +1,81 @@ +--- +name: SDK taskProgress 封装 +overview: 将 slot_sdk(或你们的 SDK 仓库)加入工作区后,可按现有 Wallet Client 模式直接实现 `player-task/task-progress` 的调用封装;wallet 侧接口文档已就绪,可作为契约来源。 +todos: + - id: add-sdk-workspace + content: 用户将 slot_sdk 加入 Cursor 工作区并告知仓库路径 + status: completed + - id: explore-sdk-patterns + content: 阅读 SDK 现有 Wallet Client / HTTP 封装与错误处理约定 + status: completed + - id: implement-client + content: 新增 taskProgress 方法、路径常量、请求/响应类型(对齐 player-task-progress-api.md) + status: completed + - id: add-tests-or-example + content: 按 SDK 惯例补单测或调用示例(若项目有测试目录) + status: completed +isProject: false +--- + +# SDK 工作区加入后的直接开发方案 + +## 结论 + +**可以。** 当前工作区只有 [slot-wallet](file:///Users/ray/Documents/project/www/ray/slot-wallet),已具备接口契约文档 [doc/player-task-progress-api.md](doc/player-task-progress-api.md) 与实现对照(`PlayerTaskController`、`PlayerTaskQueryService` 等)。把 **SDK 仓库** 作为第二个根目录(或 monorepo 子目录)加入工作区后,我可以: + +1. 阅读 SDK 里现有 Wallet/HTTP Client 的命名、基类、错误处理、DTO 约定; +2. 新增 `taskProgress`(或团队统一命名)方法、路径常量、请求/响应类型; +3. 若有单测/示例,补一条调用示例或 Feature 测试; +4. 保证字段 **snake_case** 与 HTTP JSON 一致,成功判定 `code === 0`。 + +```mermaid +flowchart LR + subgraph workspace [Cursor Workspace] + Wallet[slot-wallet] + SDK[slot_sdk] + end + Doc[player-task-progress-api.md] + Wallet --> Doc + SDK -->|reads patterns| SDK + Doc -->|contract| SDK + SDK -->|POST player-task/task-progress| Wallet +``` + +## 你需要做的准备 + +| 项 | 说明 | +|----|------| +| 加入工作区 | Cursor:**File → Add Folder to Workspace**,选中 SDK 仓库根目录 | +| 告知路径 | 例如 `company/ray/slots/slot_sdk`(与你们实际目录一致即可) | +| 语言确认 | 若是 PHP `slot_sdk`、Go、TS 等,我会跟现有 Client 语言一致,不另起一套风格 | + +无需改 wallet 代码即可开始 SDK 开发;wallet 接口已实现完毕。 + +## 我会按什么写(预期产出) + +以 SDK 现有 Wallet Client 为模板(具体类名需打开 SDK 后确认),典型改动: + +- **路径常量**:`player-task/task-progress` +- **请求**:`uid`, `currency`(可选统一附带 `trace_id` 若其他接口都有) +- **响应模型**:`summary` + `bonus_tasks[]` + `deposit_tasks[]`,字段与 [doc/player-task-progress-api.md](doc/player-task-progress-api.md) §5 一致 +- **错误**:复用 SDK 已有 `WalletApiError` / `code !== 0` 处理 + +## 跨仓库发布(团队规范) + +按 [`.cursor/rules/slot-wallet-layers-and-delivery.mdc`](file:///Users/ray/Documents/project/www/ray/slot-wallet/.cursor/rules/slot-wallet-layers-and-delivery.mdc) §8–9: + +1. **先在 SDK 仓库** `commit` → `push`; +2. 消费方(如 slot-wallet 若通过 composer 依赖 SDK)再 `composer update` 并锁 `composer.lock`。 + +当前 [composer.json](file:///Users/ray/Documents/project/www/ray/slot-wallet/composer.json) **尚未**声明 `slot_sdk` 依赖,说明 SDK 可能独立发布或由其他服务引用——这不影响我在 SDK 仓库内直接编码。 + +## 建议的确认项(加入工作区后第一条消息说明即可) + +1. SDK 仓库在本机的**绝对路径**或文件夹名; +2. 方法命名偏好:`taskProgress` / `getPlayerTaskProgress` / 与现有 `wallet()` 等方法对齐; +3. 是否需要 **foundation 常量**(如 `source_type`)进 SDK,还是仅透传 int。 + +## 不在本次默认范围 + +- 修改 wallet 服务端实现(已满足 PRD §26.1); +- 自动 `composer update` 到其他服务(除非你明确要求并给出目标仓库)。 diff --git a/plans/slot_workspace_82c92cd4.plan.md b/plans/slot_workspace_82c92cd4.plan.md new file mode 100644 index 0000000..1bad684 --- /dev/null +++ b/plans/slot_workspace_82c92cd4.plan.md @@ -0,0 +1,16 @@ +--- +name: slot workspace +overview: 在 `/Users/ray/Documents/project/www/slot` 创建一个 Cursor/VS Code workspace 文件,并把该目录下一级项目文件夹加入 workspace。默认排除隐藏目录 `.vscode`,不递归加入嵌套子目录。 +todos: [] +isProject: false +--- + +# 创建 Slot Workspace + +将创建 `[slot.code-workspace](/Users/ray/Documents/project/www/slot/slot.code-workspace)`,内容使用标准 VS Code/Cursor workspace JSON: + +```json +{ + "folders": [ + { "path": "backend" }, + { "path": "monitor" } diff --git a/plans/slot_硬规则落地_2bb799e2.plan.md b/plans/slot_硬规则落地_2bb799e2.plan.md new file mode 100644 index 0000000..4cac168 --- /dev/null +++ b/plans/slot_硬规则落地_2bb799e2.plan.md @@ -0,0 +1,176 @@ +--- +name: Slot 硬规则落地 +overview: 在用户级 `~/.cursor` 强化分层/PHPDoc 规则为可执行 checklist,并新增 Cursor Hooks:拦截宿主机 php/composer、在 Agent 结束前自动跑仓库校验脚本,未通过则注入 follow-up 要求继续修复。 +todos: + - id: hooks-scripts + content: 新建 ~/.cursor/hooks.json、block-host-php.sh、verify-slot-backend.sh 并 chmod +x + status: completed + - id: rules-mdc + content: 扩展 backend-layering.mdc;php-doc alwaysApply;新增 agent-completion-gate.mdc + status: completed + - id: plan-dod + content: 每日返水 plan 追加 Definition of Done(与脚本检查项一致) + status: completed + - id: verify-hooks + content: 手动验证:宿主机 php 拦截、docker php 放行、verify 脚本 PASS/FAIL + status: in_progress +isProject: false +--- + +# Slot 后端硬规则落地 + +## 目标 + +把当前「软约束」升级为三层: + +| 层级 | 手段 | 效果 | +|------|------|------| +| 上下文 | 规则 `alwaysApply` + 完成前 MUST | 每条对话强制可见 | +| 行为 | Agent 必须跑校验脚本并贴输出 | 可审计、可复查 | +| 机器 | Cursor Hooks `failClosed` | 违反则阻断或自动续跑修复 | + +不依赖各 slot 仓库已有 CI(目前无 `.github/workflows`、无 pre-commit),**先落在用户级 `~/.cursor`**,对你当前多根工作区(`slot_console`、`backend/slot_admin` 等)全部生效。 + +## 1. 强化 Cursor Rules(`~/.cursor/rules/`) + +### 1.1 扩展 [backend-layering.mdc](file:///Users/ray/.cursor/rules/backend-layering.mdc) + +在文末新增 **「Agent 完成前 MUST」**(短、可验证): + +- **重命名/删除类**:对本次 diff 中删除的 `*.php`,提取类名,在 `~/Documents/project/www/slot` 下 `rg` 引用数为 **0**。 +- **新建/大改 `app/api/controller/*`**:必须 `extends \slotLib\basic\BaseController`,构造注入对应 `*Logic` + `*Validator`;参照 [FreeCreditsController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/controller/FreeCreditsController.php)。 +- **禁止 Logic 型 Service**:不得新建以单业务用例编排为主、仅转发 Model/Logic 的 `app/service/*Service`(`DailyRebateCalcService` 这类**公共计算**除外)。 +- **声称完成前**:必须执行 `~/.cursor/hooks/verify-slot-backend.sh`,在最终回复粘贴脚本输出(`PASS` 或 `FAIL` 明细)。 + +### 1.2 调整 [php-doc.mdc](file:///Users/ray/.cursor/rules/php-doc.mdc) + +- `alwaysApply: false` → **`alwaysApply: true`** +- 在「Agent 执行要求」增加:**最终回复必须含一行 `PHPDoc: checked` 或列出例外文件** + +与 layering 规则叠加后,PHPDoc 不再因 glob 未命中而漏掉。 + +### 1.3 新增 [agent-completion-gate.mdc](file:///Users/ray/.cursor/rules/agent-completion-gate.mdc) + +`alwaysApply: true`,仅 15~20 行,避免与 layering 重复: + +- 引用校验脚本路径与 Hooks 行为 +- 禁止在未跑脚本前写「已完成 / 可以合并」 +- 多仓库任务:脚本会扫描 `slot_*` 与 `backend` 下有 git 的目录 + +## 2. Cursor Hooks(用户级,真正「硬」) + +新建: + +- [~/.cursor/hooks.json](file:///Users/ray/.cursor/hooks.json) +- [~/.cursor/hooks/block-host-php.sh](file:///Users/ray/.cursor/hooks/block-host-php.sh) +- [~/.cursor/hooks/verify-slot-backend.sh](file:///Users/ray/.cursor/hooks/verify-slot-backend.sh) + +```mermaid +flowchart TD + shell[Agent_Shell_command] + blockHook[block-host-php.sh] + edit[PHP_file_edits] + stop[Agent_stop] + verify[verify-slot-backend.sh] + shell --> blockHook + blockHook -->|deny_naked_php_composer| block[permission_deny] + blockHook -->|docker_exec| allow[permission_allow] + stop --> verify + verify -->|FAIL| followup[followup_message_continue_fix] + verify -->|PASS| done[session_ends] +``` + +### 2.1 `beforeShellExecution` — 落实 [dev-environment.mdc](file:///Users/ray/.cursor/rules/dev-environment.mdc) + +**`block-host-php.sh`**(`failClosed: true`): + +- 若命令以 `php` / `composer` 开头且**不包含** `docker exec` → `permission: deny` +- 允许:`docker exec ... php`、`docker exec ... composer` +- 其它命令 → `allow` + +这样宿主机误跑 PHP 会被 Hook **直接拦住**,不依赖模型记忆。 + +### 2.2 `stop` — 分层与调用方收尾 + +**`verify-slot-backend.sh`**(`failClosed: false`,失败时返回 `followup_message` 让 Agent 继续改): + +扫描根目录:`/Users/ray/Documents/project/www/slot` 下各子仓库(存在 `.git` 的 `slot_*`、`backend`): + +| 检查 | 逻辑 | +|------|------| +| 删除类无残留引用 | `git diff --diff-filter=D --name-only` 的 PHP → 解析 `class X` → `rg '\bX\b'` 全 slot 根,非 0 则 FAIL | +| API Controller 结构 | `git diff --name-only` 命中 `app/api/controller/*.php` → 文件须含 `extends \slotLib\basic\BaseController`(无则 FAIL,除非文件头有 `@layering-exempt`) | +| 禁止符号回归 | 内置 banlist(首项 `DailyRebateService`),`rg` 命中即 FAIL | +| 可选轻量 PHPDoc | 对 diff 中新增的 `public function`,上一行非 `/**` 则 WARN(不阻断,避免历史债) | + +输出格式固定: + +``` +=== verify-slot-backend === +PASS +``` + +或 `FAIL` + 逐条原因;`stop` hook 解析到 `FAIL` 时返回 `followup_message`:「校验未通过,按明细修复后重跑脚本」。 + +脚本依赖:`bash`、`git`、`rg`(你本机已有);不依赖 `jq`(用 grep/sed 解析 stdin,降低 hook 环境差异)。 + +### 2.3 `hooks.json` 草案 + +```json +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/block-host-php.sh", + "matcher": "^\\s*(php|composer)\\b", + "failClosed": true + } + ], + "stop": [ + { + "command": "./hooks/verify-slot-backend.sh", + "failClosed": false + } + ] + } +} +``` + +路径相对 **`~/.cursor/`**(用户 hook 约定)。创建后 `chmod +x` 两个脚本。 + +**验证步骤**(实施后手动测一次): + +1. Agent 尝试 `php -v` → 应被拦截 +2. `docker exec ... php -v` → 应放行 +3. 故意留 `DailyRebateService` 引用 → `stop` 应触发 follow-up + +## 3. 每日返水 Plan 补 DoD(可选,与硬规则对齐) + +在 [每日返水活动化改造_dd1575a1.plan.md](file:///Users/ray/.cursor/plans/每日返水活动化改造_dd1575a1.plan.md) 末尾加 **Definition of Done** 小节(与脚本检查项一致),便于人工对照: + +- `rg DailyRebateService` → 0 +- `DailyRebateController` 对齐 `FreeCreditsController` +- 删除 orphan `dailyRebateTierConfig/*` +- `verify-slot-backend.sh` → PASS + +(仅更新 plan 文档,不执行业务代码。) + +## 4. 不纳入首期的项(避免过度工程) + +- 各仓库 CI / pre-commit:可后续把 `verify-slot-backend.sh` 拷到 `scripts/` 进 pipeline +- `DailyRebateTierValidateService` 迁到 Validate 层:属代码重构,单独 PR +- `afterFileEdit` 每次保存都跑校验:太吵,先用 `stop` + 规则要求手动跑脚本 + +## 5. 实施顺序 + +1. 写两个 hook 脚本 + `hooks.json`,本地 chmod + 试跑脚本 +2. 改/增三个 `.mdc` 规则文件 +3. 更新每日返水 plan 的 DoD(若你同意) +4. 重启 Cursor 或确认 Hooks 面板已加载 + +## 风险与说明 + +- **Hooks 是 Cursor 产品能力**:需 Cursor 版本支持 `hooks.json`;若 `stop` 的 stdin 字段与文档不一致,首版脚本以「扫描 git diff」为主,不依赖复杂 JSON 字段。 +- **硬 ≠ 100%**:模型仍可能不写 follow-up 前的脚本输出;`stop` hook 是兜底。二者叠加后明显严于仅 Rules。 +- **多根工作区**:脚本按 `~/Documents/project/www/slot/*/git` 遍历,不依赖当前打开哪个 folder。 diff --git a/plans/status_接口字段精简_8ba244ad.plan.md b/plans/status_接口字段精简_8ba244ad.plan.md new file mode 100644 index 0000000..aa601e9 --- /dev/null +++ b/plans/status_接口字段精简_8ba244ad.plan.md @@ -0,0 +1,165 @@ +--- +name: status 接口字段精简 +overview: 对照需求文档精简 C 端 status/claim 响应字段,并将所有金额由千分位(qf)转为大单位(与全站 getNumberFormat 一致);innerapi 仍返回 qf 全量。 +todos: + - id: package-list-for-client + content: FreeCreditsPackageModel::listForClient() 返回精简字段,amount 经 getNumberFormat 转大单位 + status: completed + - id: slim-build-status + content: buildStatus 去掉 activity_id;顶层金额与 packages 均转大单位 + status: completed + - id: format-first-cashout-amount + content: firstCashout 响应 data.amount 同步转大单位(Pay 入参仍用 qf) + status: completed + - id: update-controller-phpdoc + content: 更新 FreeCreditsController PHPDoc:金额为展示单位 float,非千分位 + status: completed + - id: client-status-test + content: 单测断言字段白名单及 78500 qf → 78.5 等大单位转换 + status: completed +isProject: false +--- + +# Free Credits status 接口字段精简 + +## 结论 + +**可以且应该精简。** 当前 [`buildStatus()`](slot_console/app/api/logic/FreeCreditsLogic.php) 直接 `toArray()` 透出库表列,超出需求文档中 C 端 UI 所需信息;[`activity_id`](slot_console/app/api/logic/FreeCreditsLogic.php) 仅用于后台/内部关联,C 端无展示或交互用途。 + +你已确认: + +- **只做删减**,不新增 `total_deposit`(累计充值进度由 C 端从钱包侧获取)。 +- **金额转大单位**:C 端接口不再返回千分位整数,统一转为展示金额(与 [`AgentController::formatAmountToFloat`](slot_console/app/api/controller/AgentController.php)、[`CommonFn::getNumberFormat`](slot_lib/src/common/CommonFn.php) 一致,默认 `moneyFormat=1000`、`moneyDot=2`,如 qf `78500` → `78.5`)。 + +--- + +## 需求文档 vs 当前响应 + +需求文档([首充前免费余额定格与分档释放需求文档.md](docs/requirements/首充前免费余额定格与分档释放需求文档.md))描述的是 **UI 行为**,未定义 JSON 契约,但可反推 C 端必需数据: + +| UI 场景(文档章节) | C 端需要的数据 | +| --- | --- | +| 是否展示活动(`status === -1` 隐藏) | `status` | +| 首页 Withdraw 是否曾达赢取门槛(§5.2、§7) | `home_withdraw_unlocked` | +| 池总额 / 入口副标题「$78.50 pending」(§6.3–6.6、§9.3、§14.2) | `frozen_amount` | +| 问号弹窗 / 第一档金额(§8、§10) | `first_cash_amount`、`recharge_unlock_amount` | +| 档位列表 Withdraw / Unlock / Claim / Claimed(§9.3、§10–12) | `packages[]`:`id`、`package_type`、`amount`、`status` | +| 发起 claim / 第一档提现(现有 API) | `packages[].id` → POST `package_id` | + +**当前多出的字段:** + +- 顶层:`activity_id`(`recharge_gift_config.id`,仅 innerapi/编排用) +- `packages[]` 全表列:`player_id`、`activity_id`、`uid`、`withdraw_order_id`、`claim_biz_id`、`unlocked_time`、`claimed_time`、`completed_time`、`create_time`、`update_time` +- `package_no` 可保留(便于调试与稳定排序展示),也可仅靠数组顺序;建议 **保留**(成本低、与 DB 序号一致) + +**命名与单位:** packages 字段由 `amount_qf` 改为 **`amount`**;顶层与各档 **`amount` 均为大单位 float**(非 qf),C 端可直接用于 `$78.50` 类文案,无需再 `/1000`。 + +--- + +## 目标响应契约(C 端) + +```json +{ + "status": 4, + "home_withdraw_unlocked": 1, + "frozen_amount": 78.5, + "first_cash_amount": 20, + "recharge_unlock_amount": 50, + "packages": [ + { + "id": 101, + "package_no": 1, + "package_type": 1, + "amount": 20, + "status": 1 + } + ] +} +``` + +`status === -1` 时仍仅 `{ "status": -1 }`(与现逻辑一致)。 + +**转换规则(实现):** + +```php +// 与全站 C 端金额一致;qf 为库表 / 内部逻辑整数 +private function formatClientAmount(int $amountQf): float +{ + return (float) CommonFn::getNumberFormat($amountQf); +} +``` + +- 应用于:`frozen_amount`、`first_cash_amount`、`recharge_unlock_amount`、`packages[].amount`。 +- **`firstCashout` 成功响应** `data.amount` 同样转大单位;调用 Pay / 钱包时仍传 qf,仅 JSON 对外转换。 +- **innerapi / 单测写库** 仍使用 `_qf` 整数,不在 DB 层改单位。 + +--- + +## 实现方案 + +```mermaid +flowchart LR + statusApi[status_claim_firstCashout] + buildStatus[buildStatus] + clientDto[toClientStatusDto] + modelFull[listByPlayerId_toArray] + innerapi[innerapi_list] + statusApi --> buildStatus --> clientDto + buildStatus --> modelFull + innerapi --> modelFull +``` + +1. **Logic 层统一转换**([`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php)) + - 新增 `formatClientAmount(int $amountQf): float`(封装 `CommonFn::getNumberFormat`)。 + - 供 `buildStatus`、`listForClient`(或 Model 回调)、`firstCashout` 响应共用。 + +2. **Model 层 C 端 packages**([`FreeCreditsPackageModel`](slot_console/app/model/common/FreeCreditsPackageModel.php)) + - 新增 `listForClient(int $playerId, callable $formatAmount): array` 或 Logic 内 `array_map`:只输出 `id`、`package_no`、`package_type`、`amount`(大单位)、`status`。 + - 保留 `listByPlayerId()` 供 innerapi、dev、集成测试。 + +3. **收口 DTO**(`buildStatus`) + - 去掉 `activity_id`。 + - 顶层三金额字段经 `formatClientAmount`;`packages` 用 `listForClient`。 + - `status()`、`claim()` 经 `buildStatus` 返回,结构一致。 + +4. **firstCashout 响应** + - `return ['order_id' => ..., 'amount' => $this->formatClientAmount($package->amount_qf)]`。 + +5. **更新文档**([`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) PHPDoc) + - 删除「千分位整数、展示时除以 1000」描述;改为「金额为大单位 float,精度见 `moneyDot`」。 + +6. **单测** + - 新增 `FreeCreditsClientStatusTest`:字段白名单 + `78500` qf → `78.5` 转换断言。 + +7. **不改动** + - [`innerapi/controller/FreeCreditsController`](slot_console/app/innerapi/controller/FreeCreditsController.php) 仍返回全量 `toArray()`。 + - `FreeCreditsFreezeDev` 等内部工具继续用 `listByPlayerId`。 + +--- + +## 字段对照表(精简前后) + +| 字段 | 精简前 | 精简后 | 说明 | +| --- | --- | --- | --- | +| `activity_id` | 有 | **删** | C 端不需要 | +| `status` | 有 | 有 | 主状态机 | +| `home_withdraw_unlocked` | 有 | 有 | §5.2 | +| `frozen_amount` | qf 整数 | **float 大单位** | 如 78.5 | +| `first_cash_amount` | qf 整数 | **float 大单位** | 如 20 | +| `recharge_unlock_amount` | qf 整数 | **float 大单位** | 如 50 | +| `packages[].id` | 有 | 有 | claim/cashout | +| `packages[].package_no` | 有 | 有 | 序号展示 | +| `packages[].package_type` | 有 | 有 | 1=提现档 2=领取档 | +| `packages[].amount` | `amount_qf`(qf) | `amount`(**float 大单位**) | 重命名 + 转换 | +| `firstCashout.data.amount` | qf | **float 大单位** | 与 status 一致 | +| `packages[].status` | 有 | 有 | UI 状态 | +| `packages[].player_id/uid/...` | 有 | **删** | 内部字段 | +| `packages[].withdraw_order_id` 等 | 有 | **删** | 轮询靠主 `status` + package `status` | + +--- + +## 风险与协调 + +- **Breaking change**:C 端需改为直接使用大单位金额(不再 `/1000`);删除 `activity_id`、`amount_qf` 及 packages 审计字段。 +- **精度**:与 `ShareConfigService::moneyDot` / `moneyFormat` 绑定,勿手写 `/1000`,避免与 VIP/钱包接口不一致。 +- **累计充值进度**:不纳入本次接口;C 端继续从钱包统计接口取(钱包侧金额亦通常为大单位)。 diff --git a/plans/status_返回_help_e344e40a.plan.md b/plans/status_返回_help_e344e40a.plan.md new file mode 100644 index 0000000..e6eb1c5 --- /dev/null +++ b/plans/status_返回_help_e344e40a.plan.md @@ -0,0 +1,70 @@ +--- +name: status 返回 help +overview: 在 Free Credits C 端 status 接口(及 claim 同源 buildStatus)中增加 help 字段,读取活动配置表 recharge_gift_config.help,供问号说明弹窗等展示;status=-1 仍不返回 help。 +todos: + - id: build-status-help + content: FreeCreditsLogic::buildStatus 增加 help 字段(读 config->help) + status: completed + - id: update-phpdoc + content: FreeCreditsController::status PHPDoc 补充 help 说明 + status: completed + - id: unit-test-help + content: FreeCreditsClientStatusTest 白名单与透传断言 + status: completed +isProject: false +--- + +# status 接口增加 help 返回 + +## 目标 + +`GET /api/free-credits/status`(以及 `claim` 成功后的同源响应)在 `status !== -1` 时增加 **`help`(string)**,内容来自运营在后台活动编辑里填写的 **「帮助说明」**([`recharge_gift_config.help`](slot_console/app/model/common/RechargeGiftConfigModel.php))。 + +`status === -1` 时保持仅 `{ "status": -1 }`,不附带 help。 + +## 改动点 + +### 1. [`FreeCreditsLogic::buildStatus()`](slot_console/app/api/logic/FreeCreditsLogic.php) + +在现有返回数组中增加: + +```php +'help' => is_null($config) ? '' : strval($config->help ?? ''), +``` + +- `buildStatus` 已接收 `$config`(`activeConfig` 查出的 type=11 活动行),无需额外查库。 +- 不做 §8 模板占位符替换(轻量方案);C 端可用已有 `frozen_amount` / `recharge_unlock_amount` / `first_cash_amount` 自行替换,或原样展示运营配置的富文本/多行文案。 + +### 2. [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) PHPDoc + +在 `status` 方法成功字段列表中补充: + +- `help` (string) 活动规则说明,来自后台「帮助说明」 + +### 3. 单测 [`FreeCreditsClientStatusTest`](slot_console/tests/Unit/FreeCreditsClientStatusTest.php) + +- `PLAYER_TOP_KEYS` 增加 `help` +- 用例:`FreeCreditsConfigStub` 设置 `help` 属性,`assertSame` 透传 + +## 数据流 + +```mermaid +flowchart LR + admin[slot_admin 活动编辑 help 文本框] + db[(recharge_gift_config.help)] + logic[buildStatus] + api["/api/free-credits/status"] + admin --> db --> logic --> api +``` + +## 验收 + +- 后台 type=11 活动填写「帮助说明」并保存后,已参加用户 `status` 响应含相同 `help` 字符串 +- `status=-1` 响应无 `help` 字段 +- 单元测试通过;`claim` 返回结构同步含 `help`(复用 `buildStatus`) + +## 不在本次范围 + +- `ext_config` 独立说明模板、动态金额替换服务端拼装 +- `banner_image` 透出(如需可另开) +- 前端问号弹窗 UI diff --git a/plans/status_透出后续最小充值_47cf9e62.plan.md b/plans/status_透出后续最小充值_47cf9e62.plan.md new file mode 100644 index 0000000..afc6f44 --- /dev/null +++ b/plans/status_透出后续最小充值_47cf9e62.plan.md @@ -0,0 +1,129 @@ +--- +name: status 透出后续最小充值 +overview: 在 slot_console 的 Free Credits `GET /api/free-credits/status`(及 claim 共用 `buildStatus`)中,将后台 ext_config 的 `subsequent_min_recharge_qf` 以 C 端约定字段 `subsequent_min_recharge`(float 大单位)透出,与 `recharge_unlock_amount` 一致。 +todos: + - id: logic-expose-field + content: FreeCreditsLogic:buildConfigClientFields / buildStatus / buildPreEnrollmentStatus 透出 subsequent_min_recharge + status: completed + - id: controller-phpdoc + content: FreeCreditsController::status PHPDoc 补充 subsequent_min_recharge 说明 + status: completed + - id: unit-tests + content: 更新 FreeCreditsClientStatusTest、FreeCreditsEligibilityTest 白名单与断言 + status: completed + - id: run-phpunit + content: php82 容器跑相关单测验证 + status: completed +isProject: false +--- + +# status 透出 subsequent_min_recharge + +## 背景 + +后台 type=11 活动 [`ext_config`](backend/slot_admin_vue/src/views/game/activity/edit.vue) 已保存 **`subsequent_min_recharge_qf`**(后续解锁最小单笔充值,千分位)。 + +[`FreeCreditsLogic::advanceByRecharge()`](slot_console/app/api/logic/FreeCreditsLogic.php) 充值推进逻辑**已读取**该配置: + +```627:628:slot_console/app/api/logic/FreeCreditsLogic.php + $minRecharge = $this->configAmount($config, 'subsequent_min_recharge', self::DEFAULT_SUBSEQUENT_MIN_RECHARGE); + if ($rechargeAmount < $minRecharge || !is_null(FreeCreditsPackageModel::nextReadyReleasePackage($player->id))) { +``` + +[`configAmount()`](slot_console/app/api/logic/FreeCreditsLogic.php) 会优先读 `key_qf`(即 `subsequent_min_recharge_qf`),再 fallback 旧键 `subsequent_min_recharge`。 + +但 C 端 [`buildConfigClientFields()`](slot_console/app/api/logic/FreeCreditsLogic.php) / [`buildStatus()`](slot_console/app/api/logic/FreeCreditsLogic.php) 目前只透出 `win_threshold`、`recharge_unlock_amount`、`help`、`banner_image`,**缺少**后续档解锁门槛,前端无法展示「单笔充值满 $X 解锁下一档」类文案。 + +```mermaid +flowchart LR + extConfig["ext_config.subsequent_min_recharge_qf"] + configAmount["configAmount(subsequent_min_recharge)"] + buildFields["buildConfigClientFields"] + statusAPI["status / claim data"] + advanceByRecharge["advanceByRecharge 已有"] + extConfig --> configAmount + configAmount --> advanceByRecharge + configAmount --> buildFields --> statusAPI +``` + +## 目标契约 + +| 项 | 约定 | +| --- | --- | +| 接口 | `GET /api/free-credits/status`;`POST /api/free-credits/claim` 成功后的 `data` 结构相同 | +| 字段名 | **`subsequent_min_recharge`**(与 `recharge_unlock_amount` 命名一致,**不**在 C 端响应中带 `_qf`) | +| 类型 | `float`,展示大单位(`formatClientAmount` + `CommonFn::getNumberFormat`) | +| 配置来源 | `ext_config.subsequent_min_recharge_qf`(经现有 `configAmount`) | +| 默认值 | `DEFAULT_SUBSEQUENT_MIN_RECHARGE = 10`(千分位 → 展示 `10.0`) | +| `status=-1` | 仍仅 `{ status: -1 }`,不含本字段 | +| `status=0` 与有 player | 均返回(与 `recharge_unlock_amount` 同级) | + +响应片段示例(`status≥0`): + +```json +{ + "status": 2, + "win_threshold": 50.0, + "recharge_unlock_amount": 50.0, + "subsequent_min_recharge": 10.0, + "help": "...", + "packages": [] +} +``` + +## 实现步骤(仅 slot_console) + +### 1. Logic — [`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php) + +在 **`buildConfigClientFields()`** 增加: + +```php +'subsequent_min_recharge' => $this->formatClientAmount( + $this->configAmount($config, 'subsequent_min_recharge', self::DEFAULT_SUBSEQUENT_MIN_RECHARGE) +), +``` + +- `config === null` 时占位 `0.0`(与 `recharge_unlock_amount` 一致) +- 更新方法 `@return` 数组 shape + +在 **`buildStatus()`**、**`buildPreEnrollmentStatus()`** 顶层增加: + +```php +'subsequent_min_recharge' => $configFields['subsequent_min_recharge'], +``` + +无需改 Model / Service;不新增中转 Service。 + +### 2. PHPDoc — [`FreeCreditsController.php`](slot_console/app/api/controller/FreeCreditsController.php) + +在 `status()` 的 `data` 字段说明中,于 `recharge_unlock_amount` 后补充: + +- `subsequent_min_recharge (float)` 解锁下一后续释放档所需**单笔**充值下限(来自 `ext_config.subsequent_min_recharge_qf`) + +### 3. 单测 + +| 文件 | 改动 | +| --- | --- | +| [`FreeCreditsClientStatusTest.php`](slot_console/tests/Unit/FreeCreditsClientStatusTest.php) | `PLAYER_TOP_KEYS` 增加 `subsequent_min_recharge`;stub 配置加 `subsequent_min_recharge_qf`(如 `10000` → 展示 `10.0`)并断言 | +| [`FreeCreditsEligibilityTest.php`](slot_console/tests/Unit/FreeCreditsEligibilityTest.php) | `testStatusReturnsPreEnrollmentConfigWhenNoPlayer`:stub 加 `subsequent_min_recharge_qf`,断言 `subsequent_min_recharge` | + +### 4. 验证 + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit \ + tests/Unit/FreeCreditsClientStatusTest.php \ + tests/Unit/FreeCreditsEligibilityTest.php +``` + +## 不在本次范围 + +- C 端 / slot_pwa 消费字段与 UI 文案(仅后端补字段) +- 透出 `first_cash_amount`、`package_amount`、`max_unlock_per_recharge` 等其它 ext 配置 +- 修改 `advanceByRecharge` 业务规则(已正确读配置) + +## 验收 + +1. 有资格且 `status≥0`:`data` 含 `subsequent_min_recharge`,值与后台配置的 `subsequent_min_recharge_qf` 一致(大单位) +2. `status=-1`:无该键 +3. `claim` 成功返回的 `data` 同样含该字段 +4. 上述单测通过 diff --git a/plans/status_配置与未充值_f37679fd.plan.md b/plans/status_配置与未充值_f37679fd.plan.md new file mode 100644 index 0000000..8e7bd76 --- /dev/null +++ b/plans/status_配置与未充值_f37679fd.plan.md @@ -0,0 +1,106 @@ +--- +name: status 配置与未充值 +overview: 修复 slot_console FreeCreditsLogic::status:无玩家池时返回 status=0 及活动配置;C 端新增 win_threshold(不含 initial_amount、home_withdraw_unlocked)。Logic 已按需求变更实现,待对齐单测与 PHPDoc。 +todos: + - id: logic-pre-enrollment + content: FreeCreditsLogic:buildPreEnrollmentStatus;status() 无 player 时返回配置态(已完成) + status: completed + - id: logic-build-status-fields + content: buildStatus / buildConfigClientFields 仅透出 win_threshold(不含 initial_amount、home_withdraw_unlocked)(已完成) + status: completed + - id: align-phpdoc + content: FreeCreditsController PHPDoc 与 buildConfigClientFields 注释:删除 initial_amount、home_withdraw_unlocked;移除无用 DEFAULT_INITIAL_AMOUNT + status: completed + - id: align-unit-tests + content: FreeCreditsClientStatusTest / FreeCreditsEligibilityTest 白名单与断言与现实现一致 + status: completed +isProject: false +--- + +# Free Credits status 接口修复计划(需求变更版) + +## 需求确认(2026-05-21) + +**C 端 `GET /api/free-credits/status` 约定:** + +- **包含**:`win_threshold`(来自 `ext_config.win_threshold_qf`) +- **不包含**:`initial_amount`、`home_withdraw_unlocked` + - 注册赠送金额:仍由 `game_base.user_register_reward` 等其它接口提供 + - 首页 Withdraw 解锁:C 端用 `status`(如 `STATUS_HOME_UNLOCKED=1`)或钱包余额 + `win_threshold` 自行判断;库表 `home_withdraw_unlocked` 仍由 `syncHomeWithdrawUnlocked` 维护,仅不下发 API + +后台 type=11 的 `initial_amount_qf` 配置保留(运营后台用),**不**经 status 透出。 + +## 问题根因 + +[`FreeCreditsLogic::status()`](slot_console/app/api/logic/FreeCreditsLogic.php) 原逻辑在无 `free_credits_player` 时返回 `{ status: -1 }`,导致首充前 C 端无法展示活动(`status !== -1` 为展示开关)。 + +## 目标行为 + +| 场景 | status | 返回顶层字段 | +| --- | --- | --- | +| 无资格 / 无 type=11 配置 | `-1` | 仅 `status` | +| 有资格 + 有配置 + **无 player** | `0` | `status`, `frozen_amount`, `first_cash_amount`, `win_threshold`, `recharge_unlock_amount`, `help`, `packages`(空数组) | +| 有资格 + 有配置 + **有 player** | 玩家真实值 | 同上 + 玩家 `frozen_amount` / `first_cash_amount` / `packages` | + +```mermaid +flowchart TD + statusReq[GET status] --> eligible{资格+活动配置?} + eligible -->|否| notVisible["status=-1"] + eligible -->|是| player{有 player 行?} + player -->|无| preConfig["status=0 + win_threshold 等"] + player -->|有| fullStatus["buildStatus 玩家态"] +``` + +## 已实现 Logic(当前代码,无需再改字段集) + +[`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php) 已与上表一致: + +- `status()`:无 player → `buildPreEnrollmentStatus($config)` +- `buildConfigClientFields()`:仅 `win_threshold`、`recharge_unlock_amount`、`help` +- `buildStatus()` / `buildPreEnrollmentStatus()`:**不**返回 `initial_amount`、`home_withdraw_unlocked` + +## 待办:对齐文档与单测 + +### 1. PHPDoc — [`FreeCreditsController.php`](slot_console/app/api/controller/FreeCreditsController.php) + +- 删除 `initial_amount`、`home_withdraw_unlocked` 字段说明 +- 明确 `status=0` 时仍返回 `win_threshold`、`recharge_unlock_amount`、`help` + +### 2. 清理 Logic 注释/死代码 — [`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php) + +- 删除未使用的 `DEFAULT_INITIAL_AMOUNT` 常量(若仍保留) +- `buildConfigClientFields` 的 `@return` 改为仅含 `win_threshold`、`recharge_unlock_amount`、`help` + +### 3. 单测 + +| 文件 | 改动 | +| --- | --- | +| [`FreeCreditsClientStatusTest.php`](slot_console/tests/Unit/FreeCreditsClientStatusTest.php) | `PLAYER_TOP_KEYS` 改为:`status`, `frozen_amount`, `first_cash_amount`, `win_threshold`, `recharge_unlock_amount`, `help`, `packages`;移除 `initial_amount`、`home_withdraw_unlocked` 断言 | +| [`FreeCreditsEligibilityTest.php`](slot_console/tests/Unit/FreeCreditsEligibilityTest.php) | `testStatusReturnsPreEnrollmentConfigWhenNoPlayer`:断言 `win_threshold=50`,**不**断言 `initial_amount` / `home_withdraw_unlocked` | + +运行: + +```bash +docker exec -w /app/www/slot/slot_console php82 vendor/bin/phpunit tests/Unit/FreeCreditsClientStatusTest.php tests/Unit/FreeCreditsEligibilityTest.php +``` + +## C 端约定(供联调) + +- **展示开关**:`data.status !== -1`;首充前为 `status === 0` +- **进度条上限**:`win_threshold` +- **注册赠送**:**不要**从 status 取;走注册/大厅配置接口 +- **Withdraw 解锁**:**不要**依赖 `home_withdraw_unlocked` 字段;用 `status` 或余额逻辑 + +## 不在本次范围 + +- status 返回 `initial_amount` / `home_withdraw_unlocked` +- 注册发奖改读 `ext_config.initial_amount_qf` +- slot_pwa / gateway 前端改动 +- backend 后台 `initial_amount_qf` 编辑能力(已存在) + +## 验收清单 + +1. 无 player:`status=0`,含 `win_threshold`、`recharge_unlock_amount`、`help`,`packages=[]`,**无** `initial_amount`、`home_withdraw_unlocked` 键 +2. 有 player:含玩家态金额 + 同上配置字段 +3. 无资格/无配置:`status=-1` +4. 单测全部通过 diff --git a/plans/type11_活动初始金额_ee0e1a75.plan.md b/plans/type11_活动初始金额_ee0e1a75.plan.md new file mode 100644 index 0000000..2f81837 --- /dev/null +++ b/plans/type11_活动初始金额_ee0e1a75.plan.md @@ -0,0 +1,131 @@ +--- +name: type11 活动初始金额 +overview: 在活动类型 11(Free Credits)后台编辑表单中新增「活动初始金额」字段,以美元小数录入、落库为 ext_config.initial_amount_qf;同步补齐 slot_admin 校验与列表展示。数据经现有 ActivityService 透传至 slot_console,本任务不改 slot_console 业务逻辑。 +todos: + - id: edit-vue-initial-amount + content: edit.vue:type=11 表单项 + setFormData/submit 的 initial_amount ↔ initial_amount_qf 转换 + status: completed + - id: index-vue-display + content: index.vue:type=11 列表 ext_config 展示活动初始金额 + status: completed + - id: validate-initial-amount + content: ActivityValidate::checkFreeCreditsExt 增加 initial_amount_qf 必填与非负校验 + status: completed + - id: manual-verify + content: 本地验证新建/编辑/回填/必填/列表展示 + status: completed +isProject: false +--- + +# 活动类型 11 增加「活动初始金额」后台配置 + +## 背景与范围 + +- 需求来源:[首充前免费余额定格与分档释放需求文档.md](docs/requirements/首充前免费余额定格与分档释放需求文档.md) 第 19 节将注册赠送调整为 `$30`;运营需在 **活动类型 11** 的配置里维护「用户注册时的初始金额」。 +- **已确认键名**:`initial_amount`(表单) / `initial_amount_qf`(落库,千分位整数)。 +- **范围**:仅 [backend/slot_admin](backend/slot_admin) 与 [backend/slot_admin_vue](backend/slot_admin_vue);`ActivityController` 已透传 `ext_config` 至 `slot_console`,无需改 `slot_console` / `slot_lib`(新字段会原样写入 `s_recharge_gift_config.ext_config` JSON)。 + +## 现状(可复用) + +type=11 专属编辑已在 [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) 实现 6 个金额字段 + Banner,模式为: + +- 编辑:`setFormData` 将 `*_qf ÷ 1000` 还原为美元小数 +- 提交:`submit` 将美元小数 `× 1000` 写入 `*_qf` +- 校验:[ActivityValidate::checkFreeCreditsExt](backend/slot_admin/app/game/validate/ActivityValidate.php) 在 [ActivityController](backend/slot_admin/app/game/controller/ActivityController.php) `save` / `update(updateData)` 时触发 + +```mermaid +flowchart LR + editVue["edit.vue submit"] --> adminApi["slot_admin ActivityController"] + adminApi --> activitySvc["slotLib ActivityService"] + activitySvc --> consoleInner["slot_console innerapi/activity"] + consoleInner --> extConfig["ext_config JSON"] +``` + +## 字段定义 + +| UI 标签 | 表单字段 | 落库字段 | 说明 | +| --- | --- | --- | --- | +| 活动初始金额($) | `ext_config.initial_amount` | `initial_amount_qf` | 用户注册时赠送的免费余额金额;placeholder 建议 `30`(对齐需求文档 $30) | + +金额处理与现有 6 项一致:`Math.round(Number(v) * 1000)`,回填优先读 `_qf`。 + +## 改动清单 + +### 1. 编辑表单 — [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) + +在 `v-if="formData.type == 11"` 区块 **最上方** 增加表单项(置于「首笔赢取门槛」之前): + +```vue + + + +``` + +**`setFormData`(type===11 分支)** 增加映射: + +```js +initial_amount: e.initial_amount_qf != null ? e.initial_amount_qf / 1000 : e.initial_amount, +``` + +**`submit`(type===11 分支)** 在 `data.ext_config` 中增加: + +```js +initial_amount_qf: toQf(e.initial_amount), +``` + +### 2. 列表展示(可选但建议)— [index.vue](backend/slot_admin_vue/src/views/game/activity/index.vue) + +在 type===11 的 `ext_config` 模板中增加一行,复用已有 `formatExtAmount`: + +```vue +
活动初始金额: ${{ formatExtAmount(record.ext_config.initial_amount_qf, record.ext_config.initial_amount) }}
+``` + +### 3. 后端校验 — [ActivityValidate.php](backend/slot_admin/app/game/validate/ActivityValidate.php) + +在 `checkFreeCreditsExt()` 的 `$required` 数组追加: + +```php +'initial_amount_qf' => '活动初始金额', +``` + +校验规则与现有金额字段一致:必填、非负整数(千分位)。若业务要求注册赠送必须大于 0,可将条件改为 `(int)$extConfig[$key] <= 0` 时报错(与运营确认;默认可先仅要求 `>= 0`,与「首笔赢取门槛」等一致)。 + +更新方法 PHPDoc:由「6 个金额字段」改为「7 个金额相关字段」。 + +### 4. 无需改动的文件 + +- [ActivityController.php](backend/slot_admin/app/game/controller/ActivityController.php):已按 type===11 调用 `checkFreeCreditsExt`,无需新增分支。 +- `slot_console` / `slot_lib`:本任务只落库配置;注册发奖仍走 `game_base.user_register_reward`,后续若要从 type=11 的 `initial_amount_qf` 读配置,属独立 console 改造。 + +## 数据流示意 + +```mermaid +sequenceDiagram + participant Op as 运营后台 + participant Vue as edit.vue + participant Val as ActivityValidate + participant DB as ext_config JSON + + Op->>Vue: 填写 initial_amount=30 + Vue->>Val: submit initial_amount_qf=30000 + Val->>DB: 校验通过并保存 + Note over DB: 后续 console 可用 configAmount(config,'initial_amount',default) +``` + +## 验证步骤 + +1. 新建/编辑 type=11 活动:可见「活动初始金额」,默认 placeholder 30。 +2. 保存后 DB/API 返回的 `ext_config` 含 `initial_amount_qf`(如 30 → 30000)。 +3. 再次打开编辑:回填为 30(美元)。 +4. 必填校验:留空提交应被 `checkFreeCreditsExt` 拦截。 +5. 列表页 type=11 行展示「活动初始金额」。 +6. 仅改状态(`updateData` 为空)的 update 请求仍不触发 ext 校验(现有逻辑保持不变)。 + +## 风险与说明 + +- **与注册发奖未联动**:本任务只完成后台配置录入;`UserRegisterEventService` 仍读 `game_base.user_register_reward`,改活动配置不会自动改变注册到账,需后续 console 改造读取 `initial_amount_qf`。 +- **全量覆盖 ext_config**:`edit.vue` submit 对 type=11 会重建整个 `ext_config` 对象;新增字段必须同时写入 submit 与 setFormData,避免编辑时丢失其它键(当前实现已是全量重建,与现有一致)。 diff --git a/plans/update_返回值_intval_3a7f24c6.plan.md b/plans/update_返回值_intval_3a7f24c6.plan.md new file mode 100644 index 0000000..e31c166 --- /dev/null +++ b/plans/update_返回值_intval_3a7f24c6.plan.md @@ -0,0 +1,58 @@ +--- +name: update 返回值 intval +overview: 在 WalletUserStatModel::updateWithOptionalVersion 中对 ThinkORM update 返回值做 intval 显式转换,保证声明返回 int 与运行时一致,并统一所有经该方法的 apply*/incField 出口。 +todos: + - id: apply-intval-updateWithOptionalVersion + content: updateWithOptionalVersion 使用 $result + return (int) intval 并补 PHPDoc + status: completed +isProject: false +--- + +# updateWithOptionalVersion 返回值显式转换 + +## 问题 + +[`app/model/WalletUserStatModel.php`](app/model/WalletUserStatModel.php) 中: + +```php +private static function updateWithOptionalVersion(...): int { + // ... + return $query->update($data); // 当前仓库版本 +} +``` + +- `incField` / `applyDeposit` / `applyWithdraw` 等 **全部** 经 `updateWithOptionalVersion` 返回「影响行数」。 +- ThinkORM 链式 `static::where(...)->update($data)` 在 **BaseQuery** 上文档为 `int`,但 IDE/静态分析常因 `Model::update()` 静态方法(返回 `Modelable`)产生 **联合类型**,与 `: int` 不匹配。 +- 你本地已改为 `intval($result)`,这是正确做法:统一把 `false`、数字字符串等收敛为 `int`,避免类型告警与调用方误判。 + +## 修改(单点即可) + +在 [`updateWithOptionalVersion`](app/model/WalletUserStatModel.php) 末尾改为: + +```php +$result = $query->update($data); +return (int) $result; +``` + +或 `return intval($result);`(与你本地写法等价;项目内可统一用 `(int)` 或 `intval`,二选一即可)。 + +**无需** 在每个 `apply*` / `incField` 再包一层转换——它们已 `return static::updateWithOptionalVersion(...)`,改一处即全覆盖。 + +## PHPDoc(可选一行补充) + +在方法 `@return` 说明中注明:「对 ORM `update` 结果做整型转换,无匹配行时为 0」。 + +## 验证 + +Docker 内复用既有冒烟(`incField` / `applyDeposit`),确认返回仍为 `1`/`0` 整数: + +```bash +docker exec -w /app/www/ray/slot-wallet php82 php -r " +# bootstrap 后 WalletUserStatModel::incField(...) +" +``` + +## 不涉及 + +- 不改 MQ / WalletLogic +- 不恢复 `inc()->update()` 链式写法 diff --git a/plans/wallet-bet-win-api-split_8d6f7dea.plan.md b/plans/wallet-bet-win-api-split_8d6f7dea.plan.md new file mode 100644 index 0000000..753d8fd --- /dev/null +++ b/plans/wallet-bet-win-api-split_8d6f7dea.plan.md @@ -0,0 +1,53 @@ +--- +name: wallet-bet-win-api-split +overview: 评估并规划将 bet/win 从统一 update 入口中显式拆分为独立 API,同时保留兼容性与幂等语义。 +todos: + - id: add-bet-win-controller-endpoints + content: 新增 wallet/bet 与 wallet/win 控制器入口,保留 update 兼容 + status: completed + - id: split-validator-dto + content: 拆分 bet/win DTO 与校验场景,减少 type 分支耦合 + status: completed + - id: proxy-update-for-compat + content: 让 update 的 bet/win 分支复用新入口流程,确保行为完全一致 + status: completed + - id: docs-and-migration + content: 补充 README/doc 迁移说明与灰度/下线节奏 + status: completed +isProject: false +--- + +# Bet/Win API 拆分评估与迁移计划 + +## 结论 +- 对资金域来说,**对外 API 语义上拆分 `bet` / `win` 会更好**:可读性、接入防错、风控审计与权限隔离都会更清晰。 +- 但不建议直接废弃 `update`;建议采用“**新增独立接口 + `update` 兼容转发 + 渐进下线**”的迁移路线,避免影响现有上游与历史幂等键。 + +## 现状依据 +- 当前只有一个入口 [`app/api/controller/WalletController.php`](app/api/controller/WalletController.php) 的 `update()`,通过 `type` 分发。 +- 分发逻辑在 [`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php) 的 `ACTION_METHOD_MAP`,`bet/win` 已是独立业务方法。 +- 入参模型 [`app/api/dto/request/wallet/WalletUpdateRequestDTO.php`](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 同时承载多类交易,`round_id`、`is_end` 仅对 bet/win 有意义。 +- 校验器 [`app/validator/Wallet2Validator.php`](app/validator/Wallet2Validator.php) 也是“单场景 + 按 type 条件校验”,存在语义混杂。 + +## 目标形态 +- 提供显式 API:`wallet/bet`、`wallet/win`(自动路由下对应 Controller 方法)。 +- `wallet/update` 保留为兼容入口:内部仅做 DTO 转换与分发,不承载新能力。 +- 资金与幂等规则保持不变:仍以 `biz_id` + `type`(必要时叠加 `round_id`)确保可追溯与幂等。 + +## 实施步骤 +1. 在 [`app/api/controller/WalletController.php`](app/api/controller/WalletController.php) 新增 `bet()` 与 `win()` 方法,复用统一返回封装。 +2. 拆分请求 DTO 与校验场景: + - 新增 bet/win 专用 DTO(从 `WalletUpdateRequestDTO` 中抽取必要字段)。 + - 在 [`app/validator/Wallet2Validator.php`](app/validator/Wallet2Validator.php) 增加 `SCENE_BET`、`SCENE_WIN`,去掉“按 type 再二次判断”的耦合。 +3. 在 Logic 层保持复用: + - Controller 仍调用 [`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php) 现有 `bet()` / `win()` 实现,避免资金路径重写。 + - `update()` 对 bet/win 请求改为调用新入口共享流程(或内部代理),确保行为一致。 +4. 文档与对接迁移: + - 在 [`README.md`](README.md) 与 [`doc/wallet.md`](doc/wallet.md) 补充“新接口 + 兼容期 + 下线节奏”。 + - 给上游约定迁移窗口,监控 `update(type=bet|win)` 调用量后再决定是否下线。 + +## 验证要点 +- 幂等:重复 `biz_id` 命中行为与现状一致。 +- Round:`win` 的 `is_end=0/1` 中间派奖与最终结算语义不变。 +- 资金正确性:下注扣款顺序、派奖分配、流水字段不变。 +- 可回滚:任一阶段可回退为仅使用 `update` 入口。 \ No newline at end of file diff --git a/plans/walletuserstat_model_c0f7d053.plan.md b/plans/walletuserstat_model_c0f7d053.plan.md new file mode 100644 index 0000000..ca79d81 --- /dev/null +++ b/plans/walletuserstat_model_c0f7d053.plan.md @@ -0,0 +1,132 @@ +--- +name: WalletUserStat Model +overview: 为 rh_wallet.wallet_user_stat 新增继承 think\Model 的 WalletUserStatModel;通过 mysql 连接 + 库名.表名 的 $table 绑定跨库表;提供 inc/version 及充提首末次等领域方法;不改 thinkorm 配置与 WalletLogic。 +todos: + - id: wallet-user-stat-model + content: 新增 WalletUserStatModel extends think\Model(connection/table、PHPDoc、inc/apply* 方法) + status: completed + - id: smoke-verify + content: Docker 内冒烟验证 find/applyDeposit 与 version 行为 + status: completed +isProject: false +--- + +# rh_wallet.wallet_user_stat Model 实现计划 + +## 背景与约束 + +- 表:`rh_wallet.wallet_user_stat`,**单库单表、非分片**;主键 `(uid, currency)`;金额为 **x1000 整数**;含 `version` 乐观锁。 +- 与现有 [`WalletStatModel`](app/model/multi/WalletStatModel.php)(`SelfBaseModel` + 分片)**并存**,本次不替换其调用链。 +- **直接继承 `think\Model`**;库表绑定参考项目内惯例:`$connection` + **`$table = '库名.表名'`**(与你提供的 `s_statistics.gm_stat_summary` 写法一致)。 + +```mermaid +flowchart LR + Logic["Logic / Service"] + Model["WalletUserStatModel"] + Conn["connection mysql"] + Table["table rh_wallet.wallet_user_stat"] + + Logic --> Model + Model --> Conn + Model --> Table +``` + +## 实现方案 + +### 单文件:[`app/model/WalletUserStatModel.php`](app/model/WalletUserStatModel.php) + +```php +namespace app\model; + +use think\Model; +use think\facade\Db; + +/** + * 用户钱包累计统计(rh_wallet.wallet_user_stat) + * + * @property int $uid + * @property string $currency + * ... + */ +class WalletUserStatModel extends Model +{ + /** + * The connection name for the model. + * + * @var string|null + */ + protected $connection = 'mysql'; + + /** + * The table associated with the model. + * 跨库:库名.表名 + * + * @var string + */ + protected $table = 'rh_wallet.wallet_user_stat'; + + /** + * 复合主键 + * + * @var array|string + */ + protected $pk = ['uid', 'currency']; +} +``` + +**说明:** + +- **不改** [`config/thinkorm.php`](config/thinkorm.php):沿用现有 `mysql` 连接(`database` 可为空,与分片库同一实例),由 `$table` 全限定名定位 `rh_wallet`。 +- 主键使用 ThinkORM **数组形式** `['uid', 'currency']`,与表 `PRIMARY KEY (uid, currency)` 一致;`find` / `update` / `destroy` 等可传复合键数组。 + +### PHPDoc `@property` + +与表字段一致:`total_deposit_*`、`first/last_deposit_*`、提现/促销/返利/退款/拒付字段、`version`、`created_at`、`updated_at`;金额单位 x1000。 + +### 查询与初始化 + +| 方法 | 说明 | +|------|------| +| `findByUidCurrency(int $uid, string $currency = 'USD'): ?static` | `static::find(['uid' => $uid, 'currency' => $currency])` 或等价 where | +| `lockByUidCurrency(int $uid, string $currency): ?static` | 复合 where + `lock(true)->find()` | +| `insertInitRow(int $uid, string $currency = 'USD'): static` | `static::create(['uid' => $uid, 'currency' => $currency])` | + +### 统计更新(参考 [`WalletStatModel::inc`](app/model/multi/WalletStatModel.php)) + +**通用累加 `incField`:** + +- `$field` 白名单:仅可 `inc` 的 `*_amount` / `*_count` 列 +- `static::where(['uid','currency'])->inc($field, $amount)`,可选 `version` 条件与 `version + 1` + +**领域方法(静态,单次 `update`):** + +| 方法 | 行为 | +|------|------| +| `applyDeposit` | 累加充值金额/次数;更新 `last_deposit_*`;首充写 `first_deposit_*` | +| `applyWithdraw` | 提现对称 | +| `applyPromoBonus` / `applyCashback` / `applyRefund` / `applyChargeback` | 对应累计字段 | + +- 金额参数 `int`,禁止 `float` +- 首充/首提用 `Db::raw('IF(...)')` 保证原子性 +- 返回影响行数 + +### 暂不包含 + +- 不改 [`WalletLogic.php`](app/api/logic/WalletLogic.php)、[`RegisterService.php`](app/service/wallet/RegisterService.php) + +## 验证(实现后) + +```bash +docker exec -w /app/www/slot/slot_wallet php82 php -r " +// bootstrap 后 +var_export(app\model\WalletUserStatModel::findByUidCurrency(1, 'USD')); +" +``` + +## 文件清单 + +| 操作 | 路径 | +|------|------| +| 新增 | `app/model/WalletUserStatModel.php` | + +**无配置变更**(不新增 `rh_wallet` connection)。 diff --git a/plans/win返回余额叠加pending_151a7ce9.plan.md b/plans/win返回余额叠加pending_151a7ce9.plan.md new file mode 100644 index 0000000..68e1fd2 --- /dev/null +++ b/plans/win返回余额叠加pending_151a7ce9.plan.md @@ -0,0 +1,45 @@ +--- +name: win返回余额叠加pending +overview: 仅在 win 派奖流程中,对返回给第三方的余额叠加本局 pending 派奖,解决连续派奖期间玩家看到余额不增长的问题;不改其他接口。 +todos: + - id: win-response-overlay + content: 仅在 win is_end=0 返回值叠加 pendingRoundAmount 到 w/withdraw + status: completed + - id: idempotent-display-check + content: 确认 dedupe 命中时返回余额不重复增长 + status: completed + - id: final-settlement-regression + content: 确认 is_end=1 结算与 pending 清理行为不受影响 + status: completed +isProject: false +--- + +# win 派奖返回余额叠加 pending(仅 win 场景) + +## 目标 +在不改变真实账务入账时机(仍以 `is_end=1` Final Settlement 为准)的前提下,让第三方在连续派奖时拿到“可展示余额”: +- `is_end=0`:返回余额包含本局 pending +- 其他场景:不改 + +## 当前问题定位 +在 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()` 中: +- `is_end=0` 已做 pending 累计 +- 但返回值仍是 DB 真实余额(未叠加 pending),导致第三方展示不变 + +## 最小改动方案 +仅修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`: +1. 在 `is_end=0` 分支中,累计完成后读取 `pendingRoundAmount = getPendingWinByRound()`。 +2. 返回给第三方时,将 `withdraw`/`w` 做展示叠加: + - `w = wallet.withdraw_balance + pendingRoundAmount` + - `withdraw = wallet.withdraw_balance + pendingRoundAmount` +3. `deposit` / `bonus` 保持真实值(不动)。 +4. `is_end=1` 维持现有逻辑:合并 pending 后走真实结算并清理 pending。 + +## 兼容与风险 +- 仅影响 `type=win` 的返回体,不影响 DB、Lot、流水、下注规则。 +- 若第三方把 `w` 当“可立即下注余额”,会产生语义差异(展示值 > 真实可用);但你当前业务前提是第三方只负责展示。 + +## 验收点 +- 连续 `is_end=0`:返回 `w/withdraw` 逐次增长(包含 pending) +- 重复 `biz_id` 的 `is_end=0`:返回值不重复增长 +- `is_end=1` 后:返回值与真实结算后余额一致,pending 被清理 \ No newline at end of file diff --git a/plans/中间派奖余额可见化_175ab934.plan.md b/plans/中间派奖余额可见化_175ab934.plan.md new file mode 100644 index 0000000..7292b64 --- /dev/null +++ b/plans/中间派奖余额可见化_175ab934.plan.md @@ -0,0 +1,91 @@ +--- +name: 中间派奖余额可见化 +overview: 在保持 Final Settlement 真账口径不变的前提下,让 is_end=0 中间派奖实时反映到三方读取的余额(接口返回和钱包查询)。 +todos: + - id: pending-win-keys + content: 设计并接入 pending round/total 与 dedupe Redis key + status: pending + - id: win-is-end-branch + content: 实现 is_end=0 累计展示、is_end=1 合并结算与清理 + status: pending + - id: wallet-query-overlay + content: 让 wallet 查询叠加 pending total 返回最新展示余额 + status: pending + - id: dto-validator-update + content: 补齐 is_end DTO 与 win round_id 校验 + status: pending + - id: regression-cases + content: 验证多次派奖余额可见、最终结算、幂等与失败重试场景 + status: pending +isProject: false +--- + +# 中间派奖余额可见化(is_end)改造计划 + +## 目标 +解决“多次派奖时三方读取余额不更新”的问题,同时保持 `win.md` 的核心原则: +- 真正入账、Lot 回流、转化/解锁只在 `is_end=1`(Final Settlement)执行 +- `is_end=0` 仅做中间派奖累计与展示余额更新 + +## 核心思路 +在 Redis 维护“待结算派奖池(Pending Win)”,并把它叠加到对外返回余额: +- DB 里的 `withdraw/deposit/bonus` 仍保持真实账 +- 对外给三方的余额 = 真实账 + pendingWin + +这样能保证: +- 三方实时看到余额变化 +- 不破坏现有 `WinService` 的 Round Final Settlement 账务逻辑 + +## 具体改造 + +### 1) 新增 Pending Win 缓存层 +修改 [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php):新增 key +- `wallet:win:pending:round:{uid}:{currency}:{round_id}`(每局中间派奖累计) +- `wallet:win:pending:total:{uid}:{currency}`(用户币种维度累计,供快速读余额) +- `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}`(中间派奖严格幂等) + +### 2) win 分支行为 +修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`: +- `is_end=0`: + - 命中 dedupe 直接返回 + - 未命中则 `INCRBY pending:round` 与 `INCRBY pending:total` + - 返回余额时把 `pending:total` 叠加到 `withdraw`(仅对外展示) + - 不调用 `WinService::execute`、不写 `BIZ_TYPE_WIN` +- `is_end=1`: + - 读取 `pending:round`,`finalWin = fee + pendingRound` + - 用 `finalWin` 调用现有 [app/service/wallet/WinService.php](app/service/wallet/WinService.php) + - 成功后清理 `pending:round` 并从 `pending:total` 扣除对应值 + - 返回余额按真实账(此时 pending 已回收) + +### 3) 钱包查询返回也要可见化 +修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `query()` 返回: +- 将 `pending:total` 叠加到返回字段 `w/withdraw` +- 这样第三方无论读 `win` 返回还是调用 `wallet` 查询,都看到“最新展示余额” + +### 4) DTO/校验补齐 +- 在 [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加 `is_end`(默认 1) +- 在 [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php) 增加: + - `is_end` 仅允许 `0/1` + - `win` 场景强制 `round_id` 必传 + +## 一致性与幂等 +- `is_end=0`:Redis dedupe(biz_id 级)防重复累计 +- `is_end=1`:延续现有 `wallet_log(uid,biz_id,biz_type=win)` 幂等 +- 若 Final 失败,不清 pending,便于重试恢复 + +## 风险说明(需明确) +- 该方案是“展示余额实时、真账延后”:若上游把展示余额当可立即可下注余额,可能出现下注时余额校验不一致。 +- 若你需要“中间派奖立即可下注”,则要走另一套方案(中间账临时入库 + Final 对冲重分配),改动会显著更大。 + +## 改动文件 +- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) +- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) +- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php) +- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) + +## 验收场景 +- 多次 `is_end=0` 后,`win` 返回余额递增 +- 多次 `is_end=0` 后,`wallet` 查询余额递增 +- `is_end=1` 触发后,final 入账金额 = 中间累计 + 最后一笔 +- 重放同一 `is_end=0` `biz_id` 不重复累计 +- 重放同一 `is_end=1` `biz_id` 不重复结算 \ No newline at end of file diff --git a/plans/任务进度筛选对接_acbb7213.plan.md b/plans/任务进度筛选对接_acbb7213.plan.md new file mode 100644 index 0000000..919643b --- /dev/null +++ b/plans/任务进度筛选对接_acbb7213.plan.md @@ -0,0 +1,228 @@ +--- +name: 任务进度筛选对接 +overview: 新增独立分页接口 `player-task/task-list`(筛选 task_type + status + page/page_size),供前端双下拉表格使用;保留现有 `player-task/task-progress` 全量快照不变。同步 slot_sdk 与 API 文档。 +todos: + - id: wallet-model-paginate + content: WalletFundLotModel 新增 paginatePlayerTaskLots(按 lot_type/status 分页) + status: completed + - id: wallet-task-list-api + content: 新增 taskList 全链路(Validator/DTO/Logic/Service/Controller) + status: completed + - id: sdk-task-list + content: slot_sdk 新增 taskList 请求/响应实体与 WalletService 方法 + status: completed + - id: api-doc-list + content: 新增 doc/player-task-list-api.md(含前端双下拉 + 分页说明) + status: completed + - id: feature-tests-list + content: Feature 测试:筛选、分页、参数校验;task-progress 回归不变 + status: completed +isProject: false +--- + +# 玩家任务进度:新增分页列表接口 + +## 背景与目标 + +- **现有** [`player-task/task-progress`](doc/player-task-progress-api.md):只读全量快照(`bonus_tasks` + `deposit_tasks` + `summary`),**不改契约**,继续给大厅/SDK 轻量查询用。 +- **新增** `player-task/task-list`:面向 **前端操作页**(双下拉 + 表格),支持 **类型筛选、状态筛选、分页**。 + +前端操作区: + +| 下拉框 | 选项 | 说明 | +|--------|------|------| +| 任务类型 | **Bonus** / **Deposit** | 必填,二选一 | +| 状态 | **全部** + Waiting / Active / PendingConversion / PlayedOut | `status=0` 或不传 = 全部可见态 | + +```mermaid +flowchart TB + subgraph keep [保持不变] + TP["POST task-progress"] + TP --> Full["bonus_tasks + deposit_tasks 全量"] + end + subgraph newApi [新增] + TL["POST task-list"] + TL --> Filter["task_type + status"] + TL --> Page["page + page_size"] + Page --> List["list + 分页元数据"] + end + UI[前端表格页] --> TL + Lobby[大厅轻量展示] --> TP +``` + +--- + +## 1. 新接口契约 + +### 1.1 路由 + +| 项 | 值 | +|----|-----| +| Method | `POST` | +| Path | `player-task/task-list` | +| Controller | [`PlayerTaskController::taskList`](app/api/controller/PlayerTaskController.php)(新增方法) | + +### 1.2 请求参数 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `uid` | int | 是 | 用户 ID | +| `currency` | string | 是 | 币种,如 `TGO` | +| `task_type` | string | **是** | `bonus` \| `deposit` | +| `status` | int | 否 | `0` 或省略 = 全部玩家可见态;`1`/`2`/`3`/`5` = 单态 | +| `page` | int | 否 | 页码,默认 `1`,最小 `1` | +| `page_size` | int | 否 | 每页条数,默认 `20`,建议上限 `100` | + +校验([`PlayerTaskValidator`](app/validator/PlayerTaskValidator.php) 新 scene `list`): + +- `uid`:`require|integer` +- `currency`:`require` +- `task_type`:`require|in:bonus,deposit` +- `status`:`integer|in:0,1,2,3,5`(可空,默认 0) +- `page`:`integer|egt:1`(可空) +- `page_size`:`integer|between:1,100`(可空) + +**前端映射**: + +| UI | 请求 | +|----|------| +| Bonus | `"task_type": "bonus"` | +| Deposit | `"task_type": "deposit"` | +| 全部 | 不传 `status` 或 `"status": 0` | +| Waiting / Active / … | `status` = `1` / `2` / `3` / `5` | +| 翻页 | 修改 `page`(切换筛选时重置 `page=1`) | + +### 1.3 成功响应 `data` + +与仓库 [`SearchShardService`](app/service/search/shard/SearchShardService.php) 分页习惯对齐(`data` → `list`): + +```json +{ + "task_type": "bonus", + "list": [ /* PlayerTaskItem,与 task-progress 任务项字段相同 */ ], + "total": 15, + "page": 1, + "page_size": 20, + "last_page": 1 +} +``` + +- `list[]` 元素结构 **复用** [`PlayerTaskItemEntity::toApiArray()`](app/entity/wallet/PlayerTaskItemEntity.php)(与 `task-progress` 单条一致)。 +- 排序:`consume_priority_at ASC`, `lot_id ASC`(与现 [`listPlayerTaskLots`](app/model/multi/WalletFundLotModel.php) 一致)。 +- 状态范围:默认仍为 PRD §26.1 四态 `[1,2,3,5]`;**不含** Completed/Cancelled/Reversed(与 `task-progress` 一致)。 + +### 1.4 与 `task-progress` 的分工 + +| 接口 | 场景 | 返回 | +|------|------|------| +| `task-progress` | 一次拿全量两类任务 + 计数汇总 | `summary` + `bonus_tasks` + `deposit_tasks` | +| `task-list` | 表格页:选定类型 + 状态 + 翻页 | 单维 `list` + 分页字段 | + +--- + +## 2. wallet 实现要点 + +### 2.1 Model + +在 [`WalletFundLotModel`](app/model/multi/WalletFundLotModel.php) 新增: + +```php +/** + * 分页查询玩家任务 Lot。 + * @return array{list: array, total: int, page: int, page_size: int, last_page: int} + */ +public function paginatePlayerTaskLots( + string $currency, + int $lotType, + array $statuses, + int $page, + int $pageSize +): array +``` + +- `statuses` 为空时直接返回空分页(与 `listPlayerTaskLots` 一致)。 +- 使用 ThinkORM `paginate($pageSize, false, ['page' => $page])`,再将 `data` 重命名为 `list`。 + +### 2.2 Service + +在 [`PlayerTaskQueryService`](app/service/wallet/PlayerTaskQueryService.php) 新增 `queryTaskList(...)`: + +- `task_type` 字符串 → `LOT_TYPE_BONUS` / `LOT_TYPE_DEPOSIT` +- `status` → `PLAYER_VISIBLE_STATUSES` 或单元素数组 +- 调用 Model 分页后,`buildTaskItems()` 组装 `list` + +### 2.3 分层文件(新增/扩展) + +| 层级 | 文件 | +|------|------| +| DTO | `app/api/dto/request/PlayerTaskListRequestDTO.php`(新建) | +| Validator | `PlayerTaskValidator` 增加 `SCENE_LIST` | +| Logic | `PlayerTaskLogic::taskList()` | +| Controller | `PlayerTaskController::taskList()` | + +**不修改** `PlayerTaskProgressRequestDTO` / `taskProgress` 逻辑。 + +--- + +## 3. slot_sdk + +新增(与 wallet 字段 snake_case 一致): + +| 类型 | 文件 | +|------|------| +| 请求 | `PlayerTaskListRequestEntity.php` | +| 响应 | `PlayerTaskListResponseEntity.php`(含 `list`、`total`、`page`、`page_size`、`last_page`、`task_type`) | + +[`WalletService`](slot_sdk/src/service/wallet/WalletService.php) 新增: + +```php +public function taskList(PlayerTaskListRequestEntity $entity): ?PlayerTaskListResponseEntity +// POST api/player-task/task-list +``` + +`taskProgress` / `PlayerTaskProgressRequestEntity` **保持不变**。 + +发布:slot_sdk commit → push → slot-wallet `composer update`。 + +--- + +## 4. 文档 + +- **新建** [`doc/player-task-list-api.md`](doc/player-task-list-api.md):路由、入参、响应、前端双下拉 + 翻页交互、与 `task-progress` 对比、curl 示例。 +- [`doc/player-task-progress-api.md`](doc/player-task-progress-api.md) 顶部增加「列表分页请用 task-list」交叉引用。 +- [`doc/wallet.md`](doc/wallet.md) §26.1 补充 `task-list` 一行说明。 + +--- + +## 5. 测试 + +新建 `tests/Feature/PlayerTaskListTest.php`(造数参考 [`WalletRegisterBetWinTest`](tests/Feature/WalletRegisterBetWinTest.php)): + +| 用例 | 断言 | +|------|------| +| `task_type=bonus` + 默认分页 | `list` 非空项字段完整;`total >= len(list)` | +| `status=2` | `list` 内 `status` 均为 2 | +| `page=2` + `page_size=1` | 第二页与总数一致 | +| 缺 `task_type` | `40003` | +| `page_size=101` | `40003` | +| `task-progress` 仍返回双数组 | 回归,不受新接口影响 | + +容器内执行:`docker compose exec -T php82` + 项目 PHPUnit 命令。 + +--- + +## 6. 不在本次范围 + +- 修改 `task-progress` 入参或响应 +- 任务类型「全部」合并在一个列表 +- 运营后台终态(Completed 等)纳入筛选 +- 前端页面实现(本仓库无前端) + +--- + +## 7. 关键文件一览 + +| 仓库 | 变更 | +|------|------| +| slot-wallet | `PlayerTaskController`、`PlayerTaskLogic`、`PlayerTaskQueryService`、`WalletFundLotModel`、`PlayerTaskValidator`、新 DTO、`doc/player-task-list-api.md`、Feature 测试 | +| slot_sdk | 新 Entity ×2、`WalletService::taskList`、`readme.md` | diff --git a/plans/修复第一档绑卡类型_1c86ae8f.plan.md b/plans/修复第一档绑卡类型_1c86ae8f.plan.md new file mode 100644 index 0000000..25ea92b --- /dev/null +++ b/plans/修复第一档绑卡类型_1c86ae8f.plan.md @@ -0,0 +1,115 @@ +--- +name: 修复第一档绑卡类型 +overview: "`WithdrawService::apply` 在 Free Credits 第一档(`package_id > 0`)分支把 `checkBankInfo` 返回的收款配置数组传给了需要 `UserBankCardModel` 的 `applyFreeCreditsFirstCashout`,导致 PHP 8 类型错误且 Pay 载荷取不到 `btc/usdt/email` 等字段。" +todos: + - id: refactor-checkBankInfo-return + content: WithdrawService::checkBankInfo 返回 ['model' => UserBankCardModel, 'config' => itemConfig] 并补 PHPDoc + status: cancelled + - id: fix-apply-branch + content: WithdrawService::apply 解构返回值:FC 传 model,普通提现用 config + status: cancelled + - id: run-integration-test + content: php82 跑 FreeCreditsFirstCashoutApplyTest 验证无 TypeError + status: completed +isProject: false +--- + +# 修复第一档提现绑卡类型不匹配 + +## 问题 + +[`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 中: + +```171:175:slot_console/app/service/WithdrawService.php + $bankInfo = $this->checkBankInfo($applyDTO); + + if ($applyDTO->package_id > 0) { + return $this->applyFreeCreditsFirstCashout($applyDTO, $bankInfo); + } +``` + +[`checkBankInfo`](slot_console/app/service/WithdrawService.php) 在持久化 `UserBankCardModel` 后 **返回的是按支付类型整理的 `$itemConfig` 数组**(约 353 行 `return $itemConfig`),供普通提现填 `WithdrawalInfo`: + +```216:219:slot_console/app/service/WithdrawService.php + $withdrawalInfo->account = $bankInfo['account']; + $withdrawalInfo->address = $bankInfo['account']; + $withdrawalInfo->userName = $bankInfo['user_name'] ?? $userTag->uid; + $withdrawalInfo->card_number = $bankInfo['card_number'] ?? ''; +``` + +而 [`applyFreeCreditsFirstCashout`](slot_console/app/service/WithdrawService.php) / [`FreeCreditsLogic::buildFirstCashoutWithdrawalInfo`](slot_console/app/api/logic/FreeCreditsLogic.php) 签名要求 **`UserBankCardModel`**,并读取模型字段: + +```490:507:slot_console/app/api/logic/FreeCreditsLogic.php + 'userName' => $post['user_name'] ?? $bank->user_name ?? strval($uid), + 'account' => empty($bank->card_number) ? strval($uid) : $bank->card_number, + ... + if ($payType === 2) { + $withdrawalInfo->address = $bank->btc ?? ''; + } elseif ($payType === 3) { + $withdrawalInfo->address = $bank->usdt ?? ''; + } elseif ($payType === 6) { + $withdrawalInfo->userName = trim($bank->paypal_first_name . ' ' . $bank->paypal_last_name); + $withdrawalInfo->account = $bank->email ?? ''; +``` + +因此第一档路径在 PHP 8 下会触发 `TypeError`;即便未开严格类型,用数组当对象也会导致 Pay 下单字段错误。 + +```mermaid +flowchart LR + apply[WithdrawService.apply] + checkBank[checkBankInfo] + saveModel[save UserBankCardModel] + returnConfig["return itemConfig array"] + fcBranch[package_id greater than 0] + normalBranch[normal withdraw] + fcMethod[applyFreeCreditsFirstCashout] + logic[buildFirstCashoutWithdrawalInfo] + apply --> checkBank + checkBank --> saveModel --> returnConfig + returnConfig --> fcBranch + returnConfig --> normalBranch + fcBranch -->|"wrong: array"| fcMethod + fcMethod --> logic + logic -->|"expects UserBankCardModel"| modelFields[bank.card_number btc usdt] +``` + +## 推荐改法(单文件、无二次查询) + +在 [`WithdrawService.php`](slot_console/app/service/WithdrawService.php) 调整 `checkBankInfo` 返回值,使 `apply` 能同时满足两条路径: + +1. **`checkBankInfo` 返回结构体**(关联数组即可,无需新 DTO): + - `model` → 已 `save()` 的 `UserBankCardModel` + - `config` → 现有 `$itemConfig`(普通提现继续用) + +2. **`apply` 解构**: + - `package_id > 0` → `applyFreeCreditsFirstCashout($applyDTO, $bank['model'])` + - 否则 → `$bankConfig = $bank['config']`,后续 `account` / `user_name` / `card_number` 逻辑不变 + +3. **补 PHPDoc**(符合 [`php-doc.mdc`](/Users/ray/.cursor/rules/php-doc.mdc)): + - `checkBankInfo`: `@return array{model: UserBankCardModel, config: array}` + - `applyFreeCreditsFirstCashout`: 保持现有 `@param UserBankCardModel` + +**不改动** `FreeCreditsLogic`:其契约(绑卡已由 `WithdrawService` 写入后传入模型)是正确的。 + +### 备选(更小 diff,多一次查询) + +若不想改 `checkBankInfo` 签名,可在 `package_id > 0` 分支先调用 `checkBankInfo` 落库,再 `find($uid)` 取模型。可行但冗余,不推荐。 + +## 测试 + +- 现有集成测 [`FreeCreditsFirstCashoutApplyTest`](slot_console/tests/Integration/FreeCreditsFirstCashoutApplyTest.php) 已走 `WithdrawService::apply` + `package_id`;修复后应能越过 `TypeError`,继续表现为 Pay 不可用时的 `Throwable` / 余额校验未触发。 +- 修复后在 `php82` 容器执行: + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Integration/FreeCreditsFirstCashoutApplyTest.php +``` + +(需 `RUN_DB_TESTS=1` 时按项目惯例配置环境变量。) + +可选:在 `tests/Unit` 增加轻量测试,用反射调用 `checkBankInfo` 断言返回含 `model` 与 `config` 键——非必须,集成测已覆盖主路径。 + +## 验收 + +1. 第一档 `POST /api/withdraw/apply` + `package_id` 不再出现 `UserBankCardModel` 类型错误。 +2. 普通提现(无 `package_id`)行为与字段映射不变。 +3. Pay 下单能正确带上 `card_number` / `btc` / `usdt` / PayPal 等模型字段(与 `checkBankInfo` 写入一致)。 diff --git a/plans/充值用户_redis_集合_a31b1665.plan.md b/plans/充值用户_redis_集合_a31b1665.plan.md new file mode 100644 index 0000000..581385b --- /dev/null +++ b/plans/充值用户_redis_集合_a31b1665.plan.md @@ -0,0 +1,202 @@ +--- +name: 充值用户 Redis 集合 +overview: 在 center 注册共享 Redis key `userRecharged`,wallet 侧一次性脚本从分片 `wallet_stat` 回填历史充值用户,并在 `recharge` / `rechargeSign` 成功后实时 SADD 到共享 Redis 集合。 +todos: + - id: center-key + content: slot_center sharerediskey.php 增加 userRecharged => user:recharged + status: completed + - id: wallet-service + content: wallet:RedisKeyManagerService + RechargedUidSetService(share Redis SADD) + status: completed + - id: wallet-hook + content: WalletLogic::recharge / rechargeSign 成功后调用 RechargedUidSetService::add + status: completed + - id: backfill-cmd + content: 新建 backfillRechargedUidSet 命令:扫 wallet_stat 分片、--dry-run/--rebuild/--batch-size + status: completed + - id: tests + content: 补充 Service/集成测试或 dry-run 验证说明 + status: completed +isProject: false +--- + +# Wallet 充值用户共享 Redis 集合 + +## 目标 + +维护一个**全局、持久**的共享 Redis Set,成员为「曾成功充值」的用户 uid(口径:`wallet_stat.total_deposit > 0`,与 [每日返水需求](file:///Users/ray/.cursor/plans/每日返水需求文档_20a03147.plan.md) 中「充值用户」一致)。供后续活动(如每日返水结算)用 `SISMEMBER` 快速判断,避免逐用户查库。 + +你已确认: +- Key:`userRecharged` → Redis `user:recharged` +- `recharge` 与 `rechargeSign` 均写入集合 + +--- + +## 架构与数据流 + +```mermaid +flowchart LR + subgraph center [slot_center] + Sharerediskey["config/sharerediskey.php"] + ShareConfigAPI["/innerapi/share-config/all"] + end + subgraph wallet [slot_wallet] + BackfillCmd["BackfillRechargedUidSet command"] + WalletLogic["WalletLogic::recharge / rechargeSign"] + RechargedSvc["RechargedUidSetService"] + StatDB["wallet_XX.wallet_stat_YY"] + end + ShareRedis["Redis connection share"] + Sharerediskey --> ShareConfigAPI + ShareConfigAPI --> RechargedSvc + BackfillCmd --> StatDB + BackfillCmd --> RechargedSvc + WalletLogic --> RechargedSvc + RechargedSvc --> ShareRedis +``` + +--- + +## 1. Center:注册共享 Key + +**文件**:[`slot_center/config/sharerediskey.php`](slot_center/config/sharerediskey.php) + +在数组末尾增加(含注释): + +```php +'userRecharged' => 'user:recharged', // set:曾充值用户 uid(wallet 维护) +``` + +该文件已通过 [`ShareConfigController::all()`](slot_center/app/innerapi/controller/ShareConfigController.php) 下发为 `sharerediskey.*`,各服务经 `ShareConfigService::get('sharerediskey.userRecharged')` 读取。 + +**部署顺序**:先发布 center,再发布 wallet(否则 wallet 会落到 fallback 默认值,key 不一致)。 + +--- + +## 2. Wallet:Key 解析与写入封装 + +### 2.1 `RedisKeyManagerService` + +**文件**:[`slot_wallet/app/service/RedisKeyManagerService.php`](slot_wallet/app/service/RedisKeyManagerService.php) + +新增方法(与现有 `sourceRealDataKey` 一致,从 center 取 key): + +```php +public static function getUserRechargedSetKey(): string +{ + return ShareConfigService::get('sharerediskey.userRecharged', 'user:recharged'); +} +``` + +### 2.2 新建 `RechargedUidSetService` + +**路径**:`slot_wallet/app/service/wallet/RechargedUidSetService.php` + +职责(公共能力,避免 Logic 直接拼 Redis): + +- `add(int $uid): void` — `Redis::connection('share')->sAdd($key, (string)$uid)` +- key 来自 `RedisKeyManagerService::getUserRechargedSetKey()` +- 失败只 `LoggerService::error`,不抛异常(与 `WalletLogic::recharge()` 内 `sendConsoleBus` / `createTask` 等非阻断副作用一致) +- **不设 expire**(成员为永久「曾充值」标记;`SADD` 幂等) + +参考:[`slot_risk`](slot_risk/app/service/api/BlackUserService.php) 对 `connection('share')->sAdd` 的用法;[`config/redis.php`](slot_wallet/config/redis.php) 已配置 `share` 连接。 + +--- + +## 3. 充值链路:实时写入 + +**文件**:[`slot_wallet/app/api/logic/WalletLogic.php`](slot_wallet/app/api/logic/WalletLogic.php) + +在以下两处、`total_deposit` 递增且 `RechargeExchangeService::recharge()` 之后,调用 `RechargedUidSetService::add($uid)`: + +| 方法 | 约行号 | 说明 | +|------|--------|------| +| `recharge()` | ~782 后 | 正常支付充值 | +| `rechargeSign()` | ~820 后 | 签到购,累加统计 | + +放在现有 `try/catch` 块内即可,无需单独 try。 + +**不扩展范围**:全库仅这两处递增 `total_deposit`(已 grep 确认),无需 hook `inc()` 或其它 type。 + +--- + +## 4. 一次性回填脚本 + +### 4.1 Webman Command + +**路径**:`slot_wallet/app/command/BackfillRechargedUidSet.php` + +- `$defaultName = 'backfillRechargedUidSet'` +- 结构参考 [`CleanupWalletLog.php`](slot_wallet/app/command/CleanupWalletLog.php):`LoggerService::initGenerateTraceId`、stdout 汇总、异常返回 `FAILURE` + +**选项**: + +| Option | 作用 | +|--------|------| +| `--dry-run` | 只统计 uid 数量,不写 Redis | +| `--rebuild` | 执行前 `DEL` 目标 set(全量重建;默认增量 SADD,可重复跑) | +| `--batch-size` | 每批 `SADD` 成员数,默认 500 | + +### 4.2 扫描逻辑 + +按 [`InitDB::initWalletStat`](slot_wallet/app/command/InitDB.php) 的分片循环: + +```php +for ($i = 1; $i <= $shardInfo['database_num']; $i++) { + $db = sprintf('wallet_%02d', $i); + for ($j = 1; $j <= $shardInfo['table_num']; $j++) { + $table = "{$db}.wallet_stat_" . sprintf('%02d', $j); + // SELECT DISTINCT uid FROM {$table} WHERE total_deposit > 0 + } +} +``` + +- 使用 `DISTINCT uid`(表主键 `(uid, currency)`,多币种各一行,同一 uid 只入 set 一次) +- 分批 `sAdd($key, ...$uids)` 写入 `connection('share')` +- 输出:每表行数、总 uid 数、最终 `SCARD` + +### 4.3 执行命令(Docker) + +```bash +docker exec -w /app/www/slot/slot_wallet php82 php webman backfillRechargedUidSet --rebuild +# 预检 +docker exec -w /app/www/slot/slot_wallet php82 php webman backfillRechargedUidSet --dry-run +``` + +**建议上线步骤**: + +1. 发布 center(`userRecharged` 配置) +2. 发布 wallet(Service + Logic + Command) +3. 低峰执行 `backfillRechargedUidSet --rebuild`(或先 `--dry-run` 核对数量) +4. 之后新充值由 Logic 自动 `SADD` + +--- + +## 5. 测试(wallet 仓库) + +| 项 | 方式 | +|----|------| +| `RechargedUidSetService` | 单元测试 mock `Redis::connection('share')`,或集成测试用真实 share Redis(若 CI 有) | +| `WalletLogic` | 在现有 [`WalletControllerUpdateTest`](slot_wallet/tests/Integration/WalletControllerUpdateTest.php) / recharge 相关用例后断言 set 含 uid(可选,依赖 share Redis) | +| Command | 本地 `--dry-run` + 小环境 `--rebuild` 后 `SCARD` 抽样 `SISMEMBER` | + +至少保证:Service 在 key 存在时调用 `sAdd` 且不抛异常;Command SQL 与分片配置可读。 + +--- + +## 6. 不在本次范围(供后续) + +- **消费方**(如 `slot_console` 每日返水 cron)改为 `SISMEMBER`:需在对应服务增加 `ShareConfigService::get('sharerediskey.userRecharged')` + `connection('share')`,本次仅 wallet 写入。 +- center 本地 [`ShareRedisKeyManagerService`](slot_center/app/service/ShareRedisKeyManagerService.php) 可不增方法(wallet 直接用 `RedisKeyManagerService` + `ShareConfigService`,与现有 `sourceReal` 一致)。 + +--- + +## 涉及文件一览 + +| 仓库 | 文件 | 变更 | +|------|------|------| +| slot_center | `config/sharerediskey.php` | +1 key | +| slot_wallet | `app/service/RedisKeyManagerService.php` | +`getUserRechargedSetKey()` | +| slot_wallet | `app/service/wallet/RechargedUidSetService.php` | 新建 | +| slot_wallet | `app/api/logic/WalletLogic.php` | recharge / rechargeSign 各 +1 行调用 | +| slot_wallet | `app/command/BackfillRechargedUidSet.php` | 新建一次性脚本 | diff --git a/plans/定格冻结用户流水_68d9c5d1.plan.md b/plans/定格冻结用户流水_68d9c5d1.plan.md new file mode 100644 index 0000000..02388d8 --- /dev/null +++ b/plans/定格冻结用户流水_68d9c5d1.plan.md @@ -0,0 +1,123 @@ +--- +name: 定格冻结用户流水 +overview: 在 slot_wallet 的 `freeCreditsFreeze` 成功扣款后,补充 `addTransactionRecord` 写入 C 端可见的 `user_transaction_log`;并在 slot_pwa 注册新 `transaction_type` 与列表筛选映射。 +todos: + - id: wallet-add-transaction + content: WalletLogic::freeCreditsFreeze 增加 addTransactionRecord(type=65,负金额) + status: pending + - id: pwa-type-map + content: slot_pwa UserTransactionLogService 注册 type 65 与展示名 + status: pending + - id: wallet-unit-test + content: wallet 单测断言 freeCreditsFreeze 写入用户流水 + status: pending + - id: run-phpunit + content: php82 跑 wallet 相关单测验证 + status: pending +isProject: false +--- + +# freeCreditsFreeze 补充用户可见流水 + +## 问题 + +[`WalletLogic::freeCreditsFreeze()`](slot_wallet/app/api/logic/WalletLogic.php) 当前仅: + +1. `minus()` 扣减 deposit/withdraw +2. [`addLog()`](slot_wallet/app/api/logic/WalletLogic.php) 写入分表 **wallet_log**(`biz_type=freeCreditsFreeze`) + +**未**调用 [`addTransactionRecord()`](slot_wallet/app/api/logic/WalletLogic.php),因此 MQ 不会投递到 PWA,用户在 **Transaction / 流水页** 看不到定格扣款记录。 + +对比:普通提现冻结在同文件内已有: + +```402:402:slot_wallet/app/api/logic/WalletLogic.php + $this->addTransactionRecord('withdraw frozen', $this->requestDTO->fee * -1, $balance, 7); +``` + +需求文档 §15.2 要求「必须写钱包流水」——`wallet_log` 已满足后台审计;本需求补齐 **C 端展示流水**。 + +```mermaid +flowchart LR + freeze[freeCreditsFreeze] + addLog[addLog wallet_log] + addTx[addTransactionRecord] + mq[PWA MQ] + ui[用户流水列表] + freeze --> addLog + freeze --> addTx --> mq --> ui +``` + +说明:`freeCreditsFreeze` 是**扣款**(金额为负),不是入账;用户列表里应显示为负向变动(与 type 7「Withdraw」冻结类似)。 + +## 实现方案(slot_wallet + slot_pwa) + +### 1. slot_wallet — `freeCreditsFreeze` 增加用户流水 + +文件:[`WalletLogic.php`](slot_wallet/app/api/logic/WalletLogic.php) + +在事务内、`addLog` 成功之后、`commit` 之前增加(与 `withdrawFrozen` 一致,失败仅记日志不回滚主事务): + +```php +$this->addTransactionRecord( + 'Free Credits Freeze', + $this->requestDTO->fee * -1, + $balance, + self::TRANSACTION_TYPE_FREE_CREDITS_FREEZE // 新常量,建议 65 +); +``` + +在 `WalletLogic` 类内新增常量(或集中放在与 PWA 对齐的注释块): + +```php +/** 用户流水类型:首充定格冻结扣款,对应 freeCreditsFreeze */ +private const TRANSACTION_TYPE_FREE_CREDITS_FREEZE = 65; +``` + +字段约定(与现有 `addTransactionRecord` 一致): + +| 字段 | 值 | +| --- | --- | +| `subject` | `Free Credits Freeze`(C 端 `name` 映射用) | +| `amount` | `-fee`(千分位,负数为扣款) | +| `balance_after` | 扣款后 `deposit + withdraw` 总余额 | +| `transaction_id` | 已有规则:`substr(uid,0,3) + biz_id`(幂等键 `free_credits_freeze:{orderId}`) | +| `transaction_type` | `65` | + +**不**改 `addLog` / 余额扣减逻辑;**不**改 slot_console / slot_lib。 + +### 2. slot_pwa — 注册 transaction_type=65 + +文件:[`UserTransactionLogService.php`](slot_pwa/app/service/UserTransactionLogService.php) + +- `$typeMaps[-1]`(All)增加 `65` +- `$typeMaps[1]`(Withdraw 分类,已含 7/8)增加 `65`(定格扣款与提现类支出归同一 tab,可按产品再调) +- `$typeToClient[65] = 'Free Credits Freeze'`(或简短文案 `Free Credits`) + +可选:在 [`UserTransactionLogShardModel.php`](slot_pwa/app/model/shard/UserTransactionLogShardModel.php) 增加 `TRANSACTION_TYPE_FREE_CREDITS_FREEZE = 65` 常量与注释,wallet 侧用相同数字(wallet 不依赖 pwa 类,注释对齐即可)。 + +### 3. 单测 + +| 仓库 | 文件 | 内容 | +| --- | --- | --- | +| slot_wallet | 扩展 [`WalletLogicHarness`](slot_wallet/tests/Support/WalletLogicHarness.php) 或新建用例 | 覆写/桩 `addTransactionRecord`,断言 `freeCreditsFreeze` 成功后调用一次,且 `amount < 0`、`type=65` | +| slot_wallet | 现有 freeze 相关测 | 回归通过 | + +运行: + +```bash +docker exec -w /app/www/slot/slot_wallet php82 ./vendor/bin/phpunit tests/Unit/WalletFreeCreditsFirstKeepTest.php tests/Unit/WalletFreeCreditInitBusTest.php +# 若有新增 FreezeTransactionTest 则一并跑 +``` + +## 不在本次范围 + +- `freeCreditsClaim` / `freeCreditsFirstCashKeep` 的用户流水(若也要在流水页展示入账,可另开任务,同样走 `addTransactionRecord` + 新 type) +- 修改 `wallet_log` 表结构 +- C 端 UI 文案多语言(仅后端 `name` 英文字段) + +## 验收 + +1. 首充定格成功后,用户流水列表(All / Withdraw)出现一条 **负金额** 记录,`name` 为 Free Credits Freeze +2. `transaction_id` 与 `biz_id`(`free_credits_freeze:{orderId}`)可追溯 +3. 幂等重试 freeze:wallet_log 幂等不变;若 MQ 重复需依赖现有消费端去重(与 type 7 行为一致,本次不单独改 MQ) +4. wallet 单测通过 diff --git a/plans/定格列表盈利阈值_5a7a1ed2.plan.md b/plans/定格列表盈利阈值_5a7a1ed2.plan.md new file mode 100644 index 0000000..621b134 --- /dev/null +++ b/plans/定格列表盈利阈值_5a7a1ed2.plan.md @@ -0,0 +1,95 @@ +--- +name: 定格列表盈利阈值 +overview: 在管理后台首充定格统计列表中,按当前页 UID 调用与 game/pop/index 相同的养客配置接口(PopService → slot_pwa getDPByPlayers),为每行补充 profitthreshold(及可选 hasprofitthreshold)字段,前端新增对应列。 +todos: + - id: pop-service-batch + content: PopService 新增 mapDevelopPlayerByUids,按 uid 调用 getPageList 并容错 + status: completed + - id: logic-enrich + content: FreeCreditsStatsLogic.list 合并 profitthreshold(及可选 hasprofitthreshold) + status: completed + - id: vue-columns + content: freeCreditsStats/index.vue 增加盈利阈值列与展示格式化 + status: completed + - id: verify-pop-parity + content: 抽 1~2 个 UID 对比养客配置页与定格列表字段一致 + status: completed +isProject: false +--- + +# 首充定格列表增加盈利阈值 + +## 背景 + +- 首充定格统计已迁入 [`slot_admin`](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php),接口 `GET /game/freeCreditsStats/index` 返回列表 + 顶部汇总。 +- 养客配置页 [`game/pop/index`](backend/slot_admin_vue/src/views/game/pop/config/index.vue) 通过 [`PopController::index`](backend/slot_admin/app/game/controller/PopController.php) → [`PopService::getPageList`](backend/slot_admin/app/service/game/PopService.php) → `slot_pwa` `pop/admin/getDPByPlayers` → 游戏侧 `/api/v2/dp/getDPByPlayers`,列表含 `profitthreshold`、`hasprofitthreshold` 等字段。 +- 需求文档 §18.2 未写盈利阈值,属运营侧补充字段;数据源应与养客页一致,**不用** `UserTag.popBalance` 等本地 tag。 + +## 数据流 + +```mermaid +flowchart LR + vue[freeCreditsStats/index.vue] --> api["/game/freeCreditsStats/index"] + api --> logic[FreeCreditsStatsLogic.list] + logic --> db[(s_common free_credits_*)] + logic --> wallet[WalletService statistic] + logic --> pop[PopService getPageList] + pop --> pwa[slot_pwa getDPByPlayers] + pwa --> game["/api/v2/dp/getDPByPlayers"] +``` + +## 实现要点 + +### 1. PopService:按 UID 批量取养客配置(公共封装) + +在 [`PopService.php`](backend/slot_admin/app/service/game/PopService.php) 新增方法,例如 `mapDevelopPlayerByUids(array $uids): array`: + +- 对每个 `uid` 调用现有 `getPageList(['userid' => $uid, 'page' => 1, 'limit' => 1])`(与 pop/index 单用户筛选一致;`starttime`/`endtime` 不传,与养客页默认「全量」查询一致)。 +- 从返回 `data[0]` 取行;无记录或接口异常时该 uid 映射为 `null`(Logic 展示 `-` / 空)。 +- 单 uid 失败只 `logger()->warning`,**不**拖垮整页列表(与 `fetchTotalRecharge` 容错一致)。 + +**说明**:`getDPByPlayers` 仅支持单 `userid`,每页最多 100 条 ≈ 最多 100 次 HTTP(经 pwa 到游戏 API)。当前 `limit` 上限 100,可接受;若后续性能不足,再与游戏侧协商批量接口,不在本次范围。 + +### 2. FreeCreditsStatsLogic:列表富化 + +在 [`FreeCreditsStatsLogic::list`](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php) 中,在已有 `fetchTotalRecharge` 之后: + +- 调用 `PopService` 批量映射; +- 为每行写入(字段名与 pop/index 保持一致,便于前端复用展示逻辑): + - `profitthreshold`:`int|null`,配置值(养客页原样展示,**不**除 10000) + - 若产品确认需要第二列:`hasprofitthreshold`,游戏侧为千分位类整数,展示时与养客页 [`toNum`](backend/slot_admin_vue/src/views/game/pop/config/index.vue) 一致:`Number((val / 10000).toFixed(2))` + +无养客配置的用户:`profitthreshold` 为 `null`,前端显示 `-`。 + +**分层**:跨服务 HTTP 封装在 `PopService`;`FreeCreditsStatsLogic` 只编排,不直接拼 `pwaApiHost` URL。 + +### 3. 前端:新增列 + +修改 [`freeCreditsStats/index.vue`](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue): + +- 在「累计充值金额」后(或渠道后)增加列: + - `盈利阈值` → `profitthreshold` + - (若选两列)`已达盈利阈值` → `hasprofitthreshold`,slot 内 `/10000` 格式化 +- 无自定义 slot 时直接显示;`null`/`undefined` 显示 `-` + +路由、API 文件 [`freeCreditsStats.js`](backend/slot_admin_vue/src/api/game/freeCreditsStats.js) **不变**。 + +### 4. 不改动的部分 + +- `statistics` 顶部 7 项汇总不加盈利阈值(无聚合口径)。 +- Validate / DTO 无新筛选项。 +- `slot_console` innerapi、`slot_sdk` 无需改动(统计已在 admin)。 + +## 验证 + +- 某 UID 在「养客配置」页能查到 `profitthreshold` 时,定格列表同 UID 显示相同值。 +- 无养客配置 UID 显示 `-`,列表其余字段正常。 +- pop/pwa 不可用时:列表仍可出数,盈利阈值为空;日志有 warning。 +- 与迁前对比:除新增列外,筛选、排序、汇总、累计充值口径不变。 + +## 涉及文件 + +| 仓库 | 文件 | +|------|------| +| slot_admin | [`PopService.php`](backend/slot_admin/app/service/game/PopService.php)、[`FreeCreditsStatsLogic.php`](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php) | +| slot_admin_vue | [`freeCreditsStats/index.vue`](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue) | diff --git a/plans/定格统计迁入_admin_6bbf8d93.plan.md b/plans/定格统计迁入_admin_6bbf8d93.plan.md new file mode 100644 index 0000000..f83163d --- /dev/null +++ b/plans/定格统计迁入_admin_6bbf8d93.plan.md @@ -0,0 +1,108 @@ +--- +name: 定格统计迁入 admin +overview: 将「首充剩余定格列表」的查询与汇总逻辑从 slot_console innerapi 迁入 slot_admin,与用户列表等后台能力一致:直连 s_common + WalletService,前端路由不变。 +todos: + - id: add-models + content: 在 slot_admin 新增 FreeCreditsPlayer/Package Model(s_common) + status: completed + - id: migrate-logic + content: 迁入 FreeCreditsStatsLogic + Validate/DTO,fetchTotalRecharge 改用 WalletService + UserModel + status: completed + - id: wire-controller + content: 改造 FreeCreditsStatsController,移除 ConsoleClient 透传 + status: completed + - id: cleanup-bff + content: 删除 console innerapi 与 slot_sdk ConsoleService 中 freeCredits* 方法 + status: in_progress + - id: verify-api + content: 对比迁前后列表/汇总与用户列表充值金额口径 + status: pending +isProject: false +--- + +# 首充定格统计迁入 slot_admin(去掉 console BFF) + +## 结论:建议做 + +当前链路多一跳、职责错位: + +```mermaid +flowchart LR + vue[slot_admin_vue] --> admin[slot_admin FreeCreditsStatsController] + admin --> sdk[slot_sdk ConsoleClient] + sdk --> console[slot_console innerapi] + console --> db[(s_common free_credits_*)] + console --> wallet[slot_wallet statistics] +``` + +[`FreeCreditsStatsController.php`](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php) 注释已写明「仅透传 slot_console innerapi」;[`FreeCreditsController`](slot_console/app/innerapi/controller/FreeCreditsController.php) 也标注「仅供 slot_admin 代理调用」——本质是 **admin 专用 BFF**,与 C 端 `FreeCreditsLogic` 无关。 + +同仓库内后台惯例是 **admin 自己查库 + 按需调钱包**,例如: + +- [`UserLogic`](backend/slot_admin/app/game/logic/UserLogic.php):`WalletService::statistic` + `getNumberFormat` +- [`UserProfitDailyLogic`](backend/slot_admin/app/game/logic/UserProfitDailyLogic.php):同上 +- [`RechargeGiftPlayerLogic`](backend/slot_admin/app/game/logic/RechargeGiftPlayerLogic.php):直连 `s_common` 活动表 + +`slot_admin` 已配置 [`s_common`](backend/slot_admin/config/thinkorm.php) 连接,具备迁回条件。 + +目标链路: + +```mermaid +flowchart LR + vue[slot_admin_vue] --> admin[slot_admin Controller] + admin --> logic[FreeCreditsStatsLogic] + logic --> db[(s_common)] + logic --> wallet[WalletService statistic] +``` + +**不迁**:C 端定格/提现/领取仍在 [`slot_console` `FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php),避免动业务写路径。 + +--- + +## 实施范围 + +### 1. slot_admin 新增分层(对齐 backend-layering) + +| 层 | 文件 | 职责 | +|---|---|---| +| Validate | `app/game/validate/FreeCreditsStatsValidate.php` | `page/limit`、筛选字段类型与枚举 | +| DTO | `app/game/dto/FreeCreditsStatsQueryDTO.php` | 承接已校验参数 | +| Logic | `app/game/logic/FreeCreditsStatsLogic.php` | 从 console 迁入(以当前 [`FreeCreditsStatsLogic.php`](slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php) 为基准,含近期前端改动:无 activity_id/status 筛选、`total_deposit` 按用户 currency + `getNumberFormat`) | +| Model | `app/game/model/common/FreeCreditsPlayerModel.php`、`FreeCreditsPackageModel.php` | `connection = s_common`,表名与 console 一致,常量 `STATUS_*` / `TYPE_*` 复制 | +| Controller | 改造 [`FreeCreditsStatsController.php`](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php) | Validate → DTO → Logic;`index` 一次返回 `data/total/otherData`(Logic 内 `statistics` + `list`) | + +**累计充值**:在 admin Logic 内用已有 [`WalletService::statistic`](backend/slot_admin/app/service/WalletService.php) + [`UserModel`](backend/slot_admin/app/model/UserModel.php) 按用户 `currency` 分组(与用户列表口径一致),**不再**经 console 调 wallet SDK。 + +**对外契约**:保持 `GET/POST /game/freeCreditsStats/index` 与字段名不变 → [`slot_admin_vue`](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue) **无需改路由**。 + +### 2. 从 console / SDK 下线 admin 专用 BFF + +迁移验证通过后删除或标记废弃: + +- [`slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php`](slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php) +- [`slot_console/app/innerapi/controller/FreeCreditsController.php`](slot_console/app/innerapi/controller/FreeCreditsController.php)(若仅服务 stats) +- [`slot_sdk` `ConsoleService::freeCreditsList/statistics`](slot_sdk/src/service/console/ConsoleService.php) +- admin Controller 中 `ConsoleClient` / `buildConsoleClient` + +### 3. 不做的项 + +- 不把 C 端 `FreeCreditsLogic` 抽到 `slot_lib`(范围过大,与本次 admin 统计无关) +- 不为两张表单独建 Service 中转层(Logic 内私有方法即可) + +--- + +## 风险与注意点 + +1. **DB 权限**:确认 `slot_admin` 运行账号对 `s_common.free_credits_player/package` 有读权限(与 console 相同库)。 +2. **双份 Model**:console 仍保留 C 端用的 Model;admin 侧独立 Model 类,表结构变更需两处同步常量(可接受,与现有 `RechargeGift*` 模式一致)。 +3. **一次请求两次查询**:`statistics` + `list` 仍在单次 `index` 内完成;若后续数据量大,再在 Logic 内优化共用 `filteredPlayerIds`,不必回退 BFF。 +4. **UserProfitDaily 的 currency**:其用 `GameServerModel` 固定币种;本功能应继续用 **每用户 `UserModel.currency`**(与 `UserLogic` 一致),避免回退到固定 USD。 + +--- + +## 验证清单 + +- 列表筛选(uid/source/定格时间/第一档状态/全部完成)与汇总 7 项与迁前一致 +- 某 UID「累计充值」与后台用户列表 `wallet.r` / `total_deposit` 一致 +- 去掉对 `consoleApiHost` 的依赖后,`/game/freeCreditsStats/index` 仍可正常访问 +- console innerapi 无其它调用方后再删 SDK 方法 diff --git a/plans/异步事件单元测试_0dbff6a0.plan.md b/plans/异步事件单元测试_0dbff6a0.plan.md new file mode 100644 index 0000000..3f22169 --- /dev/null +++ b/plans/异步事件单元测试_0dbff6a0.plan.md @@ -0,0 +1,227 @@ +--- +name: 异步事件单元测试 +overview: 结合 slot_console 现有 Free Credits 测试实践,说明异步 MQ 事件应分层测试:不直连 RabbitMQ,优先测 Logic 编排,再补 Event 解析与 EventBus 路由;并给出可新增的示例用例结构。 +todos: + - id: entity-test + content: 新增 FreeCreditInitEntityTest:字段映射与 resolveWalletAmount 回退 + status: completed + - id: bus-factory + content: 新增 tests/Support/ConsoleBusMessageFactory 统一构造 MQBusEntity 载荷 + status: completed + - id: extend-logic-tests + content: 扩展 FreeCreditsHandleFreeCreditInitTest:幂等、无配置、异常路径 + status: completed + - id: optional-event-inject + content: (可选)FreeCreditInitEvent 注入 Logic + Event 单测 + status: completed + - id: optional-eventbus-nack + content: (可选)EventBus deal 单测:TYPE_FREE_CREDIT_INIT 成功 ack / 失败 nack + status: completed +isProject: false +--- + +# 异步事件单元测试写法(slot_console) + +## 当前架构 + +```mermaid +sequenceDiagram + participant Wallet as slot_wallet + participant MQ as RabbitMQ_console_bus + participant Bus as EventBus_deal + participant Ev as FreeCreditInitEvent + participant Logic as FreeCreditsLogic + + Wallet->>MQ: JSON uid/type/data + MQ->>Bus: AMQPMessage + Bus->>Ev: case TYPE_FREE_CREDIT_INIT + Ev->>Ev: FreeCreditInitEntity + Ev->>Logic: handleFreeCreditInit + Logic->>Logic: freezeFirstRecharge + wallet RPC +``` + +**结论**:异步只发生在 MQ 传输层;单测应测 **消息解析 → 路由 → 业务编排**,而不是启动真实 `event:bus` 或 RabbitMQ。 + +--- + +## 项目里已有的做法(推荐延续) + +### 1. 单元测试:直接测 Logic(主路径) + +现有 [`tests/Unit/FreeCreditsHandleFreeCreditInitTest.php`](/Users/ray/Documents/project/www/slot/slot_console/tests/Unit/FreeCreditsHandleFreeCreditInitTest.php) 已覆盖 `free_credit_init` 的**业务结果**,等价于事件消费后的效果: + +- [`FreeCreditsLogicHarness`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsLogicHarness.php):注入 `userContext` / `activeConfig` / `walletService` +- [`RecordingWalletService`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/RecordingWalletService.php):记录 `freeCreditsFreeze`,不发 HTTP +- [`FreeCreditsConfigStub`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsConfigStub.php):活动配置桩 + +断言示例(已有): + +- 定格金额 = `balance_before_qf` +- `biz_id` = `free_credits_freeze:{orderId}` +- `balance_before_qf <= 0` 时不调用 freeze + +**运行**: + +```bash +cd slot_console && ./vendor/bin/phpunit --testsuite Unit +``` + +提现三类事件同理:[`tests/Integration/FreeCreditsLogicCashoutResultTest.php`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsLogicCashoutResultTest.php) 直接调 `handleFirstCashoutResult`,不经过 EventBus。 + +### 2. 集成测试:需要 DB 时 + +[`FreeCreditsDbTestCase`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsDbTestCase.php):`RUN_DB_TESTS=1` 才跑,默认事务回滚。 + +```bash +RUN_DB_TESTS=1 ./vendor/bin/phpunit --testsuite Integration --filter FreeCredits +``` + +--- + +## 建议的分层(按投入产出排序) + +| 层级 | 测什么 | 是否必需 | 依赖 | +|------|--------|----------|------| +| A. Entity | `FreeCreditInitEntity` 字段映射、`resolveWalletAmount()` 回退 | 推荐 | 无 | +| B. Logic | `handleFreeCreditInit` / `handleFirstCashoutResult` 编排 | **已有,继续扩展** | Harness + RecordingWallet | +| C. Event | `FreeCreditInitEvent::handle` 把 `MQBusEntity` 转成 Logic 参数 | 可选 | 需可注入 Logic(见下) | +| D. EventBus | `deal()` 按 `type` 分发、失败 nack | 少量即可 | Mock `AMQPMessage` | +| E. E2E | 真 MQ + wallet 发消息 | 手工/脚本,非单测 | 环境 | + +**原则**:B 覆盖 90% 风险;A 防 payload 字段错;C/D 防「路由写错 type」和「实体解析漏字段」。 + +--- + +## 可新增的测试示例 + +### A. Entity 单测(纯数组 → 实体) + +新建 `tests/Unit/FreeCreditInitEntityTest.php`: + +```php +$entity = new FreeCreditInitEntity([ + 'balance_before_qf' => 15000, + 'wallet_amount' => 0, + 'amount' => 5000, + 'biz_id' => 'order_1', +]); +$this->assertSame(15000, $entity->balance_before_qf); +$this->assertSame(5000, $entity->resolveWalletAmount()); // wallet_amount 为 0 时回退 amount +``` + +对应生产代码:[`app/entity/mq/FreeCreditInitEntity.php`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/FreeCreditInitEntity.php)。 + +### B. 构造 MQ 载荷辅助方法 + +在 `tests/Support/` 增加工厂,统一模拟 wallet 发出的 body: + +```php +public static function consoleBusMessage(int $uid, string $type, array $data): MQBusEntity +{ + return new MQBusEntity(['uid' => $uid, 'type' => $type, 'data' => $data]); +} + +// free_credit_init 示例 +public static function freeCreditInitBus(int $uid, int $balanceBefore, int $walletAmount, string $bizId): MQBusEntity +{ + return self::consoleBusMessage($uid, MQBusEntity::TYPE_FREE_CREDIT_INIT, [ + 'balance_before_qf' => $balanceBefore, + 'wallet_amount' => $walletAmount, + 'biz_id' => $bizId, + 'source' => 'test', + 'currency' => 'INR', + ]); +} +``` + +与 [`WalletLogic::sendConsoleBus`](/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 字段保持一致。 + +### C. Event 层单测(当前结构的限制) + +[`FreeCreditInitEvent`](/Users/ray/Documents/project/www/slot/slot_console/app/command/event/FreeCreditInitEvent.php) 内部写死 `new FreeCreditsLogic()`,**无法在不改代码的情况下 mock Logic**。 + +两种做法(二选一): + +1. **小重构(推荐)**:Event 构造函数注入 `FreeCreditsLogic`,单测传入 `FreeCreditsLogicHarness`。 +2. **不测 Event**:认为 Event 只有 5 行胶水,由 B 层保证;仅加 A 层测 data 解析。 + +若采用 1,示例: + +```php +$bus = ConsoleBusMessageFactory::freeCreditInitBus($uid, 15000, 5000, 'order_1'); +$harness = (new FreeCreditsLogicHarness())->inject($context, $config, $wallet); +(new FreeCreditInitEvent($harness))->handle($bus); +$this->assertCount(1, $wallet->freezeCalls); +``` + +### D. EventBus 路由单测(少量) + +Mock `PhpAmqpLib\Message\AMQPMessage`: + +```php +$message = $this->createMock(AMQPMessage::class); +$message->method('getBody')->willReturn(json_encode([ + 'uid' => 90088002, + 'type' => MQBusEntity::TYPE_FREE_CREDIT_INIT, + 'data' => ['balance_before_qf' => 15000, 'wallet_amount' => 5000, 'biz_id' => 'x'], +])); +$message->expects($this->once())->method('ack'); // 成功应 ack + +$bus = new EventBus(); +// 若 Logic 仍 new 在 Event 内,需配合 C 的注入或接受集成测 +$bus->deal($message); +``` + +**nack 场景**([`EventBus.php` L164-167](/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php)):Logic 抛异常时,`TYPE_FREE_CREDIT_INIT` 应 `nack(true)` 且不 `ack`。可让 Harness 的 `freeCreditsFreeze` 抛异常,断言 `$message->expects($this->once())->method('nack')->with(true)`。 + +注意:`EventBus` 依赖多,路由测试宜 **只测 switch 分支 + ack/nack**,业务细节仍放 B。 + +--- + +## 四个 Free Credits 相关 type 怎么测 + +| MQ type(常量) | 单测重点 | 现有覆盖 | +|-----------------|----------|----------| +| `TYPE_FREE_CREDIT_INIT` | 定格金额、biz_id、幂等跳过 | `FreeCreditsHandleFreeCreditInitTest` | +| `TYPE_FREE_CREDITS_FIRST_CASHOUT_SUCCESS` | 档位 completed、player 状态 | `FreeCreditsLogicCashoutResultTest` | +| `TYPE_FREE_CREDITS_FIRST_CASHOUT_FAIL` | 失败回滚逻辑 | 可补 case | +| `TYPE_FREE_CREDITS_FIRST_CASHOUT_REJECTED` | rejected → ready | 已有 reject case | + +EventBus 里前三类目前 **直接调 Logic**(未走独立 Event 类),单测继续打 Logic 即可;常量定义在 [`MQBusEntity`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/MQBusEntity.php)。 + +--- + +## 不建议在单测里做的 + +- 启动 `php webman event:bus` 或连接真实 RabbitMQ +- 跨服务联调 wallet → console(留给脚本如 `scripts/run_free_credits_first_freeze.php` 或手工验收) +- 在单测里依赖 `UserTagService::getByUid`(用 Harness 注入 context) + +--- + +## 推荐落地顺序(若你要补测试) + +1. 补 `FreeCreditInitEntityTest`(A) +2. 补 `ConsoleBusMessageFactory`(B 辅助) +3. 扩展 `FreeCreditsHandleFreeCreditInitTest`:无活动配置、已定格幂等、Logic 抛错 +4. (可选)`FreeCreditInitEvent` 注入 Logic + Event 单测(C) +5. (可选)`EventBusFreeCreditInitTest` 只测 ack/nack(D) + +```mermaid +flowchart TD + subgraph unit [Unit 无 MQ] + E[EntityTest] + L[LogicHarnessTest] + end + subgraph optional [Optional] + Ev[EventTest] + Bus[EventBusRoutingTest] + end + subgraph integration [Integration RUN_DB_TESTS] + DB[DbTestCase] + end + E --> L + L --> Ev + Ev --> Bus + L --> DB +``` diff --git a/plans/异步用户统计_mq_300efb8e.plan.md b/plans/异步用户统计_mq_300efb8e.plan.md new file mode 100644 index 0000000..9529fe4 --- /dev/null +++ b/plans/异步用户统计_mq_300efb8e.plan.md @@ -0,0 +1,184 @@ +--- +name: 异步用户统计 MQ +overview: wallet_user_stat 通过 slot/foundation RabbitMQ 异步更新;废弃 WalletStatModel 分片统计表;commit 后发 MQ,command 消费落库,Redis biz_id 幂等。 +todos: + - id: mq-entity-keys + content: 事件 Entity/常量、MQ 命名;foundation RabbitMQConfig 工厂方法 + status: completed + - id: mq-producer-consumer-service + content: WalletUserStatMQService(Producer)与 WalletUserStatConsumeService + status: completed + - id: command-update-wallet-user-stat + content: UpdateWalletUserStat command 使用 RabbitMQConsumer::consume + status: completed + - id: remove-wallet-stat-model + content: 移除 WalletStatModel 全部引用,读统计改 WalletUserStatModel + status: completed + - id: wire-wallet-logic + content: 注册/充值/提现完成 commit 后 publish;RegisterService 去掉同步 stat + status: completed + - id: doc-and-verify + content: doc 留痕 + Docker 冒烟(foundation MQ + 幂等) + status: completed +isProject: false +--- + +# wallet_user_stat 异步统计更新方案(修订) + +## 变更要点(相对上一版) + +1. **废弃** [`WalletStatModel`](app/model/multi/WalletStatModel.php)(分片 `wallet_stat_XX`):删除所有同步 `inc` / `insertData` / 读统计逻辑,**不再并行双写**。 +2. **MQ 统一走** [`slot/foundation` `src/MQ`](vendor/slot/foundation/src/MQ/),**不再**为本需求扩展 [`RabbitMqService`](app/service/RabbitMqService.php)(wager/console 等历史消费者可暂保留旧封装)。 + +## 为什么异步 + +| 维度 | 说明 | +|------|------| +| 定位 | `wallet_user_stat` 为运营/展示统计,非真账 | +| 性能 | 资金事务 commit 后异步写 `rh_wallet`,缩短主路径 | +| 规范 | 主账本 DB 先提交,再发 MQ;消费幂等 | + +```mermaid +sequenceDiagram + participant Logic as WalletLogic + participant ShardDB as wallet_shard + participant Prod as RabbitMQProducer + participant Cmd as updateWalletUserStat + participant Cons as RabbitMQConsumer + participant Stat as wallet_user_stat + + Logic->>ShardDB: commit + Logic->>Prod: sendMessage event + Cmd->>Cons: consume autoAck + Cons->>Stat: WalletUserStatModel apply* +``` + +## Foundation MQ API(vendor 已具备) + +| 类 | 用途 | +|----|------| +| [`RabbitMQConfig`](vendor/slot/foundation/src/MQ/RabbitMqConfig.php) | host/port/user/password/vhost | +| [`RabbitMQProducer`](vendor/slot/foundation/src/MQ/RabbitMqProducer.php) | `getInstance($config, $exchange, $queue, 'direct', $route)->sendMessage(array\|string)` | +| [`RabbitMQConsumer`](vendor/slot/foundation/src/MQ/RabbitMqConsumer.php) | `consume($callback, $autoAck=true)`、`decodeMessage($message)` | + +配置从现有 `.env` 组装(与 [`RabbitMqService`](app/service/RabbitMqService.php) 相同变量:`MQ_HOST`、`MQ_PORT`、`MQ_USER`、`MQ_PASSWORD`、`MQ_VHOST`)。 + +本仓新增 **`WalletUserStatMqSupport`**(或写在 `WalletUserStatMQService` 内私有方法): + +```php +use slot\foundation\MQ\RabbitMQConfig; +use slot\foundation\MQ\RabbitMQProducer; +use slot\foundation\MQ\RabbitMQConsumer; + +private static function config(): RabbitMQConfig +{ + return new RabbitMQConfig( + (string) getenv('MQ_HOST'), + (int) getenv('MQ_PORT'), + (string) getenv('MQ_USER'), + (string) getenv('MQ_PASSWORD'), + (string) getenv('MQ_VHOST'), + ); +} +``` + +交换机/队列([`MQKeyManagerService`](app/service/MQKeyManagerService.php) 常量,direct,routingKey = queue 名): + +- `EXCHANGE_WALLET_USER_STAT = 'wallet_user_stat'` +- `QUEUE_WALLET_USER_STAT = 'wallet_user_stat'` + +## 消息体 + +[`app/entity/mq/WalletUserStatEventEntity.php`](app/entity/mq/WalletUserStatEventEntity.php) + [`app/constants/WalletUserStatEvent.php`](app/constants/WalletUserStatEvent.php): + +| 字段 | 说明 | +|------|------| +| `event` | `init` / `deposit` / `withdraw` / `promo_bonus` / `cashback` / `refund` / `chargeback` | +| `uid`, `currency`, `amount`, `biz_id` | 金额 x1000;`init` 时 amount=0 | +| `occurred_at` | 可选 | + +消费端映射 [`WalletUserStatModel`](app/model/WalletUserStatModel.php) 已有 `apply*` / `insertInitRow`。 + +## Service 分层 + +| 类 | 职责 | +|----|------| +| [`WalletUserStatMQService`](app/service/wallet/WalletUserStatMQService.php) | `publish(WalletUserStatEventEntity)` → `RabbitMQProducer::sendMessage($entity->activeData())` | +| [`WalletUserStatConsumeService`](app/service/wallet/WalletUserStatConsumeService.php) | 校验、Redis 幂等 `wallet:user_stat:dedupe:{event}:{biz_id}`、调用 Model | + +**禁止**在 `WalletUserStatMQService` 内再包一层无意义的 `RabbitMqService` 转发。 + +## Command + +[`app/command/UpdateWalletUserStat.php`](app/command/UpdateWalletUserStat.php)(结构参考 [`UpdateWagerTask`](app/command/UpdateWagerTask.php),但换 foundation Consumer): + +```php +$consumer = RabbitMQConsumer::getInstance( + WalletUserStatMqSupport::config(), + MQKeyManagerService::EXCHANGE_WALLET_USER_STAT, + MQKeyManagerService::QUEUE_WALLET_USER_STAT, + 'direct', + MQKeyManagerService::QUEUE_WALLET_USER_STAT, +); +$consumer->consume(function (AMQPMessage $message) use ($consumer) { + $payload = $consumer->decodeMessage($message); + (new WalletUserStatConsumeService())->handle(is_array($payload) ? $payload : []); +}, true, false); +``` + +- 成功回调后 **auto ack**(foundation 默认行为) +- 业务异常:`LoggerService::error`;是否 requeue 首版 `false`(与 wager 手工 ack 策略不同,更依赖 Redis 幂等 + 日志补数) + +启动: + +```bash +docker exec -w /app/www/ray/slot-wallet php82 php webman updateWalletUserStat +``` + +## 移除 WalletStatModel + +| 文件 | 改动 | +|------|------| +| [`WalletLogic.php`](app/api/logic/WalletLogic.php) | 删除 `WalletStatModel` use 及 `inc`/`insertData`;commit 后 `WalletUserStatMQService::publish` | +| [`RegisterService.php`](app/service/wallet/RegisterService.php) | 删除 `$walletStatModel` 与 `insertData`;注册 commit 后发 `init` 事件 | +| [`WalletLogic::getWallet`](app/api/logic/WalletLogic.php)(`needStat==1`) | 改读 `WalletUserStatModel::findByUidCurrency`:`r` ← `total_deposit_amount`,`tw` ← `total_withdraw_amount` | +| [`WalletStatModel.php`](app/model/multi/WalletStatModel.php) | **删除文件**(无引用后) | + +### 发 MQ 关键路径(post-commit) + +| 场景 | event | amount | +|------|-------|--------| +| 注册 | `init` | 0 | +| `recharge` / `rechargeSign` | `deposit` | `recharge` | +| 提现完成 `BIZ_TYPE_WITHDRAW` | `withdraw` | `fee` | + +`biz_id` 使用请求 DTO 已有 `biz_id`(幂等维度与资金接口一致)。 + +## 不涉及 + +- 不改 `config/thinkorm.php` +- 不把统计写回资金事务 +- 不改造 wager/console 等仍用 `RabbitMqService` 的旧队列(仅新统计队列用 foundation) + +## 验证 + +1. `composer` 已含 `slot/foundation`(lock 中已有 MQ 类) +2. 启动 consumer + 走注册/充值/提现 +3. 查 `rh_wallet.wallet_user_stat` +4. 重复 `biz_id` 验证 Redis 幂等 +5. 确认 `wallet_stat_XX` 不再被写入;`getWallet` 的 `r`/`tw` 来自新表 + +## 文件一览 + +| 操作 | 路径 | +|------|------| +| 新增 | `app/entity/mq/WalletUserStatEventEntity.php` | +| 新增 | `app/constants/WalletUserStatEvent.php` | +| 新增 | `app/service/wallet/WalletUserStatMqSupport.php`(可选,集中 Config/实例化) | +| 新增 | `app/service/wallet/WalletUserStatMQService.php` | +| 新增 | `app/service/wallet/WalletUserStatConsumeService.php` | +| 新增 | `app/command/UpdateWalletUserStat.php` | +| 修改 | `app/service/MQKeyManagerService.php`、`app/service/RedisKeyManagerService.php` | +| 修改 | `app/api/logic/WalletLogic.php`、`app/service/wallet/RegisterService.php` | +| 删除 | `app/model/multi/WalletStatModel.php` | +| 新增/修改 | `doc/` 说明 foundation MQ 与启动命令 | diff --git a/plans/拆分win派奖文档_adbf09a1.plan.md b/plans/拆分win派奖文档_adbf09a1.plan.md new file mode 100644 index 0000000..d4bacb2 --- /dev/null +++ b/plans/拆分win派奖文档_adbf09a1.plan.md @@ -0,0 +1,56 @@ +--- +name: 拆分win派奖文档 +overview: 将 wallet PRD 中与 win(派奖)强相关的需求从主文档拆分到独立文档,降低耦合并保持规则可追踪。计划默认采用“从 wallet.md 迁移到 win.md,并在原处保留索引入口”的方式。 +todos: + - id: identify-win-sections + content: 标记 wallet.md 中所有 win(派奖)主规则与引用依赖段落 + status: completed + - id: create-win-doc + content: 创建 win.md 并迁移/重组派奖规则、流程与示例 + status: completed + - id: refactor-wallet-doc + content: 在 wallet.md 用摘要+链接替换已迁移内容并更新目录 + status: completed + - id: consistency-pass + content: 统一术语与交叉引用,确保无重复冲突定义 + status: completed +isProject: false +--- + +# 将 win(派奖)需求拆分到独立文档 + +## 目标 +把 [doc/wallet.md](doc/wallet.md) 中“win(派奖)”相关需求整理到新文档 [doc/win.md](doc/win.md),并保证两份文档职责清晰: +- `wallet.md` 保留钱包总规则与对外索引 +- `win.md` 承载派奖、Round 结算、出资分配等细则 + +## 拆分范围(默认) +从 [doc/wallet.md](doc/wallet.md) 迁移以下“win 主体”内容到 [doc/win.md](doc/win.md): +- §12 `Slots 多次派奖与最终结算规则` +- §20 中 win 强相关子流程: + - `20.4 Round 多次派奖` + - `20.5 Round 最终结算` +- 与派奖直接相关且需避免重复维护的规则片段(在 `wallet.md` 中改为引用): + - §10.3 `派奖归属规则` + - §11 中 `Round 总派奖` 分配示例 + - §16.2 `派奖按比例拆分余数` + +## 文档结构调整 +- 在 [doc/wallet.md](doc/wallet.md): + - 保留“钱包主规则”定位 + - 将迁移段落替换为简要摘要 + 指向 [doc/win.md](doc/win.md) 的链接 + - 更新目录,新增 `win` 文档入口 +- 在 [doc/win.md](doc/win.md): + - 新建“win(派奖)需求”专题文档 + - 按“背景 → 结算时序 → 分配规则 → 边界条件 → 示例流程”重组内容 + - 明确与 `Lot`、`Task`、`Withdrawable` 的关系,保留必要术语一致性(Round/FinalSettlement/AllocatedPayout) + +## 一致性与可维护性约束 +- 术语与口径与 [doc/wallet.md](doc/wallet.md) 保持一致(Bonus/Deposit/Withdrawable、Final Settlement、PlayedOut) +- 避免双份正文维护:同一规则只在一个文档定义,另一侧做引用 +- 若保留示例在两文档同时出现,仅保留“简版示意”在 `wallet.md`,完整口径放 `win.md` + +## 验收标准 +- [doc/win.md](doc/win.md) 可单独说明“派奖”完整业务规则 +- [doc/wallet.md](doc/wallet.md) 删除/替换原 win 细节后仍可作为钱包总览 +- 两文档目录和交叉链接可直接跳转,且不存在明显冲突描述 \ No newline at end of file diff --git a/plans/支持_webp_上传_1fe8ff80.plan.md b/plans/支持_webp_上传_1fe8ff80.plan.md new file mode 100644 index 0000000..9b5bc63 --- /dev/null +++ b/plans/支持_webp_上传_1fe8ff80.plan.md @@ -0,0 +1,80 @@ +--- +name: 支持 webp 上传 +overview: slot_admin 后端已允许 webp;需在 slot_admin_vue 的图片上传组件默认 accept 中加入 `.webp`(及 MIME),即可在管理端选择并上传 webp 文件。 +todos: + - id: update-accept-default + content: 在 sa-upload-image/index.vue 的 accept 默认值与模板 fallback 中加入 .webp、image/webp,并修正 .bpm → .bmp + status: completed + - id: manual-verify-upload + content: 在活动/Banner 等页面上传 webp,确认选择器可选且接口返回 URL + status: completed +isProject: false +--- + +# slot_admin_vue 支持 WebP 图片上传 + +## 问题定位 + +管理端图片上传统一走 [`sa-upload-image`](backend/slot_admin_vue/src/components/sa-upload-image/index.vue) 组件,通过 Arco `a-upload` 的 `accept` 限制文件选择器可见格式: + +```28:28:backend/slot_admin_vue/src/components/sa-upload-image/index.vue + :accept="props.accept ?? '.jpg,jpeg,.gif,.png,.svg,.bpm'" +``` + +```67:67:backend/slot_admin_vue/src/components/sa-upload-image/index.vue + accept: { type: String, default: '.jpg,jpeg,.gif,.png,.svg,.bpm' }, +``` + +当前默认列表**不含** `.webp`,因此在系统文件对话框中无法选择 webp(或部分浏览器会拦截)。组件内 `uploadImageHandler` **没有**再做扩展名校验,上传逻辑直接调用 `/saimulti/system/uploadImage`。 + +**后端已支持 webp**,无需改动: + +```10:10:backend/slot_admin/plugin/saimulti/config/upload.php + 'upload_allow_image' => 'jpg,jpeg,png,gif,svg,bmp,webp,avif', +``` + +[`UploadService`](backend/slot_admin/plugin/saimulti/service/storage/UploadService.php) 按扩展名与白名单比对,webp 已在列表中。 + +```mermaid +flowchart LR + Browser["浏览器文件选择器 accept"] + SaUpload["sa-upload-image"] + API["POST /saimulti/system/uploadImage"] + UploadService["UploadService 扩展名校验"] + Browser -->|"缺 .webp 时被过滤"| SaUpload + SaUpload --> API + API --> UploadService + UploadService -->|"webp 已允许"| Storage["S3/本地存储"] +``` + +全项目使用 `sa-upload-image` 时均未自定义 `:accept`,改组件默认值即可覆盖活动图、Banner、头像、配置项等所有入口。 + +## 修改方案(仅前端,单文件) + +**文件**:[`backend/slot_admin_vue/src/components/sa-upload-image/index.vue`](backend/slot_admin_vue/src/components/sa-upload-image/index.vue) + +1. 将 `accept` 默认值与模板中的 fallback 统一为包含 webp,建议与后端白名单对齐: + + ``` + image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.jpg,.jpeg,.png,.gif,.webp,.svg,.bmp + ``` + + - 同时写上 **MIME**(`image/webp`)和 **扩展名**(`.webp`),兼容 macOS/Chrome 对 webp 的识别方式。 + - 顺带修正现有笔误:`.bpm` → `.bmp`(与后端 `bmp` 一致)。 + +2. 两处需同步修改(保持 `props.accept ?? ...` 与 `defineProps` default 一致): + - 第 28 行模板 `:accept` + - 第 67 行 `accept` prop default + +**不需要改后端**:`upload.php` 已含 `webp`。 + +## 可选(非本次必须) + +若还需在**富文本编辑器**「资源选择器」里插入已有 webp 资源,[`ma-wangEditor/index.vue`](backend/slot_admin_vue/src/components/ma-wangEditor/index.vue) 第 67–72 行对图片 URL 的判断未包含 `.webp`,可另开小改;与本地上传 `sa-upload-image` 无关。 + +## 验证步骤 + +1. 本地启动 slot_admin_vue,打开任意带 `sa-upload-image` 的页面(如活动编辑、Banner 编辑)。 +2. 点击「本地上传」,文件选择器应能显示 `.webp` 文件。 +3. 上传一张 webp(建议 < 5MB,与 `upload_size` 一致),应成功返回 URL 并预览。 +4. 若仍报错「不支持该格式的文件上传」,检查运行环境 `plugin/saimulti/config/upload.php` 是否被覆盖(线上配置应与仓库一致)。 diff --git a/plans/旧提现接口兼容_package_id_9ba88779.plan.md b/plans/旧提现接口兼容_package_id_9ba88779.plan.md new file mode 100644 index 0000000..7fb815b --- /dev/null +++ b/plans/旧提现接口兼容_package_id_9ba88779.plan.md @@ -0,0 +1,94 @@ +--- +name: 旧提现接口兼容 package_id +overview: 在已落地的 `POST /api/free-credits/withdraw-first-cash` 基础上,恢复 `POST /api/withdraw/apply` 对 `package_id` 的兼容:参数校验与业务编排与新接口一致,内部转发 `applyFreeCreditsFirstCash`,不重复维护 WithdrawValidator 的 `*_fc` scene。 +todos: + - id: withdraw-controller-compat + content: WithdrawController::apply 恢复 package_id 分支,校验用 FreeCreditsValidator,调用 applyFreeCreditsFirstCash + status: completed + - id: comments-sync + content: FreeCreditsController / WithdrawApplyDTO 注释标明兼容与推荐路径 + status: completed + - id: regression-test + content: 跑相关 PHPUnit 确认无回归 + status: completed +isProject: false +--- + +# 旧提现接口兼容 package_id + +## 背景 + +[第一档提现独立接口](第一档提现独立接口_6b3aaf7e.plan.md) 已实现新路径,但 [`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php) 当前**一律**走普通提现(`amount` 必填),旧 C 端若仍调用 `POST /api/withdraw/apply` + `package_id` 会校验失败或走错逻辑。 + +需求:**保留原接口**,传 `package_id` 时行为与 `withdraw-first-cash` 一致(兼容期转发,不删新接口)。 + +```mermaid +flowchart TD + apply["POST /api/withdraw/apply"] + apply -->|"package_id > 0"| fcPath["FreeCreditsValidator + applyFreeCreditsFirstCash"] + apply -->|"无 package_id"| normalPath["WithdrawValidator + apply"] + newApi["POST /api/free-credits/withdraw-first-cash"] + newApi --> fcPath +``` + +## 改动(仅 slot_console) + +### 1. [`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php) + +- `use app\api\validator\FreeCreditsValidator` +- 分支逻辑(与拆分前一致,编排指向新方法): + +```php +$post = $request->post(); +$type = intval($post['type'] ?? input('type', 1)); + +if (!empty($post['package_id'])) { + $error = (new FreeCreditsValidator())->scene(FreeCreditsValidator::firstCashoutScene($type))->check($post); + // 失败 return PARAMS_ERROR + $this->service->setUid($uid); + $res = $this->service->applyFreeCreditsFirstCash(new WithdrawApplyDTO($post)); + return $this->success($res, 'Submitted successfully! Your order is under review...'); +} + +$error = $this->validate((string) $type, $post); +// 现有普通提现 apply + 'Submit successfully' +``` + +- 方法 PHPDoc 注明:`package_id` 为**兼容**字段,推荐改用 `/api/free-credits/withdraw-first-cash`。 + +**不在** [`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php) 恢复 `*_fc` scene,避免两套校验重复;兼容路径复用 [`FreeCreditsValidator`](slot_console/app/api/validator/FreeCreditsValidator.php)。 + +### 2. 注释同步 + +| 文件 | 内容 | +| --- | --- | +| [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) 类注释 | 补充:旧路径 `withdraw/apply` + `package_id` 仍可用,建议迁移新接口 | +| [`WithdrawApplyDTO`](slot_console/app/api/dto/request/WithdrawApplyDTO.php) | `package_id` 注释改为「新接口首选;withdraw/apply 兼容」 | + +### 3. 单测(可选、建议加) + +在 [`tests/Unit/`](slot_console/tests/Unit/) 或扩展现有 Controller 测: + +- 仅验证 `WithdrawController` 在 `package_id` 非空时选用 `FreeCreditsValidator::firstCashoutScene`(可 mock validate,或沿用 validator 单测 + 文档约定)。 + +集成行为已由 [`FreeCreditsFirstCashoutApplyTest`](slot_console/tests/Integration/FreeCreditsFirstCashoutApplyTest.php) 覆盖 `applyFreeCreditsFirstCash`,兼容层无额外 Service 逻辑,**可不新增 DB 集成测**。 + +运行回归: + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit \ + tests/Unit/FreeCreditsValidatorWithdrawFirstCashTest.php \ + tests/Unit/FreeCreditsValidatorTest.php +``` + +## 不变项 + +- [`WithdrawService::applyFreeCreditsFirstCash`](slot_console/app/service/WithdrawService.php) 实现不动 +- `POST /api/free-credits/withdraw-first-cash` 仍为推荐入口 +- 普通 `withdraw/apply`(无 `package_id`)行为不变 + +## 验收 + +1. `withdraw/apply` + `package_id` + 绑卡 → 与 `withdraw-first-cash` 相同返回与档位状态 +2. `withdraw/apply` 无 `package_id` → 仍须 `amount`,走余额/手续费校验 +3. 新接口行为无回归 diff --git a/plans/每日返水活动化改造_dd1575a1.plan.md b/plans/每日返水活动化改造_dd1575a1.plan.md new file mode 100644 index 0000000..6d5895b --- /dev/null +++ b/plans/每日返水活动化改造_dd1575a1.plan.md @@ -0,0 +1,225 @@ +--- +name: 每日返水活动化改造 +overview: 将每日返水从独立档位表方案,改为与首充定格(Free Credits, type=11)一致的活动配置模式:新增活动类型 12、档位写入 recharge_gift_config.ext_config,保留 daily_rebate_record 做用户日统计与领取,综合统计页独立不变。 +todos: + - id: activity-type-12 + content: 新增 TYPE_DAILY_REBATE=12:RechargeGiftConfigModel、getDailyRebateActiveInfo、Consts、activity_type 字典 SQL + status: completed + - id: activity-ext-config + content: ActivityValidate::checkDailyRebateExt + ActivityController save/update;activity/edit.vue & index.vue type=12 档位 UI + status: completed + - id: refactor-daily-rebate-service + content: DailyRebateService 改读 recharge_gift_config.ext_config;daily_rebate_record 增加 activity_id + status: completed + - id: remove-tier-table + content: 删除 daily_rebate_tier_config 及相关 Admin CRUD/菜单;更新 daily_rebate.sql + status: completed + - id: complete-stats-api + content: 补全 dailyRebateStats 前端页 + 确认 DailyRebateSettle/Expire/API/流水类型 65 + status: completed + - id: tests-acceptance + content: DailyRebateCalcServiceTest + 端到端验收(配置→结算→领取→统计) + status: completed +isProject: false +--- + +# 每日返水 — 活动化改造方案(对齐首充定格) + +## 背景与纠偏 + +你已指出:**这是活动,应新增活动类型,返水档位在「活动配置」里维护**,参考首充定格(Free Credits)。 + +现有代码库中首充定格模式: + +| 层级 | 实现 | +|------|------| +| 配置表 | [`s_recharge_gift_config`](slot_console/app/model/common/RechargeGiftConfigModel.php)(后台称「充值活动配置」) | +| 活动类型 | `TYPE_FREE_CREDITS = 11` | +| 业务参数 | `ext_config` JSON(千分位 `_qf` 字段) | +| 后台入口 | [活动管理 → 充值活动配置](backend/slot_admin_vue/src/views/game/activity/index.vue) + [edit.vue type==11 表单项](backend/slot_admin_vue/src/views/game/activity/edit.vue) | +| 校验 | [`ActivityValidate::checkFreeCreditsExt`](backend/slot_admin/app/game/validate/ActivityValidate.php) + [`ActivityController::save/update`](backend/slot_admin/app/game/controller/ActivityController.php) | +| 运行时查询 | [`RechargeGiftConfigModel::getFreeCreditsActiveInfo`](slot_console/app/model/common/RechargeGiftConfigModel.php) | +| 用户状态 | 独立表 `free_credits_player` / `free_credits_package` | + +**先前中断实现的问题**:已创建 [`daily_rebate_tier_config`](slot_console/db/daily_rebate.sql) 独立表及 [`DailyRebateTierConfig*`](backend/slot_admin/app/game/controller/DailyRebateTierConfigController.php) 后台 — **与产品约定不符,应废弃并改为 ext_config**。 + +**可保留部分**(逻辑正确,仅需改配置来源): + +- [`DailyRebateCalcService`](slot_console/app/service/DailyRebateCalcService.php) — 累进分段计算 +- [`daily_rebate_record`](slot_console/db/daily_rebate.sql) — 用户日返水记录(需加 `activity_id`) +- [`DailyRebateSettle` / `DailyRebateExpire`](slot_console/app/command/DailyRebateSettle.php) 定时任务 +- [`DailyRebateController`](slot_console/app/api/controller/DailyRebateController.php) C 端 info/claim +- [`DailyRebateStats*`](backend/slot_admin/app/game/controller/DailyRebateStatsController.php) — **综合统计/每日返水统计**(独立菜单,同 Free Credits 统计) + +```mermaid +flowchart TB + subgraph admin [活动管理 slot_admin] + ActivityUI["activity/edit.vue type=12"] + ActivityAPI["ActivityController + ActivityService"] + end + subgraph config [s_common] + RGC["recharge_gift_config ext_config.tiers"] + DRR["daily_rebate_record"] + end + subgraph runtime [slot_console] + Settle["dailyRebateSettle"] + API["DailyRebateService info/claim"] + end + ActivityUI --> ActivityAPI --> RGC + Settle --> RGC + Settle --> DRR + API --> RGC + API --> DRR +``` + +--- + +## 1. 新增活动类型 `12` — 每日返水 + +### 1.1 常量与模型 + +- [`RechargeGiftConfigModel`](slot_console/app/model/common/RechargeGiftConfigModel.php) 增加: + ```php + const TYPE_DAILY_REBATE = 12; // 每日返水 + ``` +- 新增方法 `getDailyRebateActiveInfo(string $source)`,逻辑**复制** `getFreeCreditsActiveInfo`:`status=1`、`type=12`、时间窗、`source` 优先于 `all`。 +- [`slot_lib/Consts.php`](slot_lib/src/common/const/Consts.php) 增加 `ACTIVITY_TYPE_DAILY_REBATE = 12`(与现有 1–9 并列,便于订单/日志引用)。 +- 字典 `activity_type` 增加一条(SQL 或后台字典):`value=12, label=每日返水`(与 type=11「首充定格/Free Credits」同级)。 + +### 1.2 `ext_config` 结构(档位配置) + +```json +{ + "tiers": [ + { "sort": 1, "min_bet_qf": 0, "max_bet_qf": 1000000, "rate_percent": 3 }, + { "sort": 2, "min_bet_qf": 1000001, "max_bet_qf": 3000000, "rate_percent": 2 }, + { "sort": 3, "min_bet_qf": 3000001, "max_bet_qf": 5000000, "rate_percent": 1 }, + { "sort": 4, "min_bet_qf": 5000001, "max_bet_qf": null, "rate_percent": 0.5 } + ] +} +``` + +- 金额与全站一致:**存储千分位(厘)**;后台表单用美元展示,提交时 `×1000`(与 type=11 的 `toQf` 一致)。 +- 默认种子:可在活动保存时由前端预填 4 档,或提供 migration 插入一条 `type=12` 的 default 活动(`source=all`)。 + +### 1.3 校验 + +在 [`ActivityValidate`](backend/slot_admin/app/game/validate/ActivityValidate.php) 新增 `checkDailyRebateExt(array $extConfig)`: + +- `tiers` 非空数组,每档含 `sort/min_bet_qf/rate_percent`;非最后一档 `max_bet_qf` 必填。 +- 复用 [`DailyRebateCalcService::validateTiersContinuous`](slot_console/app/service/DailyRebateCalcService.php)(入参转为 `min_bet`/`max_bet` 厘)。 + +在 [`ActivityController`](backend/slot_admin/app/game/controller/ActivityController.php) 的 `save` / `update`(`updateData` 时)对 `type === 12` 调用该校验(与 type=11 并列)。 + +--- + +## 2. 管理后台 — 活动配置 UI(核心) + +参考 type=11,在 [`activity/edit.vue`](backend/slot_admin_vue/src/views/game/activity/edit.vue) 增加 **`formData.type == 12`** 区块: + +| 表单项 | 说明 | +|--------|------| +| 返水档位表 | 可增删行:排序、下限($)、上限($,最后一档可空)、比例(%) | +| 规则说明 | 复用活动主表 `help` 字段(与 Free Credits 一致) | +| 展示时间/生效时间 | 复用现有 `show_*` / `start_time` / `end_time` | + +**交互约束**(与 type=11 相同): + +- `onlyGift` 计算属性加入 `12` → 不展示「赠送配置 goods」区块。 +- `submit` 时:`type===12` → `data.goods=[]`,`ext_config.tiers` 做 `toQf` 转换。 +- `setFormData` 时:`tiers` 从 `_qf` 还原为美元展示。 + +[`activity/index.vue`](backend/slot_admin_vue/src/views/game/activity/index.vue) 列表 `ext_config` 列:type=12 时展示各档比例摘要(类似 type=11 展示门槛)。 + +**删除**(不再单独维护档位): + +- [`DailyRebateTierConfigController`](backend/slot_admin/app/game/controller/DailyRebateTierConfigController.php) 及 Logic/Validate/Model +- [`dailyRebateTierConfig.js`](backend/slot_admin_vue/src/api/game/dailyRebateTierConfig.js) 与对应 vue(若已创建) +- [`menu-daily-rebate.sql`](backend/slot_admin/db/menu-daily-rebate.sql) 中「每日返水档位」菜单项(3082) +- 表 `daily_rebate_tier_config`(不建或迁移后 DROP) + +**保留菜单**:仅「综合统计 → 每日返水统计」(3084),挂在活动管理同级或统计分组下。 + +--- + +## 3. 运行时改造(slot_console) + +### 3.1 配置读取 + +改造 [`DailyRebateService`](slot_console/app/service/DailyRebateService.php): + +- 删除对 `DailyRebateTierConfigModel` 的依赖。 +- 通过 `RechargeGiftConfigModel::getDailyRebateActiveInfo($source)` 取活动;无生效活动 → C 端 `unlocked=false` 或整活动不可用(与未开 Free Credits 一致)。 +- 从 `ext_config['tiers']` 解析为 `DailyRebateCalcService` 入参(`min_bet`/`max_bet`/`rate_percent`)。 +- `info()` 返回的 `tiers` 来自当前活动配置;`tier_snapshot` 结算时写入完整 tiers JSON。 + +### 3.2 用户记录表 + +调整 [`daily_rebate_record`](slot_console/db/daily_rebate.sql): + +- 增加 `activity_id bigint`(`recharge_gift_config.id`),结算/待定时写入。 +- **删除** `daily_rebate_tier_config` 建表与种子 SQL。 + +### 3.3 结算与领取 + +- [`DailyRebateSettle`](slot_console/app/command/DailyRebateSettle.php):按渠道解析活动配置后再算返利;未充值用户仍跳过。 +- 领取:继续 `TRANSACTION_TYPE_DAILY_REBATE = 65`(已在 [`Consts.php`](slot_lib/src/common/const/Consts.php) 规划/部分落地)。 + +### 3.4 C 端 API(不变路径) + +- `GET /api/daily-rebate/info` +- `POST /api/daily-rebate/claim` + +需校验:**当前用户渠道存在生效的 type=12 活动**,否则返回活动未开启。 + +--- + +## 4. 综合统计页(保持独立) + +与 [Free Credits 统计](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php) 相同定位: + +- 菜单:**综合统计 / 每日返水统计**(非活动配置页) +- 实现:沿用已规划的 [`DailyRebateStatsLogic`](backend/slot_admin/app/game/logic/DailyRebateStatsLogic.php) + [`dailyRebateStats/index.vue`](backend/slot_admin_vue/src/views/game/dailyRebateStats/index.vue)(若 vue 未写完则补全) +- 数据源:`daily_rebate_record`;筛选/汇总规则不变 + +--- + +## 5. 与首充定格的差异对照 + +| 项 | Free Credits (11) | 每日返水 (12) | +|----|-------------------|---------------| +| ext_config | 门槛、分档金额、Banner | **tiers 累进档位** | +| 用户状态表 | free_credits_player/package | daily_rebate_record | +| 统计页 | Free Credits 统计 | 每日返水统计 | +| 充值门槛 | 首充定格业务 | **历史充值过即可**(wallet `r>0`) | +| 计费基础 | 定格池 | 当日 `user_profit_daily.bet` | + +--- + +## 6. 实施步骤(建议顺序) + +1. **活动类型与 ext_config 契约**:Model 常量 + `getDailyRebateActiveInfo` + ActivityValidate + ActivityController + 字典 SQL。 +2. **后台 activity/edit.vue + index.vue**:type=12 档位表 UI 与 qf 换算。 +3. **重构 DailyRebateService**:读活动 ext_config;record 加 `activity_id`。 +4. **清理**:移除 tier 独立表/Controller/菜单;更新 `daily_rebate.sql`。 +5. **补全**:统计 vue、PWA 流水类型 65、单元测试仍用 tiers 数组驱动 CalcService。 +6. **验收**:后台新建 type=12 活动 → 用户下注 → 日切结算 → 领取 → 统计页数字一致。 + +--- + +## 7. 验收要点 + +1. 仅在「充值活动配置」可创建/编辑 type=12,档位保存后 `ext_config.tiers` 正确落库。 +2. 同渠道多条 type=12 时行为与 type=11 一致(优先具体 source)。 +3. 改档位仅影响**新结算日**(`tier_snapshot` 审计旧记录)。 +4. 不再出现「每日返水档位」独立菜单。 +5. $2000 下注 → 次日返水 $50,与累进公式一致。 + +--- + +## Definition of Done(硬规则对齐) + +- [ ] `grep -r DailyRebateService ~/Documents/project/www/slot --include='*.php'` → 无命中(排除 vendor) +- [ ] [`DailyRebateController`](slot_console/app/api/controller/DailyRebateController.php) 对齐 [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php)(`BaseController` + Logic + Validate) +- [ ] 已删除 orphan:`slot_admin_vue` 下 `dailyRebateTierConfig/*`、`api/game/dailyRebateTierConfig.js` +- [ ] `~/.cursor/hooks/verify-slot-backend.sh` 输出 `PASS` diff --git a/plans/每日返水需求文档_20a03147.plan.md b/plans/每日返水需求文档_20a03147.plan.md new file mode 100644 index 0000000..c9b0b0f --- /dev/null +++ b/plans/每日返水需求文档_20a03147.plan.md @@ -0,0 +1,408 @@ +--- +name: 每日返水需求文档 +overview: 在现有 user_profit_daily 日下注统计基础上,新增「每日返水」活动:累进档位计费、仅充值用户参与、次日 00:00 起可领且 24 小时内有效;配套用户端 API、后台档位配置与综合统计页。 +todos: + - id: schema + content: 设计并评审 daily_rebate_record、daily_rebate_tier_config 表结构与索引 + status: in_progress + - id: calc-service + content: 实现累进分段 RebateCalcService + tier_snapshot + 单元测试(含 $2000→$50 等用例) + status: pending + - id: cron + content: slot_console:dailyRebateSettle / dailyRebateExpire 定时任务 + status: pending + - id: user-api + content: slot_console 用户端 info/claim API + slot_wallet 入账与新 transaction type + status: pending + - id: admin-tier + content: slot_admin 每日返水档位 CRUD 页 + status: pending + - id: admin-stats + content: slot_admin 综合统计/每日返水统计(列表+筛选+排序+汇总,参考 FreeCreditsStats) + status: pending + - id: pwa-ui + content: 前端三态 UI、7 日表、倒计时、领取按钮 + status: pending +isProject: false +--- + +# 每日返水(Daily Rebate 3%)需求规格书 + +## 1. 背景与目标 + +- **业务目标**:对已充值用户,按自然日累计有效下注给予累进比例返水,提升留存与投注激励。 +- **数据基础**:`[slot_console](slot_console)` 已通过 `[LoseReturnDeposit](slot_console/app/command/LoseReturnDeposit.php)` + Redis `[UserProfitService](slot_pwa/app/service/user/UserProfitService.php)` 维护 `[s_statistics.user_profit_daily](slot_console/app/model/statistics/UserProfitDaily.php)`(字段 `bet`/`win`/`profit`/`create_date`/`source`)。 +- **与 VIP 亏损返利的区别**:VIP 返利基于「当日净亏损」且需 VIP 等级;本活动基于「当日总下注额」、仅需历史充值、档位为累进分段,产品独立。 + +--- + +## 2. 术语 + + +| 术语 | 定义 | +| ---- | ------------------------------------------------------------------------------------------------------------- | +| 自然日 | 服务器时区 `Asia/Shanghai`(与 `[slot_center/config/app.php](slot_center/config/app.php)` 一致)的 `00:00:00`–`23:59:59` | +| 有效下注 | **等于** `user_profit_daily.bet`(厘),与 PWA 下注时 `incBet` 累计值一致;无单独过滤规则 | +| 返水金额 | 按档位配置对当日有效下注做**累进分段**计算后的金额(厘) | +| 统计日期 | 发生下注的自然日 `create_date`,非领取日 | +| 充值用户 | 结算时刻钱包 `total_deposit > 0`(`[WalletStatModel](slot_wallet/app/model/multi/WalletStatModel.php)`) | + + +--- + +## 3. 核心业务规则 + +### 3.1 参与资格 + +- **未充值**:不参与返水;前端展示「锁定」态(Unlock Cashback),不可领、列表可置灰或仅展示引导文案。 +- **已充值**:自充值成功当日起,当日及之后有有效下注的自然日可产生返水记录;历史未充值日的下注不补发。 + +### 3.2 返水计算(累进分段,非整笔单一比例) + +**已确认**:采用分段累进,与原型及后台示例一致($2000 → $50)。 + +默认档位(首版种子数据,**后台可配置**): + + +| 分段序号 | 下注区间(含边界,美元展示) | 比例 | +| ---- | --------------- | ---- | +| 1 | $0 – $1,000 | 3% | +| 2 | $1,001 – $3,000 | 2% | +| 3 | $3,001 – $5,000 | 1% | +| 4 | $5,001 及以上 | 0.5% | + + +**计算公式**(`B` = 当日有效下注,美元;存储与计算用厘,`1 USD = 1000` 厘,与 `[share_config.moneyFormat](slot_center/config/share_config.php)` 一致): + +``` +rebate_li = Σ segment_i( min(B, max_i) - min(B, min_i) + ε_i ) × rate_i +``` + +- 各档 `min_i`/`max_i` 以后台配置为准(最后一档 `max` 为空表示无上限)。 +- 分段左闭右闭:第 1 档覆盖 `[0, 1000]`,第 2 档覆盖 `(1000, 3000]`,依此类推(实现时用「上一档上限 + 1」作为下一档起点,避免重复计费)。 +- **舍入**:返水金额入库前 `floor` 到厘;前端展示保留 2 位小数美元。 + +**计算示例**: + + +| 有效下注 | 计算过程 | 返水 | +| ------- | --------------------------------------- | ------ | +| $125.56 | 125.56 × 3% | $3.77 | +| $200 | 200 × 3% | $6.00 | +| $2,000 | 1000×3% + 1000×2% | $50.00 | +| $6,000 | 1000×3% + 2000×2% + 2000×1% + 1000×0.5% | $95.00 | + + +**待定态(当天)**:用 Redis 当日 `bet` 实时重算返水预览;不入库终态金额,或与 DB 行 `status=pending` 同步更新。 + +### 3.3 结算与领取时间轴 + +```mermaid +sequenceDiagram + participant User + participant PWA as slot_pwa + participant Redis + participant Cron as slot_console_cron + participant DB as daily_rebate_record + + Note over User,Redis: D日 00:00-23:59 + User->>PWA: 下注 + PWA->>Redis: incBet + Note over DB: status=pending 实时 bet/rebate + + Note over Cron,DB: D+1日 00:00后 cron + Cron->>Redis: 读取 D日 bet + Cron->>DB: 写入/更新 settle: claimable, dead_time=D+2 00:00 + + Note over User,DB: D+1日 仅可领 D日 返水 + User->>PWA: claim(stat_date=D) + PWA->>DB: status=claimed + + Note over DB: 超过 dead_time 未领 + Cron->>DB: status=expired +``` + + + + +| 时点 | 行为 | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| D 日进行中 | 状态 **待定**;有效下注、返水金额随 Redis 刷新 | +| D+1 日 00:00 | 定时任务结算 D 日:有 `bet>0` 且已充值 → 生成/更新记录,状态 **待领取**;`claimable_at = D+1 00:00:00`,`expire_at = D+2 00:00:00`(领取窗口 **24 小时**,对齐 VIP `[vip_rebate_lose_time](slot_center/config/share_config.php)`) | +| D+1 日全天 | 用户**只能领取 stat_date=D** 的返水(「前一天」) | +| D+2 日 00:00 前未领 | 状态 **过期**;`rebate_amount>0` 且未领 | +| 领取成功 | 状态 **已领**;记 `claimed_at`;钱包入账 | + + +**无有效下注**:不生成记录(后台列表、前端 7 日表均不展示该日)。 + +**返水为 0**:若档位计算为 0(`bet=0` 已排除),`bet>0` 但舍入为 0 时可不落库或落库且不可领——建议 **不落库**,与「未有有效下注不显示」一致。 + +### 3.4 状态机 + + +| 状态码 | 中文 | 条件 | +| ----------- | --- | --------------------------------------------------------------------- | +| `pending` | 待定 | `stat_date = 今天`;实时统计未结束 | +| `claimable` | 待领取 | `stat_date < 今天`;`rebate_amount > 0`;`claimed_at` 空;`now < expire_at` | +| `claimed` | 已领 | `claimed_at` 非空 | +| `expired` | 过期 | `rebate_amount > 0`;未领;`now >= expire_at` | + + +状态迁移: + +- `pending` → `claimable`:日切结算任务(仅昨日及更早批量处理;今日保持 pending) +- `claimable` → `claimed`:用户领取接口(幂等) +- `claimable` → `expired`:过期扫描任务或领取时校验 + +前端映射(英文 UI): + + +| 状态 | 展示 | 行样式 | +| --------- | --------- | -------------------- | +| pending | Pending | 绿色标签(进行中) | +| claimable | Claimable | 绿色可点 | +| claimed | Claimed | 白色/灰色 | +| expired | Expired | 红色/灰色 | +| 无记录 | No Bets | 灰字(仅前端 7 日占位,无 DB 行) | + + +### 3.5 领取规则 + +- 每次领取**一条**指定 `stat_date`(通常为昨日)。 +- 并发:DB 行级锁 / `UPDATE ... WHERE status=claimable` 防重复领。 +- 入账:新增钱包流水类型(建议 `TRANSACTION_TYPE_DAILY_REBATE = 65`,在 `[Consts.php](slot_lib/src/common/const/Consts.php)` 登记);更新 `total_cashback`。 +- 失败:事务回滚,状态不变。 + +--- + +## 4. 数据设计 + +### 4.1 沿用表 + +- `**s_statistics.user_profit_daily`**:只读来源,提供 `bet`、`source`、`create_date`;**不扩展**状态/返水字段。 + +### 4.2 新建表 `daily_rebate_record`(建议库:`s_common`) + + +| 字段 | 类型 | 说明 | +| ----------------------- | ----------------- | ----------------------------------------- | +| id | bigint PK | | +| uid | bigint | 用户 ID | +| source | varchar | 渠道号(结算时快照) | +| stat_date | date | 统计日期(下注日) | +| bet_amount | bigint | 有效下注(厘) | +| rebate_amount | bigint | 返水金额(厘) | +| status | tinyint | 1 pending 2 claimable 3 claimed 4 expired | +| tier_snapshot | json | 结算时档位快照(审计) | +| claimable_at | datetime | 可领取开始 | +| expire_at | datetime | 过期时间 | +| claimed_at | datetime nullable | 领取时间 | +| created_at / updated_at | datetime | | + + +**唯一索引**:`(uid, stat_date)`。 + +**索引**:`(stat_date, status)`、`(source, stat_date)`、`(uid)`。 + +### 4.3 新建表 `daily_rebate_tier_config`(后台可配置) + + +| 字段 | 说明 | +| ----------------------- | ------------------ | +| id | PK | +| sort | 排序(从小到大) | +| min_bet | 区间下限(厘,含) | +| max_bet | 区间上限(厘,含;NULL=无上限) | +| rate_percent | 比例,如 3.00 表示 3% | +| status | 启用/停用 | +| updated_by / updated_at | 审计 | + + +校验:档位连续无空洞、无重叠;至少一档;最后一档可无 `max_bet`。 + +--- + +## 5. 定时任务(slot_console) + + +| 任务 | 触发 | 职责 | +| ----------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- | +| `dailyRebateSettle` | 每日 00:05(可配置) | 结算 **昨日** `user_profit_daily` + 充值校验 → upsert `daily_rebate_record` 为 claimable | +| `dailyRebateExpire` | 每小时或 00:10 | 将 `claimable` 且 `now>=expire_at` 置为 expired | +| (可选)与现有 `[loseReturnDeposit](slot_console/app/command/LoseReturnDeposit.php)` | 同批读 Redis | 避免重复扫 Redis;**推荐独立命令**以免耦合 VIP 逻辑 | + + +**待定实时**:用户打开活动页时,Logic 读 Redis `user:profit:{Ymd}` + 当前档位配置计算预览,不写 claimable。 + +--- + +## 6. 用户端(PWA / Gateway) + +### 6.1 页面态(对齐原型) + + +| 态 | 条件 | 主按钮 | 表格 | +| --- | ------------------------------- | --------------- | ------------- | +| 锁定 | `total_deposit==0` | Unlock Cashback | 模糊/遮罩 | +| 已解锁 | 已充值,无可领 | Play Now | 近 7 日明细 | +| 可领取 | 存在 `claimable` 且 `stat_date=昨天` | Claim($X.XX) | 昨日行 Claimable | + + +**倒计时**:「THIS ROUND」= 距今日自然日结束的秒数(与 Rates reset 00:00 文案一致)。 + +### 6.2 API(建议落在 slot_console `/api/daily-rebate/*`,经 gateway 转发) + +**GET `/api/daily-rebate/info`** + +响应示例字段: + +```json +{ + "unlocked": true, + "countdown_seconds": 86399, + "claimable": { "stat_date": "2026-05-25", "rebate_amount": 3000, "display": "3.00" }, + "tiers": [{ "min": 0, "max": 1000000, "rate": 3 }], + "records": [ + { "stat_date": "05/26", "bet_amount": 125560, "rebate_amount": 3770, "status": "pending" } + ] +} +``` + +- `records`:最近 7 个自然日,**有 bet 或 DB 行**的日期;无下注日可不返回或前端填 No Bets。 +- 打开弹窗时 **强制刷新** 当日 pending 数据。 + +**POST `/api/daily-rebate/claim`** + +- 入参:`stat_date`(可选,默认昨天) +- 校验:已充值、status=claimable、未过期、rebate>0 +- 出参:领取后余额/流水号 + +**GET `/api/daily-rebate/tiers`**(可选,info 已含则省略) + +--- + +## 7. 管理后台(slot_admin + slot_admin_vue) + +### 7.1 菜单 + +- **活动配置 / 每日返水档位**(新建):CRUD [`daily_rebate_tier_config`],校验区间连续。 +- **综合统计 / 每日返水统计**(新建):列表 + 汇总。 + +实现参考:列表+汇总 `[FreeCreditsStats](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php)` + 前端 `[freeCreditsStats/index.vue](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue)`。 + +### 7.2 统计列表 + +**列**:ID(uid) | 渠道号 | 有效下注额⇅ | 返水金额⇅ | 状态 | 统计日期 + +**筛选**: + +- UID(精确) +- 渠道(`source`,可搜索下拉,复用 `commonStore.allSourcesOptionsNoAll`) +- 状态:待定 / 待领取 / 已领 / 过期(多选) +- 统计日期范围(`stat_date[]`) + +**排序白名单**:`bet_amount`、`rebate_amount`(默认 `stat_date desc, id desc`) + +**数据范围**:仅 `total_deposit>0` 用户在结算时已入库的记录;**待定**行可对「今天」合并 Redis 实时 bet(与 Free Credits「打开刷新」一致)。 + +### 7.3 顶部汇总(随筛选变化) + + +| 指标 | 计算 | +| ---- | ----------------------------------------------- | +| 总领取 | `status=claimed` 的 `sum(rebate_amount)` | +| 总待领取 | `status=claimable` 的 sum | +| 总过期 | `status=expired` 的 sum | +| 待定 | `status=pending` 的 sum;**若筛选日期范围不含今天则为 0** | +| 领取率 | `总领取 / (总领取 + 总过期 + 总待领取)`;分母为 0 时显示 `0%` 或 `-` | + + +展示格式:`$888.88`(`rebate_amount / 1000`,2 位小数)。 + +--- + +## 8. 非功能需求 + +- **幂等**:结算任务对 `(uid, stat_date)` upsert;领取 CAS 更新状态。 +- **性能**:日活结算批量按日期分页;后台列表分页默认 100,最大 100。 +- **审计**:`tier_snapshot` 保留结算时档位;领取写 wallet log `biz_extra: {activity:"daily_rebate", stat_date}`。 +- **监控**:结算失败/领取失败打日志 + 指标;过期数量日报。 + +--- + +## 9. 边界与异常 + + +| 场景 | 处理 | +| ------------- | ----------------------------------------------------------- | +| 结算日用户刚充值、昨日下注 | 昨日结算时已按当时 `total_deposit` 判断;若需「充值后立即对历史补发」→ **不做**,以结算时刻为准 | +| 结算任务重复跑 | upsert,不重复入账 | +| 时区变更 | 禁止随意改;变更需重算规则文档化 | +| bet>0 未充值 | 不生成记录 | +| 领取时档位已改 | 以 `tier_snapshot` 为准,不受后续配置变更影响 | +| 跨天未关弹窗 | 前端倒计时结束刷新 info;pending→claimable 由后端状态驱动 | + + +--- + +## 10. 实施拆分(供研发排期) + +```mermaid +flowchart LR + subgraph phase1 [Phase1 数据与配置] + T1[daily_rebate_tier_config] + T2[daily_rebate_record] + T3[admin 档位 CRUD] + end + subgraph phase2 [Phase2 结算与过期] + C1[dailyRebateSettle] + C2[dailyRebateExpire] + L1[rebate calc service] + end + subgraph phase3 [Phase3 用户端] + A1[info API] + A2[claim API + wallet] + F1[PWA 三态 UI] + end + subgraph phase4 [Phase4 后台统计] + B1[DailyRebateStats Logic] + B2[admin vue 列表汇总] + end + phase1 --> phase2 --> phase3 + phase2 --> phase4 +``` + + + + +| 仓库 | 改动要点 | +| ---------------------- | ---------------------------------------------------------- | +| slot_console | Model、结算/过期 Command、RebateCalcService、用户 API、Redis 读当日 bet | +| slot_wallet | 新 transaction type、领取入账 | +| slot_lib | Consts 新类型 | +| backend/slot_admin | Controller/Logic/Validate、菜单 SQL | +| backend/slot_admin_vue | 档位页 + 统计页 | +| slot_sdk | 若 gateway 经 sdk 调 console,补 Client 方法 | + + +**不建议**复用 `vip_rebate_record` 表:业务语义、状态机、计费基础均不同。 + +--- + +## 11. 验收标准(摘要) + +1. 充值用户 D 日下注 $2000,D+1 00:00 后显示待领取 $50,24h 内领取成功,流水类型正确。 +2. 未充值用户仅见锁定态,无领取接口成功路径。 +3. 超 24h 未领变过期,不可再领。 +4. 后台筛选/status/排序/汇总与文档公式一致;改档位仅影响新结算日。 +5. 前端 7 日表、Claim 按钮金额与后台一致;当日为 Pending 且随下注刷新。 + +--- + +## 12. 已确认决策 + +- 计费方式:**累进分段**(非整笔落档单一比例)。 +- 有效下注:**等同 `user_profit_daily.bet`**。 +- 档位:**后台可配置**(`daily_rebate_tier_config`)。 + diff --git a/plans/注册奖励随机游戏_9f41e4af.plan.md b/plans/注册奖励随机游戏_9f41e4af.plan.md new file mode 100644 index 0000000..4d77800 --- /dev/null +++ b/plans/注册奖励随机游戏_9f41e4af.plan.md @@ -0,0 +1,160 @@ +--- +name: 注册奖励随机游戏 +overview: 在 slot_console 注册活动(type=7)领取成功响应中,从活动 ext_config.game_ids 配置的候选池里,按当前渠道 game_model_id 过滤已上架游戏后随机返回 game_id,供前端直接进游戏。 +todos: + - id: model-published-ids + content: GameApiModel 新增 publishedGameIds(gameModelId, gameIds) 查询 + status: completed + - id: logic-pick-random + content: 新增 RegisterRewardLogic::pickRandomGameId 解析 ext_config 并随机 + status: completed + - id: entity-receive + content: ActivityConfigEntity ACTIVITY_TYPE_REG 成功分支接入 Logic 并扩展返回 + status: completed + - id: controller-phpdoc + content: GiftController::receive PHPDoc 补充 game_id 字段说明 + status: completed + - id: unit-tests + content: RegisterRewardLogicTest + php82 容器跑单测 + status: completed +isProject: false +--- + +# 注册奖励领取后随机返回 game_id + +## 背景与范围 + +- **入口**:[`POST /api/gift/receive`](slot_console/app/api/controller/GiftController.php) → [`ActivityConfigEntity::receive()`](slot_console/app/entity/activity/ActivityConfigEntity.php) 中 `ACTIVITY_TYPE_REG`(type=7)。 +- **不在范围**:注册自动到账(`UserRegisterEventService`)、后台 Vue 配置页、前端进游戏逻辑。 +- **配置来源**(已确认):活动表 `s_recharge_gift_config.ext_config.game_ids`。 + +当前领取成功仅返回金额: + +```750:750:slot_console/app/entity/activity/ActivityConfigEntity.php + return ['gift_coin' => CommonFn::getNumberFormat($giftAmount), 'gift_bonus' => CommonFn::getNumberFormat($giftBonus)]; +``` + +`game_id` 语义与大厅一致:[`s_game_api.game_id`](slot_console/app/model/GameApiModel.php) = `game.id`,前端用该 ID 调 `POST /api/game/login` 的 `gameId` 或 napi 搜索同款字段。 + +## 配置契约 + +在对应注册活动(type=7)的 `ext_config` 中增加: + +```json +{ + "game_ids": [101, 205, 308] +} +``` + +- `game_ids`:`int[]`,运营在 DB/后台 JSON 中维护;本任务不实现 admin UI。 +- 未配置、空数组、或过滤后无有效游戏:**不阻断领取**,响应中 **省略 `game_id` 字段**(与 `withdraw_guide` 用 `0` 不同,避免前端误开游戏)。 + +## 数据流 + +```mermaid +sequenceDiagram + Client->>GiftController: POST /api/gift/receive id=activityId + GiftController->>ActivityConfigEntity: receive() + ActivityConfigEntity->>WalletService: gift() 注册赠送入账 + ActivityConfigEntity->>RegisterRewardLogic: pickRandomGameId(ext_config, gameModelId) + RegisterRewardLogic->>GameApiModel: 过滤 status=1 且已发布 + RegisterRewardLogic-->>ActivityConfigEntity: game_id|null + ActivityConfigEntity-->>Client: gift_coin, gift_bonus, game_id? +``` + +## 实现要点 + +### 1. 新增 Logic:随机选游戏 + +新建 [`slot_console/app/api/logic/RegisterRewardLogic.php`](slot_console/app/api/logic/RegisterRewardLogic.php)(参考 [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php) 的独立 Logic 拆分方式): + +| 方法 | 职责 | +| --- | --- | +| `pickRandomGameId(array\|null $extConfig, int $gameModelId): ?int` | 解析 `game_ids` → 去重/转 int → 调 Model 过滤 → `array_rand` 返回一个 id | + +解析规则: + +- 支持 `game_ids` 为 JSON 数组或逗号分隔字符串(防御性,与部分旧配置风格兼容)。 +- 非法/非正整数丢弃。 + +### 2. Model:候选池与渠道上架交集 + +在 [`GameApiModel`](slot_console/app/model/GameApiModel.php) 增加查询方法,例如: + +```php +public static function publishedGameIds(int $gameModelId, array $gameIds): array +``` + +条件:`game_id IN (...)`、`game_model_id = $gameModelId`、`status = STATUS_ON`;返回可用 `game_id` 列表。 + +不在 Logic 里直接拼 SQL,符合分层规则。 + +### 3. 改动领取分支 + +在 [`ActivityConfigEntity::receive()`](slot_console/app/entity/activity/ActivityConfigEntity.php) 的 `ACTIVITY_TYPE_REG` case: + +- 发奖逻辑保持不变(`WalletService::gift` + `sendRegisterGiftWagerTask` + `setActivityFinishById`)。 +- **仅在 `$res` 非空(领取成功)后**: + - 用 `$this->where('id', $activityId)->find()` 读取原始 `ext_config`(`getActivityByInfo()` 经 `getGiftItemsByActivity` 组装,**不含** ext_config,与兑换码分支读库方式一致)。 + - `$gameModelId = $this->_modelId`(已由 `setSource` 设置)。 + - 调用 `RegisterRewardLogic::pickRandomGameId()`。 +- 在方法末尾统一组装返回:有值则追加 `'game_id' => $pickedId`。 + +[`GiftController::receive()`](slot_console/app/api/controller/GiftController.php) 补充 PHPDoc:`data.game_id`(int,可选,领取注册活动成功且配置了有效候选池时返回)。 + +### 4. 响应示例 + +成功且命中游戏: + +```json +{ + "code": 0, + "data": { + "gift_coin": "10.00", + "gift_bonus": "0.00", + "game_id": 205 + } +} +``` + +成功但无可用游戏:仅 `gift_coin` / `gift_bonus`(与现网兼容)。 + +## 单测 + +新建 [`slot_console/tests/Unit/RegisterRewardLogicTest.php`](slot_console/tests/Unit/RegisterRewardLogicTest.php): + +- `game_ids` 为空 / 缺失 → `null` +- 字符串 `"1,2,3"` 解析 +- Model 层可用 stub 或 sqlite/内存 mock(若项目已有 GameApi 测试惯例则对齐) + +在 docker 内执行: + +```bash +docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/RegisterRewardLogicTest.php +``` + +## 运维配置说明(无 UI) + +对目标渠道 type=7 活动,更新 `s_recharge_gift_config.ext_config`: + +```sql +-- 示例:在现有 ext_config 上合并 game_ids +UPDATE s_recharge_gift_config +SET ext_config = JSON_SET(COALESCE(ext_config, '{}'), '$.game_ids', JSON_ARRAY(101, 205)) +WHERE id = AND type = 7; +``` + +`game_ids` 须为当前 `model_id` 下已在 `s_game_api` 上架的 `game.id`。 + +## 风险与边界 + +| 场景 | 行为 | +| --- | --- | +| 配置了已下架/未发布游戏 | 从池中剔除;池空则不下发 `game_id` | +| 重复领取 | 现有 `Is Got` 拦截,不会二次随机 | +| 多注册活动同渠道 | 按请求的 `id` 读对应活动 `ext_config` | + +## 后续可选(本任务不做) + +- slot_admin 活动编辑页 type=7 增加「推荐游戏」多选,写入 `ext_config.game_ids` +- `getActivityList` 预展示候选游戏(非领取必需) diff --git a/plans/注册活动列表_game_id_2136b4fb.plan.md b/plans/注册活动列表_game_id_2136b4fb.plan.md new file mode 100644 index 0000000..5d72ba8 --- /dev/null +++ b/plans/注册活动列表_game_id_2136b4fb.plan.md @@ -0,0 +1,127 @@ +--- +name: 注册活动列表 game_id +overview: 在 C 端活动列表(type=7 注册活动)透出 `game_id` 供前端展示/进游戏;同时从领取接口 `POST /api/gift/receive` 移除 `game_id` 字段,复用现有 `RegisterRewardLogic` 与 `ext_config.game_ids` 配置。 +todos: + - id: list-game-id + content: ActivityConfigEntity::getGiftItemsByActivity type=7 分支调用 RegisterRewardLogic 写入 game_id + status: completed + - id: remove-receive-game-id + content: ActivityConfigEntity::receive 移除注册活动 game_id 逻辑 + status: completed + - id: logic-stable-pick + content: RegisterRewardLogic 增加 pickGameIdForUser(稳定随机)并更新注释 + status: completed + - id: phpdoc-tests + content: GiftController PHPDoc 调整 + RegisterRewardLogicTest 补充用例 + status: completed +isProject: false +--- + +# 注册活动列表透出 game_id,领取不再返回 + +## 需求变更(相对初版) + +| 接口 | 原行为 | 目标行为 | +|------|--------|----------| +| `POST /api/gift/getActivityList` | 注册活动无 `game_id` | type=7 项增加可选 `game_id` | +| `POST /api/gift/receive`(type=7) | 成功时随机返回 `game_id` | **仅** `gift_coin`、`gift_bonus` | + +后台 `ext_config.game_ids` 配置与 [`RegisterRewardLogic`](slot_console/app/api/logic/RegisterRewardLogic.php) 过滤已上架游戏逻辑**保持不变**,仅调整调用位置。 + +## 数据流 + +```mermaid +sequenceDiagram + Client->>GiftController: getActivityList + GiftController->>GiftService: getUserActivityList + GiftService->>ActivityConfigEntity: getGiftItemsByActivity type=7 + ActivityConfigEntity->>RegisterRewardLogic: pickGameId from ext_config.game_ids + ActivityConfigEntity-->>Client: activity item with optional game_id + + Client->>GiftController: receive id=activityId + GiftController->>ActivityConfigEntity: receive ACTIVITY_TYPE_REG + ActivityConfigEntity-->>Client: gift_coin, gift_bonus only +``` + +## 实现(仅 slot_console) + +### 1. 活动列表组装:增加 `game_id` + +文件:[`slot_console/app/entity/activity/ActivityConfigEntity.php`](slot_console/app/entity/activity/ActivityConfigEntity.php) + +在 `getGiftItemsByActivity()` 的 `switch ($activityType)` 中新增 `ACTIVITY_TYPE_REG` 分支(与首充/兑换码同级,约在 340 行附近): + +- 用已有 `normalizeExtConfig($activity['ext_config'])` 读取 `game_ids` +- 调用 `RegisterRewardLogic` 得到 `game_id` +- 仅当非 `null` 时写入 `$activityInfo['game_id']`(与领取侧旧约定一致:**省略字段**而非 `0`) + +`$activity` 来自 `getActiveList()` 的 DB 模型,已含 `ext_config`;`ActivityConfigEntity` 在 `GiftService::getUserActivityList()` 中已通过 `setUid` / `setSource` 设置 `_modelId`。 + +**列表随机策略(建议默认)**:同一用户、同一活动多次拉列表应看到同一款游戏,避免卡片闪烁。在 `RegisterRewardLogic` 增加例如 `pickGameIdForUser(?array $extConfig, int $gameModelId, int $uid, int $activityId): ?int`: + +- 复用 `parseGameIds` + `GameApiModel::publishedGameIds` +- 对 `published` **排序**后,用 `crc32("{$uid}:{$activityId}") % count` 取下标(非 `array_rand`) + +列表与领取不再需要对齐,但稳定随机对仅列表展示更友好。若产品希望每次刷新换游戏,可继续用现有 `pickRandomGameId`。 + +### 2. 领取分支:移除 `game_id` + +同一文件 `receive()`: + +- 删除 `ACTIVITY_TYPE_REG` 内读取 `ext_config`、`RegisterRewardLogic::pickRandomGameId` 及 `$registerRewardGameId` 变量 +- 删除返回数组末尾对 `game_id` 的合并(约 763–765 行) + +注册发奖、`sendRegisterGiftWagerTask`、`setActivityFinishById` **不动**。 + +### 3. Controller PHPDoc + +[`GiftController.php`](slot_console/app/api/controller/GiftController.php): + +- `receive()`:去掉 `game_id` 说明,仅 `gift_coin` / `gift_bonus` +- 可选:为 `getActivityList()` 补充一行说明——type=7 且配置了有效 `game_ids` 时,对应项含 `game_id`(int) + +### 4. Logic 注释与单测 + +[`RegisterRewardLogic.php`](slot_console/app/api/logic/RegisterRewardLogic.php):类注释改为「活动列表推荐进游戏」;保留 `pickRandomGameId` 或改为对外暴露 `pickGameIdForUser`(二选一,避免死代码)。 + +[`RegisterRewardLogicTest.php`](slot_console/tests/Unit/RegisterRewardLogicTest.php):为 `pickGameIdForUser` 增加确定性用例(相同 uid+activityId 同结果;无候选返回 null)。 + +## 响应示例 + +**活动列表**(type=7 片段): + +```json +{ + "gift_id": 123, + "type": 7, + "title": "注册奖励", + "gift_coin": "10.00", + "gift_bonus": "0.00", + "game_id": 205, + "data": [{ "goods_id": -1, "gift_coin": "10.00", ... }] +} +``` + +无有效 `game_ids` / 无上架游戏:与现网其它活动项一致,**无 `game_id` 字段**。 + +**领取**: + +```json +{ + "gift_coin": "10.00", + "gift_bonus": "0.00" +} +``` + +## 验证 + +1. 后台 type=7 活动配置 `ext_config.game_ids`(已有 admin 能力)。 +2. `getActivityList`:未领取用户可见注册活动且含 `game_id`(在候选有效时)。 +3. `receive`:成功响应**不含** `game_id`。 +4. `game_ids` 为空或均未上架:列表无 `game_id`,领取正常。 +5. Docker 内跑单测:`RegisterRewardLogicTest.php`。 + +## 不涉及 + +- slot_admin / admin_vue(配置已具备) +- 前端仓库(由 C 端自行改列表消费、去掉领取后对 `game_id` 的依赖) diff --git a/plans/注册活动推荐游戏配置_0766f9ed.plan.md b/plans/注册活动推荐游戏配置_0766f9ed.plan.md new file mode 100644 index 0000000..02587a9 --- /dev/null +++ b/plans/注册活动推荐游戏配置_0766f9ed.plan.md @@ -0,0 +1,183 @@ +--- +name: 注册活动推荐游戏配置 +overview: 在 slot_admin 活动管理 type=7 编辑页增加「推荐游戏」多选,写入 ext_config.game_ids;配套列表展示与保存校验,与 slot_console 已实现的领取随机进游戏逻辑对齐。 +todos: + - id: admin-model-api + content: GameApiModel 查询方法 + ActivityController::publishedGames + status: completed + - id: admin-validate + content: ActivityValidate::checkRegisterRewardExt + save/update 调用 + status: completed + - id: vue-edit + content: activity/edit.vue type=7 推荐游戏多选与 submit/setFormData + status: completed + - id: vue-api-index + content: activity.js API + index.vue type=7 列表展示 + status: completed + - id: manual-verify + content: 后台保存后 C 端领取验证 game_id 随机返回 + status: completed +isProject: false +--- + +# 注册活动 type=7 推荐游戏后台配置 + +## 背景 + +C 端已在 [`RegisterRewardLogic`](slot_console/app/api/logic/RegisterRewardLogic.php) 读取 `ext_config.game_ids`,领取注册活动成功后随机返回 `game_id`。本任务补齐 **后台配置入口**,范围: + +- [`backend/slot_admin_vue`](backend/slot_admin_vue) 活动编辑/列表 +- [`backend/slot_admin`](backend/slot_admin) 保存校验与游戏选项 API + +不涉及 slot_console 改动(`updateConfig` 已原样持久化 `ext_config` JSON)。 + +## 配置契约(与 C 端一致) + +```json +{ + "game_ids": [101, 205, 308] +} +``` + +- 值为 `game.id`(`s_game_api.game_id`),须属于活动 `model_id` 下 **status=1** 的已发布游戏。 +- 允许为空:领取仍成功,响应不含 `game_id`。 + +## 数据流 + +```mermaid +sequenceDiagram + AdminVue->>ActivityController: GET publishedGames?model_id= + ActivityController->>GameApiModel: 已上架 game_id 列表 + AdminVue->>ActivityController: POST save/update ext_config.game_ids + ActivityController->>ActivityValidate: checkRegisterRewardExt + ActivityController->>ConsoleInnerapi: ActivityService add/update + ConsoleInnerapi->>DB: s_recharge_gift_config.ext_config +``` + +## 1. slot_admin 后端 + +### 1.1 游戏选项 API + +在 [`ActivityController`](backend/slot_admin/app/game/controller/ActivityController.php) 新增: + +- **`publishedGames(Request $request)`** + - 入参:`model_id`(必填,对应活动「游戏模型」) + - 出参:`[{ game_id, name }]`,供多选下拉 + +查询放在 [`GameApiModel`](backend/slot_admin/app/model/GameApiModel.php)(`s_common.s_game_api`): + +```php +public static function optionsByGameModelId(int $gameModelId): array +``` + +条件:`game_model_id`、`status = 1`,`field('game_id,name')`,按 `sort` 排序;可对 `game_id` `group` 去重(与 console 侧一致)。 + +### 1.2 保存校验 + +在 [`ActivityValidate`](backend/slot_admin/app/game/validate/ActivityValidate.php) 新增 **`checkRegisterRewardExt(array $extConfig, int $gameModelId)`**: + +| 规则 | 说明 | +| --- | --- | +| `game_ids` 可选 | 缺失或 `[]` 直接通过 | +| 元素为正整数 | 非法 ID 抛 `ValidateException` | +| 与渠道一致(推荐) | 调用 `GameApiModel::publishedGameIds($gameModelId, $ids)`(与 console 同名语义),提交 ID 必须全部在已发布集合内,否则提示未上架 ID | + +在 `ActivityController::save` / `update`(`updateData` 全量更新时)中,当 `type === 7` 时调用: + +```php +$this->validate->checkRegisterRewardExt( + (array) input('ext_config', []), + (int) input('model_id', 0) +); +``` + +对齐 type=11 的 [`checkFreeCreditsExt`](backend/slot_admin/app/game/validate/ActivityValidate.php) 调用方式。 + +可在 `GameApiModel` 复用与 console 相同的 `publishedGameIds` 静态方法(admin 连接 `s_common`,表结构一致)。 + +## 2. slot_admin_vue 前端 + +### 2.1 API + +在 [`src/api/game/activity.js`](backend/slot_admin_vue/src/api/game/activity.js) 增加: + +```js +publishedGames(params) { + return request({ url: '/game/activity/publishedGames', method: 'get', params }) +} +``` + +(路径随 Webman 路由约定:`game/activity/publishedGames`。) + +### 2.2 编辑页 type=7 表单项 + +文件:[`src/views/game/activity/edit.vue`](backend/slot_admin_vue/src/views/game/activity/edit.vue) + +在 **仅 type=7** 的 `