211 lines
9.8 KiB
Markdown
211 lines
9.8 KiB
Markdown
---
|
||
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 用 (totalR−baseline)≥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
|
||
阶段3–5: 不足整档,跳过
|
||
阶段6: 1×$8.50 → 剩 $0(unlock_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=41000,totalR=50000,unlock=9000 → 解锁;baseline 更新为 50000
|
||
- 同上后再充到 59000 → 第二档 unlock=9000 可解锁;一笔 50000→59000 只够第二档若 baseline 已重置
|
||
- 尾档 unlock=15000,amount=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
|
||
```
|