--- 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` | 新建一次性脚本 |