--- 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`)。