Files
cursor/plans/首充剩余分档规则_509262bd.plan.md
ray zhou 225fb2bd28 ok
2026-05-29 19:26:16 +08:00

211 lines
9.8 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: 将 Free Credits 剩余金额改为 10/20/30/50/100 瀑布分档;后续档解锁为「每档独立的新增累计充值」(自上一档解锁后重新计数,非终身 totalR 一刀切);尾档 unlock 按大单位整元向下取整。
todos:
- id: split-algorithm
content: 实现 buildReleasePackages 瀑布拆分,替换 splitReleasePackageAmounts + createPackages
status: completed
- id: db-unlock-qf
content: package 增 unlock_recharge_qf本档所需新增累计充值player 增 recharge_baseline_qf迁移脚本
status: completed
- id: advance-unlock
content: advanceByRecharge 用 (totalRbaseline)≥unlock 逐档解锁并重置 baseline存量回退旧逻辑
status: completed
- id: admin-config
content: 废弃 package_amount/subsequent_min 后台必填;更新 ActivityValidate
status: completed
- id: tests-docs
content: 单测/集成测 + 需求文档 §5.7/5.8 更新
status: completed
isProject: false
---
# 首充剩余定格 — 分档与解锁规则变更
## 背景与现状
核心逻辑在 [`slot_console/app/api/logic/FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php)
- **定格剩余**`leftAmount = frozenAmount - firstCashAmount`(第一档默认 $20来自 `ext_config.first_cash_amount`
- **当前拆分**`splitReleasePackageAmounts($left, package_amount)` — 每档 `min(package_amount, 剩余)` 循环切分(默认 $10/档)
- **当前解锁**:第一档看 `totalRecharge >= recharge_unlock_amount`(默认 $50后续档看 **本笔** `rechargeAmount >= subsequent_min_recharge`(默认 $10且受 `max_unlock_per_recharge` 限制
需求文档旧版 §5.7 见 [`docs/requirements/首充前免费余额定格与分档释放需求文档.md`](docs/requirements/首充前免费余额定格与分档释放需求文档.md)。
## 新规则(产品口径)
**基数**`剩余 = 定格金额 第一档免打码提现额($20`
按顺序「吃掉」剩余金额(瀑布式,非按总额选一档):
| 阶段 | 单档面额 | 本阶段最多档数 | 本档所需**新增**累计充值(自上一档解锁后起算) |
|------|---------|---------------|------------------------------------------|
| 1 | $10 | 3 | 每档再充 ≥ $9 |
| 2 | $20 | 5 | 每档再充 ≥ $19 |
| 3 | $30 | 10 | 每档再充 ≥ $29 |
| 4 | $50 | 10 | 每档再充 ≥ $49 |
| 5 | $100 | 直到剩余 < $100 | 每档再充 ≥ $99 |
| 6 | 尾档 | 剩余 < $100 的一整档 | 再充 ≥ **面额整元向下取整**(见下) |
**重要**:表中金额为「每一档单独重新累计」的充值要求,**不是**钱包终身 `totalRecharge` 达到该绝对值。上一档变为 `ready` 后,充值计数归零,从当前 `totalRecharge` 重新累计下一档。
金额在代码中为 **千分位整数qf**$10 → `10000`
### 示例(定格 $78.50,第一档 $20剩余 $58.50
```
阶段1: 3×$10 = $30 → 剩 $28.50
阶段2: 1×$20 = $20 → 剩 $8.50
阶段35: 不足整档,跳过
阶段6: 1×$8.50 → 剩 $0unlock_recharge_qf = 8000即 $8
```
共 5 个 release 档:`10,10,10,20,8.5`(旧规则为 6 档均 $10 + 尾档)。
### 尾档解锁门槛:大单位整数(不允许小数)
尾档 **`amount_qf` 仍保留实际剩余**(可含「分」级精度,如 `15340` qf = $15.34),但写入 `unlock_recharge_qf` 时必须 **向下取整到大单位整数美元**
```text
unlock_recharge_qf = intdiv(amount_qf, 1000) * 1000 // 去掉 qf 余数,等价 floor 到整元
```
| amount_qf小单位/千分位) | 档位展示面额 | unlock_recharge_qf累计充值门槛 |
|-------------------------|-------------|-----------------------------------|
| `15340` | $15.34 | `15000`(本档需**新增**累计充 $15非 $15.34 |
| `8500` | $8.50 | `8000`$8 |
| `10000` | $10.00 | `10000`$10 |
实现时抽私有方法,例如 `calcTailUnlockRechargeQf(int $amountQf): int`,仅用于阶段 6标准档10/20/30/50/100仍为 `amount - 1000` qf本身已是整元。
```mermaid
flowchart TD
left[剩余金额 leftQf]
b1["阶段1: 最多3档 x 10元"]
b2["阶段2: 最多5档 x 20元"]
b3["阶段3: 最多10档 x 30元"]
b4["阶段4: 最多10档 x 50元"]
b5["阶段5: 整百100元直到剩小于100"]
b6["阶段6: 尾档=剩余金额"]
left --> b1 --> b2 --> b3 --> b4 --> b5 --> b6
```
## 实现方案
### 1. 拆分算法(替换 `splitReleasePackageAmounts`
`FreeCreditsLogic` 中新增结构化方法,例如:
```php
/**
* @return list<array{amount_qf: int, unlock_recharge_qf: int}>
*/
public static function buildReleasePackages(int $leftAmountQf): array
```
- 用常量表描述 4 个固定阶段 `{amount: 10000|20000|30000|50000, maxCount: 3|5|10|10, unlock: amount-1000}`
- 阶段 5`while ($left >= 100000)` 追加 `{100000, 99000}`
- 阶段 6`if ($left > 0)` 追加 `{amount_qf: left, unlock_recharge_qf: calcTailUnlockRechargeQf(left)}`**unlock 为整元,不等于 left**
- 每阶段只取 `min(maxCount, floor(left / amount))` 个**整档**,余数进入下一阶段
`createPackages()` 改为遍历上述结构写入 `free_credits_package`,不再读 `package_amount` 配置。
### 2. 库表:本档门槛 + 充值基线
**`free_credits_package`** 新增:
```sql
unlock_recharge_qf bigint unsigned NOT NULL DEFAULT 0
COMMENT '解锁本档所需新增累计真实充值(千分位),自上一档解锁后起算'
```
**`free_credits_player`** 新增:
```sql
recharge_baseline_qf bigint unsigned NOT NULL DEFAULT 0
COMMENT '后续档计数起点wallet.totalRecharge 快照(千分位)'
```
- 定格时:写入各档 `unlock_recharge_qf``recharge_baseline_qf = 定格时 wallet.totalRecharge`(首笔 release 档从定格后**新增**充值开始计)
- **存量**grandfather`unlock_recharge_qf=0` 的包仍走旧逻辑(单笔 `subsequent_min_recharge`
### 3. 解锁逻辑(`advanceByRecharge`)— 每档「新的」累计充值
第一档(免打码提现)**不变**:仍用终身 `totalRecharge >= recharge_unlock_amount`(默认 $50
后续 release 档:
```text
incrementalRecharge = wallet.totalRecharge - player.recharge_baseline_qf
若 incrementalRecharge >= nextPackage.unlock_recharge_qf → 解锁该档为 ready
→ recharge_baseline_qf = wallet.totalRecharge // 下一档重新累计
```
循环至多 `max_unlock_per_recharge` 次(同一笔充值回调内可连续解锁多档,但每解锁一档都会 **重置 baseline**,故单笔 $27 通常只够解锁一档 $9 档)。
| 项 | 旧 | 新 |
|----|----|-----|
| 条件 | 本笔 `rechargeAmount >= subsequent_min` | `(totalR - baseline) >= unlock_recharge_qf` |
| 计数范围 | 单笔 | **档间新增累计**(解锁后 baseline 前移) |
| 顺序 | `package_no` 升序 | 不变 |
```mermaid
sequenceDiagram
participant Wallet
participant Logic
participant Player
Wallet->>Logic: totalRecharge=50000
Note over Player: baseline=41000
Logic->>Logic: incremental=9000 >= unlock 9000
Logic->>Player: baseline=50000
Note over Logic: 下一档需再新增累计充值
```
**与「领取 Claim」**Claim 仍要求档位 `ready`;解锁条件只影响何时变 `ready`,不改变 Claim 入账逻辑。
### 4. 配置与后台
- [`package_amount`](backend/slot_admin_vue/src/views/game/activity/edit.vue) / [`subsequent_min_recharge`](backend/slot_admin_vue/src/views/game/activity/edit.vue):对新定格**不再参与拆分/解锁**;后台表单项可标为「已废弃」或隐藏,校验 [`ActivityValidate::checkFreeCreditsExt`](backend/slot_admin/app/game/validate/ActivityValidate.php) 改为非必填(避免运营误填)
- `max_unlock_per_recharge``first_cash_amount``recharge_unlock_amount` 继续有效
### 5. C 端 API可选增强
[`FreeCreditsController::status`](slot_console/app/api/controller/FreeCreditsController.php) 当前 `packages``id/amount/status`。若前端要展示「再充 $X 解锁下一档」,可在每项增加 `unlock_recharge_amount`(大单位 float**非必须**help 文案可先说明规则。
### 6. 测试
更新/新增 [`FreeCreditsLogicAmountTest.php`](slot_console/tests/Unit/FreeCreditsLogicAmountTest.php)
- DataProvider`58500` qf 剩余 → `[10000×3, 20000, 8500]`,尾档 unlock=`8000`(非 8500
- 尾档取整:`15340` → amount=`15340`, unlock=`15000`
- 大额:`1000000` qf 剩余 → 验证各阶段档数上限与总和守恒
- 边界:`left=0`、整阶段边界30/100/500…
集成测 [`FreeCreditsFirstRechargeFreezeTest`](slot_console/tests/Integration/FreeCreditsFirstRechargeFreezeTest.php) 断言 package 行数与金额。
解锁单测(增量累计):
- baseline=41000totalR=50000unlock=9000 → 解锁baseline 更新为 50000
- 同上后再充到 59000 → 第二档 unlock=9000 可解锁;一笔 50000→59000 只够第二档若 baseline 已重置
- 尾档 unlock=15000amount=15340需新增累计 15000非 15340
### 7. 文档
更新 [`docs/requirements/首充前免费余额定格与分档释放需求文档.md`](docs/requirements/首充前免费余额定格与分档释放需求文档.md) §5.7、§5.8 与示例表。
## 影响范围(不改)
- `slot_wallet` 定格/冻结、Claim 入账
- `slot_pay` 第一档提现
- 第一档 $20 / 累计 $50 解锁第一档 — **本次需求未改**
## 风险与验收
- **金额守恒**`sum(release.amount_qf) === frozen - first_cash`
- **幂等**:定格仍按 `first_recharge_order_id` 幂等,不重复拆档
- **存量**:默认 grandfather上线前确认是否有在途「旧档位」用户
- 验收脚本:`verify-slot-backend.sh` + phpunit `FreeCreditsLogicAmountTest`
```bash
docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/FreeCreditsLogicAmountTest.php
```