--- todos: - id: "ddl" content: "新增 slot-pwa/db/game_user_recharge_rtp_stat.sql(用户 DDL 原样)" status: pending - id: "service-model" content: "GameUserRechargeRtpStatModel + GameUserRechargeRtpStatService + UserRechargeStatusService" status: pending - id: "wire-wallet-service" content: "WalletService bet/win 成功后挂载累计与 RTP 重算" status: pending - id: "redis-key" content: "RedisKeyManagerService 补充 recharge:exchange key" status: pending isProject: false --- # game_user_recharge_rtp_stat(slot-pwa) ## 变更范围 - **表**:按你提供的 DDL,不做额外字段 - **服务**:[`slot-pwa`](slot-pwa) 统一处理(游戏回调 → 调 wallet 的入口) - **不做**:wallet 侧改表、移除 `TrialValidBetNotifyService`、activity 同步(后续再定) --- ## 表结构(原样采用) 库:`s_common`(与 [`game_launch_session`](slot-pwa/db/game_launch_session.sql) 同库) ```sql CREATE TABLE `game_user_recharge_rtp_stat` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `uid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户ID', `before_recharge_bet_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '充值前累计下注金额,金额扩大1000倍', `before_recharge_payout_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '充值前累计派奖金额,金额扩大1000倍', `before_recharge_rtp_rate` DECIMAL(10, 6) NOT NULL DEFAULT 0.000000 COMMENT '充值前RTP,payout / bet', `after_recharge_bet_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '充值后累计下注金额,金额扩大1000倍', `after_recharge_payout_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '充值后累计派奖金额,金额扩大1000倍', `after_recharge_rtp_rate` DECIMAL(10, 6) NOT NULL DEFAULT 0.000000 COMMENT '充值后RTP,payout / bet', `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), UNIQUE KEY `uk_uid` (`uid`), KEY `idx_before_rtp_bet` (`before_recharge_rtp_rate`, `before_recharge_bet_amount`), KEY `idx_after_rtp_bet` (`after_recharge_rtp_rate`, `after_recharge_bet_amount`), KEY `idx_before_bet` (`before_recharge_bet_amount`), KEY `idx_after_bet` (`after_recharge_bet_amount`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户充值前后下注派奖RTP累计统计表'; ``` DDL 文件:[`slot-pwa/db/game_user_recharge_rtp_stat.sql`](slot-pwa/db/game_user_recharge_rtp_stat.sql) --- ## 为什么在 slot-pwa 所有厂商下注/派奖最终都走 [`WalletService::bet()`](slot-pwa/app/service/WalletService.php) / `win()`(pop / quick / jsgame / jdb / self / gasea / bbgt 等),与现有 [`UserProfitService`](slot-pwa/app/service/user/UserProfitService.php) 挂载点一致,**一处写入、全覆盖**。 ```mermaid flowchart LR Provider[厂商回调] --> PwaCash[pop/quick/... CashLogic] PwaCash --> WalletSvc[WalletService bet/win] WalletSvc --> WalletApi[slot-wallet API] WalletSvc --> RtpStat[GameUserRechargeRtpStatService] RtpStat --> Db[(game_user_recharge_rtp_stat)] ``` --- ## 阶段判定(充值前 / 充值后) 读 wallet 同款 Redis 充值标记([`RechargeExchangeService::isRecharge`](slot-wallet/app/service/wallet/RechargeExchangeService.php)): - Key:`recharge:exchange:{uid % 10}`,field = `uid`,`recharge > 0` → **充值后** - 否则 → **充值前** 在 slot-pwa 新增薄封装 `UserRechargeStatusService`(读 default Redis,key 规则与 wallet 一致),**不新增表字段**。 --- ## 写入口径 挂载点:[`WalletService::bet()`](slot-pwa/app/service/WalletService.php) / `win()`,在 **`updateWallet()` 成功返回非 null 之后** 写统计(比现有 `UserProfitService` 更早调用更安全,避免 wallet 失败仍累加)。 | 动作 | 条件 | 更新字段 | |------|------|----------| | bet 成功 | 未充值 | `before_recharge_bet_amount += fee` | | bet 成功 | 已充值 | `after_recharge_bet_amount += fee` | | win 成功 | `is_end === 1` 且未充值 | `before_recharge_payout_amount += fee` | | win 成功 | `is_end === 1` 且已充值 | `after_recharge_payout_amount += fee` | **派奖仅计 `is_end=1`**:与 wallet 实际入账一致(中间派奖 `is_end=0` 只写 Redis pending,不落库)。 **RTP 重算**(同事务内 SQL 更新): ``` before_recharge_rtp_rate = before_bet > 0 ? ROUND(before_payout / before_bet, 6) : 0 after_recharge_rtp_rate = after_bet > 0 ? ROUND(after_payout / after_bet, 6) : 0 ``` **幂等**:依赖 slot-pwa 已有 `provider_tx` 幂等(同一 biz 不会重复调 wallet);不在本表再存 biz_id。 **fee=0**:跳过累加(zero win 等场景)。 --- ## 代码落点 | 文件 | 职责 | |------|------| | `slot-pwa/db/game_user_recharge_rtp_stat.sql` | 建表 | | `slot-pwa/app/model/game/GameUserRechargeRtpStatModel.php` | ThinkORM Model + 完整 PHPDoc | | `slot-pwa/app/service/game/GameUserRechargeRtpStatService.php` | `accumulateBet` / `accumulatePayout`、RTP 重算、upsert by uid | | `slot-pwa/app/service/user/UserRechargeStatusService.php` | 读 Redis 判断是否已充值 | | `slot-pwa/app/service/WalletService.php` | bet/win 成功后调用 Service | | `slot-pwa/app/service/RedisKeyManagerService.php` | 补充 `getRechargeExchangeKey`(与 wallet 一致) | --- ## 与 wallet / activity 的关系(后续) - 本表成为 **充值前后 bet/payout/RTP 权威数据源** - activity 的 `valid_bet_amount` / 4→5 解锁、移除 `trial_valid_bet_accumulate` MQ → **下一阶段**,本计划不改动 --- ## 验收 1. 未充值用户游戏:只涨 `before_*`,RTP 随 bet/win 更新 2. 首充后:只涨 `after_*` 3. `is_end=0` 的中间 win 不计入 payout 4. wallet 调用失败时不写表 5. 风控可按 `idx_before_rtp_bet` / `idx_after_rtp_bet` 扫异常 RTP