Files
cursor/plans/每日返水需求文档_20a03147.plan.md
ray zhou f71a5c59af ok
2026-05-29 11:21:40 +08:00

409 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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_consoledailyRebateSettle / 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` 为准,不受后续配置变更影响 |
| 跨天未关弹窗 | 前端倒计时结束刷新 infopending→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 日下注 $2000D+1 00:00 后显示待领取 $5024h 内领取成功,流水类型正确。
2. 未充值用户仅见锁定态,无领取接口成功路径。
3. 超 24h 未领变过期,不可再领。
4. 后台筛选/status/排序/汇总与文档公式一致;改档位仅影响新结算日。
5. 前端 7 日表、Claim 按钮金额与后台一致;当日为 Pending 且随下注刷新。
---
## 12. 已确认决策
- 计费方式:**累进分段**(非整笔落档单一比例)。
- 有效下注:**等同 `user_profit_daily.bet`**。
- 档位:**后台可配置**`daily_rebate_tier_config`)。