active
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,8 +32,8 @@
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
# Team shared plans, optional
|
# Team shared plans, optional
|
||||||
# =========================================================
|
# =========================================================
|
||||||
!plans/
|
plans/
|
||||||
!plans/**
|
plans/**
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# Do NOT track Cursor projects runtime files
|
# Do NOT track Cursor projects runtime files
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
name: Create Cursor Workspace
|
|
||||||
overview: 在 `/Users/ray/Documents/project/www/slot` 创建一个 Cursor/VS Code workspace 文件,并把该目录下的一级项目文件夹加入 workspace。默认排除 `.vscode` 和普通文件。
|
|
||||||
todos:
|
|
||||||
- id: check-existing
|
|
||||||
content: 检查 `/Users/ray/Documents/project/www/slot/slot.code-workspace` 是否已存在
|
|
||||||
status: completed
|
|
||||||
- id: write-workspace
|
|
||||||
content: 创建或合并 workspace folders 列表
|
|
||||||
status: completed
|
|
||||||
- id: validate-json
|
|
||||||
content: 校验 `.code-workspace` JSON 格式
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Create Slot Workspace
|
|
||||||
|
|
||||||
将在 [`/Users/ray/Documents/project/www/slot/slot.code-workspace`](/Users/ray/Documents/project/www/slot/slot.code-workspace) 创建 workspace 文件,内容使用标准 `.code-workspace` JSON 格式。
|
|
||||||
|
|
||||||
计划加入这些一级非隐藏文件夹:
|
|
||||||
|
|
||||||
- [`backend`](/Users/ray/Documents/project/www/slot/backend)
|
|
||||||
- [`monitor`](/Users/ray/Documents/project/www/slot/monitor)
|
|
||||||
- [`slot-foundation`](/Users/ray/Documents/project/www/slot/slot-foundation)
|
|
||||||
- [`slot_agent`](/Users/ray/Documents/project/www/slot/slot_agent)
|
|
||||||
- [`slot_agent_vue`](/Users/ray/Documents/project/www/slot/slot_agent_vue)
|
|
||||||
- [`slot_center`](/Users/ray/Documents/project/www/slot/slot_center)
|
|
||||||
- [`slot_console`](/Users/ray/Documents/project/www/slot/slot_console)
|
|
||||||
- [`slot_gateway`](/Users/ray/Documents/project/www/slot/slot_gateway)
|
|
||||||
- [`slot_hub`](/Users/ray/Documents/project/www/slot/slot_hub)
|
|
||||||
- [`slot_lib`](/Users/ray/Documents/project/www/slot/slot_lib)
|
|
||||||
- [`slot_notification`](/Users/ray/Documents/project/www/slot/slot_notification)
|
|
||||||
- [`slot_pay`](/Users/ray/Documents/project/www/slot/slot_pay)
|
|
||||||
- [`slot_pwa`](/Users/ray/Documents/project/www/slot/slot_pwa)
|
|
||||||
- [`slot_risk`](/Users/ray/Documents/project/www/slot/slot_risk)
|
|
||||||
- [`slot_sdk`](/Users/ray/Documents/project/www/slot/slot_sdk)
|
|
||||||
- [`slot_user`](/Users/ray/Documents/project/www/slot/slot_user)
|
|
||||||
- [`slot_wallet`](/Users/ray/Documents/project/www/slot/slot_wallet)
|
|
||||||
|
|
||||||
Workspace 文件结构会类似:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{ "path": "backend" },
|
|
||||||
{ "path": "monitor" },
|
|
||||||
{ "path": "slot-foundation" }
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
实施步骤:
|
|
||||||
|
|
||||||
1. 检查目标文件是否已存在,避免覆盖已有 workspace 配置。
|
|
||||||
2. 如果不存在,创建 [`slot.code-workspace`](/Users/ray/Documents/project/www/slot/slot.code-workspace)。
|
|
||||||
3. 如果已存在,先读取现有内容,再合并缺失的文件夹,保留已有设置。
|
|
||||||
4. 校验生成的 JSON 格式可被 Cursor 打开。
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
---
|
|
||||||
name: Docker 环境 Cursor 规则
|
|
||||||
overview: 建议把 Docker 开发环境信息写成 Cursor 规则,但单独一条、尽量简短;优先放在 slot 项目级规则,只有跨多个仓库共用同一套 Docker 时才放到用户级规则。
|
|
||||||
todos:
|
|
||||||
- id: decide-scope
|
|
||||||
content: 确认 Docker 规则放项目级 (slot) 还是用户级 (~/.cursor/rules)
|
|
||||||
status: completed
|
|
||||||
- id: confirm-php-container
|
|
||||||
content: 确认 slot 后端默认容器 php82 与各服务 working_dir
|
|
||||||
status: completed
|
|
||||||
- id: create-dev-rule
|
|
||||||
content: 新建 dev-environment.mdc(15–30 行,含 compose 路径、容器名、端口、exec 示例)
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Docker 环境是否写入 Cursor 规则
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
**值得写进 Cursor 规则**,但**不必**和 [`backend-layering.mdc`](/Users/ray/.cursor/rules/backend-layering.mdc) 混在同一条里,也**不必**默认全部塞进「用户级 + alwaysApply」。
|
|
||||||
|
|
||||||
原因:Agent 在帮你跑 `php`、`composer`、`artisan`、迁移、单测、连 Redis/MySQL 时,若不知道服务在容器里,常会错误地在宿主机执行,或连错端口(例如 MySQL 映射是 `3309:3306`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 用户级 vs 项目级:怎么选
|
|
||||||
|
|
||||||
| 放置位置 | 路径 | 适用场景 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| **项目级(推荐)** | 例如在 slot 多根工作区根目录建 [`.cursor/rules/dev-environment.mdc`](file:///Users/ray/Documents/project/www/slot/.cursor/rules/dev-environment.mdc) | 只有 slot / `www/slot` 相关仓库用这套 Docker |
|
|
||||||
| **用户级** | [`~/.cursor/rules/dev-environment.mdc`](/Users/ray/.cursor/rules/dev-environment.mdc) | 多个不相关项目都共用 [`/Users/ray/Documents/project/docker/docker-compose.yml`](file:///Users/ray/Documents/project/docker/docker-compose.yml) |
|
|
||||||
|
|
||||||
你当前用户级只有分层规范一条,且 `alwaysApply: true`。Docker 信息属于**运行环境**,和编码规范是不同关注点:
|
|
||||||
|
|
||||||
- **分层规则**:继续 `alwaysApply: true`(跨项目仍有用)
|
|
||||||
- **Docker 规则**:建议 `alwaysApply: false`,或仅在 slot 工作区用项目级规则;避免在写前端、文档、Figma 时也占用上下文
|
|
||||||
|
|
||||||
当前 slot 工作区下**没有** [`.cursor/rules/`](file:///Users/ray/Documents/project/www/slot/.cursor/rules),更适合为 slot 单独加一条 `dev-environment.mdc`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 规则里应写什么(可操作、短)
|
|
||||||
|
|
||||||
根据你的 [`docker-compose.yml`](file:///Users/ray/Documents/project/docker/docker-compose.yml),建议只写 Agent **执行命令时必需** 的信息:
|
|
||||||
|
|
||||||
1. **Compose 位置**:`/Users/ray/Documents/project/docker/docker-compose.yml`
|
|
||||||
2. **容器名 → 用途**(执行命令用 `docker exec`,不要用宿主机 PHP/CLI):
|
|
||||||
- `php82` — PHP 8.2(slot 后端主环境,按你实际版本确认)
|
|
||||||
- `php72` — PHP 7.2(若有老项目)
|
|
||||||
- `goMysql` — MySQL 8(容器内 `3306`,宿主机 **`3309`**)
|
|
||||||
- `redis` — `6379`;`goredis` — 宿主机 `6378`
|
|
||||||
3. **挂载路径**:宿主机 `/Users/ray/Documents/project` → 容器内 `/app`(项目在容器内路径如 `/app/www/slot/backend`)
|
|
||||||
4. **命令约定**(示例,按你项目真实入口改):
|
|
||||||
- PHP:`docker exec -w /app/www/slot/backend php82 php ...`
|
|
||||||
- Composer / artisan:同样在 `php82` 内、对应 `working_dir` 执行
|
|
||||||
- MySQL CLI:`docker exec -it goMysql mysql -uroot -proot ...`(或注明用宿主机 `127.0.0.1:3309`)
|
|
||||||
5. **明确禁止/避免**:不要在 macOS 宿主机直接跑 `php`/`composer`(除非已确认本机也有同版本环境)
|
|
||||||
|
|
||||||
**不建议写进规则的内容**:
|
|
||||||
|
|
||||||
- 完整 `docker-compose.yml` 复制(冗长、易过期)
|
|
||||||
- 所有服务密码细节(compose 里已有;规则里写「以 compose 为准」即可)
|
|
||||||
- RabbitMQ、Milvus 等与当前 slot 任务无关的服务(除非经常用到)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 与现有文档的关系
|
|
||||||
|
|
||||||
- **Cursor 规则**:给 Agent 的「默认假设」,每次对话自动带上(按 `alwaysApply` / `globs`)
|
|
||||||
- **仓库 README / `docs/dev-setup.md`**:给人看的完整说明;规则可写一句「详细步骤见 xxx」
|
|
||||||
|
|
||||||
两者可并存:规则 15–30 行,文档可更长。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 建议的规则骨架(项目级示例)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
description: Slot 本地 Docker 开发环境(PHP / MySQL / Redis)
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Local Dev (Docker)
|
|
||||||
|
|
||||||
- Compose: `/Users/ray/Documents/project/docker/docker-compose.yml`
|
|
||||||
- Host project root: `/Users/ray/Documents/project` → container `/app`
|
|
||||||
- Run PHP/Composer/Artisan inside container `php82`, not on macOS host.
|
|
||||||
- MySQL: container `goMysql`; from host use `127.0.0.1:3309`.
|
|
||||||
- Redis: container `redis`, host port `6379`.
|
|
||||||
- Example: `docker exec -w /app/www/slot/backend php82 php artisan ...`
|
|
||||||
```
|
|
||||||
|
|
||||||
若放在**用户级**,把 `alwaysApply` 改为 `false`,或标题改成「Project docker (ray)」以免污染非 slot 项目。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐决策
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
Q[多个仓库共用同一套 Docker?]
|
|
||||||
Q -->|是| UserRule["~/.cursor/rules/dev-environment.mdc\nalwaysApply: false"]
|
|
||||||
Q -->|否 仅 slot| ProjRule["www/slot/.cursor/rules/dev-environment.mdc\nalwaysApply: true 在该工作区"]
|
|
||||||
Both[保留 backend-layering 在用户级 alwaysApply]
|
|
||||||
UserRule --> Both
|
|
||||||
ProjRule --> Both
|
|
||||||
```
|
|
||||||
|
|
||||||
**对你当前情况**:slot 多仓库工作区 + Docker 在 `project/docker`,**优先项目级规则**;仅当你打开的其他 Cursor 工作区(非 slot)也依赖同一 compose 时,再复制一份到用户级。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 下一步(你确认后可执行)
|
|
||||||
|
|
||||||
1. 在 [`www/slot/.cursor/rules/`](file:///Users/ray/Documents/project/www/slot/.cursor/rules) 新建 `dev-environment.mdc`(约 20 行)
|
|
||||||
2. 确认 slot 后端默认用 `php82` 还是 `php72`,以及各子服务在容器内的 `working_dir`
|
|
||||||
3. 可选:在 [`docs/`](file:///Users/ray/Documents/project/www/slot/docs) 增加 `dev-setup.md` 供人查阅,规则里链过去
|
|
||||||
|
|
||||||
无需修改现有的 `backend-layering.mdc`。
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
---
|
|
||||||
name: firstCashout 合并代码评审
|
|
||||||
overview: 你对「merge post + 统一 WithdrawService::apply」的改法方向正确,但当前 WithdrawService 在 package_id>0 时仍执行 checkInfo/手续费/黑规则,且 Pay 失败无回滚,会导致第一档提现不可用或卡在 processing。
|
|
||||||
todos:
|
|
||||||
- id: fc-early-return
|
|
||||||
content: WithdrawService::apply 中 package_id>0 走独立 applyFreeCreditsFirstCashout 并 early return
|
|
||||||
status: completed
|
|
||||||
- id: skip-checkinfo-fc
|
|
||||||
content: FC 分支跳过 checkInfo、getAmountAndFee、黑规则(或产品确认保留项)
|
|
||||||
status: completed
|
|
||||||
- id: pay-fail-rollback
|
|
||||||
content: Pay apply 失败时 handleFirstCashoutResult 回滚 processing
|
|
||||||
status: completed
|
|
||||||
- id: fc-withdrawal-info
|
|
||||||
content: FC 使用 package amount_qf、fee=0、auditType=2 组 WithdrawalInfo
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# firstCashout 合并改动 — 代码评审
|
|
||||||
|
|
||||||
## 你的改动(理解正确)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Ctrl as WithdrawController
|
|
||||||
participant FC as FreeCreditsLogic
|
|
||||||
participant WS as WithdrawService
|
|
||||||
participant Pay as slot_pay
|
|
||||||
|
|
||||||
Ctrl->>FC: mergeFirstCashoutIntoPost
|
|
||||||
Note over FC: amount + bizType + package_id
|
|
||||||
Ctrl->>WS: apply(DTO)
|
|
||||||
WS->>FC: firstCashout(uid, packageId, orderId)
|
|
||||||
Note over FC: markFirstCashoutProcessing
|
|
||||||
WS->>Pay: apply(WithdrawalInfo)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php):有 `package_id` 先 merge,再**同一套** `validate($type)` — 符合预期。
|
|
||||||
- [`mergeFirstCashoutIntoPost`](slot_console/app/api/logic/FreeCreditsLogic.php):补 `amount` / `bizType` / `package_id` — 符合预期。
|
|
||||||
- [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 收窄为只 `markFirstCashoutProcessing` + 外部统一 `PayService::apply` — 思路可行。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P0:FC 仍会走 `checkInfo`,大概率直接失败
|
|
||||||
|
|
||||||
[`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$bankInfo = $this->checkBankInfo($applyDTO);
|
|
||||||
$this->checkInfo($applyDTO->type, $applyDTO->amount); // 始终执行
|
|
||||||
// ...
|
|
||||||
if ($applyDTO->package_id > 0) {
|
|
||||||
firstCashout(...); // 永远走不到(checkInfo 已抛错)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`checkInfo` 会校验**钱包可提现余额** ≥ amount([`WithdrawService.php` L333-335](slot_console/app/service/WithdrawService.php))。
|
|
||||||
Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额里 → 典型报错 **`Insufficient balance`**。
|
|
||||||
|
|
||||||
**结论**:与需求「不进入普通钱包」冲突,第一档线上基本提不了现。
|
|
||||||
|
|
||||||
**建议**:`package_id > 0`(或 `bizType === free_credit_first_cashout`)时 **跳过** `checkInfo`(及与之绑定的首提 min/max 规则)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P0:Pay 失败时档位已置为 processing,无回滚
|
|
||||||
|
|
||||||
你现在在 **Pay 之前** 调用 `firstCashout` → `markFirstCashoutProcessing`(status=2)。
|
|
||||||
|
|
||||||
原实现是:mark → `PayService::apply` → **catch 时** `handleFirstCashoutResult($orderId, false)` 恢复 ready。
|
|
||||||
|
|
||||||
当前 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) L236 调 pay **没有** try/catch 回滚 → 申请失败时 package 会一直 **processing**,用户无法重试。
|
|
||||||
|
|
||||||
**建议**:
|
|
||||||
|
|
||||||
```php
|
|
||||||
if ($applyDTO->package_id > 0) {
|
|
||||||
$orderId = CommonFn::generateOrderId(4, $userTag->uid);
|
|
||||||
(new FreeCreditsLogic())->firstCashout($userTag->uid, $applyDTO->package_id, $orderId);
|
|
||||||
$withdrawalInfo->orderId = $orderId;
|
|
||||||
$withdrawalInfo->bizType = FreeCreditsLogic::BIZ_TYPE_FIRST_CASHOUT;
|
|
||||||
try {
|
|
||||||
$res = PayService::getInstance()->apply($withdrawalInfo->toArray());
|
|
||||||
return $res;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
(new FreeCreditsLogic())->handleFirstCashoutResult($orderId, false);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(FC 分支应 **early return**,不要继续走下面黑规则 + `getAmountAndFee`。)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P1:FC 仍走普通手续费 / 黑规则 / auditType
|
|
||||||
|
|
||||||
合并后 FC 路径仍执行:
|
|
||||||
|
|
||||||
- `getAuditType($applyDTO->amount)`、`BlackApiService`、提现倍数检测
|
|
||||||
- `getAmountAndFee($applyDTO->amount, $this->withdrawal)` — 可能扣手续费、按钱包余额改金额
|
|
||||||
|
|
||||||
原 [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 约定:
|
|
||||||
|
|
||||||
- `fee = 0`
|
|
||||||
- `auditType = 2`(人工审核)
|
|
||||||
- `amount = package->amount_qf`(千分位,不经手续费逻辑)
|
|
||||||
|
|
||||||
`merge` 写入的 `amount` 是 **展示大单位**(`getNumberFormat(qf)`),再经 `getAmountAndFee` 可能与 pay 侧期望的千分位不一致。
|
|
||||||
|
|
||||||
**建议**:FC 分支单独组 `WithdrawalInfo`:
|
|
||||||
|
|
||||||
| 字段 | FC 取值 |
|
|
||||||
|------|---------|
|
|
||||||
| amount | 从 package 读 `amount_qf`(或 merge 时额外缓存 `_amount_qf`) |
|
|
||||||
| fee | 0 |
|
|
||||||
| auditType | 2 |
|
|
||||||
| bizType | `free_credit_first_cashout` |
|
|
||||||
| 黑规则 | 跳过(或产品确认是否要对 FC 也做) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P1:`firstCashout` 与 `markFirstCashoutProcessing` 重复校验
|
|
||||||
|
|
||||||
[`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 内再次查 `STATUS_READY` 后调用 `markFirstCashoutProcessing`,而 `markFirstCashoutProcessing` ** again** 要求 `STATUS_READY`(L343)。
|
|
||||||
逻辑重复但无害;可简化为只调 `markFirstCashoutProcessing`,或只保留一处校验。
|
|
||||||
|
|
||||||
注意:`mergeFirstCashoutIntoPost` 已校验 ready,到 `firstCashout` 时若并发重复提交,第二次会在 mark 阶段失败 — 符合「处理中不可重复提交」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P2:行为变化(需产品确认)
|
|
||||||
|
|
||||||
| 项 | 原 FC | 现统一 apply |
|
|
||||||
|----|--------|----------------|
|
|
||||||
| `is_bind_name` | 不校验 | **校验**(L157) |
|
|
||||||
| Redis 5s 频控 | 无 | **有** |
|
|
||||||
| 返回值 | `{order_id, amount}` | pay `apply` 原始结构 |
|
|
||||||
|
|
||||||
若 C 端依赖 `data.order_id` / `data.amount` 展示,需确认 pay 返回是否一致。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 做得好的部分
|
|
||||||
|
|
||||||
- Controller 单入口 + merge post:**正确**。
|
|
||||||
- 去掉独立 FC validator scene:**正确**。
|
|
||||||
- [`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php) 兼容路径与主入口一致:**正确**。
|
|
||||||
- pay 层仍靠 `bizType` 跳过 `withdrawFrozen`:**只要 bizType 确实传到 pay 就没问题**(当前 L184 已设)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐修复结构(最小 diff)
|
|
||||||
|
|
||||||
在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 开头(绑卡、频控之后):
|
|
||||||
|
|
||||||
```php
|
|
||||||
if ($applyDTO->package_id > 0) {
|
|
||||||
return $this->applyFreeCreditsFirstCashout($applyDTO, $bankInfo);
|
|
||||||
}
|
|
||||||
// 原有普通提现逻辑不变
|
|
||||||
```
|
|
||||||
|
|
||||||
`applyFreeCreditsFirstCashout` 内:
|
|
||||||
|
|
||||||
1. 跳过 `checkInfo`、黑规则、`getAmountAndFee`
|
|
||||||
2. `orderId` → `firstCashout`(mark)→ 组 `WithdrawalInfo`(qf amount, fee=0, auditType=2, bizType)
|
|
||||||
3. try/catch pay + `handleFirstCashoutResult` 回滚
|
|
||||||
|
|
||||||
**不要**在普通流程中间用 `if ($package_id > 0) { firstCashout; }` 再接着跑普通逻辑 — 这是当前问题的根源。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收清单
|
|
||||||
|
|
||||||
- [ ] `package_id>0` 且钱包 withdraw=0:能成功提交 pay(不报 Insufficient balance)
|
|
||||||
- [ ] pay 申请失败:package 回到 ready,可再次提现
|
|
||||||
- [ ] pay 成功 + 回调:package completed,player `FIRST_CASH_DONE`
|
|
||||||
- [ ] 普通提现无 `package_id`:行为与改前一致
|
|
||||||
- [ ] `remark`/bizType 在 pay 侧仍为 `free_credit_first_cashout`,不冻结钱包
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
---
|
|
||||||
name: firstCashout 合并提现
|
|
||||||
overview: 可以合并:推荐在 Withdraw 入口与 Pay 下单层统一,用 package_id 区分;活动档位状态机仍留在 FreeCreditsLogic,不能把 FC 逻辑硬塞进 WithdrawService::apply 主流程中间。
|
|
||||||
todos:
|
|
||||||
- id: dto-package-id
|
|
||||||
content: WithdrawApplyDTO 增加 package_id;WithdrawValidator 增加 FC scene(package_id 替代 amount)
|
|
||||||
status: completed
|
|
||||||
- id: withdraw-fc-branch
|
|
||||||
content: WithdrawService::apply 增加 applyFreeCreditsFirstCashout early return(跳过 checkInfo/手续费/黑规则)
|
|
||||||
status: completed
|
|
||||||
- id: controller-unify
|
|
||||||
content: WithdrawController::apply 统一入口;FreeCreditsController::firstCashout 改为兼容 alias
|
|
||||||
status: completed
|
|
||||||
- id: bank-persist
|
|
||||||
content: FC 分支复用 checkBankInfo,与普通提现绑卡行为一致
|
|
||||||
status: completed
|
|
||||||
- id: pay-rollback-test
|
|
||||||
content: 补单测/集成测:FC 无 withdraw 余额可提交、Pay 失败回滚 ready
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# firstCashout 能否合并进现有提现逻辑
|
|
||||||
|
|
||||||
## 结论(后端)
|
|
||||||
|
|
||||||
**可以合并,且推荐合并「入口 + 校验 + 绑卡 + Pay 下单」;不应把 Free Credits 编排塞进 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 主流程中间。**
|
|
||||||
|
|
||||||
当前仓库状态:**尚未合并**——仍是双入口:
|
|
||||||
|
|
||||||
| 入口 | 现状 |
|
|
||||||
|------|------|
|
|
||||||
| [`POST /api/free-credits/first-cashout`](slot_console/app/api/controller/FreeCreditsController.php) | `FreeCreditsLogic::firstCashout` 完整编排 |
|
|
||||||
| [`POST /api/withdraw/apply`](slot_console/app/api/controller/WithdrawController.php) | `WithdrawService::apply`,无 `package_id` |
|
|
||||||
|
|
||||||
Pay 层**已经统一**:两类提现最终都走 [`WithdrawalOrderEntity::apply`](slot_pay/app/entity/WithdrawalOrderEntity.php),靠 `bizType=free_credit_first_cashout` 跳过 `withdrawFrozen` 并走独立 MQ 回调。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 为什么「可以」合并
|
|
||||||
|
|
||||||
C 端参数与需求 §11 一致:独立提现页 = 普通提现页精简版,收款字段相同,仅差:
|
|
||||||
|
|
||||||
| 字段 | 普通提现 | Free Credits 第一档 |
|
|
||||||
|------|----------|----------------------|
|
|
||||||
| `amount` | 用户输入 | **不传**,服务端取 package |
|
|
||||||
| `package_id` | 无 | **必填** |
|
|
||||||
|
|
||||||
因此用 **同一 `POST /api/withdraw/apply` + 可选 `package_id`** 区分业务是合理契约。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 为什么不能「整段并入」WithdrawService::apply
|
|
||||||
|
|
||||||
[`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序(L164–219):
|
|
||||||
|
|
||||||
1. `checkBankInfo`
|
|
||||||
2. **`checkInfo`(校验钱包 withdraw 余额 ≥ amount)**
|
|
||||||
3. 黑规则、`getAmountAndFee`(手续费)
|
|
||||||
4. `PayService::apply`
|
|
||||||
|
|
||||||
Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额。若在中间插入 `firstCashout` 而不 **early return**,会先被 `checkInfo` 打成 `Insufficient balance`(评审计划已记录为 P0)。
|
|
||||||
|
|
||||||
此外 FC 固定规则与普通提现不同:
|
|
||||||
|
|
||||||
| 项 | 普通提现 | FC 第一档 |
|
|
||||||
|----|----------|-----------|
|
|
||||||
| 金额来源 | 用户 `amount` | `package->amount_qf` |
|
|
||||||
| 手续费 | `getAmountAndFee` | **0** |
|
|
||||||
| 审核 | 动态 `getAuditType` + 黑规则 | **auditType=2** |
|
|
||||||
| 钱包冻结 | 有 | **无**(pay 侧 bizType 分支) |
|
|
||||||
| 活动状态 | 无 | `markFirstCashoutProcessing` / 失败回滚 |
|
|
||||||
| Pay 失败回滚 | 钱包解冻 | **`handleFirstCashoutResult` 恢复 ready** |
|
|
||||||
|
|
||||||
这些差异属于 **Logic 编排**,符合 [`backend-layering`](file:///Users/ray/.cursor/rules/backend-layering.mdc):不应把 `FreeCreditsLogic` 整段搬进 `WithdrawService` 当「又一层 Service」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐合并结构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
Client["POST /api/withdraw/apply"]
|
|
||||||
Client --> Branch{package_id 存在?}
|
|
||||||
Branch -->|否| Normal["WithdrawService::apply 原逻辑"]
|
|
||||||
Branch -->|是| FC["WithdrawService::applyFreeCreditsFirstCashout"]
|
|
||||||
FC --> Mark["FreeCreditsLogic::markFirstCashoutProcessing"]
|
|
||||||
FC --> Pay["PayService::apply bizType=free_credit_first_cashout"]
|
|
||||||
Normal --> Pay2["PayService::apply 普通"]
|
|
||||||
Pay --> PayEntity["slot_pay WithdrawalOrderEntity"]
|
|
||||||
Pay2 --> PayEntity
|
|
||||||
```
|
|
||||||
|
|
||||||
### 合并层(推荐做)
|
|
||||||
|
|
||||||
1. **[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php)**
|
|
||||||
- 有 `package_id` → 走 FC 分支(或 DTO 带 `package_id` 后交给 Service early return)
|
|
||||||
- 无 `package_id` → 现有普通提现
|
|
||||||
|
|
||||||
2. **[`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php)**
|
|
||||||
- 新增 FC scene:与普通 apply 对称,把 `amount` 换成 `package_id`(`SCENE_APPLY_CASH_FC` 等)
|
|
||||||
|
|
||||||
3. **[`WithdrawApplyDTO`](slot_console/app/api/dto/request/WithdrawApplyDTO.php)**
|
|
||||||
- 增加可选 `package_id`
|
|
||||||
|
|
||||||
4. **绑卡**
|
|
||||||
- FC 复用 `WithdrawService::checkBankInfo`(当前 `firstCashout` 只读绑卡、不写库,合并后应对齐普通提现)
|
|
||||||
|
|
||||||
5. **[`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php)**
|
|
||||||
- 保留为 **兼容 alias**(内部转调 withdraw apply),或标记 deprecated
|
|
||||||
|
|
||||||
### 保持分离(必须)
|
|
||||||
|
|
||||||
- [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php):`markFirstCashoutProcessing`、`handleFirstCashoutResult`、package 状态机、`assertEligibleParticipant`
|
|
||||||
- [`WithdrawService::applyFreeCreditsFirstCashout`](slot_console/app/service/WithdrawService.php)(新建私有方法):
|
|
||||||
- 跳过 `checkInfo`、黑规则、`getAmountAndFee`
|
|
||||||
- `amount = package->amount_qf`,`fee = 0`,`auditType = 2`
|
|
||||||
- try/catch Pay,失败时 `handleFirstCashoutResult($orderId, false)`
|
|
||||||
|
|
||||||
### 已统一、无需再改
|
|
||||||
|
|
||||||
- **slot_pay**:`bizType` / `remark` 分支(跳过冻结、Success/Fail/Rejected → Console MQ)
|
|
||||||
- **slot_console EventBus**:`FreeCreditsFirstCashoutSuccess/Fail/Rejected` → `handleFirstCashoutResult`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 不推荐的做法
|
|
||||||
|
|
||||||
在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 中间写:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$this->checkInfo(...);
|
|
||||||
// ...
|
|
||||||
if ($package_id > 0) {
|
|
||||||
(new FreeCreditsLogic())->firstCashout(...);
|
|
||||||
}
|
|
||||||
$amountInfo = $this->getAmountAndFee(...);
|
|
||||||
PayService::apply(...);
|
|
||||||
```
|
|
||||||
|
|
||||||
会导致:余额校验失败、手续费错误、Pay 失败不回滚档位(processing 卡死)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 与「单独提现」需求的关系
|
|
||||||
|
|
||||||
| 需求 | 合并后是否满足 |
|
|
||||||
|------|----------------|
|
|
||||||
| 不进入普通钱包(§15.3) | 是,仍靠 pay `bizType` |
|
|
||||||
| 固定第一档金额(§11.3) | 是,服务端取 package |
|
|
||||||
| 处理中不可重复提交(§5.6) | 是,仍由 package status 控制 |
|
|
||||||
| 成功/失败/拒绝状态(§20.2) | 是,回调逻辑不变 |
|
|
||||||
|
|
||||||
合并的是 **HTTP 入口与收款参数校验**,不是改掉「独立提现」的账务语义。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实施要点(若执行)
|
|
||||||
|
|
||||||
1. `WithdrawApplyDTO` 增加 `package_id`
|
|
||||||
2. `WithdrawService::apply` 在绑卡 + Redis 频控后 **early return** 到 `applyFreeCreditsFirstCashout`
|
|
||||||
3. FC 分支内:先 `markFirstCashoutProcessing`,再 Pay,catch 回滚
|
|
||||||
4. 单测:FC 在 `withdraw=0` 时可提交;Pay 失败 package 回 ready;普通 apply 无回归
|
|
||||||
5. 文档:`/api/free-credits/first-cashout` → 指向 `/api/withdraw/apply` + `package_id`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收清单
|
|
||||||
|
|
||||||
- `package_id>0` 且钱包可提现余额为 0:可成功提交 pay
|
|
||||||
- pay 申请失败:package 回到 `ready`,可重试
|
|
||||||
- pay 回调成功:package `completed`,player `FIRST_CASH_DONE`
|
|
||||||
- 不传 `package_id`:普通提现与现网一致
|
|
||||||
- pay 订单 `remark` 仍为 `free_credit_first_cashout`,不触发 `withdrawFrozen`
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
---
|
|
||||||
name: firstCashout 合并评估
|
|
||||||
overview: 客户端入参与普通提现几乎一致(仅多 package_id、不传 amount),推荐在 WithdrawController::apply 做薄入口合并并复用 WithdrawValidator;编排仍留 FreeCreditsLogic,不并入 WithdrawService::apply 内部。
|
|
||||||
todos:
|
|
||||||
- id: merge-withdraw-entry
|
|
||||||
content: WithdrawController::apply 增加 package_id 分支,委托 FreeCreditsLogic::firstCashout;复用 WithdrawValidator 新增 scene(type 1/2/3/6 + package_id,无 amount)
|
|
||||||
status: completed
|
|
||||||
- id: align-bank-persist
|
|
||||||
content: firstCashout 改为复用 WithdrawService::checkBankInfo(或抽 helper),与普通提现一致写绑卡信息
|
|
||||||
status: completed
|
|
||||||
- id: deprecate-fc-endpoint
|
|
||||||
content: /api/free-credits/first-cashout 保留作兼容 alias 或标记 deprecated,文档指向 /api/withdraw/apply?package_id=
|
|
||||||
status: completed
|
|
||||||
- id: keep-logic-split
|
|
||||||
content: FreeCreditsLogic 仍负责 package 状态机 + bizType;WithdrawService::apply 不增加 Free Credits 分支
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# firstCashout 合并进现有提现接口 — 修订评估
|
|
||||||
|
|
||||||
## 用户反馈:客户端参数基本一致
|
|
||||||
|
|
||||||
对照 [`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php) 与 [`FreeCreditsValidator::SCENE_FIRST_CASHOUT`](slot_console/app/api/validator/FreeCreditsValidator.php):
|
|
||||||
|
|
||||||
| 字段 | 普通提现 apply | Free Credits firstCashout |
|
|
||||||
|------|----------------|---------------------------|
|
|
||||||
| `type` | require (1/2/3/6) | require (1/2/3/6) |
|
|
||||||
| `pay_net` | require (1/2/3) | Logic 使用,Validator scene 未含(可补齐) |
|
|
||||||
| `user_name` / `cash_tag` | type=1 时 require | post 传入,Logic 读取 |
|
|
||||||
| `btc` / `usdt` | type=2/3 时 require | 同上 |
|
|
||||||
| `paypal_*` / `email` | type=6 时 require | 同上 |
|
|
||||||
| `amount` | **require,用户输入** | **不传,服务端取 package 金额** |
|
|
||||||
| `package_id` | 无 | **require,活动档位 id** |
|
|
||||||
|
|
||||||
**结论**:C 端独立提现页(需求 §11)本来就是对普通提现页的精简——同一套收款字段,只是隐藏 amount 输入框。从接口契约看,**完全可以用同一个 apply 入口**,用 `package_id` 有无区分业务类型。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 仍建议合并的范围:入口 + 校验 + 组单,不是 WithdrawService 内部
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
Client["C 端 POST /api/withdraw/apply"]
|
|
||||||
Client --> Branch{package_id 存在?}
|
|
||||||
Branch -->|是| FC["FreeCreditsLogic::firstCashout"]
|
|
||||||
Branch -->|否| WS["WithdrawService::apply"]
|
|
||||||
FC --> Pay["PayService::apply bizType=free_credit_first_cashout"]
|
|
||||||
WS --> Pay2["PayService::apply 普通"]
|
|
||||||
Pay --> PayEntity["WithdrawalOrderEntity::apply 已统一"]
|
|
||||||
Pay2 --> PayEntity
|
|
||||||
```
|
|
||||||
|
|
||||||
### 可以合并(推荐)
|
|
||||||
|
|
||||||
1. **统一入口**:[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php) 检测 `package_id`,有则委托 `FreeCreditsLogic::firstCashout`,无则走 `WithdrawService::apply`。
|
|
||||||
2. **统一校验**:在 `WithdrawValidator` 新增 scene,与普通 apply 对称,仅把 `amount` 换成 `package_id`:
|
|
||||||
- `SCENE_APPLY_CASH_FC` => `['package_id', 'type', 'user_name', 'cash_tag']`
|
|
||||||
- `SCENE_APPLY_BTC_FC` => `['package_id', 'type', 'btc']`
|
|
||||||
- 等
|
|
||||||
3. **统一绑卡写库**:`firstCashout` 目前只读 `UserBankCardModel`;普通提现通过 `WithdrawService::checkBankInfo` 会更新绑卡。合并入口后应 **复用同一绑卡逻辑**,避免两套行为。
|
|
||||||
4. **统一 DTO**:`WithdrawApplyDTO` 增加可选 `package_id` 字段即可,不必维护两套 post 结构。
|
|
||||||
|
|
||||||
### 不应合并进 WithdrawService::apply
|
|
||||||
|
|
||||||
编排差异仍在 Logic 层,不应塞进 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php):
|
|
||||||
|
|
||||||
| 仍分离的逻辑 | 原因 |
|
|
||||||
|-------------|------|
|
|
||||||
| 金额 | 普通:用户 amount;FC:package->amount_qf |
|
|
||||||
| 余额/手续费/VIP/黑规则 | 普通:checkInfo + getAmountAndFee;FC:跳过 |
|
|
||||||
| 活动状态 | FC:markFirstCashoutProcessing / 失败回滚 |
|
|
||||||
| pay 回调 | 已通过 bizType 在 pay 层分支,无需 console 再分 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐实现(修订后方案 B)
|
|
||||||
|
|
||||||
[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php):
|
|
||||||
|
|
||||||
```php
|
|
||||||
public function apply(Request $request)
|
|
||||||
{
|
|
||||||
$post = $request->post();
|
|
||||||
$type = input('type', 1);
|
|
||||||
|
|
||||||
if (!empty($post['package_id'])) {
|
|
||||||
// 复用 WithdrawValidator 的 FC scene(无 amount)
|
|
||||||
$error = $this->validate(WithdrawValidator::SCENE_APPLY_CASH_FC /* 按 type */, $post);
|
|
||||||
if ($error !== true) {
|
|
||||||
return $this->errorCode(ErrorCode::PARAMS_ERROR, $error);
|
|
||||||
}
|
|
||||||
return $this->success(
|
|
||||||
(new FreeCreditsLogic())->firstCashout(
|
|
||||||
$request->userEntity->uid,
|
|
||||||
(int) $post['package_id'],
|
|
||||||
$post
|
|
||||||
),
|
|
||||||
'Submitted successfully! ...'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原有普通提现
|
|
||||||
$error = $this->validate($type, $post);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[`FreeCreditsLogic::firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 内部改动:
|
|
||||||
|
|
||||||
- 绑卡:改为调用 `WithdrawService` 的 `checkBankInfo`(需将 `checkBankInfo` 改为 `protected` 公开方法,或抽到 helper)。
|
|
||||||
- 其余不变:package 校验、固定金额、`bizType`、状态机。
|
|
||||||
|
|
||||||
[`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php):
|
|
||||||
|
|
||||||
- 保留为 **兼容 alias**(内部同样调 Logic),或直接 deprecated 指向 withdraw apply。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 与初版评估的差异
|
|
||||||
|
|
||||||
| 初版 | 修订 |
|
|
||||||
|------|------|
|
|
||||||
| 方案 A 推荐保持双入口 | **方案 B 升为推荐** — 参数一致,双入口无必要 |
|
|
||||||
| 强调「参数/校验不同故难合并」 | 参数 **高度重合**,差异仅 `package_id` vs `amount`;校验可共用 Validator scene |
|
|
||||||
| 合并障碍在入口层 | 合并障碍仅在 **WithdrawService 内部编排**,入口层应合并 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收
|
|
||||||
|
|
||||||
- C 端独立提现页调用 `POST /api/withdraw/apply`,传 `package_id + type + 收款字段`,**不传 amount**
|
|
||||||
- 普通提现不传 `package_id`,行为与现网一致
|
|
||||||
- Free Credits 仍不走 `withdrawFrozen`,回调仍更新 package/player
|
|
||||||
- 绑卡信息与走普通 apply 后一致落库
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
---
|
|
||||||
name: free credits activity edit
|
|
||||||
overview: 在管理后台活动配置编辑表单中为活动类型 11(首充前免费余额定格与分档释放 / Free Credits)增加专属表单分支,并补齐字典与服务端校验;金额输入沿用 type==10 的「美元小数 → 千分位整数」模式,写入 ext_config 的 _qf 后缀键,与 slot_console `FreeCreditsLogic::configAmount()` 的读优先级一致。
|
|
||||||
todos:
|
|
||||||
- id: dict-activity-type-11
|
|
||||||
content: 字典 sm_system_dict_data 追加 code='activity_type' 的 value=11 / label='Free Credits(首充前免费余额)'(DB 或字典管理页)
|
|
||||||
status: completed
|
|
||||||
- id: edit-vue-type11-form
|
|
||||||
content: edit.vue 增加 v-if=type==11 的表单块(6 字段 + Banner)
|
|
||||||
status: completed
|
|
||||||
- id: edit-vue-skip-goods
|
|
||||||
content: edit.vue 让 type==11 跳过 goods 区块:扩展 onlyGift / 调整「添加赠送」与 goods 卡片的 v-if
|
|
||||||
status: completed
|
|
||||||
- id: edit-vue-submit-conv
|
|
||||||
content: edit.vue submit() 增加 type==11 的金额 ×1000 写入 _qf 键、清空 goods
|
|
||||||
status: completed
|
|
||||||
- id: edit-vue-setform-conv
|
|
||||||
content: edit.vue setFormData() 增加 type==11 的 _qf ÷1000 回填还原
|
|
||||||
status: completed
|
|
||||||
- id: validate-free-credits-ext
|
|
||||||
content: ActivityValidate 增加 checkFreeCreditsExt(extConfig) 方法,含必填、非负、max_unlock_per_recharge≥1、first_cash ≤ recharge_unlock 约束
|
|
||||||
status: completed
|
|
||||||
- id: controller-trigger-ext-check
|
|
||||||
content: ActivityController::save / update 在 checkData 后按 type==11 调用 checkFreeCreditsExt;update 兼容仅改状态请求
|
|
||||||
status: completed
|
|
||||||
- id: verify-end-to-end
|
|
||||||
content: 本地验证:新建/编辑/必填/业务约束/类型切换/C 端 FreeCreditsLogic 读取
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Free Credits 活动后台编辑落地计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
让运营在「活动管理」编辑弹窗里选活动类型 `11`(首充前免费余额)时,能直接编辑需求文档第 17 节列出的所有配置项;保存后落到 `s_recharge_gift_config.ext_config` JSON 字段,C 端 `FreeCreditsLogic` 立即生效。
|
|
||||||
|
|
||||||
## 现状要点
|
|
||||||
|
|
||||||
- `slot_console` 已实现 `RechargeGiftConfigModel::TYPE_FREE_CREDITS = 11`、`FreeCreditsLogic`、运行时配置读取(`ext_config.{key}_qf` 优先)。
|
|
||||||
- `slot_admin` 后端 [ActivityController](backend/slot_admin/app/game/controller/ActivityController.php) 走 `slotLib\services\ActivityService` 透传到 `slot_console innerapi/activity/*`,`ext_config` 已原样落库,**不需要改动 slot_console**。
|
|
||||||
- 管理前端 [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) 现仅对 6 / 9 / 10 做了 `v-if` 分支,`type==11` 无任何 UI;字典 `activity_type` 也无 value=11 条目。
|
|
||||||
|
|
||||||
## 字段映射(type=11 专属 `ext_config`)
|
|
||||||
|
|
||||||
UI 输入用美元小数,提交时 ×1000 写入 `_qf` 键(与 type==10 模式一致;`FreeCreditsLogic::configAmount()` 优先读 `_qf`):
|
|
||||||
|
|
||||||
- `win_threshold_qf` ← 首笔赢取门槛(默认 $50)
|
|
||||||
- `recharge_unlock_amount_qf` ← 累计充值解锁第一档(默认 $50)
|
|
||||||
- `first_cash_amount_qf` ← 第一档免打码金额(默认 $20)
|
|
||||||
- `package_amount_qf` ← 后续每档拆分金额(默认 $10)
|
|
||||||
- `subsequent_min_recharge_qf` ← 解锁下一档单笔充值下限(默认 $10)
|
|
||||||
- `max_unlock_per_recharge` ← 整数,每笔充值最多解锁档数(默认 1,**不走 _qf**)
|
|
||||||
- `banner_image` ← Free Play to Go 弹窗 Banner 图片 URL(可空)
|
|
||||||
|
|
||||||
## 改动清单
|
|
||||||
|
|
||||||
### 1. 字典:追加 `activity_type` value=11
|
|
||||||
|
|
||||||
在系统管理「字典管理 → activity_type」以有,不需要再执行了
|
|
||||||
|
|
||||||
### 2. 前端:[backend/slot_admin_vue/src/views/game/activity/edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue)
|
|
||||||
|
|
||||||
#### 2.1 增加 type==11 表单块
|
|
||||||
|
|
||||||
参考 [edit.vue line 108-123](backend/slot_admin_vue/src/views/game/activity/edit.vue) type==10 的写法,新增:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template v-if="formData.type == 11">
|
|
||||||
<a-col :span="24">
|
|
||||||
<a-form-item label="首笔赢取门槛($)" help="免费余额曾达到该值后解锁首页 Withdraw" :rules="[{ required: true, message: '必填' }]">
|
|
||||||
<a-input-number v-model="formData.ext_config.win_threshold" placeholder="如 50" :min="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="充值解锁门槛($)" help="累计真实充值满该金额释放第一档" :rules="[{ required: true, message: '必填' }]">
|
|
||||||
<a-input-number v-model="formData.ext_config.recharge_unlock_amount" placeholder="如 50" :min="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="免打码提现额($)" help="第一档可免打码直接提现金额" :rules="[{ required: true, message: '必填' }]">
|
|
||||||
<a-input-number v-model="formData.ext_config.first_cash_amount" placeholder="如 20" :min="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="解锁拆分金额($)" help="后续每档释放金额" :rules="[{ required: true, message: '必填' }]">
|
|
||||||
<a-input-number v-model="formData.ext_config.package_amount" placeholder="如 10" :min="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="后续解锁最小充值($)" help="单笔充值达到该金额才解锁下一档">
|
|
||||||
<a-input-number v-model="formData.ext_config.subsequent_min_recharge" placeholder="如 10" :min="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="每笔最多解锁档数" help="防止一笔充值解锁多档">
|
|
||||||
<a-input-number v-model="formData.ext_config.max_unlock_per_recharge" placeholder="如 1" :min="1" :precision="0" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="Banner 图片" help="Free Play to Go 底部弹窗 Banner">
|
|
||||||
<sa-upload-image v-model="formData.ext_config.banner_image" :limit="1" :multiple="false" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 让 type==11 跳过 goods 区块
|
|
||||||
|
|
||||||
- [edit.vue line 60](backend/slot_admin_vue/src/views/game/activity/edit.vue) `onlyGift` computed 追加 `|| formData.type == 11`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let onlyGift = computed(() => {
|
|
||||||
return formData.type == 7 || formData.type == 8 || formData.type == 9 || formData.type == 11;
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- [edit.vue line 125](backend/slot_admin_vue/src/views/game/activity/edit.vue) 把「添加赠送」按钮与下方 `<a-row v-else>` 的 goods 卡片包成一组条件,排除 type==10 和 type==11:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template v-if="formData.type !== 10 && formData.type !== 11">
|
|
||||||
<a-button type="primary" ... @click="add()">添加赠送</a-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<a-row :gutter="20" v-if="formData.type !== 9 && formData.type !== 10 && formData.type !== 11">
|
|
||||||
...goods 卡片...
|
|
||||||
</a-row>
|
|
||||||
```
|
|
||||||
|
|
||||||
(保留原 type==9 的 exchange 表格 v-if 不变。)
|
|
||||||
|
|
||||||
#### 2.3 `submit()` 中追加 type==11 的金额 ×1000 转换
|
|
||||||
|
|
||||||
[edit.vue line 370](backend/slot_admin_vue/src/views/game/activity/edit.vue) 类似 type==10 的处理,把 6 个美元字段写到 `_qf` 键,整数字段原样保留:
|
|
||||||
|
|
||||||
```js
|
|
||||||
if (formData.type === 11) {
|
|
||||||
const e = data.ext_config || {};
|
|
||||||
const toQf = (v) => v === '' || v == null ? undefined : Math.round(Number(v) * 1000);
|
|
||||||
data.ext_config = {
|
|
||||||
win_threshold_qf: toQf(e.win_threshold),
|
|
||||||
recharge_unlock_amount_qf: toQf(e.recharge_unlock_amount),
|
|
||||||
first_cash_amount_qf: toQf(e.first_cash_amount),
|
|
||||||
package_amount_qf: toQf(e.package_amount),
|
|
||||||
subsequent_min_recharge_qf: toQf(e.subsequent_min_recharge),
|
|
||||||
max_unlock_per_recharge: e.max_unlock_per_recharge == null ? undefined : Math.round(Number(e.max_unlock_per_recharge)),
|
|
||||||
banner_image: e.banner_image || '',
|
|
||||||
};
|
|
||||||
data.goods = [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.4 `setFormData()` 中追加 type==11 的回填还原
|
|
||||||
|
|
||||||
[edit.vue line 344-351](backend/slot_admin_vue/src/views/game/activity/edit.vue) 仿照 type==10:
|
|
||||||
|
|
||||||
```js
|
|
||||||
if (data.type === 11 && data.ext_config) {
|
|
||||||
const e = data.ext_config;
|
|
||||||
formData.ext_config = {
|
|
||||||
win_threshold: e.win_threshold_qf != null ? e.win_threshold_qf / 1000 : e.win_threshold,
|
|
||||||
recharge_unlock_amount: e.recharge_unlock_amount_qf != null ? e.recharge_unlock_amount_qf / 1000 : e.recharge_unlock_amount,
|
|
||||||
first_cash_amount: e.first_cash_amount_qf != null ? e.first_cash_amount_qf / 1000 : e.first_cash_amount,
|
|
||||||
package_amount: e.package_amount_qf != null ? e.package_amount_qf / 1000 : e.package_amount,
|
|
||||||
subsequent_min_recharge: e.subsequent_min_recharge_qf != null ? e.subsequent_min_recharge_qf / 1000 : e.subsequent_min_recharge,
|
|
||||||
max_unlock_per_recharge: e.max_unlock_per_recharge ?? 1,
|
|
||||||
banner_image: e.banner_image || '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 后端校验:[backend/slot_admin/app/game/validate/ActivityValidate.php](backend/slot_admin/app/game/validate/ActivityValidate.php)
|
|
||||||
|
|
||||||
ThinkValidate 对嵌套 JSON 支持有限,采用「在 Controller 按 `type` 触发额外校验」+「Validate 提供专用方法」的模式,避免污染既有 scene。
|
|
||||||
|
|
||||||
#### 3.1 ActivityValidate 增加一个公开方法
|
|
||||||
|
|
||||||
```php
|
|
||||||
/**
|
|
||||||
* Free Credits(type=11)专用 ext_config 校验。
|
|
||||||
*
|
|
||||||
* @param array $extConfig 前端提交的 ext_config,金额已 ×1000 写入 _qf 键
|
|
||||||
* @throws \think\exception\ValidateException
|
|
||||||
*/
|
|
||||||
public function checkFreeCreditsExt(array $extConfig): void
|
|
||||||
{
|
|
||||||
$required = [
|
|
||||||
'win_threshold_qf' => '首笔赢取门槛',
|
|
||||||
'recharge_unlock_amount_qf' => '充值解锁门槛',
|
|
||||||
'first_cash_amount_qf' => '免打码提现额',
|
|
||||||
'package_amount_qf' => '解锁拆分金额',
|
|
||||||
'subsequent_min_recharge_qf' => '后续解锁最小充值',
|
|
||||||
'max_unlock_per_recharge' => '每笔最多解锁档数',
|
|
||||||
];
|
|
||||||
foreach ($required as $key => $label) {
|
|
||||||
if (!isset($extConfig[$key]) || $extConfig[$key] === '' || (int)$extConfig[$key] < 0) {
|
|
||||||
throw new \think\exception\ValidateException("{$label}必须填写且为非负数");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((int)$extConfig['max_unlock_per_recharge'] < 1) {
|
|
||||||
throw new \think\exception\ValidateException('每笔最多解锁档数必须 ≥ 1');
|
|
||||||
}
|
|
||||||
// 业务约束:免打码提现额 ≤ 累计充值解锁门槛
|
|
||||||
if ((int)$extConfig['first_cash_amount_qf'] > (int)$extConfig['recharge_unlock_amount_qf']) {
|
|
||||||
throw new \think\exception\ValidateException('免打码提现额不能超过充值解锁门槛');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2 ActivityController save / update 触发
|
|
||||||
|
|
||||||
[ActivityController::save / update](backend/slot_admin/app/game/controller/ActivityController.php) 在 `checkData()` 之后追加:
|
|
||||||
|
|
||||||
```php
|
|
||||||
if ((int)input('type') === 11) {
|
|
||||||
$this->validate->checkFreeCreditsExt((array)input('ext_config', []));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`update` 里同样判断,且要兼容仅改状态的请求(无 `updateData` 时不校验 ext_config):
|
|
||||||
|
|
||||||
```php
|
|
||||||
if (!empty(input('updateData')) && (int)input('type') === 11) {
|
|
||||||
$this->validate->checkFreeCreditsExt((array)input('ext_config', []));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 不需要改的部分
|
|
||||||
|
|
||||||
- `slot_console` 侧 `ActivityConfigEntity::updateConfig` 已原样写入 `ext_config`,无变更。
|
|
||||||
- `Consts::ACTIVITY_TYPE_*` 不必新增 11;C 端已通过 `RechargeGiftConfigModel::TYPE_FREE_CREDITS` 引用。
|
|
||||||
- 不新增独立菜单页,复用通用「活动管理」编辑弹窗。
|
|
||||||
|
|
||||||
## 数据流
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
edit[edit.vue type=11 form] -->|"美元×1000 → _qf"| save["POST /game/activity/save|update"]
|
|
||||||
save --> ctrl[ActivityController]
|
|
||||||
ctrl -->|"checkFreeCreditsExt"| validate[ActivityValidate]
|
|
||||||
ctrl --> svc[ActivityService HTTP]
|
|
||||||
svc --> console["slot_console innerapi/activity/update"]
|
|
||||||
console --> entity[ActivityConfigEntity::updateConfig]
|
|
||||||
entity --> db["s_recharge_gift_config.ext_config"]
|
|
||||||
db --> fc["FreeCreditsLogic::configAmount key_qf 优先"]
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 验收
|
|
||||||
|
|
||||||
- 新建 type=11 活动:填写 6 字段 + Banner,保存成功;DB `ext_config` 是 `*_qf` 整数 + `max_unlock_per_recharge` + `banner_image`。
|
|
||||||
- 编辑回填:再次打开同一条活动,美元字段显示为原值(如 50 / 20 / 10),整数字段为 1,Banner 显示已上传图。
|
|
||||||
- 必填校验:清空任一必填项保存,返回明确错误信息(如「首笔赢取门槛必须填写且为非负数」)。
|
|
||||||
- 业务约束:免打码提现额填 60、充值解锁门槛填 50,保存被拒。
|
|
||||||
- 类型切换:type 选 11→1,再切回 11,goods 区块不出现,formData 不污染;保存 type==1 时 `_qf` 字段不会被带到 ext_config。
|
|
||||||
- C 端:调 `FreeCreditsLogic::status($uid)`,门槛 / 第一档金额 / 解锁拆分金额能读到刚配的值。
|
|
||||||
- type 字典:管理后台编辑弹窗活动类型下拉出现「Free Credits(首充前免费余额)」选项。
|
|
||||||
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
---
|
|
||||||
name: free credits release
|
|
||||||
overview: 为“首充前免费余额定格与分档释放”准备开发方案,按钱包账务、充值提现事件、客户端 API、后台配置统计分阶段落地。重点遵循现有后端分层规则,避免把业务编排错误地下沉到 Service。
|
|
||||||
todos:
|
|
||||||
- id: confirm-wallet-fields
|
|
||||||
content: 按已确认口径使用 deposit_balance + withdraw_balance 作为免费余额
|
|
||||||
status: completed
|
|
||||||
- id: design-schema
|
|
||||||
content: 设计独立 Free Credits 活动主表、档位明细表、流水关联和旧活动 ID 关联
|
|
||||||
status: completed
|
|
||||||
- id: implement-console-core
|
|
||||||
content: 在 slot_console 实现活动领域模型、定格、解锁、Claim 和状态查询 Logic
|
|
||||||
status: pending
|
|
||||||
- id: wire-recharge
|
|
||||||
content: 接入 slot_pay/slot_console 充值成功链路并保证首充定格幂等
|
|
||||||
status: completed
|
|
||||||
- id: wire-cashout
|
|
||||||
content: 实现第一档独立提现订单和回调状态同步
|
|
||||||
status: completed
|
|
||||||
- id: add-console-apis
|
|
||||||
content: 补充大厅状态、活动入口、后台配置统计相关 API
|
|
||||||
status: completed
|
|
||||||
- id: test-acceptance
|
|
||||||
content: 按需求文档核心规则和账务验收补测试/联调用例
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 首充前免费余额定格与分档释放开发准备
|
|
||||||
|
|
||||||
## 目标范围
|
|
||||||
|
|
||||||
本次需求核心实现应以后端账务和状态机为主,前端展示依赖新增/扩展 API。主要涉及:
|
|
||||||
|
|
||||||
- [`/Users/ray/Documents/project/www/slot/slot_console`](slot_console):Free Credits 活动领域模型、Pool、档位、状态机、定格/释放/Claim 编排、大厅/提现页接口、活动入口、配置读取、后台统计入口。
|
|
||||||
- [`/Users/ray/Documents/project/www/slot/slot_wallet`](slot_wallet):只提供钱包原子能力,例如余额扣减/入账、钱包流水、Deposit Balance 入账、Y1 流水任务创建;不放活动领域模型和活动状态机。
|
|
||||||
- [`/Users/ray/Documents/project/www/slot/slot_pay`](slot_pay):充值成功异步通知、第一档独立提现订单。
|
|
||||||
- [`/Users/ray/Documents/project/www/slot/backend/slot_admin`](backend/slot_admin):运营后台配置和统计页,如该项目负责管理端。
|
|
||||||
- 客户端 UI 由前端人员在其它仓库实现,本仓库只提供后端接口和状态数据。
|
|
||||||
|
|
||||||
已有统一活动管理表可作为关联来源:
|
|
||||||
|
|
||||||
- `s_common.s_recharge_gift_config`:现有充值赠送活动主表。
|
|
||||||
- `recharge_gift_player`:现有充值赠送活动参与玩家表。
|
|
||||||
|
|
||||||
本需求不直接复用这两张表承载 Free Credits 账务状态,采用独立 Free Credits 表设计,并保留与旧活动 ID 的关联,避免把“首充前免费余额定格”与已有充值赠送活动规则混在同一张参与表里。
|
|
||||||
|
|
||||||
## 数据表 DDL 草案
|
|
||||||
|
|
||||||
金额字段建议沿用现有活动表中的 `_qf` 口径,按千分位整数保存,避免小数精度问题。
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE `free_credits_player` (
|
|
||||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
||||||
`activity_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '关联 s_recharge_gift_config.id',
|
|
||||||
`uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
|
|
||||||
`source` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道',
|
|
||||||
`model_id` int unsigned NOT NULL DEFAULT '0' COMMENT '游戏模型ID',
|
|
||||||
`home_withdraw_unlocked` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '首页Withdraw是否已解锁 0否 1是',
|
|
||||||
`frozen_amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '首充时定格金额,千分位',
|
|
||||||
`first_cash_amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '第一档免打码提现金额,千分位',
|
|
||||||
`first_recharge_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '触发定格的首笔充值订单号',
|
|
||||||
`first_recharge_time` datetime DEFAULT NULL COMMENT '首笔充值成功时间',
|
|
||||||
`first_cashout_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '第一档独立提现订单号',
|
|
||||||
`status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '主状态 0未开始 1首页已解锁 2已定格 3待充值解锁 4第一档可提现 5第一档提现中 6第一档已提现 7后续释放中 10全部完成',
|
|
||||||
`completed_time` datetime DEFAULT NULL COMMENT '全部完成时间',
|
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uniq_activity_uid` (`activity_id`,`uid`),
|
|
||||||
KEY `idx_uid` (`uid`),
|
|
||||||
KEY `idx_activity_status` (`activity_id`,`status`),
|
|
||||||
KEY `idx_first_recharge_order` (`first_recharge_order_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Free Credits用户活动主表';
|
|
||||||
```
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE `free_credits_package` (
|
|
||||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
||||||
`player_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'free_credits_player.id',
|
|
||||||
`activity_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '关联 s_recharge_gift_config.id',
|
|
||||||
`uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
|
|
||||||
`package_no` int unsigned NOT NULL DEFAULT '0' COMMENT '档位序号,从1开始',
|
|
||||||
`package_type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '档位类型 1第一档免打码提现 2后续释放档',
|
|
||||||
`amount_qf` bigint unsigned NOT NULL DEFAULT '0' COMMENT '档位金额,千分位',
|
|
||||||
`status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '档位状态 0锁定 1可操作 2处理中 3已完成 4失败 5风控拒绝',
|
|
||||||
`withdraw_order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '第一档提现订单号',
|
|
||||||
`claim_biz_id` varchar(64) NOT NULL DEFAULT '' COMMENT '后续档Claim入账幂等业务号',
|
|
||||||
`unlocked_time` datetime DEFAULT NULL COMMENT '解锁时间',
|
|
||||||
`claimed_time` datetime DEFAULT NULL COMMENT '领取时间',
|
|
||||||
`completed_time` datetime DEFAULT NULL COMMENT '完成时间',
|
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uniq_player_package` (`player_id`,`package_no`),
|
|
||||||
UNIQUE KEY `uniq_claim_biz` (`claim_biz_id`),
|
|
||||||
KEY `idx_uid_status` (`uid`,`status`),
|
|
||||||
KEY `idx_activity_status` (`activity_id`,`status`),
|
|
||||||
KEY `idx_withdraw_order` (`withdraw_order_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Free Credits档位明细表';
|
|
||||||
```
|
|
||||||
|
|
||||||
## 建议数据流
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
preDepositUser["未首充用户"] --> winThreshold["免费余额曾达到门槛"]
|
|
||||||
winThreshold --> homeUnlocked["首页 Withdraw 解锁"]
|
|
||||||
preDepositUser --> firstRecharge["任意入口首笔真实充值成功"]
|
|
||||||
firstRecharge --> freezeFreeCredits["定格当前免费余额"]
|
|
||||||
freezeFreeCredits --> freeCreditsPool["slot_console 保存 Free Credits Pool 与档位"]
|
|
||||||
firstRecharge --> walletTotalRecharge["查询钱包累计充值总额"]
|
|
||||||
walletTotalRecharge --> firstReady["累计充值满门槛释放第一档"]
|
|
||||||
firstReady --> firstCashout["第一档独立提现"]
|
|
||||||
firstCashout --> laterPackages["后续档位按充值解锁"]
|
|
||||||
laterPackages --> claimToWallet["Claim 入 Deposit Balance"]
|
|
||||||
claimToWallet --> y1Task["创建 Y1 流水任务"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 后端落地方案
|
|
||||||
|
|
||||||
1. 在 `slot_console` 新增独立 Free Credits 活动领域模型。
|
|
||||||
- 新增用户池主表,保存 `activity_id`、`uid`、定格金额、首充订单、主状态、首页解锁标记、完成时间等;主表不保存累计充值。
|
|
||||||
- 新增档位明细表,一档一行,保存序号、类型、金额、状态、关联提现单/Claim 流水、解锁/完成时间。
|
|
||||||
- `activity_id` 关联现有 `s_common.s_recharge_gift_config` 或后台活动配置 ID,但 Free Credits 的进度、档位和账务状态不写入 `recharge_gift_player`。
|
|
||||||
- 表归属按 `slot_console` 现有业务库和活动模块规范处理;`slot_wallet` 不新增 Free Credits 活动表。
|
|
||||||
|
|
||||||
2. 在 `slot_console` 增加 Logic 编排定格、解锁、Claim。
|
|
||||||
- Controller 只做请求接收、Validate、DTO、统一响应。
|
|
||||||
- Validate 处理参数必填/类型/枚举。
|
|
||||||
- DTO 只承载已校验字段。
|
|
||||||
- Logic 负责首充定格、查询钱包累计充值总额、档位状态流转、事务和幂等。
|
|
||||||
- Model 负责查询/写入和状态条件更新。
|
|
||||||
- Service 仅用于已有公共能力,如调用钱包原子 API、配置读取、外部系统封装,不新增单纯转发 Service。
|
|
||||||
- 调用 `slot_wallet` 时只请求余额变更、钱包流水、入 Deposit Balance、创建 Y1 任务等钱包能力,不把活动状态写入钱包服务。
|
|
||||||
|
|
||||||
3. 接入充值成功链路。
|
|
||||||
- 现有充值成功链路在 [`/Users/ray/Documents/project/www/slot/slot_pay/app/command/EventRecharge.php`](slot_pay/app/command/EventRecharge.php) 调用钱包充值入账。
|
|
||||||
- 充值成功后由 `slot_pay` 或事件消费者通知 `slot_console`,由 `slot_console` 判断是否首笔真实充值并触发定格。
|
|
||||||
- 定格需要调用 `slot_wallet` 原子能力扣减当前免费余额并写钱包流水,再由 `slot_console` 落 Free Credits Pool 和档位。
|
|
||||||
- 第一档门槛通过调用钱包查询用户累计充值总额判断;`slot_console` 主表不保存累计充值,也不新增充值事件明细表。
|
|
||||||
|
|
||||||
4. 实现第一档独立提现。
|
|
||||||
- 第一档不进入普通钱包余额,创建独立提现订单类型 `free_credit_first_cashout`。
|
|
||||||
- 需要在 `slot_pay` 的提现订单模型/实体中支持新订单类型,提现处理中防重复,成功/失败/拒绝回写档位状态。
|
|
||||||
- 第一档提现成功后关闭首页状态条,但活动入口保留到所有档位完成。
|
|
||||||
|
|
||||||
5. 实现后续档位解锁和 Claim。
|
|
||||||
- 后续每笔符合条件真实充值最多解锁下一档,按配置控制最小充值金额和每笔最多解锁档数。
|
|
||||||
- Claim 由 `slot_console` 校验档位状态和顺序后,调用 `slot_wallet` 将档位金额入 `Deposit Balance`,创建 Deposit Lot 和 Y1 Wager Task。
|
|
||||||
- `slot_console` 负责 Claim 幂等和档位状态流转;`slot_wallet` 负责钱包入账幂等和流水一致性。失败时档位保持 `ready`,重复点击不能重复入账。
|
|
||||||
|
|
||||||
6. 增加客户端查询与操作 API。
|
|
||||||
- 查询用户活动状态:首页状态条、活动入口、Free Play to Go 弹窗需要同一份状态数据。
|
|
||||||
- 操作 API:第一档提现、后续 Claim、后续 Unlock 跳充值。
|
|
||||||
- `slot_console` 可在 [`/Users/ray/Documents/project/www/slot/slot_console/app/napi/controller/LobbyController.php`](slot_console/app/napi/controller/LobbyController.php) 或独立 API 暴露大厅所需数据。
|
|
||||||
|
|
||||||
7. 增加后台配置与统计。
|
|
||||||
- 配置项优先绑定到现有统一活动管理的活动 ID;Free Credits 专属配置包括活动开关、首笔赢取门槛、充值解锁门槛、免打码提现额、拆分金额、后续最小充值、每笔最多解锁档数、Y1 倍数、Banner。
|
|
||||||
- 统计项:定格人数、完成提现人数、全部完成人数、定格总金额、已提现金额、已领取金额、待释放金额。
|
|
||||||
- 筛选排序按文档要求实现。
|
|
||||||
|
|
||||||
## 已确认口径
|
|
||||||
|
|
||||||
- 免费余额 = `deposit_balance + withdraw_balance`,即钱包余额。
|
|
||||||
- 第一档不需要流水,完全绕开普通可提现余额计算。
|
|
||||||
- 第一档解锁通过充值成功异步通知驱动,并调用钱包查询累计充值总额判断是否达到门槛。
|
|
||||||
- Free Credits 定格逻辑以独立活动配置为准。
|
|
||||||
- 客户端 UI 由前端人员处理,本仓库不包含对应页面代码。
|
|
||||||
|
|
||||||
## 关键风险与需确认点
|
|
||||||
|
|
||||||
- 文档中的 `Deposite Balance / Deposite Lot` 建议统一确认是否为历史命名还是拼写问题。
|
|
||||||
|
|
||||||
## 建议开发顺序
|
|
||||||
|
|
||||||
1. 先实现 `slot_console` 活动数据模型、配置读取、状态机和只读查询接口。
|
|
||||||
2. 梳理并补齐 `slot_wallet` 需要暴露的钱包原子能力,包括查询累计充值总额、冻结/扣减免费余额、入 Deposit Balance、创建 Y1 任务和幂等流水。
|
|
||||||
3. 接入充值成功链路,完成 `slot_pay` 到 `slot_console` 的事件通知、首充定格和调用钱包累计充值总额解锁第一档。
|
|
||||||
4. 实现第一档独立提现链路和提现回调到 `slot_console` 的状态同步。
|
|
||||||
5. 实现后续档位解锁、Claim 调用钱包入账和 Y1 流水任务创建。
|
|
||||||
6. 补 `slot_console` 大厅/活动入口接口、后台统计、客户端 UI、文案、埋点和验收用例。
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
---
|
|
||||||
name: free credits 后台统计页
|
|
||||||
overview: 实现需求 §18 的 Free Credits 后台统计与展示:在 slot_console 增强 innerapi(加筛选、加列表字段、富化累计充值与档位聚合),slot_lib 新增 FreeCreditsService 并给 WalletService 补一个 statistics 批量方法,slot_admin 新增 FreeCreditsStatsController 代理,slot_admin_vue 新增 views/game/freeCreditsStats/index.vue 一页搞定,顶部统计走 sa-table 的 otherData 模式。
|
|
||||||
todos:
|
|
||||||
- id: slot-console-stats-logic
|
|
||||||
content: slot_console 新建 innerapi/logic/FreeCreditsStatsLogic.php,封装 statistics + list 的查询编排(含 status>=2 口径、批量 package 聚合 SQL)
|
|
||||||
status: completed
|
|
||||||
- id: slot-console-list-enrich
|
|
||||||
content: slot_console FreeCreditsController::list 加筛选 + 字段富化(first_cash_status / progress / claimed / first_cashout / remaining / total_recharge)
|
|
||||||
status: completed
|
|
||||||
- id: slot-console-stats-filter
|
|
||||||
content: slot_console FreeCreditsController::statistics 加 source / first_recharge_time / status 等筛选;frozen_user_count 改为 status>=2
|
|
||||||
status: completed
|
|
||||||
- id: slot-lib-wallet-stats
|
|
||||||
content: slot_lib WalletService 新增 statistics(uids, currency) 方法 → 调 /api/wallet/statistics
|
|
||||||
status: completed
|
|
||||||
- id: slot-lib-free-credits-service
|
|
||||||
content: slot_lib 新建 FreeCreditsService(basePath=innerapi/free-credits,含 list + statistics)
|
|
||||||
status: completed
|
|
||||||
- id: slot-admin-controller
|
|
||||||
content: slot_admin 新建 game/controller/FreeCreditsStatsController.php,index 一次调两个 service 合并 otherData 返回
|
|
||||||
status: completed
|
|
||||||
- id: slot-admin-vue-api
|
|
||||||
content: slot_admin_vue 新建 api/game/freeCreditsStats.js,导出 getPageList
|
|
||||||
status: completed
|
|
||||||
- id: slot-admin-vue-page
|
|
||||||
content: "slot_admin_vue 新建 views/game/freeCreditsStats/index.vue:sa-table + #tableAfterButtons 顶部 7 统计 + 12 列表 + 筛选 + 排序白名单"
|
|
||||||
status: completed
|
|
||||||
- id: menu-permission
|
|
||||||
content: 在 sm_system_menu 加 1 条 L 菜单 + 1 条 B 按钮码(/game/freeCreditsStats/index),并分配角色
|
|
||||||
status: completed
|
|
||||||
- id: manual-verify
|
|
||||||
content: docker 内 A/B/C 三种玩家手测:默认排序、7 项统计、筛选联动、排序白名单、权限拦截
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
## 数据流
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
vue[freeCreditsStats/index.vue sa-table]
|
|
||||||
vue -->|"POST /game/FreeCreditsStats/index"| ctrl[FreeCreditsStatsController]
|
|
||||||
ctrl -->|"FreeCreditsService::list"| svc[slot_lib FreeCreditsService]
|
|
||||||
ctrl -->|"FreeCreditsService::statistics"| svc
|
|
||||||
svc -->|"POST innerapi/free-credits/list"| console[slot_console FreeCreditsController]
|
|
||||||
svc -->|"POST innerapi/free-credits/statistics"| console
|
|
||||||
console -->|"WalletService statistics uids currency"| wallet["slot_wallet api/wallet/statistics"]
|
|
||||||
console -->|"sum on free_credits_package"| db[(s_common.free_credits_player + free_credits_package)]
|
|
||||||
```
|
|
||||||
|
|
||||||
slot_admin Controller 一次代理两个 innerapi,合并成 `{ data, total, otherData: <statistics> }` 给 sa-table,避免前端两次请求。
|
|
||||||
|
|
||||||
## 字段契约(最终对外)
|
|
||||||
|
|
||||||
### `/game/FreeCreditsStats/index` 请求
|
|
||||||
|
|
||||||
筛选(与 §18.3 对齐)
|
|
||||||
|
|
||||||
- `uid` 精确
|
|
||||||
- `source` 字符串(沿用 channel_game_model + source 二级选择)
|
|
||||||
- `activity_id` 可选(默认取当前生效的 type=11 活动)
|
|
||||||
- `first_recharge_time` 范围 `[start, end]` → `whereBetween`
|
|
||||||
- `status` 数组(player.status:0/1/2/3/4/5/6/7/10)
|
|
||||||
- `first_cash_status` 数组(package.status:0/1/2/3/4/5,限定 `package_no=1`)
|
|
||||||
- `is_completed` 0/1(status=10 与否)
|
|
||||||
- `page`, `limit`, `orderBy`, `orderType`
|
|
||||||
|
|
||||||
排序
|
|
||||||
|
|
||||||
- DB 可排序:`frozen_amount_qf`、`first_recharge_time`(默认 `first_recharge_time desc`)
|
|
||||||
- 其它字段(进度、剩余、累计充值)派生,前端不开启排序
|
|
||||||
|
|
||||||
### 响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [{
|
|
||||||
"uid": 123,
|
|
||||||
"source": "us_01",
|
|
||||||
"frozen_amount_qf": 78500,
|
|
||||||
"first_cash_amount_qf": 20000,
|
|
||||||
"total_recharge_amount_qf": 30000,
|
|
||||||
"first_cash_status": 3,
|
|
||||||
"progress_done": 3,
|
|
||||||
"progress_total": 7,
|
|
||||||
"status": 7,
|
|
||||||
"remaining_amount_qf": 28500,
|
|
||||||
"claimed_amount_qf": 30000,
|
|
||||||
"first_recharge_time": "2026-05-12 11:23:01",
|
|
||||||
"completed_time": null
|
|
||||||
}],
|
|
||||||
"total": 240,
|
|
||||||
"otherData": {
|
|
||||||
"frozen_user_count": 240,
|
|
||||||
"first_cashout_user_count": 180,
|
|
||||||
"completed_user_count": 35,
|
|
||||||
"frozen_amount_qf": 18650000,
|
|
||||||
"first_cashout_amount_qf": 3520000,
|
|
||||||
"claimed_amount_qf": 8800000,
|
|
||||||
"pending_amount_qf": 6330000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
金额字段一律以 `*_qf`(千分位整数)传到 Vue,Vue 用 `/ 1000` + `toFixed(2)` 显示(与 edit.vue 同口径)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 改动清单
|
|
||||||
|
|
||||||
### 1. slot_console — 增强 innerapi
|
|
||||||
|
|
||||||
文件:[slot_console/app/innerapi/controller/FreeCreditsController.php](slot_console/app/innerapi/controller/FreeCreditsController.php)
|
|
||||||
|
|
||||||
#### 1.1 `statistics()` 加筛选
|
|
||||||
|
|
||||||
参照需求 §18.1,新增可选参数并贯穿到所有子查询:
|
|
||||||
|
|
||||||
- `activity_id`、`source`、`first_recharge_time_start`、`first_recharge_time_end`、`uid`
|
|
||||||
- 子查询 `frozenTotal / completedCount / firstCashoutCount / firstCashoutAmount / claimedAmount` 全部基于同一个 `playerIds` 数组(按筛选后取出)
|
|
||||||
- `frozen_user_count` 口径改为 `status >= STATUS_FROZEN(2)` 的玩家数(与需求 "已创建 Free Credits Pool" 一致;当前实现把 status=1 也计入,定义不准)
|
|
||||||
|
|
||||||
#### 1.2 `list()` 加筛选 + 字段富化
|
|
||||||
|
|
||||||
- 新增筛选参数:`source`、`first_recharge_time` 范围、`status[]`、`first_cash_status[]`、`is_completed`、`orderBy/orderType`(白名单:`first_recharge_time`, `frozen_amount_qf`)
|
|
||||||
- 输出每行追加:
|
|
||||||
- `first_cash_status`:对应 `free_credits_package.status` where `player_id=? AND package_no=1`
|
|
||||||
- `progress_done` / `progress_total`:`SUM(CASE WHEN status=3 THEN 1 ELSE 0 END)` / `COUNT(*)`,一次 `GROUP BY player_id` 跑完
|
|
||||||
- `claimed_amount_qf`:`SUM(amount_qf) WHERE package_type=2 AND status=3 GROUP BY player_id`
|
|
||||||
- `first_cashout_amount_qf`:`SUM(amount_qf) WHERE package_type=1 AND status=3 GROUP BY player_id`
|
|
||||||
- `remaining_amount_qf`:`frozen_amount_qf - first_cashout_amount_qf - claimed_amount_qf`
|
|
||||||
- `total_recharge_amount_qf`:调用 `WalletService::statistics(uids, currency='USD')`,把 `total_deposit` 映射进来。currency 默认 'USD'(活动只面向美国 RMG,currency 字段后续如多币种再扩展)
|
|
||||||
- 一次性聚合查询,避免 N+1:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SELECT player_id,
|
|
||||||
SUM(CASE WHEN status=3 THEN 1 ELSE 0 END) AS progress_done,
|
|
||||||
COUNT(*) AS progress_total,
|
|
||||||
SUM(CASE WHEN package_type=1 AND status=3 THEN amount_qf ELSE 0 END) AS first_cashout_amount_qf,
|
|
||||||
SUM(CASE WHEN package_type=2 AND status=3 THEN amount_qf ELSE 0 END) AS claimed_amount_qf,
|
|
||||||
MAX(CASE WHEN package_no=1 THEN status END) AS first_cash_status
|
|
||||||
FROM s_common.free_credits_package
|
|
||||||
WHERE player_id IN (...)
|
|
||||||
GROUP BY player_id
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.3 控制器分层
|
|
||||||
|
|
||||||
按 backend-layering 规则,把 1.1 / 1.2 的查询编排沉到 Logic:新增 [slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php](slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php)(或直接挂在已有 `api/logic/FreeCreditsLogic.php`,建议新建避免膨胀),Controller 只负责接参 / 调 Logic / 返回。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. slot_lib — 新增 FreeCreditsService + 补 WalletService
|
|
||||||
|
|
||||||
#### 2.1 [slot_lib/src/services/FreeCreditsService.php](slot_lib/src/services/FreeCreditsService.php) 新建
|
|
||||||
|
|
||||||
```php
|
|
||||||
class FreeCreditsService extends BaseApiService
|
|
||||||
{
|
|
||||||
use SingletonService;
|
|
||||||
protected $hostKey = 'consoleApiHost';
|
|
||||||
protected $basePath = 'innerapi/free-credits';
|
|
||||||
|
|
||||||
public function statistics(array $params) { return $this->postAction('statistics', $params); }
|
|
||||||
public function list(array $params) { return $this->postAction('list', $params); }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
理由:不与既有 `ActivityService::join/add/update` 混杂;basePath 完全独立,匹配 slot_console 的路由。
|
|
||||||
|
|
||||||
#### 2.2 [slot_lib/src/services/WalletService.php](slot_lib/src/services/WalletService.php) 补 `statistics`
|
|
||||||
|
|
||||||
```php
|
|
||||||
public function statistics(array $uids, string $currency)
|
|
||||||
{
|
|
||||||
return $this->postAction('statistics', ['uids' => $uids, 'currency' => $currency]);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
action 名 `statistics`,URL `{walletApiHost}/api/wallet/statistics`,对应 slot_wallet `WalletController::statistics`。slot_console 侧 `FreeCreditsStatsLogic` 调用此方法。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. slot_admin — 新增代理 Controller
|
|
||||||
|
|
||||||
文件:[backend/slot_admin/app/game/controller/FreeCreditsStatsController.php](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php)
|
|
||||||
|
|
||||||
只做接参 + Service 调用 + 合并:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class FreeCreditsStatsController extends AdminController
|
|
||||||
{
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$params = $request->all();
|
|
||||||
$params['page'] = (int)$request->get('page', 1);
|
|
||||||
$params['limit'] = (int)$request->get('limit', 20);
|
|
||||||
|
|
||||||
$list = FreeCreditsService::getInstance()->list($params);
|
|
||||||
$stats = FreeCreditsService::getInstance()->statistics($params);
|
|
||||||
|
|
||||||
$list['otherData'] = $stats;
|
|
||||||
return $this->success($list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 不需要 Validate(参数全部可选;非法值 slot_console 侧拦)
|
|
||||||
- 自动路由路径 `/game/freeCreditsStats/index`
|
|
||||||
- 不新增 `export()` action(V1 不做导出)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. slot_admin_vue — 新页 + API
|
|
||||||
|
|
||||||
#### 4.1 [backend/slot_admin_vue/src/api/game/freeCreditsStats.js](backend/slot_admin_vue/src/api/game/freeCreditsStats.js) 新建
|
|
||||||
|
|
||||||
```js
|
|
||||||
import request from '@/utils/request'
|
|
||||||
const url = '/game/freeCreditsStats'
|
|
||||||
export default {
|
|
||||||
getPageList: (params) => request.get(`${url}/index`, params),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 [backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue) 新建
|
|
||||||
|
|
||||||
骨架仿 `views/game/order/recharge/index.vue`:
|
|
||||||
|
|
||||||
- `defineOptions({ name: 'game/freeCreditsStats/index' })`
|
|
||||||
- `<sa-table>` `options.api = api.getPageList`,`options.add` / `options.delete` 关闭
|
|
||||||
- `searchForm`:`uid`、`source`(用 `commonStore.allSourcesOptionsNoAll`)、`first_recharge_time`(`a-range-picker`)、`status`、`first_cash_status`、`is_completed`
|
|
||||||
- 顶部统计走 `#tableAfterButtons`:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template #tableAfterButtons>
|
|
||||||
<a-space>
|
|
||||||
<a-typography-text>定格人数:{{ stats.frozen_user_count }}</a-typography-text>
|
|
||||||
<a-typography-text>完成提现人数:{{ stats.first_cashout_user_count }}</a-typography-text>
|
|
||||||
<a-typography-text>全部完成人数:{{ stats.completed_user_count }}</a-typography-text>
|
|
||||||
<a-typography-text>定格总金额:${{ qfToDollar(stats.frozen_amount_qf) }}</a-typography-text>
|
|
||||||
<a-typography-text>已提现金额:${{ qfToDollar(stats.first_cashout_amount_qf) }}</a-typography-text>
|
|
||||||
<a-typography-text>已领取金额:${{ qfToDollar(stats.claimed_amount_qf) }}</a-typography-text>
|
|
||||||
<a-typography-text>待释放金额:${{ qfToDollar(stats.pending_amount_qf) }}</a-typography-text>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `stats = computed(() => crudRef.value?.getTableOtherData() ?? {})`
|
|
||||||
- 列定义:12 列对齐 §18.2;`status`、`first_cash_status` 直接用前端 map 表(不新建字典):
|
|
||||||
|
|
||||||
```js
|
|
||||||
const PLAYER_STATUS_MAP = {
|
|
||||||
0: '未触发', 1: 'Home Withdraw 已解锁', 2: '已定格', 3: '充值进行中',
|
|
||||||
4: '第一档可提现', 5: '第一档处理中', 6: '第一档已提现',
|
|
||||||
7: '后续档释放中', 10: '全部完成',
|
|
||||||
}
|
|
||||||
const PACKAGE_STATUS_MAP = {
|
|
||||||
0: '未解锁', 1: '可操作', 2: '处理中', 3: '已完成', 4: '失败', 5: '已拒绝',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `qfToDollar = (v) => ((Number(v||0))/1000).toFixed(2)`
|
|
||||||
- 不再调单独的 stats 接口,`tableAfterButtons` 数据从 `otherData` 取
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 字典与菜单
|
|
||||||
|
|
||||||
#### 5.1 字典:不新增
|
|
||||||
|
|
||||||
`status` / `first_cash_status` 直接走前端 map 表,避免与 `RechargeGiftConfigModel` 共用字典污染。
|
|
||||||
|
|
||||||
#### 5.2 菜单:DB 表 `sm_system_menu` 追加(运营在「系统管理 → 菜单管理」操作即可)
|
|
||||||
|
|
||||||
需要的 2 条数据(指引性 SQL,实际可走 UI):
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO sm_system_menu (parent_id, type, title, name, path, component, sort)
|
|
||||||
VALUES (<game_parent_id>, 'L', 'Free Credits 统计',
|
|
||||||
'game/freeCreditsStats/index', '/game/freeCreditsStats',
|
|
||||||
'game/freeCreditsStats/index', 50);
|
|
||||||
INSERT INTO sm_system_menu (parent_id, type, title, code)
|
|
||||||
VALUES (<leaf_id>, 'B', '列表', '/game/freeCreditsStats/index');
|
|
||||||
```
|
|
||||||
|
|
||||||
写完后给「超级管理员 / 运营」角色分配该菜单与按钮码。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 不动的地方
|
|
||||||
|
|
||||||
- `slot_console` 既有 `FreeCreditsLogic` C 端逻辑、`free_credits_player` / `free_credits_package` 表结构、`ext_config` 配置读取等不动
|
|
||||||
- `s_recharge_gift_config` 不需要新增列
|
|
||||||
- `slot_pay` 不新增 innerapi(不需要订单表精确口径,wallet `total_deposit` 已足够)
|
|
||||||
- 现有 `ActivityController` 编辑表单(type=11 分支)已落地,不动
|
|
||||||
- `RechargeOrderController` / `withdrawal` 等无关模块不动
|
|
||||||
|
|
||||||
## 验收(手测)
|
|
||||||
|
|
||||||
按 [dev-environment](.cursor/rules/dev-environment.mdc) 在 docker 内调试:
|
|
||||||
|
|
||||||
1. 准备 3 个测试账号:A 未定格、B 已定格未提现首档、C 全部完成;各 source 至少一个
|
|
||||||
2. 访问 `/game/freeCreditsStats` 列表:
|
|
||||||
- 不传参 → 默认按 `first_recharge_time desc`,A 不出现(status<2 被过滤),B / C 都在
|
|
||||||
- 顶部 7 个统计数字与逐条手算结果一致
|
|
||||||
- 列表里 `total_recharge_amount_qf` 与 `slot_wallet.api/wallet/statistics` 直接拉的 `total_deposit` 一致
|
|
||||||
- `progress_done/total` 等于 DB 中 `free_credits_package` 实际行数
|
|
||||||
3. 筛选:
|
|
||||||
- 选 `source=us_01` 列表与统计同步收敛
|
|
||||||
- 选 `first_recharge_time` 范围,统计 7 项随之变化
|
|
||||||
- 选 `is_completed=1`,列表只剩 C
|
|
||||||
- 选 `first_cash_status=3`,列表只剩第一档已成功的玩家
|
|
||||||
4. 排序:分别点击「定格金额」「定格时间」列头,order 切换;其它字段不可排序
|
|
||||||
5. 权限:用未授权账号访问 → 403;授权后正常
|
|
||||||
6. 仅改活动状态时(已存在的活动编辑场景)回归一遍,确认未触发 type=11 ext 校验路径回归
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
---
|
|
||||||
name: Free Credits 老用户隐藏评审
|
|
||||||
overview: EventBus 写库确定参加、status 只读展示;在此基础上用环境变量 FREE_CREDITS_ENROLL_REG_AFTER 做注册时间门槛——regTime 晚于该时间的用户才有资格参加,否则一律 status=-1 且 EventBus 跳过写池。
|
|
||||||
todos:
|
|
||||||
- id: env-reg-cutoff
|
|
||||||
content: 新增 FREE_CREDITS_ENROLL_REG_AFTER 环境变量与 FreeCreditsLogic::isEligibleByRegTime(uid) 统一门禁
|
|
||||||
status: completed
|
|
||||||
- id: apply-gate-all-paths
|
|
||||||
content: 在 status、syncHomeWithdrawUnlocked、handleFreeCreditInit、advanceAfterRecharge、claim、firstCashout 入口应用注册时间门禁
|
|
||||||
status: completed
|
|
||||||
- id: fix-docs-lobby
|
|
||||||
content: 修正 FreeCreditsController PHPDoc(含 status=-1 含老用户/未达注册门槛);LobbyController 清理无用 import
|
|
||||||
status: completed
|
|
||||||
- id: guard-claim-cashout
|
|
||||||
content: 无资格或无 player 时 claim/firstCashout 返回明确业务错误
|
|
||||||
status: completed
|
|
||||||
- id: verify-reg-cutoff
|
|
||||||
content: 联调:regTime 早于门槛 status=-1 且 EventBus 不建池;晚于门槛走 win/首充完整流程
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Free Credits 老用户隔离:注册时间门槛(计划)
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
C 端全量更新后,**注册时间早于活动上线节点的用户视为老用户,不参加、不展示**;**注册时间晚于该节点的新用户**,在现有「EventBus 写库 = 参加」模型下正常走活动。
|
|
||||||
|
|
||||||
「默认参加」含义:**有资格参加**(EventBus 允许建池、status 可读),**不是**无需 EventBus 自动插入 `free_credits_player`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 分层架构(保持不变)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
gate{regTime > FREE_CREDITS_ENROLL_REG_AFTER?}
|
|
||||||
gate -->|否| block[不参加: status=-1 EventBus return]
|
|
||||||
gate -->|是| eligible[有资格]
|
|
||||||
eligible --> win[EventBus syncHomeWithdrawUnlocked]
|
|
||||||
eligible --> init[EventBus free_credit_init]
|
|
||||||
eligible --> api[status 只读 player 行]
|
|
||||||
win --> pool[(free_credits_player)]
|
|
||||||
init --> pool
|
|
||||||
api --> pool
|
|
||||||
```
|
|
||||||
|
|
||||||
| 路径 | 行为 |
|
|
||||||
| --- | --- |
|
|
||||||
| **读** `status()` | 未达注册门槛 → `status=-1`;达门槛且无 player 行 → `-1`;有行 → `buildStatus` |
|
|
||||||
| **写** EventBus | 未达注册门槛 → 各 Logic 方法开头直接 return,不建池、不定格、不推进 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现要点
|
|
||||||
|
|
||||||
### 1. 环境变量
|
|
||||||
|
|
||||||
在 [`.env`](slot_console/.env) / 部署说明中增加(示例):
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 用户注册时间(Unix 或 Y-m-d H:i:s,建议与 TIMEZONE 一致)晚于此值才可参加 Free Credits
|
|
||||||
FREE_CREDITS_ENROLL_REG_AFTER=2026-05-20 00:00:00
|
|
||||||
```
|
|
||||||
|
|
||||||
- 在 [`config/app.php`](slot_console/config/app.php) 或新建 `config/free_credits.php` 读取:`getenv('FREE_CREDITS_ENROLL_REG_AFTER')`,`strtotime` 解析为 `enroll_reg_after_ts`(启动时或首次调用缓存)。
|
|
||||||
- **未配置时的默认策略(已确认)**:`FREE_CREDITS_ENROLL_REG_AFTER` 为空或未配置 → **全员不参加**(`isEligibleByRegTime` 恒 false,`status=-1`,EventBus 不写池)。
|
|
||||||
|
|
||||||
### 2. 统一门禁方法(建议放在 [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php))
|
|
||||||
|
|
||||||
```php
|
|
||||||
/**
|
|
||||||
* 是否具备 Free Credits 参与资格(注册时间晚于环境变量门槛)。
|
|
||||||
*/
|
|
||||||
protected function isEligibleByRegTime(int $uid): bool
|
|
||||||
{
|
|
||||||
$cutoff = self::enrollRegAfterTimestamp(); // 0 表示未配置
|
|
||||||
if ($cutoff <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$userInfo = \app\service\user\UserService::getUserInfoEntity($uid);
|
|
||||||
if ($userInfo === null || $userInfo->create_at === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$regTime = strtotime($userInfo->create_at);
|
|
||||||
return $regTime !== false && $regTime > $cutoff; // 严格「晚于」
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **数据来源**:[`app\service\user\UserService::getUserInfoEntity`](slot_console/app/service/user/UserService.php)(调 user 服 `/innerapi/user/info`),与 [`EventBus::registerEvent`](slot_console/app/command/EventBus.php) 等同用法。
|
|
||||||
- **注册时间字段**:[`app\entity\UserInfoEntity::$create_at`](slot_console/app/entity/UserInfoEntity.php)(`Y-m-d H:i:s`),比较前 `strtotime` 为 Unix 秒。
|
|
||||||
- **不用** `UserTagService::tagInfo->regTime`(标签缓存,与账号创建时间可能不一致)。
|
|
||||||
- 备选:若 Logic 内已大量使用 slotLib,可用 `\slotLib\services\UserService::getInstance()->setUid($uid)->getUserInfo()?->regTime`(构造函数内由 `create_at` 解析),但 console 侧优先统一 `app\service\user\UserService`。
|
|
||||||
|
|
||||||
### 3. 调用点(读写对称)
|
|
||||||
|
|
||||||
| 方法 | 未达门槛时 |
|
|
||||||
| --- | --- |
|
|
||||||
| `status()` | 直接 `return ['status' => -1]` |
|
|
||||||
| `syncHomeWithdrawUnlocked()` | `return null` |
|
|
||||||
| `handleFreeCreditInit()` | `return` |
|
|
||||||
| `advanceAfterRecharge()` | `return` |
|
|
||||||
| `claim()` / `firstCashout()` | `throw BusinessException('...')` 或统一文案 |
|
|
||||||
|
|
||||||
**说明**:已达门槛、但库中无 player 行 → 仍为 `status=-1`(尚未被 EventBus 纳入);达门槛且 win 后 → EventBus 建池 → status 非 `-1`。
|
|
||||||
|
|
||||||
### 4. 与「已首充」的关系
|
|
||||||
|
|
||||||
- 注册门槛解决:**老账号 / 老注册** 不进入活动。
|
|
||||||
- 钱包侧 [`maybeSendFreeCreditInit`](slot_wallet/app/api/logic/WalletLogic.php) 仍仅 **首充** 发定格,二者叠加,互不替代。
|
|
||||||
|
|
||||||
### 5. 撤销项
|
|
||||||
|
|
||||||
- 不再单独做 `total_deposit == 0` 的 EventBus 门禁(由注册时间门槛覆盖「老用户」定义)。
|
|
||||||
- 不再要求 `frozen_amount > 0` 才展示(保留 HOME_UNLOCKED 阶段)。
|
|
||||||
|
|
||||||
### 6. 文档与测试
|
|
||||||
|
|
||||||
- 更新 [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php):`status=-1` = 未参加 / 活动关 / **注册时间早于门槛**。
|
|
||||||
- 单测:mock `regTime` 与 env cutoff,覆盖 eligible / ineligible 的 status 与 freeze 是否被调用。
|
|
||||||
- 联调矩阵见下。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 联调矩阵
|
|
||||||
|
|
||||||
| regTime vs 门槛 | win 达门槛 | 首充定格 | status | EventBus 建池 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| 早于 | - | - | `-1` | 否 |
|
|
||||||
| 晚于 | 否 | 否 | `-1` | 否 |
|
|
||||||
| 晚于 | 是 | 否 | `1` | sync 建池 |
|
|
||||||
| 晚于 | - | 是 | `3/4` + packages | freeze |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 新 C 端约定(不变)
|
|
||||||
|
|
||||||
- **`status === -1`**:隐藏(含老用户、未达注册门槛、未参加)。
|
|
||||||
- **`status !== -1`**:已参加,按 `FreeCreditsPlayerModel` 状态渲染。
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
---
|
|
||||||
name: Free Credits 验收核查
|
|
||||||
overview: 「首充前免费余额定格与分档释放」的后端核心链路(定格、分档、首档提现、后续 Claim、充值事件、钱包账务)已在 slot_console / slot_wallet / slot_pay / slot_lib 落地约 70–80%,但尚未达到需求文档 V1.0 全量验收标准;运营后台、客户端 UI、广播、异常风控、部分 API 字段与自动化测试仍缺失或未对齐。
|
|
||||||
todos:
|
|
||||||
- id: p0-wallet-atomicity
|
|
||||||
content: 补齐 Claim+Y1 原子性/回滚;确认是否需要 Deposit Lot
|
|
||||||
status: pending
|
|
||||||
- id: p0-status-api-fields
|
|
||||||
content: status/buildStatus 增加 total_recharge、show_home_status_bar、show_activity_entry
|
|
||||||
status: pending
|
|
||||||
- id: p0-peak-balance
|
|
||||||
content: 首页解锁「曾达到门槛」:增加峰值记录或可靠触发点
|
|
||||||
status: pending
|
|
||||||
- id: p1-admin-config-stats
|
|
||||||
content: slot_admin 增加 type=11 配置页与统计页(对接 innerapi)
|
|
||||||
status: pending
|
|
||||||
- id: p1-refund-risk
|
|
||||||
content: 实现充值退款/拒付暂停未释放档位(文档 20.1)
|
|
||||||
status: pending
|
|
||||||
- id: p2-frontend-broadcast
|
|
||||||
content: 前端仓实现 UI/广播;本仓可增加 broadcast API
|
|
||||||
status: pending
|
|
||||||
- id: p2-tests
|
|
||||||
content: 按文档 22.1/22.2 补充自动化或验收用例
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 首充前免费余额定格与分档释放 — 实现完成度核查
|
|
||||||
|
|
||||||
**结论:未实现完。** 后端主流程可联调,但对照 [需求文档](file:///Users/ray/Documents/project/www/slot/docs/requirements/首充前免费余额定格与分档释放需求文档.md) 第 22 节验收标准,仍有若干 **P0 账务/规则缺口** 与大量 **前端/运营/统计** 范围未覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现分布(已落地)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph console [slot_console]
|
|
||||||
RechargeEvent --> FreeCreditsLogic
|
|
||||||
EventBus --> FreeCreditsLogic
|
|
||||||
FreeCreditsLogic --> DB[(free_credits_player/package)]
|
|
||||||
ApiCtrl[FreeCreditsController] --> FreeCreditsLogic
|
|
||||||
Lobby[LobbyController.frontData] --> FreeCreditsLogic
|
|
||||||
Inner[innerapi FreeCreditsController] --> DB
|
|
||||||
end
|
|
||||||
subgraph wallet [slot_wallet]
|
|
||||||
FreeCreditsLogic --> WalletLogic
|
|
||||||
WalletLogic --> freeze[freeCreditsFreeze]
|
|
||||||
WalletLogic --> claim[freeCreditsClaim]
|
|
||||||
claim --> Y1[createTask SOURCE_TYPE_FREE]
|
|
||||||
end
|
|
||||||
subgraph pay [slot_pay]
|
|
||||||
firstCashout[firstCashout API] --> WithdrawalOrder
|
|
||||||
WithdrawalOrder --> MQ[FreeCreditsFirstCashout*]
|
|
||||||
MQ --> EventBus
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
| 模块 | 关键文件 | 覆盖的需求章节 |
|
|
||||||
|------|----------|----------------|
|
|
||||||
| 领域与状态机 | [FreeCreditsLogic.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php) | 5.3–5.9 核心规则 |
|
|
||||||
| 数据表 | [install.sql](file:///Users/ray/Documents/project/www/slot/slot_console/db/install.sql) L36–86 | 活动池 + 档位 |
|
|
||||||
| 活动配置 | [RechargeGiftConfigModel.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/model/common/RechargeGiftConfigModel.php) `TYPE_FREE_CREDITS=11` | 17 配置(读库) |
|
|
||||||
| 客户端 API | [FreeCreditsController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/controller/FreeCreditsController.php) | status / claim / firstCashout |
|
|
||||||
| 大厅聚合 | [LobbyController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/napi/controller/LobbyController.php) `free_credits` | 7 状态条数据源(部分) |
|
|
||||||
| 充值事件 | [RechargeEvent.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php) | 5.3 首充定格、5.5 累计充值解锁 |
|
|
||||||
| 首页解锁 | [EventBus.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php) win/bonus 等 → `syncHomeWithdrawUnlocked` | 5.2 |
|
|
||||||
| 钱包原子能力 | [WalletLogic.php](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) | 15.2–15.4 |
|
|
||||||
| 首档提现 | [WithdrawalOrderEntity.php](file:///Users/ray/Documents/project/www/slot/slot_pay/app/entity/WithdrawalOrderEntity.php) + [EventWithdrawal.php](file:///Users/ray/Documents/project/www/slot/slot_pay/app/command/EventWithdrawal.php) | 5.6、15.3、20.2 |
|
|
||||||
| 运营统计 API | [innerapi/FreeCreditsController.php](file:///Users/ray/Documents/project/www/slot/slot_console/app/innerapi/controller/FreeCreditsController.php) | 18.1–18.2(简化版) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 按需求章节对照(22.1 核心规则)
|
|
||||||
|
|
||||||
| 验收项(文档 22.1) | 实现情况 | 说明 |
|
|
||||||
|---------------------|----------|------|
|
|
||||||
| 未达 $50 点 Withdraw 提示 | **前端** | 后端 `status` 返回 `win_threshold` / `home_withdraw_unlocked`,无专用提示 API |
|
|
||||||
| 曾达 $50 后输回仍解锁 | **部分** | 已解锁用户靠 `home_withdraw_unlocked=1` 保持;**未持久化历史峰值**,仅在 MQ 事件时若 `balance >= threshold` 才写库,存在漏解锁边界(例如达峰后无 win/bonus 类事件即输掉) |
|
|
||||||
| 任意入口首充定格 | **是** | `RechargeEvent` + `handleRecharge`,`frozenAmount = balance_after - wallet_amount` |
|
|
||||||
| 首充失败不定格 | **是** | 仅充值成功 MQ 触发 |
|
|
||||||
| 定格后余额扣除、活动展示 | **是** | `freeCreditsFreeze` + player/package 记录 |
|
|
||||||
| 累计未满 $50 第一档不可提现 | **是** | 首包 `STATUS_LOCKED` 直至 `wallet.r >= recharge_unlock_amount` |
|
|
||||||
| 累计满 $50 第一档可提现 | **是** | `advanceByRecharge` 解锁 package_no=1 |
|
|
||||||
| 定格 < $20 时第一档=定格额 | **是** | `min(frozen, first_cash_amount)` |
|
|
||||||
| 第一档提现成功后关首页状态条 | **后端未显式字段** | 有 `STATUS_FIRST_CASH_DONE`,但 `buildStatus` **未返回** `show_home_status_bar` 等 UI 开关,需前端自行推断 |
|
|
||||||
| 后续充值只解锁下一档 | **是** | `max_unlock_per_recharge` + `nextLockedReleasePackage` |
|
|
||||||
| Claim 入 Deposit + Y1 | **部分** | 入 deposit_balance + `createTask(SOURCE_TYPE_FREE)`;**无 Deposit Lot**(文档 15.4、22.2) |
|
|
||||||
| 全部完成关活动入口 | **部分** | `STATUS_COMPLETED=10`;API 未返回 `show_activity_entry` 布尔字段 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 按需求章节对照(22.2 账务)
|
|
||||||
|
|
||||||
| 验收项 | 实现情况 | 风险 |
|
|
||||||
|--------|----------|------|
|
|
||||||
| 定格有钱包流水 | **是** | `BIZ_TYPE_FREE_CREDITS_FREEZE` |
|
|
||||||
| 定格前后余额正确 | **是** | `addLog` 记录 |
|
|
||||||
| 第一档不进普通钱包 | **是** | 独立 `free_credit_first_cashout`,跳过 `withdrawFrozen` |
|
|
||||||
| Claim 创建 Deposit Lot | **否** | 全仓库无 `DepositLot`/`deposit_lot` 实现 |
|
|
||||||
| Claim 创建 Y1 | **是** | 但 `createTask` 在钱包事务 **commit 之后**;失败仅打日志,**不满足** 文档 20.3「流水任务失败整体回滚」 |
|
|
||||||
| 重复回调不重复定格 | **是** | `frozen_amount_qf > 0` 跳过 |
|
|
||||||
| 重复 Claim 不重复入账 | **部分** | package 状态 + `claim_biz_id` 唯一;钱包层 **未见 biz_id 幂等查重** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 明确未实现 / 未对齐项
|
|
||||||
|
|
||||||
### P0(影响验收或资金一致性)
|
|
||||||
|
|
||||||
1. **Deposit Lot**:文档 15.4 / 22.2 要求 Claim 后创建;当前仅 `inc` deposit + Y1 task。
|
|
||||||
2. **Claim 与 Y1 原子性**:`freeCreditsClaim` 先 commit 入账,再 `createTask`;与 20.3 冲突。
|
|
||||||
3. **充值退款/拒付暂停档位**(文档 20.1):`FreeCreditsLogic` 无 refund/chargeback 处理。
|
|
||||||
4. **status API 缺累计充值进度**:定格后 UI 需要 `$9.99 / $50`(文档 6.3、7.3),`buildStatus` 未返回 `total_recharge`(内部用 `wallet.r`,未透出)。
|
|
||||||
5. **「曾经达到 $50」**:无 `peak_balance` 字段;依赖事件驱动 `syncHomeWithdrawUnlocked`,与 5.2 字面规则不完全一致。
|
|
||||||
|
|
||||||
### P1(运营与配置)
|
|
||||||
|
|
||||||
6. **后台配置模块**(文档 17):`backend/slot_admin` / `slot_admin_vue` **无** `TYPE_FREE_CREDITS=11` 的 CRUD 页面;仅能手工维护 `s_recharge_gift_config.ext_config`。
|
|
||||||
7. **后台统计页**(文档 18):仅有 [innerapi](file:///Users/ray/Documents/project/www/slot/slot_console/app/innerapi/controller/FreeCreditsController.php) 简化接口;列表缺 **渠道、时间范围、第一档状态、排序项、累计充值、完成进度** 等字段/筛选项。
|
|
||||||
8. **可配置 Y1 倍数**(文档 17 `后续档打码倍数`):代码写死 `createTask(0, fee, SOURCE_TYPE_FREE)`,未读 `ext_config`。
|
|
||||||
9. **状态机 `STATUS_FROZEN=2`**:模型定义了但 Logic **从未赋值**(定格后直接 `DEPOSIT_PENDING` / `FIRST_CASH_READY`)。
|
|
||||||
|
|
||||||
### P2(产品体验 / 文档其他章节)
|
|
||||||
|
|
||||||
10. **客户端 UI**(文档 6–14):`slot_pwa` 等仓库 **零引用** `free_credits`;首页状态条、Free Play to Go、独立提现页、广播模块均未实现(原开发计划也标明前端另仓)。
|
|
||||||
11. **广播模块**(文档 13):无最近 10 条提现/领取记录 API。
|
|
||||||
12. **注册赠送改 $30**(文档 19):与本活动解耦,**未在本需求代码变更中验证**是否已改配置/落地页。
|
|
||||||
13. **自动化测试**:无 FreeCredits 相关单测/集成测试。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 与现有开发计划的一致性
|
|
||||||
|
|
||||||
[free_credits_release 计划](file:///Users/ray/.cursor/plans/free_credits_release_902566c9.plan.md) 中 `implement-console-core` 仍为 **pending**,与代码现状(Logic 已较完整)不一致;`test-acceptance` 标 completed 但仓库内 **无测试文件**,建议以文档 22 节手工/自动化用例重新验收。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 建议验收顺序(若需补齐)
|
|
||||||
|
|
||||||
1. **手工走通 P0 账务用例**:首充定格幂等、满 $50 解锁、首档提现回调、顺序 Claim、重复 Claim/回调。
|
|
||||||
2. **补齐 status 字段**:`total_recharge`、`show_home_status_bar`、`show_activity_entry`、`current_balance`。
|
|
||||||
3. **对齐钱包**:Deposit Lot(若钱包规范要求)、Claim+Y1 同事务或补偿回滚。
|
|
||||||
4. **运营**:slot_admin 增加 type=11 配置页 + 统计页对接 innerapi。
|
|
||||||
5. **前端仓**:按文档 6–14 接 API(本 monorepo 外)。
|
|
||||||
6. **补测试**:覆盖 22.1 / 22.2 表格各行。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 总览评分(仅供沟通)
|
|
||||||
|
|
||||||
| 范围 | 完成度(估) |
|
|
||||||
|------|-------------|
|
|
||||||
| 后端核心状态机 + 充值/提现/Claim 链路 | ~75–85% |
|
|
||||||
| 钱包账务与文档完全一致 | ~60% |
|
|
||||||
| 客户端 API 字段/UI 支撑 | ~50% |
|
|
||||||
| 运营后台配置与统计 | ~25% |
|
|
||||||
| 前端 UI / 文案 / 广播 | ~0%(本仓库) |
|
|
||||||
| 自动化测试 | ~0% |
|
|
||||||
|
|
||||||
**综合:需求文档 V1.0 不能判定为「已全部实现」;可判定为「后端 MVP 已具备,待补齐账务细节、运营与前端后全量验收」。**
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: is_end派奖结算改造
|
|
||||||
overview: 基于现有 BetFunding/WinService 实现,新增 is_end 分支:中间派奖仅累计不入账,结束派奖统一按 Round Final Settlement 入账;并对中间派奖增加 biz_id 严格幂等。
|
|
||||||
todos:
|
|
||||||
- id: dto-validator-is-end
|
|
||||||
content: 扩展 DTO 与校验器,支持 is_end 并强制 win 场景 round_id 校验
|
|
||||||
status: pending
|
|
||||||
- id: redis-win-keys
|
|
||||||
content: 新增 pending/dedupe Redis key 生成方法与 TTL 约定
|
|
||||||
status: pending
|
|
||||||
- id: logic-win-branching
|
|
||||||
content: 在 WalletLogic::win 中实现 is_end=0 累计与 is_end=1 最终结算分支
|
|
||||||
status: pending
|
|
||||||
- id: idempotency-regression
|
|
||||||
content: 补充关键回归场景说明并验证与现有幂等不冲突
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# is_end 驱动的派奖结算改造计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
让 `win` 接口支持第三方 `is_end` 语义:
|
|
||||||
- `is_end=0`:仅记录/累计本局派奖,不做钱包入账、不做 Lot 终态收敛
|
|
||||||
- `is_end=1`:将本次派奖 + 已累计派奖合并后,执行一次 Final Settlement(现有 `WinService::execute`)
|
|
||||||
|
|
||||||
## 现状结论(基于代码)
|
|
||||||
- 入口路由在 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `type=win -> win()`。
|
|
||||||
- Final Settlement 主流程在 [app/service/wallet/WinService.php](app/service/wallet/WinService.php)。
|
|
||||||
- 当前 `win` 默认即 Final Settlement,不区分中间派奖事件。
|
|
||||||
- 下注资金事实表在 [app/model/multi/WalletBetFundingModel.php](app/model/multi/WalletBetFundingModel.php)。
|
|
||||||
|
|
||||||
## 实现方案(按你选择)
|
|
||||||
### 1) 入参扩展与校验
|
|
||||||
- 在 [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加字段:`is_end`(默认 `1`,仅允许 `0|1`)。
|
|
||||||
- 在 [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php):
|
|
||||||
- 增加 `is_end` 校验规则(整数且取值 `0/1`)。
|
|
||||||
- 将 `win` 也纳入 `round_id` 必传校验(当前仅 bet 必传)。
|
|
||||||
|
|
||||||
### 2) Redis Key 设计
|
|
||||||
- 在 [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) 新增两个 key 生成器:
|
|
||||||
- `wallet:win:pending:{uid}:{currency}:{round_id}`:累计未结算派奖金额
|
|
||||||
- `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}`:中间派奖幂等标记
|
|
||||||
- 过期策略建议:`pending` 48h,`dedupe` 72h(与重放窗口对齐)。
|
|
||||||
|
|
||||||
### 3) win 主流程分支
|
|
||||||
- 修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`:
|
|
||||||
- `is_end=0`:
|
|
||||||
- 命中 dedupe key 则直接返回当前钱包(幂等)
|
|
||||||
- 未命中则将 `fee` 累加到 pending key,写 dedupe key,返回当前钱包(不写 `BIZ_TYPE_WIN` 流水)
|
|
||||||
- `is_end=1`:
|
|
||||||
- 读取 pending 累计并与本次 `fee` 合并为 `finalWinAmount`
|
|
||||||
- 用 `finalWinAmount` 调用现有 `WinService::execute`
|
|
||||||
- 事务提交后清理 pending key(失败不清)
|
|
||||||
|
|
||||||
### 4) 幂等与一致性
|
|
||||||
- `is_end=1` 继续沿用现有 `wallet_log(uid,biz_id,biz_type=win)` 幂等。
|
|
||||||
- `is_end=0` 使用 Redis dedupe key 防重,避免重复累计。
|
|
||||||
- 若 `is_end=1` 无待分配 funding,保持当前行为(报错),便于暴露上游时序问题。
|
|
||||||
|
|
||||||
### 5) 兼容与回归
|
|
||||||
- 默认 `is_end=1`,兼容未传该字段的旧调用。
|
|
||||||
- 回归场景:
|
|
||||||
- 单次结算(只发 `is_end=1`)
|
|
||||||
- 多次派奖(多条 `is_end=0` + 一条 `is_end=1`)
|
|
||||||
- `is_end=0` 重放同 biz_id 不重复累计
|
|
||||||
- `is_end=1` 重放同 biz_id 不重复入账
|
|
||||||
- `is_end=1` 失败后 pending 不丢失
|
|
||||||
|
|
||||||
## 关键改动文件
|
|
||||||
- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php)
|
|
||||||
- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php)
|
|
||||||
- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php)
|
|
||||||
- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php)
|
|
||||||
|
|
||||||
## 风险与边界
|
|
||||||
- 该方案不新增 `wallet_game_round` / `wallet_game_round_event` 持久化表;中间派奖仅 Redis 暂存,审计粒度弱于 DB 事件流。
|
|
||||||
- 若某局长期无 `is_end=1`,pending 依赖 TTL 过期清理。后续可升级为 DB 事件表方案。
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: is_end结算回退方案
|
|
||||||
overview: 取消“中间派奖实时余额可见化”调整,回退到严格 Final Settlement 方案:is_end=0 仅累计,is_end=1 才入账与状态收敛。
|
|
||||||
todos:
|
|
||||||
- id: dto-validator-is-end
|
|
||||||
content: 增加 is_end 字段及 win round_id 强校验
|
|
||||||
status: completed
|
|
||||||
- id: pending-dedupe-keys
|
|
||||||
content: 新增 pending 与 dedupe Redis key 并接入 is_end=0 累计
|
|
||||||
status: completed
|
|
||||||
- id: win-final-merge
|
|
||||||
content: is_end=1 合并 pending 后调用现有 WinService 结算并清理缓存
|
|
||||||
status: completed
|
|
||||||
- id: regression-check
|
|
||||||
content: 回归验证多次派奖累计、最终结算与幂等行为
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# is_end 结算回退方案(不做中间余额更新)
|
|
||||||
|
|
||||||
## 变更决策
|
|
||||||
取消这部分:
|
|
||||||
- 多次派奖期间(`is_end=0`)对第三方返回余额进行实时更新
|
|
||||||
- 对 `wallet` 查询叠加 pending 金额
|
|
||||||
|
|
||||||
保留并执行:
|
|
||||||
- `is_end=0`:仅做中间派奖累计,不改钱包余额、不做 Lot 收敛
|
|
||||||
- `is_end=1`:合并累计派奖后一次 Final Settlement(调用现有 win 主流程)
|
|
||||||
|
|
||||||
## 实现边界
|
|
||||||
- 严格遵循 [doc/win.md](doc/win.md) 的“Final Settlement 才做真实入账”原则
|
|
||||||
- 不引入展示层余额叠加逻辑,避免展示余额与真实可下注余额不一致
|
|
||||||
|
|
||||||
## 具体改造
|
|
||||||
1. DTO / 校验
|
|
||||||
- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加 `is_end`(默认 `1`)
|
|
||||||
- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php)
|
|
||||||
- 增加 `is_end` 仅允许 `0/1`
|
|
||||||
- `win` 场景强制 `round_id` 必传
|
|
||||||
|
|
||||||
2. Redis 累计与幂等(仅中间派奖)
|
|
||||||
- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php) 增加 key:
|
|
||||||
- `wallet:win:pending:{uid}:{currency}:{round_id}`
|
|
||||||
- `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}`
|
|
||||||
- `is_end=0` 命中 dedupe 则忽略,未命中则累计 pending
|
|
||||||
|
|
||||||
3. win 结算分支
|
|
||||||
- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php)
|
|
||||||
- `is_end=0`:只累计并返回当前真实钱包余额
|
|
||||||
- `is_end=1`:读取 pending 并与本次 `fee` 合并后走现有 `WinService::execute`,成功后清理 pending
|
|
||||||
|
|
||||||
## 验收
|
|
||||||
- 多条 `is_end=0` 不触发真实入账
|
|
||||||
- 一条 `is_end=1` 触发一次最终结算,金额=中间累计+末笔
|
|
||||||
- 中间派奖重复 `biz_id` 不重复累计
|
|
||||||
- Final 失败时 pending 不丢失,可重试
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
---
|
|
||||||
name: PHPDoc Cursor 规则
|
|
||||||
overview: 在 Cursor 全局规则中新增 PHP PHPDoc 严格规范(类、方法、常量全覆盖),与现有 backend-layering 规则并列,并明确适用范围与模板,避免与「不写显而易见注释」的原则冲突。
|
|
||||||
todos:
|
|
||||||
- id: create-php-doc-mdc
|
|
||||||
content: "新建 /Users/ray/.cursor/rules/php-doc.mdc(globs: **/*.php,严格 PHPDoc 正文 + 示例)"
|
|
||||||
status: completed
|
|
||||||
- id: verify-rule-active
|
|
||||||
content: 在 Cursor 中打开任意 .php 文件,确认规则被注入;用 WalletLogic 缺注释方法做一次试写验证
|
|
||||||
status: completed
|
|
||||||
- id: optional-readme-sync
|
|
||||||
content: (可选)将 PHPDoc §3.4 同步到 slot_wallet 等 README,与 Cursor 规则保持一致
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 新增 PHPDoc 严格 Cursor 规则
|
|
||||||
|
|
||||||
## 结论:要加,但不要写成「所有符号一刀切」的空话
|
|
||||||
|
|
||||||
你选择了 **严格全覆盖**。建议在 Cursor 里加一条 **独立规则文件**,与现有的 [`backend-layering.mdc`](/Users/ray/.cursor/rules/backend-layering.mdc) 并列,而不是塞进 layering 里(职责不同:一个管分层,一个管文档)。
|
|
||||||
|
|
||||||
**不建议**写成模糊的「所有都要 PHPDoc」——应写清 **哪些符号、最少哪些 tag、何时可写短句**。否则 Agent 会在 trivial 代码上堆 `@param int $uid uid` 这类无意义注释,与 [`slot_agent/README.md`](/Users/ray/Documents/project/www/slot/slot_agent/README.md) 第 9.3 节「不解释显而易见语句」打架。
|
|
||||||
|
|
||||||
## 现状
|
|
||||||
|
|
||||||
| 来源 | PHPDoc 要求 |
|
|
||||||
|------|-------------|
|
|
||||||
| [`.cursor/rules/`](/Users/ray/.cursor/rules/) | 仅有 layering + dev-environment,**无 PHPDoc** |
|
|
||||||
| [`slot_agent/README.md`](/Users/ray/Documents/project/www/slot/slot_agent/README.md) §6.2 | 已要求:业务类说明、公开方法说明、参数/返回值说明 |
|
|
||||||
| 实际代码(如 [`WalletLogic.php`](/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php)) | **不一致**:`run`/`register` 有块注释,`initWalletWithoutMoney`、`getBalance` 无 |
|
|
||||||
|
|
||||||
终端里曾出现给 `slot_wallet/README.md` 增加 §3.4 PHPDoc 的 diff,但当前 README **尚未落地**该节——规范应优先进 **Cursor rule**(Agent 每次都会读),README 可作为人类文档二次同步(可选)。
|
|
||||||
|
|
||||||
## 推荐规则文件
|
|
||||||
|
|
||||||
**路径**:[`/Users/ray/.cursor/rules/php-doc.mdc`](/Users/ray/.cursor/rules/php-doc.mdc)
|
|
||||||
|
|
||||||
**Frontmatter 建议**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
description: PHP PHPDoc requirements for all classes, methods, and constants
|
|
||||||
globs: "**/*.php"
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
- 用 `globs: **/*.php`:编辑 PHP 时自动注入,不污染 Vue/TS 会话
|
|
||||||
- `alwaysApply: false`:与 layering 的 `true` 区分,减少非 PHP 任务 token
|
|
||||||
|
|
||||||
## 规则正文(严格版,建议写入 mdc)
|
|
||||||
|
|
||||||
### 1. 适用范围
|
|
||||||
|
|
||||||
- **新增或修改**的 PHP 文件中的:**class / interface / trait / enum**、**所有方法**(`public` / `protected` / `private`)、**所有类常量**(`const`)
|
|
||||||
- 适用目录:`slot_*`、`backend/**` 下 PHP 业务代码
|
|
||||||
- **不追溯**改历史未动代码;但 **本次 diff 触及的符号** 若缺 PHPDoc,须一并补齐
|
|
||||||
|
|
||||||
### 2. 最低 PHPDoc 内容
|
|
||||||
|
|
||||||
| 符号 | 必须包含 |
|
|
||||||
|------|----------|
|
|
||||||
| 类 / 接口 / Trait | 一行职责说明;复杂类可加 `@package`(可选) |
|
|
||||||
| 方法 | 职责说明 + 每个参数的 `@param` + `@return`;有 `throw` 的须 `@throws` |
|
|
||||||
| 类常量 | 一行说明业务含义(单位、枚举语义、与配置/表字段对应关系) |
|
|
||||||
| 属性(若新增) | `@var` 或 typed property + 一行说明(仅当类型/语义不直观时) |
|
|
||||||
|
|
||||||
已有 **PHP 8+ 标量/对象类型声明** 时,`@param`/`@return` 仍要保留(与你选的 strict 一致),但 **描述句可短**,禁止空块或只复制类型名。
|
|
||||||
|
|
||||||
### 3. 禁止项(与 slot_agent 注释原则对齐)
|
|
||||||
|
|
||||||
- 禁止无 `@param` / `@return` 的空 `/** */`
|
|
||||||
- 禁止 `@param int $id id` 式同义反复;语义、单位、边界写进描述
|
|
||||||
- 禁止用 PHPDoc 替代 Validate / Logic 里的业务校验说明
|
|
||||||
|
|
||||||
### 4. 分层补充(与 layering 一致)
|
|
||||||
|
|
||||||
对以下层 **额外** 要求写清业务语义(不仅是类型):
|
|
||||||
|
|
||||||
- **Controller**:接口用途、幂等/鉴权前提(若有)
|
|
||||||
- **Logic**:用例步骤、事务边界、失败时行为
|
|
||||||
- **Service**:复用场景、调用方约束
|
|
||||||
- **Model**:查询条件、分表键、金额字段单位
|
|
||||||
- **DTO / Validate**:字段含义、与上游参数映射
|
|
||||||
|
|
||||||
### 5. 示例模板(写入规则供 Agent 照抄)
|
|
||||||
|
|
||||||
```php
|
|
||||||
/**
|
|
||||||
* 首充前冻结免费余额。
|
|
||||||
*
|
|
||||||
* @return WalletEntity|null 成功返回钱包实体;无需冻结时返回 null
|
|
||||||
* @throws WalletException 余额不足或钱包不存在
|
|
||||||
*/
|
|
||||||
public function freeCreditsFreeze(): ?WalletEntity
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
/** 释放档位:单位分,对应配置 free_credits.release_tiers */
|
|
||||||
public const RELEASE_TIER_MIN = 100;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 与工具链的关系(可选,本期可不做的)
|
|
||||||
|
|
||||||
Cursor rule **不能**在 CI 里自动 fail。若以后要机器 enforce,再单独加:
|
|
||||||
|
|
||||||
- PHPStan + `phpstan/phpdoc-parser` 或
|
|
||||||
- PHPCS `Squiz.Commenting` / 自定义 sniff
|
|
||||||
|
|
||||||
本期仅 Cursor 规则即可满足「Agent 写码时遵守」。
|
|
||||||
|
|
||||||
## 实施步骤
|
|
||||||
|
|
||||||
1. 新建 [`php-doc.mdc`](/Users/ray/.cursor/rules/php-doc.mdc),按上文写入 frontmatter + 正文
|
|
||||||
2. 在 Cursor Settings → Rules 确认该规则对 PHP 文件生效(`globs` 匹配)
|
|
||||||
3. (可选)把相同 §3.4 同步进 [`slot_wallet/README.md`](/Users/ray/Documents/project/www/slot/slot_wallet/README.md) 与其它服务 README,供人工 review 对照
|
|
||||||
4. 用一次小改动验证:例如在 `WalletLogic` 给无注释的 `getBalance` 补 PHPDoc,看 Agent 是否自动遵循
|
|
||||||
|
|
||||||
## 风险与预期
|
|
||||||
|
|
||||||
- **Diff 变大**:strict 下每个新方法多 5–15 行注释,属预期成本
|
|
||||||
- **历史债**:全库补 doc 工作量巨大;规则应写明 **仅 touch 到的符号**,避免 Agent 一次性重构整文件
|
|
||||||
- **类型重复**:strict 仍保留 `@param`/`@return` 类型,利于 IDE/静态分析;描述聚焦「为什么/单位/边界」
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph rules [Cursor Rules]
|
|
||||||
layering[backend-layering.mdc]
|
|
||||||
phpdoc[php-doc.mdc]
|
|
||||||
devenv[dev-environment.mdc]
|
|
||||||
end
|
|
||||||
subgraph code [PHP 改动]
|
|
||||||
edit[编辑 PHP 文件]
|
|
||||||
agent[Agent 生成/修改代码]
|
|
||||||
end
|
|
||||||
edit --> phpdoc
|
|
||||||
edit --> layering
|
|
||||||
agent --> phpdoc
|
|
||||||
agent --> layering
|
|
||||||
```
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
name: PHPUnit 钱包链路测试
|
|
||||||
overview: 补齐本仓库的 PHPUnit 基建,并新增 register/bet/win 集成测试用例,默认在本地 php82 容器执行、连接真实依赖,以 HTTP 结果与余额变化作为核心断言。
|
|
||||||
todos:
|
|
||||||
- id: setup-phpunit
|
|
||||||
content: 补齐 PHPUnit 基建(composer require-dev、phpunit.xml、tests/bootstrap.php)
|
|
||||||
status: completed
|
|
||||||
- id: implement-feature-test
|
|
||||||
content: 实现 register/bet/win 集成测试与幂等/异常断言
|
|
||||||
status: completed
|
|
||||||
- id: run-in-container
|
|
||||||
content: 在 php82 容器执行测试并根据结果修正
|
|
||||||
status: completed
|
|
||||||
- id: document-runbook
|
|
||||||
content: 补充测试执行说明与可选增强项
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# PHPUnit Register/Bet/Win 测试落地计划
|
|
||||||
|
|
||||||
## 默认口径(基于当前信息)
|
|
||||||
- 测试框架:PHPUnit(当前仓库尚无 `phpunit.xml` 和 `tests/`)。
|
|
||||||
- 执行环境:本地 `php82` 容器。
|
|
||||||
- 依赖模式:默认连本地真实 MySQL/Redis(贴近联调)。
|
|
||||||
- 断言范围:默认先做 API 返回与余额变化断言;DB 深断言作为第二阶段可选扩展。
|
|
||||||
|
|
||||||
## 现状结论
|
|
||||||
- 缺少 PHPUnit 基建:未发现 `phpunit*.xml` 与 `tests/` 目录。
|
|
||||||
- `register/bet/win` 入口与逻辑已具备:
|
|
||||||
- 接口入口:[app/api/controller/WalletController.php](app/api/controller/WalletController.php)
|
|
||||||
- 业务编排:[app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php)
|
|
||||||
- 注册服务:[app/service/wallet/RegisterService.php](app/service/wallet/RegisterService.php)
|
|
||||||
|
|
||||||
## 实施步骤
|
|
||||||
1. **补测试基建**
|
|
||||||
- 在 `require-dev` 增加 `phpunit/phpunit`。
|
|
||||||
- 新增 `phpunit.xml`(定义 `tests/` 目录、bootstrap、环境变量覆盖)。
|
|
||||||
- 新增 `tests/bootstrap.php`(加载自动加载与测试环境初始化)。
|
|
||||||
|
|
||||||
2. **新增钱包链路集成测试**
|
|
||||||
- 新建测试文件:[tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php)
|
|
||||||
- 用例覆盖:
|
|
||||||
- `register` 成功(通过 `wallet/update` + `type=register`)
|
|
||||||
- `bet` 成功并校验扣款后余额变化
|
|
||||||
- `win is_end=0` 中间派奖仅影响待结算展示
|
|
||||||
- `win is_end=1` 最终结算落账
|
|
||||||
- `bet` 重复 `biz_id` 幂等
|
|
||||||
- `win` 重复 `biz_id` 幂等
|
|
||||||
- `bet/win` 缺 `round_id` 参数错误
|
|
||||||
- `win` 非法 `is_end` 参数错误
|
|
||||||
|
|
||||||
3. **测试数据与可重复执行设计**
|
|
||||||
- 每个测试生成独立 `uid/round_id/biz_id/trace_id`(时间戳+随机后缀),避免脏数据冲突。
|
|
||||||
- 用例内封装统一请求方法(POST JSON)与响应断言助手,降低重复代码。
|
|
||||||
- 优先按顺序单线程执行此特性测试,避免并发串扰。
|
|
||||||
|
|
||||||
4. **执行与验证**
|
|
||||||
- 在容器内执行:`docker compose exec -T php82 php vendor/bin/phpunit --filter WalletRegisterBetWinTest`
|
|
||||||
- 验证输出:
|
|
||||||
- 所有用例通过
|
|
||||||
- 幂等场景无重复记账(通过返回余额不重复变化来断言)
|
|
||||||
|
|
||||||
5. **第二阶段可选增强(不在首版强制)**
|
|
||||||
- 增加 DB 断言(`wallet_log`、`wallet_fund_lot`、`wallet_bet_funding`)与 Redis pending key 清理断言。
|
|
||||||
- 将该测试纳入 CI job(需环境可用性与测试隔离策略先达成一致)。
|
|
||||||
|
|
||||||
## 目标结构图
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
testBootstrap[phpunitBootstrap] --> featureTest[WalletRegisterBetWinTest]
|
|
||||||
featureTest --> registerStep[register_update]
|
|
||||||
registerStep --> betStep[bet]
|
|
||||||
betStep --> winMidStep[win_is_end_0]
|
|
||||||
winMidStep --> winFinalStep[win_is_end_1]
|
|
||||||
winFinalStep --> idemCheck[idempotencyChecks]
|
|
||||||
idemCheck --> negativeCheck[invalidParamsChecks]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 受影响文件(计划新增/修改)
|
|
||||||
- [composer.json](composer.json)
|
|
||||||
- [phpunit.xml](phpunit.xml)
|
|
||||||
- [tests/bootstrap.php](tests/bootstrap.php)
|
|
||||||
- [tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: PHPUnit 集成测试落地
|
|
||||||
overview: 补齐本仓库 PHPUnit 基建,并新增 register/bet/win 集成测试(连接真实 MySQL/Redis)与关键 DB 断言,支持在本地 php82 容器执行。
|
|
||||||
todos: []
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Register/Bet/Win PHPUnit 实施计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
- 新增可
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
name: SDK taskProgress 封装
|
|
||||||
overview: 将 slot_sdk(或你们的 SDK 仓库)加入工作区后,可按现有 Wallet Client 模式直接实现 `player-task/task-progress` 的调用封装;wallet 侧接口文档已就绪,可作为契约来源。
|
|
||||||
todos:
|
|
||||||
- id: add-sdk-workspace
|
|
||||||
content: 用户将 slot_sdk 加入 Cursor 工作区并告知仓库路径
|
|
||||||
status: completed
|
|
||||||
- id: explore-sdk-patterns
|
|
||||||
content: 阅读 SDK 现有 Wallet Client / HTTP 封装与错误处理约定
|
|
||||||
status: completed
|
|
||||||
- id: implement-client
|
|
||||||
content: 新增 taskProgress 方法、路径常量、请求/响应类型(对齐 player-task-progress-api.md)
|
|
||||||
status: completed
|
|
||||||
- id: add-tests-or-example
|
|
||||||
content: 按 SDK 惯例补单测或调用示例(若项目有测试目录)
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# SDK 工作区加入后的直接开发方案
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
**可以。** 当前工作区只有 [slot-wallet](file:///Users/ray/Documents/project/www/ray/slot-wallet),已具备接口契约文档 [doc/player-task-progress-api.md](doc/player-task-progress-api.md) 与实现对照(`PlayerTaskController`、`PlayerTaskQueryService` 等)。把 **SDK 仓库** 作为第二个根目录(或 monorepo 子目录)加入工作区后,我可以:
|
|
||||||
|
|
||||||
1. 阅读 SDK 里现有 Wallet/HTTP Client 的命名、基类、错误处理、DTO 约定;
|
|
||||||
2. 新增 `taskProgress`(或团队统一命名)方法、路径常量、请求/响应类型;
|
|
||||||
3. 若有单测/示例,补一条调用示例或 Feature 测试;
|
|
||||||
4. 保证字段 **snake_case** 与 HTTP JSON 一致,成功判定 `code === 0`。
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph workspace [Cursor Workspace]
|
|
||||||
Wallet[slot-wallet]
|
|
||||||
SDK[slot_sdk]
|
|
||||||
end
|
|
||||||
Doc[player-task-progress-api.md]
|
|
||||||
Wallet --> Doc
|
|
||||||
SDK -->|reads patterns| SDK
|
|
||||||
Doc -->|contract| SDK
|
|
||||||
SDK -->|POST player-task/task-progress| Wallet
|
|
||||||
```
|
|
||||||
|
|
||||||
## 你需要做的准备
|
|
||||||
|
|
||||||
| 项 | 说明 |
|
|
||||||
|----|------|
|
|
||||||
| 加入工作区 | Cursor:**File → Add Folder to Workspace**,选中 SDK 仓库根目录 |
|
|
||||||
| 告知路径 | 例如 `company/ray/slots/slot_sdk`(与你们实际目录一致即可) |
|
|
||||||
| 语言确认 | 若是 PHP `slot_sdk`、Go、TS 等,我会跟现有 Client 语言一致,不另起一套风格 |
|
|
||||||
|
|
||||||
无需改 wallet 代码即可开始 SDK 开发;wallet 接口已实现完毕。
|
|
||||||
|
|
||||||
## 我会按什么写(预期产出)
|
|
||||||
|
|
||||||
以 SDK 现有 Wallet Client 为模板(具体类名需打开 SDK 后确认),典型改动:
|
|
||||||
|
|
||||||
- **路径常量**:`player-task/task-progress`
|
|
||||||
- **请求**:`uid`, `currency`(可选统一附带 `trace_id` 若其他接口都有)
|
|
||||||
- **响应模型**:`summary` + `bonus_tasks[]` + `deposit_tasks[]`,字段与 [doc/player-task-progress-api.md](doc/player-task-progress-api.md) §5 一致
|
|
||||||
- **错误**:复用 SDK 已有 `WalletApiError` / `code !== 0` 处理
|
|
||||||
|
|
||||||
## 跨仓库发布(团队规范)
|
|
||||||
|
|
||||||
按 [`.cursor/rules/slot-wallet-layers-and-delivery.mdc`](file:///Users/ray/Documents/project/www/ray/slot-wallet/.cursor/rules/slot-wallet-layers-and-delivery.mdc) §8–9:
|
|
||||||
|
|
||||||
1. **先在 SDK 仓库** `commit` → `push`;
|
|
||||||
2. 消费方(如 slot-wallet 若通过 composer 依赖 SDK)再 `composer update` 并锁 `composer.lock`。
|
|
||||||
|
|
||||||
当前 [composer.json](file:///Users/ray/Documents/project/www/ray/slot-wallet/composer.json) **尚未**声明 `slot_sdk` 依赖,说明 SDK 可能独立发布或由其他服务引用——这不影响我在 SDK 仓库内直接编码。
|
|
||||||
|
|
||||||
## 建议的确认项(加入工作区后第一条消息说明即可)
|
|
||||||
|
|
||||||
1. SDK 仓库在本机的**绝对路径**或文件夹名;
|
|
||||||
2. 方法命名偏好:`taskProgress` / `getPlayerTaskProgress` / 与现有 `wallet()` 等方法对齐;
|
|
||||||
3. 是否需要 **foundation 常量**(如 `source_type`)进 SDK,还是仅透传 int。
|
|
||||||
|
|
||||||
## 不在本次默认范围
|
|
||||||
|
|
||||||
- 修改 wallet 服务端实现(已满足 PRD §26.1);
|
|
||||||
- 自动 `composer update` 到其他服务(除非你明确要求并给出目标仓库)。
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: slot workspace
|
|
||||||
overview: 在 `/Users/ray/Documents/project/www/slot` 创建一个 Cursor/VS Code workspace 文件,并把该目录下一级项目文件夹加入 workspace。默认排除隐藏目录 `.vscode`,不递归加入嵌套子目录。
|
|
||||||
todos: []
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 创建 Slot Workspace
|
|
||||||
|
|
||||||
将创建 `[slot.code-workspace](/Users/ray/Documents/project/www/slot/slot.code-workspace)`,内容使用标准 VS Code/Cursor workspace JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{ "path": "backend" },
|
|
||||||
{ "path": "monitor" }
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
---
|
|
||||||
name: status 接口字段精简
|
|
||||||
overview: 对照需求文档精简 C 端 status/claim 响应字段,并将所有金额由千分位(qf)转为大单位(与全站 getNumberFormat 一致);innerapi 仍返回 qf 全量。
|
|
||||||
todos:
|
|
||||||
- id: package-list-for-client
|
|
||||||
content: FreeCreditsPackageModel::listForClient() 返回精简字段,amount 经 getNumberFormat 转大单位
|
|
||||||
status: completed
|
|
||||||
- id: slim-build-status
|
|
||||||
content: buildStatus 去掉 activity_id;顶层金额与 packages 均转大单位
|
|
||||||
status: completed
|
|
||||||
- id: format-first-cashout-amount
|
|
||||||
content: firstCashout 响应 data.amount 同步转大单位(Pay 入参仍用 qf)
|
|
||||||
status: completed
|
|
||||||
- id: update-controller-phpdoc
|
|
||||||
content: 更新 FreeCreditsController PHPDoc:金额为展示单位 float,非千分位
|
|
||||||
status: completed
|
|
||||||
- id: client-status-test
|
|
||||||
content: 单测断言字段白名单及 78500 qf → 78.5 等大单位转换
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Free Credits status 接口字段精简
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
**可以且应该精简。** 当前 [`buildStatus()`](slot_console/app/api/logic/FreeCreditsLogic.php) 直接 `toArray()` 透出库表列,超出需求文档中 C 端 UI 所需信息;[`activity_id`](slot_console/app/api/logic/FreeCreditsLogic.php) 仅用于后台/内部关联,C 端无展示或交互用途。
|
|
||||||
|
|
||||||
你已确认:
|
|
||||||
|
|
||||||
- **只做删减**,不新增 `total_deposit`(累计充值进度由 C 端从钱包侧获取)。
|
|
||||||
- **金额转大单位**:C 端接口不再返回千分位整数,统一转为展示金额(与 [`AgentController::formatAmountToFloat`](slot_console/app/api/controller/AgentController.php)、[`CommonFn::getNumberFormat`](slot_lib/src/common/CommonFn.php) 一致,默认 `moneyFormat=1000`、`moneyDot=2`,如 qf `78500` → `78.5`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 需求文档 vs 当前响应
|
|
||||||
|
|
||||||
需求文档([首充前免费余额定格与分档释放需求文档.md](docs/requirements/首充前免费余额定格与分档释放需求文档.md))描述的是 **UI 行为**,未定义 JSON 契约,但可反推 C 端必需数据:
|
|
||||||
|
|
||||||
| UI 场景(文档章节) | C 端需要的数据 |
|
|
||||||
| --- | --- |
|
|
||||||
| 是否展示活动(`status === -1` 隐藏) | `status` |
|
|
||||||
| 首页 Withdraw 是否曾达赢取门槛(§5.2、§7) | `home_withdraw_unlocked` |
|
|
||||||
| 池总额 / 入口副标题「$78.50 pending」(§6.3–6.6、§9.3、§14.2) | `frozen_amount` |
|
|
||||||
| 问号弹窗 / 第一档金额(§8、§10) | `first_cash_amount`、`recharge_unlock_amount` |
|
|
||||||
| 档位列表 Withdraw / Unlock / Claim / Claimed(§9.3、§10–12) | `packages[]`:`id`、`package_type`、`amount`、`status` |
|
|
||||||
| 发起 claim / 第一档提现(现有 API) | `packages[].id` → POST `package_id` |
|
|
||||||
|
|
||||||
**当前多出的字段:**
|
|
||||||
|
|
||||||
- 顶层:`activity_id`(`recharge_gift_config.id`,仅 innerapi/编排用)
|
|
||||||
- `packages[]` 全表列:`player_id`、`activity_id`、`uid`、`withdraw_order_id`、`claim_biz_id`、`unlocked_time`、`claimed_time`、`completed_time`、`create_time`、`update_time`
|
|
||||||
- `package_no` 可保留(便于调试与稳定排序展示),也可仅靠数组顺序;建议 **保留**(成本低、与 DB 序号一致)
|
|
||||||
|
|
||||||
**命名与单位:** packages 字段由 `amount_qf` 改为 **`amount`**;顶层与各档 **`amount` 均为大单位 float**(非 qf),C 端可直接用于 `$78.50` 类文案,无需再 `/1000`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目标响应契约(C 端)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": 4,
|
|
||||||
"home_withdraw_unlocked": 1,
|
|
||||||
"frozen_amount": 78.5,
|
|
||||||
"first_cash_amount": 20,
|
|
||||||
"recharge_unlock_amount": 50,
|
|
||||||
"packages": [
|
|
||||||
{
|
|
||||||
"id": 101,
|
|
||||||
"package_no": 1,
|
|
||||||
"package_type": 1,
|
|
||||||
"amount": 20,
|
|
||||||
"status": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`status === -1` 时仍仅 `{ "status": -1 }`(与现逻辑一致)。
|
|
||||||
|
|
||||||
**转换规则(实现):**
|
|
||||||
|
|
||||||
```php
|
|
||||||
// 与全站 C 端金额一致;qf 为库表 / 内部逻辑整数
|
|
||||||
private function formatClientAmount(int $amountQf): float
|
|
||||||
{
|
|
||||||
return (float) CommonFn::getNumberFormat($amountQf);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 应用于:`frozen_amount`、`first_cash_amount`、`recharge_unlock_amount`、`packages[].amount`。
|
|
||||||
- **`firstCashout` 成功响应** `data.amount` 同样转大单位;调用 Pay / 钱包时仍传 qf,仅 JSON 对外转换。
|
|
||||||
- **innerapi / 单测写库** 仍使用 `_qf` 整数,不在 DB 层改单位。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现方案
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
statusApi[status_claim_firstCashout]
|
|
||||||
buildStatus[buildStatus]
|
|
||||||
clientDto[toClientStatusDto]
|
|
||||||
modelFull[listByPlayerId_toArray]
|
|
||||||
innerapi[innerapi_list]
|
|
||||||
statusApi --> buildStatus --> clientDto
|
|
||||||
buildStatus --> modelFull
|
|
||||||
innerapi --> modelFull
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **Logic 层统一转换**([`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php))
|
|
||||||
- 新增 `formatClientAmount(int $amountQf): float`(封装 `CommonFn::getNumberFormat`)。
|
|
||||||
- 供 `buildStatus`、`listForClient`(或 Model 回调)、`firstCashout` 响应共用。
|
|
||||||
|
|
||||||
2. **Model 层 C 端 packages**([`FreeCreditsPackageModel`](slot_console/app/model/common/FreeCreditsPackageModel.php))
|
|
||||||
- 新增 `listForClient(int $playerId, callable $formatAmount): array` 或 Logic 内 `array_map`:只输出 `id`、`package_no`、`package_type`、`amount`(大单位)、`status`。
|
|
||||||
- 保留 `listByPlayerId()` 供 innerapi、dev、集成测试。
|
|
||||||
|
|
||||||
3. **收口 DTO**(`buildStatus`)
|
|
||||||
- 去掉 `activity_id`。
|
|
||||||
- 顶层三金额字段经 `formatClientAmount`;`packages` 用 `listForClient`。
|
|
||||||
- `status()`、`claim()` 经 `buildStatus` 返回,结构一致。
|
|
||||||
|
|
||||||
4. **firstCashout 响应**
|
|
||||||
- `return ['order_id' => ..., 'amount' => $this->formatClientAmount($package->amount_qf)]`。
|
|
||||||
|
|
||||||
5. **更新文档**([`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) PHPDoc)
|
|
||||||
- 删除「千分位整数、展示时除以 1000」描述;改为「金额为大单位 float,精度见 `moneyDot`」。
|
|
||||||
|
|
||||||
6. **单测**
|
|
||||||
- 新增 `FreeCreditsClientStatusTest`:字段白名单 + `78500` qf → `78.5` 转换断言。
|
|
||||||
|
|
||||||
7. **不改动**
|
|
||||||
- [`innerapi/controller/FreeCreditsController`](slot_console/app/innerapi/controller/FreeCreditsController.php) 仍返回全量 `toArray()`。
|
|
||||||
- `FreeCreditsFreezeDev` 等内部工具继续用 `listByPlayerId`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 字段对照表(精简前后)
|
|
||||||
|
|
||||||
| 字段 | 精简前 | 精简后 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `activity_id` | 有 | **删** | C 端不需要 |
|
|
||||||
| `status` | 有 | 有 | 主状态机 |
|
|
||||||
| `home_withdraw_unlocked` | 有 | 有 | §5.2 |
|
|
||||||
| `frozen_amount` | qf 整数 | **float 大单位** | 如 78.5 |
|
|
||||||
| `first_cash_amount` | qf 整数 | **float 大单位** | 如 20 |
|
|
||||||
| `recharge_unlock_amount` | qf 整数 | **float 大单位** | 如 50 |
|
|
||||||
| `packages[].id` | 有 | 有 | claim/cashout |
|
|
||||||
| `packages[].package_no` | 有 | 有 | 序号展示 |
|
|
||||||
| `packages[].package_type` | 有 | 有 | 1=提现档 2=领取档 |
|
|
||||||
| `packages[].amount` | `amount_qf`(qf) | `amount`(**float 大单位**) | 重命名 + 转换 |
|
|
||||||
| `firstCashout.data.amount` | qf | **float 大单位** | 与 status 一致 |
|
|
||||||
| `packages[].status` | 有 | 有 | UI 状态 |
|
|
||||||
| `packages[].player_id/uid/...` | 有 | **删** | 内部字段 |
|
|
||||||
| `packages[].withdraw_order_id` 等 | 有 | **删** | 轮询靠主 `status` + package `status` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 风险与协调
|
|
||||||
|
|
||||||
- **Breaking change**:C 端需改为直接使用大单位金额(不再 `/1000`);删除 `activity_id`、`amount_qf` 及 packages 审计字段。
|
|
||||||
- **精度**:与 `ShareConfigService::moneyDot` / `moneyFormat` 绑定,勿手写 `/1000`,避免与 VIP/钱包接口不一致。
|
|
||||||
- **累计充值进度**:不纳入本次接口;C 端继续从钱包统计接口取(钱包侧金额亦通常为大单位)。
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
name: status 返回 help
|
|
||||||
overview: 在 Free Credits C 端 status 接口(及 claim 同源 buildStatus)中增加 help 字段,读取活动配置表 recharge_gift_config.help,供问号说明弹窗等展示;status=-1 仍不返回 help。
|
|
||||||
todos:
|
|
||||||
- id: build-status-help
|
|
||||||
content: FreeCreditsLogic::buildStatus 增加 help 字段(读 config->help)
|
|
||||||
status: completed
|
|
||||||
- id: update-phpdoc
|
|
||||||
content: FreeCreditsController::status PHPDoc 补充 help 说明
|
|
||||||
status: completed
|
|
||||||
- id: unit-test-help
|
|
||||||
content: FreeCreditsClientStatusTest 白名单与透传断言
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# status 接口增加 help 返回
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
`GET /api/free-credits/status`(以及 `claim` 成功后的同源响应)在 `status !== -1` 时增加 **`help`(string)**,内容来自运营在后台活动编辑里填写的 **「帮助说明」**([`recharge_gift_config.help`](slot_console/app/model/common/RechargeGiftConfigModel.php))。
|
|
||||||
|
|
||||||
`status === -1` 时保持仅 `{ "status": -1 }`,不附带 help。
|
|
||||||
|
|
||||||
## 改动点
|
|
||||||
|
|
||||||
### 1. [`FreeCreditsLogic::buildStatus()`](slot_console/app/api/logic/FreeCreditsLogic.php)
|
|
||||||
|
|
||||||
在现有返回数组中增加:
|
|
||||||
|
|
||||||
```php
|
|
||||||
'help' => is_null($config) ? '' : strval($config->help ?? ''),
|
|
||||||
```
|
|
||||||
|
|
||||||
- `buildStatus` 已接收 `$config`(`activeConfig` 查出的 type=11 活动行),无需额外查库。
|
|
||||||
- 不做 §8 模板占位符替换(轻量方案);C 端可用已有 `frozen_amount` / `recharge_unlock_amount` / `first_cash_amount` 自行替换,或原样展示运营配置的富文本/多行文案。
|
|
||||||
|
|
||||||
### 2. [`FreeCreditsController`](slot_console/app/api/controller/FreeCreditsController.php) PHPDoc
|
|
||||||
|
|
||||||
在 `status` 方法成功字段列表中补充:
|
|
||||||
|
|
||||||
- `help` (string) 活动规则说明,来自后台「帮助说明」
|
|
||||||
|
|
||||||
### 3. 单测 [`FreeCreditsClientStatusTest`](slot_console/tests/Unit/FreeCreditsClientStatusTest.php)
|
|
||||||
|
|
||||||
- `PLAYER_TOP_KEYS` 增加 `help`
|
|
||||||
- 用例:`FreeCreditsConfigStub` 设置 `help` 属性,`assertSame` 透传
|
|
||||||
|
|
||||||
## 数据流
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
admin[slot_admin 活动编辑 help 文本框]
|
|
||||||
db[(recharge_gift_config.help)]
|
|
||||||
logic[buildStatus]
|
|
||||||
api["/api/free-credits/status"]
|
|
||||||
admin --> db --> logic --> api
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验收
|
|
||||||
|
|
||||||
- 后台 type=11 活动填写「帮助说明」并保存后,已参加用户 `status` 响应含相同 `help` 字符串
|
|
||||||
- `status=-1` 响应无 `help` 字段
|
|
||||||
- 单元测试通过;`claim` 返回结构同步含 `help`(复用 `buildStatus`)
|
|
||||||
|
|
||||||
## 不在本次范围
|
|
||||||
|
|
||||||
- `ext_config` 独立说明模板、动态金额替换服务端拼装
|
|
||||||
- `banner_image` 透出(如需可另开)
|
|
||||||
- 前端问号弹窗 UI
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
name: status 配置与未充值
|
|
||||||
overview: 修复 slot_console FreeCreditsLogic::status:无玩家池时返回 status=0 及活动配置;C 端新增 win_threshold(不含 initial_amount、home_withdraw_unlocked)。Logic 已按需求变更实现,待对齐单测与 PHPDoc。
|
|
||||||
todos:
|
|
||||||
- id: logic-pre-enrollment
|
|
||||||
content: FreeCreditsLogic:buildPreEnrollmentStatus;status() 无 player 时返回配置态(已完成)
|
|
||||||
status: completed
|
|
||||||
- id: logic-build-status-fields
|
|
||||||
content: buildStatus / buildConfigClientFields 仅透出 win_threshold(不含 initial_amount、home_withdraw_unlocked)(已完成)
|
|
||||||
status: completed
|
|
||||||
- id: align-phpdoc
|
|
||||||
content: FreeCreditsController PHPDoc 与 buildConfigClientFields 注释:删除 initial_amount、home_withdraw_unlocked;移除无用 DEFAULT_INITIAL_AMOUNT
|
|
||||||
status: completed
|
|
||||||
- id: align-unit-tests
|
|
||||||
content: FreeCreditsClientStatusTest / FreeCreditsEligibilityTest 白名单与断言与现实现一致
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Free Credits status 接口修复计划(需求变更版)
|
|
||||||
|
|
||||||
## 需求确认(2026-05-21)
|
|
||||||
|
|
||||||
**C 端 `GET /api/free-credits/status` 约定:**
|
|
||||||
|
|
||||||
- **包含**:`win_threshold`(来自 `ext_config.win_threshold_qf`)
|
|
||||||
- **不包含**:`initial_amount`、`home_withdraw_unlocked`
|
|
||||||
- 注册赠送金额:仍由 `game_base.user_register_reward` 等其它接口提供
|
|
||||||
- 首页 Withdraw 解锁:C 端用 `status`(如 `STATUS_HOME_UNLOCKED=1`)或钱包余额 + `win_threshold` 自行判断;库表 `home_withdraw_unlocked` 仍由 `syncHomeWithdrawUnlocked` 维护,仅不下发 API
|
|
||||||
|
|
||||||
后台 type=11 的 `initial_amount_qf` 配置保留(运营后台用),**不**经 status 透出。
|
|
||||||
|
|
||||||
## 问题根因
|
|
||||||
|
|
||||||
[`FreeCreditsLogic::status()`](slot_console/app/api/logic/FreeCreditsLogic.php) 原逻辑在无 `free_credits_player` 时返回 `{ status: -1 }`,导致首充前 C 端无法展示活动(`status !== -1` 为展示开关)。
|
|
||||||
|
|
||||||
## 目标行为
|
|
||||||
|
|
||||||
| 场景 | status | 返回顶层字段 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 无资格 / 无 type=11 配置 | `-1` | 仅 `status` |
|
|
||||||
| 有资格 + 有配置 + **无 player** | `0` | `status`, `frozen_amount`, `first_cash_amount`, `win_threshold`, `recharge_unlock_amount`, `help`, `packages`(空数组) |
|
|
||||||
| 有资格 + 有配置 + **有 player** | 玩家真实值 | 同上 + 玩家 `frozen_amount` / `first_cash_amount` / `packages` |
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
statusReq[GET status] --> eligible{资格+活动配置?}
|
|
||||||
eligible -->|否| notVisible["status=-1"]
|
|
||||||
eligible -->|是| player{有 player 行?}
|
|
||||||
player -->|无| preConfig["status=0 + win_threshold 等"]
|
|
||||||
player -->|有| fullStatus["buildStatus 玩家态"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 已实现 Logic(当前代码,无需再改字段集)
|
|
||||||
|
|
||||||
[`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php) 已与上表一致:
|
|
||||||
|
|
||||||
- `status()`:无 player → `buildPreEnrollmentStatus($config)`
|
|
||||||
- `buildConfigClientFields()`:仅 `win_threshold`、`recharge_unlock_amount`、`help`
|
|
||||||
- `buildStatus()` / `buildPreEnrollmentStatus()`:**不**返回 `initial_amount`、`home_withdraw_unlocked`
|
|
||||||
|
|
||||||
## 待办:对齐文档与单测
|
|
||||||
|
|
||||||
### 1. PHPDoc — [`FreeCreditsController.php`](slot_console/app/api/controller/FreeCreditsController.php)
|
|
||||||
|
|
||||||
- 删除 `initial_amount`、`home_withdraw_unlocked` 字段说明
|
|
||||||
- 明确 `status=0` 时仍返回 `win_threshold`、`recharge_unlock_amount`、`help`
|
|
||||||
|
|
||||||
### 2. 清理 Logic 注释/死代码 — [`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php)
|
|
||||||
|
|
||||||
- 删除未使用的 `DEFAULT_INITIAL_AMOUNT` 常量(若仍保留)
|
|
||||||
- `buildConfigClientFields` 的 `@return` 改为仅含 `win_threshold`、`recharge_unlock_amount`、`help`
|
|
||||||
|
|
||||||
### 3. 单测
|
|
||||||
|
|
||||||
| 文件 | 改动 |
|
|
||||||
| --- | --- |
|
|
||||||
| [`FreeCreditsClientStatusTest.php`](slot_console/tests/Unit/FreeCreditsClientStatusTest.php) | `PLAYER_TOP_KEYS` 改为:`status`, `frozen_amount`, `first_cash_amount`, `win_threshold`, `recharge_unlock_amount`, `help`, `packages`;移除 `initial_amount`、`home_withdraw_unlocked` 断言 |
|
|
||||||
| [`FreeCreditsEligibilityTest.php`](slot_console/tests/Unit/FreeCreditsEligibilityTest.php) | `testStatusReturnsPreEnrollmentConfigWhenNoPlayer`:断言 `win_threshold=50`,**不**断言 `initial_amount` / `home_withdraw_unlocked` |
|
|
||||||
|
|
||||||
运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -w /app/www/slot/slot_console php82 vendor/bin/phpunit tests/Unit/FreeCreditsClientStatusTest.php tests/Unit/FreeCreditsEligibilityTest.php
|
|
||||||
```
|
|
||||||
|
|
||||||
## C 端约定(供联调)
|
|
||||||
|
|
||||||
- **展示开关**:`data.status !== -1`;首充前为 `status === 0`
|
|
||||||
- **进度条上限**:`win_threshold`
|
|
||||||
- **注册赠送**:**不要**从 status 取;走注册/大厅配置接口
|
|
||||||
- **Withdraw 解锁**:**不要**依赖 `home_withdraw_unlocked` 字段;用 `status` 或余额逻辑
|
|
||||||
|
|
||||||
## 不在本次范围
|
|
||||||
|
|
||||||
- status 返回 `initial_amount` / `home_withdraw_unlocked`
|
|
||||||
- 注册发奖改读 `ext_config.initial_amount_qf`
|
|
||||||
- slot_pwa / gateway 前端改动
|
|
||||||
- backend 后台 `initial_amount_qf` 编辑能力(已存在)
|
|
||||||
|
|
||||||
## 验收清单
|
|
||||||
|
|
||||||
1. 无 player:`status=0`,含 `win_threshold`、`recharge_unlock_amount`、`help`,`packages=[]`,**无** `initial_amount`、`home_withdraw_unlocked` 键
|
|
||||||
2. 有 player:含玩家态金额 + 同上配置字段
|
|
||||||
3. 无资格/无配置:`status=-1`
|
|
||||||
4. 单测全部通过
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
---
|
|
||||||
name: type11 活动初始金额
|
|
||||||
overview: 在活动类型 11(Free Credits)后台编辑表单中新增「活动初始金额」字段,以美元小数录入、落库为 ext_config.initial_amount_qf;同步补齐 slot_admin 校验与列表展示。数据经现有 ActivityService 透传至 slot_console,本任务不改 slot_console 业务逻辑。
|
|
||||||
todos:
|
|
||||||
- id: edit-vue-initial-amount
|
|
||||||
content: edit.vue:type=11 表单项 + setFormData/submit 的 initial_amount ↔ initial_amount_qf 转换
|
|
||||||
status: completed
|
|
||||||
- id: index-vue-display
|
|
||||||
content: index.vue:type=11 列表 ext_config 展示活动初始金额
|
|
||||||
status: completed
|
|
||||||
- id: validate-initial-amount
|
|
||||||
content: ActivityValidate::checkFreeCreditsExt 增加 initial_amount_qf 必填与非负校验
|
|
||||||
status: completed
|
|
||||||
- id: manual-verify
|
|
||||||
content: 本地验证新建/编辑/回填/必填/列表展示
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 活动类型 11 增加「活动初始金额」后台配置
|
|
||||||
|
|
||||||
## 背景与范围
|
|
||||||
|
|
||||||
- 需求来源:[首充前免费余额定格与分档释放需求文档.md](docs/requirements/首充前免费余额定格与分档释放需求文档.md) 第 19 节将注册赠送调整为 `$30`;运营需在 **活动类型 11** 的配置里维护「用户注册时的初始金额」。
|
|
||||||
- **已确认键名**:`initial_amount`(表单) / `initial_amount_qf`(落库,千分位整数)。
|
|
||||||
- **范围**:仅 [backend/slot_admin](backend/slot_admin) 与 [backend/slot_admin_vue](backend/slot_admin_vue);`ActivityController` 已透传 `ext_config` 至 `slot_console`,无需改 `slot_console` / `slot_lib`(新字段会原样写入 `s_recharge_gift_config.ext_config` JSON)。
|
|
||||||
|
|
||||||
## 现状(可复用)
|
|
||||||
|
|
||||||
type=11 专属编辑已在 [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) 实现 6 个金额字段 + Banner,模式为:
|
|
||||||
|
|
||||||
- 编辑:`setFormData` 将 `*_qf ÷ 1000` 还原为美元小数
|
|
||||||
- 提交:`submit` 将美元小数 `× 1000` 写入 `*_qf`
|
|
||||||
- 校验:[ActivityValidate::checkFreeCreditsExt](backend/slot_admin/app/game/validate/ActivityValidate.php) 在 [ActivityController](backend/slot_admin/app/game/controller/ActivityController.php) `save` / `update(updateData)` 时触发
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
editVue["edit.vue submit"] --> adminApi["slot_admin ActivityController"]
|
|
||||||
adminApi --> activitySvc["slotLib ActivityService"]
|
|
||||||
activitySvc --> consoleInner["slot_console innerapi/activity"]
|
|
||||||
consoleInner --> extConfig["ext_config JSON"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 字段定义
|
|
||||||
|
|
||||||
| UI 标签 | 表单字段 | 落库字段 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 活动初始金额($) | `ext_config.initial_amount` | `initial_amount_qf` | 用户注册时赠送的免费余额金额;placeholder 建议 `30`(对齐需求文档 $30) |
|
|
||||||
|
|
||||||
金额处理与现有 6 项一致:`Math.round(Number(v) * 1000)`,回填优先读 `_qf`。
|
|
||||||
|
|
||||||
## 改动清单
|
|
||||||
|
|
||||||
### 1. 编辑表单 — [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue)
|
|
||||||
|
|
||||||
在 `v-if="formData.type == 11"` 区块 **最上方** 增加表单项(置于「首笔赢取门槛」之前):
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<a-form-item
|
|
||||||
label="活动初始金额($)"
|
|
||||||
help="用户注册时赠送的免费余额金额"
|
|
||||||
:rules="[{ required: true, message: '必填' }]">
|
|
||||||
<a-input-number v-model="formData.ext_config.initial_amount" placeholder="如 30" :min="0"/>
|
|
||||||
</a-form-item>
|
|
||||||
```
|
|
||||||
|
|
||||||
**`setFormData`(type===11 分支)** 增加映射:
|
|
||||||
|
|
||||||
```js
|
|
||||||
initial_amount: e.initial_amount_qf != null ? e.initial_amount_qf / 1000 : e.initial_amount,
|
|
||||||
```
|
|
||||||
|
|
||||||
**`submit`(type===11 分支)** 在 `data.ext_config` 中增加:
|
|
||||||
|
|
||||||
```js
|
|
||||||
initial_amount_qf: toQf(e.initial_amount),
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 列表展示(可选但建议)— [index.vue](backend/slot_admin_vue/src/views/game/activity/index.vue)
|
|
||||||
|
|
||||||
在 type===11 的 `ext_config` 模板中增加一行,复用已有 `formatExtAmount`:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<div>活动初始金额: ${{ formatExtAmount(record.ext_config.initial_amount_qf, record.ext_config.initial_amount) }}</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 后端校验 — [ActivityValidate.php](backend/slot_admin/app/game/validate/ActivityValidate.php)
|
|
||||||
|
|
||||||
在 `checkFreeCreditsExt()` 的 `$required` 数组追加:
|
|
||||||
|
|
||||||
```php
|
|
||||||
'initial_amount_qf' => '活动初始金额',
|
|
||||||
```
|
|
||||||
|
|
||||||
校验规则与现有金额字段一致:必填、非负整数(千分位)。若业务要求注册赠送必须大于 0,可将条件改为 `(int)$extConfig[$key] <= 0` 时报错(与运营确认;默认可先仅要求 `>= 0`,与「首笔赢取门槛」等一致)。
|
|
||||||
|
|
||||||
更新方法 PHPDoc:由「6 个金额字段」改为「7 个金额相关字段」。
|
|
||||||
|
|
||||||
### 4. 无需改动的文件
|
|
||||||
|
|
||||||
- [ActivityController.php](backend/slot_admin/app/game/controller/ActivityController.php):已按 type===11 调用 `checkFreeCreditsExt`,无需新增分支。
|
|
||||||
- `slot_console` / `slot_lib`:本任务只落库配置;注册发奖仍走 `game_base.user_register_reward`,后续若要从 type=11 的 `initial_amount_qf` 读配置,属独立 console 改造。
|
|
||||||
|
|
||||||
## 数据流示意
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Op as 运营后台
|
|
||||||
participant Vue as edit.vue
|
|
||||||
participant Val as ActivityValidate
|
|
||||||
participant DB as ext_config JSON
|
|
||||||
|
|
||||||
Op->>Vue: 填写 initial_amount=30
|
|
||||||
Vue->>Val: submit initial_amount_qf=30000
|
|
||||||
Val->>DB: 校验通过并保存
|
|
||||||
Note over DB: 后续 console 可用 configAmount(config,'initial_amount',default)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证步骤
|
|
||||||
|
|
||||||
1. 新建/编辑 type=11 活动:可见「活动初始金额」,默认 placeholder 30。
|
|
||||||
2. 保存后 DB/API 返回的 `ext_config` 含 `initial_amount_qf`(如 30 → 30000)。
|
|
||||||
3. 再次打开编辑:回填为 30(美元)。
|
|
||||||
4. 必填校验:留空提交应被 `checkFreeCreditsExt` 拦截。
|
|
||||||
5. 列表页 type=11 行展示「活动初始金额」。
|
|
||||||
6. 仅改状态(`updateData` 为空)的 update 请求仍不触发 ext 校验(现有逻辑保持不变)。
|
|
||||||
|
|
||||||
## 风险与说明
|
|
||||||
|
|
||||||
- **与注册发奖未联动**:本任务只完成后台配置录入;`UserRegisterEventService` 仍读 `game_base.user_register_reward`,改活动配置不会自动改变注册到账,需后续 console 改造读取 `initial_amount_qf`。
|
|
||||||
- **全量覆盖 ext_config**:`edit.vue` submit 对 type=11 会重建整个 `ext_config` 对象;新增字段必须同时写入 submit 与 setFormData,避免编辑时丢失其它键(当前实现已是全量重建,与现有一致)。
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
name: wallet-bet-win-api-split
|
|
||||||
overview: 评估并规划将 bet/win 从统一 update 入口中显式拆分为独立 API,同时保留兼容性与幂等语义。
|
|
||||||
todos:
|
|
||||||
- id: add-bet-win-controller-endpoints
|
|
||||||
content: 新增 wallet/bet 与 wallet/win 控制器入口,保留 update 兼容
|
|
||||||
status: completed
|
|
||||||
- id: split-validator-dto
|
|
||||||
content: 拆分 bet/win DTO 与校验场景,减少 type 分支耦合
|
|
||||||
status: completed
|
|
||||||
- id: proxy-update-for-compat
|
|
||||||
content: 让 update 的 bet/win 分支复用新入口流程,确保行为完全一致
|
|
||||||
status: completed
|
|
||||||
- id: docs-and-migration
|
|
||||||
content: 补充 README/doc 迁移说明与灰度/下线节奏
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Bet/Win API 拆分评估与迁移计划
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
- 对资金域来说,**对外 API 语义上拆分 `bet` / `win` 会更好**:可读性、接入防错、风控审计与权限隔离都会更清晰。
|
|
||||||
- 但不建议直接废弃 `update`;建议采用“**新增独立接口 + `update` 兼容转发 + 渐进下线**”的迁移路线,避免影响现有上游与历史幂等键。
|
|
||||||
|
|
||||||
## 现状依据
|
|
||||||
- 当前只有一个入口 [`app/api/controller/WalletController.php`](app/api/controller/WalletController.php) 的 `update()`,通过 `type` 分发。
|
|
||||||
- 分发逻辑在 [`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php) 的 `ACTION_METHOD_MAP`,`bet/win` 已是独立业务方法。
|
|
||||||
- 入参模型 [`app/api/dto/request/wallet/WalletUpdateRequestDTO.php`](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 同时承载多类交易,`round_id`、`is_end` 仅对 bet/win 有意义。
|
|
||||||
- 校验器 [`app/validator/Wallet2Validator.php`](app/validator/Wallet2Validator.php) 也是“单场景 + 按 type 条件校验”,存在语义混杂。
|
|
||||||
|
|
||||||
## 目标形态
|
|
||||||
- 提供显式 API:`wallet/bet`、`wallet/win`(自动路由下对应 Controller 方法)。
|
|
||||||
- `wallet/update` 保留为兼容入口:内部仅做 DTO 转换与分发,不承载新能力。
|
|
||||||
- 资金与幂等规则保持不变:仍以 `biz_id` + `type`(必要时叠加 `round_id`)确保可追溯与幂等。
|
|
||||||
|
|
||||||
## 实施步骤
|
|
||||||
1. 在 [`app/api/controller/WalletController.php`](app/api/controller/WalletController.php) 新增 `bet()` 与 `win()` 方法,复用统一返回封装。
|
|
||||||
2. 拆分请求 DTO 与校验场景:
|
|
||||||
- 新增 bet/win 专用 DTO(从 `WalletUpdateRequestDTO` 中抽取必要字段)。
|
|
||||||
- 在 [`app/validator/Wallet2Validator.php`](app/validator/Wallet2Validator.php) 增加 `SCENE_BET`、`SCENE_WIN`,去掉“按 type 再二次判断”的耦合。
|
|
||||||
3. 在 Logic 层保持复用:
|
|
||||||
- Controller 仍调用 [`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php) 现有 `bet()` / `win()` 实现,避免资金路径重写。
|
|
||||||
- `update()` 对 bet/win 请求改为调用新入口共享流程(或内部代理),确保行为一致。
|
|
||||||
4. 文档与对接迁移:
|
|
||||||
- 在 [`README.md`](README.md) 与 [`doc/wallet.md`](doc/wallet.md) 补充“新接口 + 兼容期 + 下线节奏”。
|
|
||||||
- 给上游约定迁移窗口,监控 `update(type=bet|win)` 调用量后再决定是否下线。
|
|
||||||
|
|
||||||
## 验证要点
|
|
||||||
- 幂等:重复 `biz_id` 命中行为与现状一致。
|
|
||||||
- Round:`win` 的 `is_end=0/1` 中间派奖与最终结算语义不变。
|
|
||||||
- 资金正确性:下注扣款顺序、派奖分配、流水字段不变。
|
|
||||||
- 可回滚:任一阶段可回退为仅使用 `update` 入口。
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
name: win返回余额叠加pending
|
|
||||||
overview: 仅在 win 派奖流程中,对返回给第三方的余额叠加本局 pending 派奖,解决连续派奖期间玩家看到余额不增长的问题;不改其他接口。
|
|
||||||
todos:
|
|
||||||
- id: win-response-overlay
|
|
||||||
content: 仅在 win is_end=0 返回值叠加 pendingRoundAmount 到 w/withdraw
|
|
||||||
status: completed
|
|
||||||
- id: idempotent-display-check
|
|
||||||
content: 确认 dedupe 命中时返回余额不重复增长
|
|
||||||
status: completed
|
|
||||||
- id: final-settlement-regression
|
|
||||||
content: 确认 is_end=1 结算与 pending 清理行为不受影响
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# win 派奖返回余额叠加 pending(仅 win 场景)
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
在不改变真实账务入账时机(仍以 `is_end=1` Final Settlement 为准)的前提下,让第三方在连续派奖时拿到“可展示余额”:
|
|
||||||
- `is_end=0`:返回余额包含本局 pending
|
|
||||||
- 其他场景:不改
|
|
||||||
|
|
||||||
## 当前问题定位
|
|
||||||
在 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()` 中:
|
|
||||||
- `is_end=0` 已做 pending 累计
|
|
||||||
- 但返回值仍是 DB 真实余额(未叠加 pending),导致第三方展示不变
|
|
||||||
|
|
||||||
## 最小改动方案
|
|
||||||
仅修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`:
|
|
||||||
1. 在 `is_end=0` 分支中,累计完成后读取 `pendingRoundAmount = getPendingWinByRound()`。
|
|
||||||
2. 返回给第三方时,将 `withdraw`/`w` 做展示叠加:
|
|
||||||
- `w = wallet.withdraw_balance + pendingRoundAmount`
|
|
||||||
- `withdraw = wallet.withdraw_balance + pendingRoundAmount`
|
|
||||||
3. `deposit` / `bonus` 保持真实值(不动)。
|
|
||||||
4. `is_end=1` 维持现有逻辑:合并 pending 后走真实结算并清理 pending。
|
|
||||||
|
|
||||||
## 兼容与风险
|
|
||||||
- 仅影响 `type=win` 的返回体,不影响 DB、Lot、流水、下注规则。
|
|
||||||
- 若第三方把 `w` 当“可立即下注余额”,会产生语义差异(展示值 > 真实可用);但你当前业务前提是第三方只负责展示。
|
|
||||||
|
|
||||||
## 验收点
|
|
||||||
- 连续 `is_end=0`:返回 `w/withdraw` 逐次增长(包含 pending)
|
|
||||||
- 重复 `biz_id` 的 `is_end=0`:返回值不重复增长
|
|
||||||
- `is_end=1` 后:返回值与真实结算后余额一致,pending 被清理
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
---
|
|
||||||
name: 中间派奖余额可见化
|
|
||||||
overview: 在保持 Final Settlement 真账口径不变的前提下,让 is_end=0 中间派奖实时反映到三方读取的余额(接口返回和钱包查询)。
|
|
||||||
todos:
|
|
||||||
- id: pending-win-keys
|
|
||||||
content: 设计并接入 pending round/total 与 dedupe Redis key
|
|
||||||
status: pending
|
|
||||||
- id: win-is-end-branch
|
|
||||||
content: 实现 is_end=0 累计展示、is_end=1 合并结算与清理
|
|
||||||
status: pending
|
|
||||||
- id: wallet-query-overlay
|
|
||||||
content: 让 wallet 查询叠加 pending total 返回最新展示余额
|
|
||||||
status: pending
|
|
||||||
- id: dto-validator-update
|
|
||||||
content: 补齐 is_end DTO 与 win round_id 校验
|
|
||||||
status: pending
|
|
||||||
- id: regression-cases
|
|
||||||
content: 验证多次派奖余额可见、最终结算、幂等与失败重试场景
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 中间派奖余额可见化(is_end)改造计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
解决“多次派奖时三方读取余额不更新”的问题,同时保持 `win.md` 的核心原则:
|
|
||||||
- 真正入账、Lot 回流、转化/解锁只在 `is_end=1`(Final Settlement)执行
|
|
||||||
- `is_end=0` 仅做中间派奖累计与展示余额更新
|
|
||||||
|
|
||||||
## 核心思路
|
|
||||||
在 Redis 维护“待结算派奖池(Pending Win)”,并把它叠加到对外返回余额:
|
|
||||||
- DB 里的 `withdraw/deposit/bonus` 仍保持真实账
|
|
||||||
- 对外给三方的余额 = 真实账 + pendingWin
|
|
||||||
|
|
||||||
这样能保证:
|
|
||||||
- 三方实时看到余额变化
|
|
||||||
- 不破坏现有 `WinService` 的 Round Final Settlement 账务逻辑
|
|
||||||
|
|
||||||
## 具体改造
|
|
||||||
|
|
||||||
### 1) 新增 Pending Win 缓存层
|
|
||||||
修改 [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php):新增 key
|
|
||||||
- `wallet:win:pending:round:{uid}:{currency}:{round_id}`(每局中间派奖累计)
|
|
||||||
- `wallet:win:pending:total:{uid}:{currency}`(用户币种维度累计,供快速读余额)
|
|
||||||
- `wallet:win:dedupe:{uid}:{currency}:{round_id}:{biz_id}`(中间派奖严格幂等)
|
|
||||||
|
|
||||||
### 2) win 分支行为
|
|
||||||
修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `win()`:
|
|
||||||
- `is_end=0`:
|
|
||||||
- 命中 dedupe 直接返回
|
|
||||||
- 未命中则 `INCRBY pending:round` 与 `INCRBY pending:total`
|
|
||||||
- 返回余额时把 `pending:total` 叠加到 `withdraw`(仅对外展示)
|
|
||||||
- 不调用 `WinService::execute`、不写 `BIZ_TYPE_WIN`
|
|
||||||
- `is_end=1`:
|
|
||||||
- 读取 `pending:round`,`finalWin = fee + pendingRound`
|
|
||||||
- 用 `finalWin` 调用现有 [app/service/wallet/WinService.php](app/service/wallet/WinService.php)
|
|
||||||
- 成功后清理 `pending:round` 并从 `pending:total` 扣除对应值
|
|
||||||
- 返回余额按真实账(此时 pending 已回收)
|
|
||||||
|
|
||||||
### 3) 钱包查询返回也要可见化
|
|
||||||
修改 [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php) 的 `query()` 返回:
|
|
||||||
- 将 `pending:total` 叠加到返回字段 `w/withdraw`
|
|
||||||
- 这样第三方无论读 `win` 返回还是调用 `wallet` 查询,都看到“最新展示余额”
|
|
||||||
|
|
||||||
### 4) DTO/校验补齐
|
|
||||||
- 在 [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php) 增加 `is_end`(默认 1)
|
|
||||||
- 在 [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php) 增加:
|
|
||||||
- `is_end` 仅允许 `0/1`
|
|
||||||
- `win` 场景强制 `round_id` 必传
|
|
||||||
|
|
||||||
## 一致性与幂等
|
|
||||||
- `is_end=0`:Redis dedupe(biz_id 级)防重复累计
|
|
||||||
- `is_end=1`:延续现有 `wallet_log(uid,biz_id,biz_type=win)` 幂等
|
|
||||||
- 若 Final 失败,不清 pending,便于重试恢复
|
|
||||||
|
|
||||||
## 风险说明(需明确)
|
|
||||||
- 该方案是“展示余额实时、真账延后”:若上游把展示余额当可立即可下注余额,可能出现下注时余额校验不一致。
|
|
||||||
- 若你需要“中间派奖立即可下注”,则要走另一套方案(中间账临时入库 + Final 对冲重分配),改动会显著更大。
|
|
||||||
|
|
||||||
## 改动文件
|
|
||||||
- [app/service/RedisKeyManagerService.php](app/service/RedisKeyManagerService.php)
|
|
||||||
- [app/api/dto/request/wallet/WalletUpdateRequestDTO.php](app/api/dto/request/wallet/WalletUpdateRequestDTO.php)
|
|
||||||
- [app/validator/Wallet2Validator.php](app/validator/Wallet2Validator.php)
|
|
||||||
- [app/api/logic/WalletLogic.php](app/api/logic/WalletLogic.php)
|
|
||||||
|
|
||||||
## 验收场景
|
|
||||||
- 多次 `is_end=0` 后,`win` 返回余额递增
|
|
||||||
- 多次 `is_end=0` 后,`wallet` 查询余额递增
|
|
||||||
- `is_end=1` 触发后,final 入账金额 = 中间累计 + 最后一笔
|
|
||||||
- 重放同一 `is_end=0` `biz_id` 不重复累计
|
|
||||||
- 重放同一 `is_end=1` `biz_id` 不重复结算
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
---
|
|
||||||
name: 任务进度筛选对接
|
|
||||||
overview: 新增独立分页接口 `player-task/task-list`(筛选 task_type + status + page/page_size),供前端双下拉表格使用;保留现有 `player-task/task-progress` 全量快照不变。同步 slot_sdk 与 API 文档。
|
|
||||||
todos:
|
|
||||||
- id: wallet-model-paginate
|
|
||||||
content: WalletFundLotModel 新增 paginatePlayerTaskLots(按 lot_type/status 分页)
|
|
||||||
status: completed
|
|
||||||
- id: wallet-task-list-api
|
|
||||||
content: 新增 taskList 全链路(Validator/DTO/Logic/Service/Controller)
|
|
||||||
status: completed
|
|
||||||
- id: sdk-task-list
|
|
||||||
content: slot_sdk 新增 taskList 请求/响应实体与 WalletService 方法
|
|
||||||
status: completed
|
|
||||||
- id: api-doc-list
|
|
||||||
content: 新增 doc/player-task-list-api.md(含前端双下拉 + 分页说明)
|
|
||||||
status: completed
|
|
||||||
- id: feature-tests-list
|
|
||||||
content: Feature 测试:筛选、分页、参数校验;task-progress 回归不变
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 玩家任务进度:新增分页列表接口
|
|
||||||
|
|
||||||
## 背景与目标
|
|
||||||
|
|
||||||
- **现有** [`player-task/task-progress`](doc/player-task-progress-api.md):只读全量快照(`bonus_tasks` + `deposit_tasks` + `summary`),**不改契约**,继续给大厅/SDK 轻量查询用。
|
|
||||||
- **新增** `player-task/task-list`:面向 **前端操作页**(双下拉 + 表格),支持 **类型筛选、状态筛选、分页**。
|
|
||||||
|
|
||||||
前端操作区:
|
|
||||||
|
|
||||||
| 下拉框 | 选项 | 说明 |
|
|
||||||
|--------|------|------|
|
|
||||||
| 任务类型 | **Bonus** / **Deposit** | 必填,二选一 |
|
|
||||||
| 状态 | **全部** + Waiting / Active / PendingConversion / PlayedOut | `status=0` 或不传 = 全部可见态 |
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph keep [保持不变]
|
|
||||||
TP["POST task-progress"]
|
|
||||||
TP --> Full["bonus_tasks + deposit_tasks 全量"]
|
|
||||||
end
|
|
||||||
subgraph newApi [新增]
|
|
||||||
TL["POST task-list"]
|
|
||||||
TL --> Filter["task_type + status"]
|
|
||||||
TL --> Page["page + page_size"]
|
|
||||||
Page --> List["list + 分页元数据"]
|
|
||||||
end
|
|
||||||
UI[前端表格页] --> TL
|
|
||||||
Lobby[大厅轻量展示] --> TP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 新接口契约
|
|
||||||
|
|
||||||
### 1.1 路由
|
|
||||||
|
|
||||||
| 项 | 值 |
|
|
||||||
|----|-----|
|
|
||||||
| Method | `POST` |
|
|
||||||
| Path | `player-task/task-list` |
|
|
||||||
| Controller | [`PlayerTaskController::taskList`](app/api/controller/PlayerTaskController.php)(新增方法) |
|
|
||||||
|
|
||||||
### 1.2 请求参数
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `uid` | int | 是 | 用户 ID |
|
|
||||||
| `currency` | string | 是 | 币种,如 `TGO` |
|
|
||||||
| `task_type` | string | **是** | `bonus` \| `deposit` |
|
|
||||||
| `status` | int | 否 | `0` 或省略 = 全部玩家可见态;`1`/`2`/`3`/`5` = 单态 |
|
|
||||||
| `page` | int | 否 | 页码,默认 `1`,最小 `1` |
|
|
||||||
| `page_size` | int | 否 | 每页条数,默认 `20`,建议上限 `100` |
|
|
||||||
|
|
||||||
校验([`PlayerTaskValidator`](app/validator/PlayerTaskValidator.php) 新 scene `list`):
|
|
||||||
|
|
||||||
- `uid`:`require|integer`
|
|
||||||
- `currency`:`require`
|
|
||||||
- `task_type`:`require|in:bonus,deposit`
|
|
||||||
- `status`:`integer|in:0,1,2,3,5`(可空,默认 0)
|
|
||||||
- `page`:`integer|egt:1`(可空)
|
|
||||||
- `page_size`:`integer|between:1,100`(可空)
|
|
||||||
|
|
||||||
**前端映射**:
|
|
||||||
|
|
||||||
| UI | 请求 |
|
|
||||||
|----|------|
|
|
||||||
| Bonus | `"task_type": "bonus"` |
|
|
||||||
| Deposit | `"task_type": "deposit"` |
|
|
||||||
| 全部 | 不传 `status` 或 `"status": 0` |
|
|
||||||
| Waiting / Active / … | `status` = `1` / `2` / `3` / `5` |
|
|
||||||
| 翻页 | 修改 `page`(切换筛选时重置 `page=1`) |
|
|
||||||
|
|
||||||
### 1.3 成功响应 `data`
|
|
||||||
|
|
||||||
与仓库 [`SearchShardService`](app/service/search/shard/SearchShardService.php) 分页习惯对齐(`data` → `list`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_type": "bonus",
|
|
||||||
"list": [ /* PlayerTaskItem,与 task-progress 任务项字段相同 */ ],
|
|
||||||
"total": 15,
|
|
||||||
"page": 1,
|
|
||||||
"page_size": 20,
|
|
||||||
"last_page": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `list[]` 元素结构 **复用** [`PlayerTaskItemEntity::toApiArray()`](app/entity/wallet/PlayerTaskItemEntity.php)(与 `task-progress` 单条一致)。
|
|
||||||
- 排序:`consume_priority_at ASC`, `lot_id ASC`(与现 [`listPlayerTaskLots`](app/model/multi/WalletFundLotModel.php) 一致)。
|
|
||||||
- 状态范围:默认仍为 PRD §26.1 四态 `[1,2,3,5]`;**不含** Completed/Cancelled/Reversed(与 `task-progress` 一致)。
|
|
||||||
|
|
||||||
### 1.4 与 `task-progress` 的分工
|
|
||||||
|
|
||||||
| 接口 | 场景 | 返回 |
|
|
||||||
|------|------|------|
|
|
||||||
| `task-progress` | 一次拿全量两类任务 + 计数汇总 | `summary` + `bonus_tasks` + `deposit_tasks` |
|
|
||||||
| `task-list` | 表格页:选定类型 + 状态 + 翻页 | 单维 `list` + 分页字段 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. wallet 实现要点
|
|
||||||
|
|
||||||
### 2.1 Model
|
|
||||||
|
|
||||||
在 [`WalletFundLotModel`](app/model/multi/WalletFundLotModel.php) 新增:
|
|
||||||
|
|
||||||
```php
|
|
||||||
/**
|
|
||||||
* 分页查询玩家任务 Lot。
|
|
||||||
* @return array{list: array, total: int, page: int, page_size: int, last_page: int}
|
|
||||||
*/
|
|
||||||
public function paginatePlayerTaskLots(
|
|
||||||
string $currency,
|
|
||||||
int $lotType,
|
|
||||||
array $statuses,
|
|
||||||
int $page,
|
|
||||||
int $pageSize
|
|
||||||
): array
|
|
||||||
```
|
|
||||||
|
|
||||||
- `statuses` 为空时直接返回空分页(与 `listPlayerTaskLots` 一致)。
|
|
||||||
- 使用 ThinkORM `paginate($pageSize, false, ['page' => $page])`,再将 `data` 重命名为 `list`。
|
|
||||||
|
|
||||||
### 2.2 Service
|
|
||||||
|
|
||||||
在 [`PlayerTaskQueryService`](app/service/wallet/PlayerTaskQueryService.php) 新增 `queryTaskList(...)`:
|
|
||||||
|
|
||||||
- `task_type` 字符串 → `LOT_TYPE_BONUS` / `LOT_TYPE_DEPOSIT`
|
|
||||||
- `status` → `PLAYER_VISIBLE_STATUSES` 或单元素数组
|
|
||||||
- 调用 Model 分页后,`buildTaskItems()` 组装 `list`
|
|
||||||
|
|
||||||
### 2.3 分层文件(新增/扩展)
|
|
||||||
|
|
||||||
| 层级 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| DTO | `app/api/dto/request/PlayerTaskListRequestDTO.php`(新建) |
|
|
||||||
| Validator | `PlayerTaskValidator` 增加 `SCENE_LIST` |
|
|
||||||
| Logic | `PlayerTaskLogic::taskList()` |
|
|
||||||
| Controller | `PlayerTaskController::taskList()` |
|
|
||||||
|
|
||||||
**不修改** `PlayerTaskProgressRequestDTO` / `taskProgress` 逻辑。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. slot_sdk
|
|
||||||
|
|
||||||
新增(与 wallet 字段 snake_case 一致):
|
|
||||||
|
|
||||||
| 类型 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 请求 | `PlayerTaskListRequestEntity.php` |
|
|
||||||
| 响应 | `PlayerTaskListResponseEntity.php`(含 `list`、`total`、`page`、`page_size`、`last_page`、`task_type`) |
|
|
||||||
|
|
||||||
[`WalletService`](slot_sdk/src/service/wallet/WalletService.php) 新增:
|
|
||||||
|
|
||||||
```php
|
|
||||||
public function taskList(PlayerTaskListRequestEntity $entity): ?PlayerTaskListResponseEntity
|
|
||||||
// POST api/player-task/task-list
|
|
||||||
```
|
|
||||||
|
|
||||||
`taskProgress` / `PlayerTaskProgressRequestEntity` **保持不变**。
|
|
||||||
|
|
||||||
发布:slot_sdk commit → push → slot-wallet `composer update`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 文档
|
|
||||||
|
|
||||||
- **新建** [`doc/player-task-list-api.md`](doc/player-task-list-api.md):路由、入参、响应、前端双下拉 + 翻页交互、与 `task-progress` 对比、curl 示例。
|
|
||||||
- [`doc/player-task-progress-api.md`](doc/player-task-progress-api.md) 顶部增加「列表分页请用 task-list」交叉引用。
|
|
||||||
- [`doc/wallet.md`](doc/wallet.md) §26.1 补充 `task-list` 一行说明。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 测试
|
|
||||||
|
|
||||||
新建 `tests/Feature/PlayerTaskListTest.php`(造数参考 [`WalletRegisterBetWinTest`](tests/Feature/WalletRegisterBetWinTest.php)):
|
|
||||||
|
|
||||||
| 用例 | 断言 |
|
|
||||||
|------|------|
|
|
||||||
| `task_type=bonus` + 默认分页 | `list` 非空项字段完整;`total >= len(list)` |
|
|
||||||
| `status=2` | `list` 内 `status` 均为 2 |
|
|
||||||
| `page=2` + `page_size=1` | 第二页与总数一致 |
|
|
||||||
| 缺 `task_type` | `40003` |
|
|
||||||
| `page_size=101` | `40003` |
|
|
||||||
| `task-progress` 仍返回双数组 | 回归,不受新接口影响 |
|
|
||||||
|
|
||||||
容器内执行:`docker compose exec -T php82` + 项目 PHPUnit 命令。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 不在本次范围
|
|
||||||
|
|
||||||
- 修改 `task-progress` 入参或响应
|
|
||||||
- 任务类型「全部」合并在一个列表
|
|
||||||
- 运营后台终态(Completed 等)纳入筛选
|
|
||||||
- 前端页面实现(本仓库无前端)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 关键文件一览
|
|
||||||
|
|
||||||
| 仓库 | 变更 |
|
|
||||||
|------|------|
|
|
||||||
| slot-wallet | `PlayerTaskController`、`PlayerTaskLogic`、`PlayerTaskQueryService`、`WalletFundLotModel`、`PlayerTaskValidator`、新 DTO、`doc/player-task-list-api.md`、Feature 测试 |
|
|
||||||
| slot_sdk | 新 Entity ×2、`WalletService::taskList`、`readme.md` |
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
---
|
|
||||||
name: 异步事件单元测试
|
|
||||||
overview: 结合 slot_console 现有 Free Credits 测试实践,说明异步 MQ 事件应分层测试:不直连 RabbitMQ,优先测 Logic 编排,再补 Event 解析与 EventBus 路由;并给出可新增的示例用例结构。
|
|
||||||
todos:
|
|
||||||
- id: entity-test
|
|
||||||
content: 新增 FreeCreditInitEntityTest:字段映射与 resolveWalletAmount 回退
|
|
||||||
status: completed
|
|
||||||
- id: bus-factory
|
|
||||||
content: 新增 tests/Support/ConsoleBusMessageFactory 统一构造 MQBusEntity 载荷
|
|
||||||
status: completed
|
|
||||||
- id: extend-logic-tests
|
|
||||||
content: 扩展 FreeCreditsHandleFreeCreditInitTest:幂等、无配置、异常路径
|
|
||||||
status: completed
|
|
||||||
- id: optional-event-inject
|
|
||||||
content: (可选)FreeCreditInitEvent 注入 Logic + Event 单测
|
|
||||||
status: completed
|
|
||||||
- id: optional-eventbus-nack
|
|
||||||
content: (可选)EventBus deal 单测:TYPE_FREE_CREDIT_INIT 成功 ack / 失败 nack
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 异步事件单元测试写法(slot_console)
|
|
||||||
|
|
||||||
## 当前架构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Wallet as slot_wallet
|
|
||||||
participant MQ as RabbitMQ_console_bus
|
|
||||||
participant Bus as EventBus_deal
|
|
||||||
participant Ev as FreeCreditInitEvent
|
|
||||||
participant Logic as FreeCreditsLogic
|
|
||||||
|
|
||||||
Wallet->>MQ: JSON uid/type/data
|
|
||||||
MQ->>Bus: AMQPMessage
|
|
||||||
Bus->>Ev: case TYPE_FREE_CREDIT_INIT
|
|
||||||
Ev->>Ev: FreeCreditInitEntity
|
|
||||||
Ev->>Logic: handleFreeCreditInit
|
|
||||||
Logic->>Logic: freezeFirstRecharge + wallet RPC
|
|
||||||
```
|
|
||||||
|
|
||||||
**结论**:异步只发生在 MQ 传输层;单测应测 **消息解析 → 路由 → 业务编排**,而不是启动真实 `event:bus` 或 RabbitMQ。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目里已有的做法(推荐延续)
|
|
||||||
|
|
||||||
### 1. 单元测试:直接测 Logic(主路径)
|
|
||||||
|
|
||||||
现有 [`tests/Unit/FreeCreditsHandleFreeCreditInitTest.php`](/Users/ray/Documents/project/www/slot/slot_console/tests/Unit/FreeCreditsHandleFreeCreditInitTest.php) 已覆盖 `free_credit_init` 的**业务结果**,等价于事件消费后的效果:
|
|
||||||
|
|
||||||
- [`FreeCreditsLogicHarness`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsLogicHarness.php):注入 `userContext` / `activeConfig` / `walletService`
|
|
||||||
- [`RecordingWalletService`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/RecordingWalletService.php):记录 `freeCreditsFreeze`,不发 HTTP
|
|
||||||
- [`FreeCreditsConfigStub`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsConfigStub.php):活动配置桩
|
|
||||||
|
|
||||||
断言示例(已有):
|
|
||||||
|
|
||||||
- 定格金额 = `balance_before_qf`
|
|
||||||
- `biz_id` = `free_credits_freeze:{orderId}`
|
|
||||||
- `balance_before_qf <= 0` 时不调用 freeze
|
|
||||||
|
|
||||||
**运行**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd slot_console && ./vendor/bin/phpunit --testsuite Unit
|
|
||||||
```
|
|
||||||
|
|
||||||
提现三类事件同理:[`tests/Integration/FreeCreditsLogicCashoutResultTest.php`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsLogicCashoutResultTest.php) 直接调 `handleFirstCashoutResult`,不经过 EventBus。
|
|
||||||
|
|
||||||
### 2. 集成测试:需要 DB 时
|
|
||||||
|
|
||||||
[`FreeCreditsDbTestCase`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsDbTestCase.php):`RUN_DB_TESTS=1` 才跑,默认事务回滚。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RUN_DB_TESTS=1 ./vendor/bin/phpunit --testsuite Integration --filter FreeCredits
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 建议的分层(按投入产出排序)
|
|
||||||
|
|
||||||
| 层级 | 测什么 | 是否必需 | 依赖 |
|
|
||||||
|------|--------|----------|------|
|
|
||||||
| A. Entity | `FreeCreditInitEntity` 字段映射、`resolveWalletAmount()` 回退 | 推荐 | 无 |
|
|
||||||
| B. Logic | `handleFreeCreditInit` / `handleFirstCashoutResult` 编排 | **已有,继续扩展** | Harness + RecordingWallet |
|
|
||||||
| C. Event | `FreeCreditInitEvent::handle` 把 `MQBusEntity` 转成 Logic 参数 | 可选 | 需可注入 Logic(见下) |
|
|
||||||
| D. EventBus | `deal()` 按 `type` 分发、失败 nack | 少量即可 | Mock `AMQPMessage` |
|
|
||||||
| E. E2E | 真 MQ + wallet 发消息 | 手工/脚本,非单测 | 环境 |
|
|
||||||
|
|
||||||
**原则**:B 覆盖 90% 风险;A 防 payload 字段错;C/D 防「路由写错 type」和「实体解析漏字段」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 可新增的测试示例
|
|
||||||
|
|
||||||
### A. Entity 单测(纯数组 → 实体)
|
|
||||||
|
|
||||||
新建 `tests/Unit/FreeCreditInitEntityTest.php`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$entity = new FreeCreditInitEntity([
|
|
||||||
'balance_before_qf' => 15000,
|
|
||||||
'wallet_amount' => 0,
|
|
||||||
'amount' => 5000,
|
|
||||||
'biz_id' => 'order_1',
|
|
||||||
]);
|
|
||||||
$this->assertSame(15000, $entity->balance_before_qf);
|
|
||||||
$this->assertSame(5000, $entity->resolveWalletAmount()); // wallet_amount 为 0 时回退 amount
|
|
||||||
```
|
|
||||||
|
|
||||||
对应生产代码:[`app/entity/mq/FreeCreditInitEntity.php`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/FreeCreditInitEntity.php)。
|
|
||||||
|
|
||||||
### B. 构造 MQ 载荷辅助方法
|
|
||||||
|
|
||||||
在 `tests/Support/` 增加工厂,统一模拟 wallet 发出的 body:
|
|
||||||
|
|
||||||
```php
|
|
||||||
public static function consoleBusMessage(int $uid, string $type, array $data): MQBusEntity
|
|
||||||
{
|
|
||||||
return new MQBusEntity(['uid' => $uid, 'type' => $type, 'data' => $data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// free_credit_init 示例
|
|
||||||
public static function freeCreditInitBus(int $uid, int $balanceBefore, int $walletAmount, string $bizId): MQBusEntity
|
|
||||||
{
|
|
||||||
return self::consoleBusMessage($uid, MQBusEntity::TYPE_FREE_CREDIT_INIT, [
|
|
||||||
'balance_before_qf' => $balanceBefore,
|
|
||||||
'wallet_amount' => $walletAmount,
|
|
||||||
'biz_id' => $bizId,
|
|
||||||
'source' => 'test',
|
|
||||||
'currency' => 'INR',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
与 [`WalletLogic::sendConsoleBus`](/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 字段保持一致。
|
|
||||||
|
|
||||||
### C. Event 层单测(当前结构的限制)
|
|
||||||
|
|
||||||
[`FreeCreditInitEvent`](/Users/ray/Documents/project/www/slot/slot_console/app/command/event/FreeCreditInitEvent.php) 内部写死 `new FreeCreditsLogic()`,**无法在不改代码的情况下 mock Logic**。
|
|
||||||
|
|
||||||
两种做法(二选一):
|
|
||||||
|
|
||||||
1. **小重构(推荐)**:Event 构造函数注入 `FreeCreditsLogic`,单测传入 `FreeCreditsLogicHarness`。
|
|
||||||
2. **不测 Event**:认为 Event 只有 5 行胶水,由 B 层保证;仅加 A 层测 data 解析。
|
|
||||||
|
|
||||||
若采用 1,示例:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$bus = ConsoleBusMessageFactory::freeCreditInitBus($uid, 15000, 5000, 'order_1');
|
|
||||||
$harness = (new FreeCreditsLogicHarness())->inject($context, $config, $wallet);
|
|
||||||
(new FreeCreditInitEvent($harness))->handle($bus);
|
|
||||||
$this->assertCount(1, $wallet->freezeCalls);
|
|
||||||
```
|
|
||||||
|
|
||||||
### D. EventBus 路由单测(少量)
|
|
||||||
|
|
||||||
Mock `PhpAmqpLib\Message\AMQPMessage`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$message = $this->createMock(AMQPMessage::class);
|
|
||||||
$message->method('getBody')->willReturn(json_encode([
|
|
||||||
'uid' => 90088002,
|
|
||||||
'type' => MQBusEntity::TYPE_FREE_CREDIT_INIT,
|
|
||||||
'data' => ['balance_before_qf' => 15000, 'wallet_amount' => 5000, 'biz_id' => 'x'],
|
|
||||||
]));
|
|
||||||
$message->expects($this->once())->method('ack'); // 成功应 ack
|
|
||||||
|
|
||||||
$bus = new EventBus();
|
|
||||||
// 若 Logic 仍 new 在 Event 内,需配合 C 的注入或接受集成测
|
|
||||||
$bus->deal($message);
|
|
||||||
```
|
|
||||||
|
|
||||||
**nack 场景**([`EventBus.php` L164-167](/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php)):Logic 抛异常时,`TYPE_FREE_CREDIT_INIT` 应 `nack(true)` 且不 `ack`。可让 Harness 的 `freeCreditsFreeze` 抛异常,断言 `$message->expects($this->once())->method('nack')->with(true)`。
|
|
||||||
|
|
||||||
注意:`EventBus` 依赖多,路由测试宜 **只测 switch 分支 + ack/nack**,业务细节仍放 B。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四个 Free Credits 相关 type 怎么测
|
|
||||||
|
|
||||||
| MQ type(常量) | 单测重点 | 现有覆盖 |
|
|
||||||
|-----------------|----------|----------|
|
|
||||||
| `TYPE_FREE_CREDIT_INIT` | 定格金额、biz_id、幂等跳过 | `FreeCreditsHandleFreeCreditInitTest` |
|
|
||||||
| `TYPE_FREE_CREDITS_FIRST_CASHOUT_SUCCESS` | 档位 completed、player 状态 | `FreeCreditsLogicCashoutResultTest` |
|
|
||||||
| `TYPE_FREE_CREDITS_FIRST_CASHOUT_FAIL` | 失败回滚逻辑 | 可补 case |
|
|
||||||
| `TYPE_FREE_CREDITS_FIRST_CASHOUT_REJECTED` | rejected → ready | 已有 reject case |
|
|
||||||
|
|
||||||
EventBus 里前三类目前 **直接调 Logic**(未走独立 Event 类),单测继续打 Logic 即可;常量定义在 [`MQBusEntity`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/MQBusEntity.php)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 不建议在单测里做的
|
|
||||||
|
|
||||||
- 启动 `php webman event:bus` 或连接真实 RabbitMQ
|
|
||||||
- 跨服务联调 wallet → console(留给脚本如 `scripts/run_free_credits_first_freeze.php` 或手工验收)
|
|
||||||
- 在单测里依赖 `UserTagService::getByUid`(用 Harness 注入 context)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐落地顺序(若你要补测试)
|
|
||||||
|
|
||||||
1. 补 `FreeCreditInitEntityTest`(A)
|
|
||||||
2. 补 `ConsoleBusMessageFactory`(B 辅助)
|
|
||||||
3. 扩展 `FreeCreditsHandleFreeCreditInitTest`:无活动配置、已定格幂等、Logic 抛错
|
|
||||||
4. (可选)`FreeCreditInitEvent` 注入 Logic + Event 单测(C)
|
|
||||||
5. (可选)`EventBusFreeCreditInitTest` 只测 ack/nack(D)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
subgraph unit [Unit 无 MQ]
|
|
||||||
E[EntityTest]
|
|
||||||
L[LogicHarnessTest]
|
|
||||||
end
|
|
||||||
subgraph optional [Optional]
|
|
||||||
Ev[EventTest]
|
|
||||||
Bus[EventBusRoutingTest]
|
|
||||||
end
|
|
||||||
subgraph integration [Integration RUN_DB_TESTS]
|
|
||||||
DB[DbTestCase]
|
|
||||||
end
|
|
||||||
E --> L
|
|
||||||
L --> Ev
|
|
||||||
Ev --> Bus
|
|
||||||
L --> DB
|
|
||||||
```
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
name: 拆分win派奖文档
|
|
||||||
overview: 将 wallet PRD 中与 win(派奖)强相关的需求从主文档拆分到独立文档,降低耦合并保持规则可追踪。计划默认采用“从 wallet.md 迁移到 win.md,并在原处保留索引入口”的方式。
|
|
||||||
todos:
|
|
||||||
- id: identify-win-sections
|
|
||||||
content: 标记 wallet.md 中所有 win(派奖)主规则与引用依赖段落
|
|
||||||
status: completed
|
|
||||||
- id: create-win-doc
|
|
||||||
content: 创建 win.md 并迁移/重组派奖规则、流程与示例
|
|
||||||
status: completed
|
|
||||||
- id: refactor-wallet-doc
|
|
||||||
content: 在 wallet.md 用摘要+链接替换已迁移内容并更新目录
|
|
||||||
status: completed
|
|
||||||
- id: consistency-pass
|
|
||||||
content: 统一术语与交叉引用,确保无重复冲突定义
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 将 win(派奖)需求拆分到独立文档
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
把 [doc/wallet.md](doc/wallet.md) 中“win(派奖)”相关需求整理到新文档 [doc/win.md](doc/win.md),并保证两份文档职责清晰:
|
|
||||||
- `wallet.md` 保留钱包总规则与对外索引
|
|
||||||
- `win.md` 承载派奖、Round 结算、出资分配等细则
|
|
||||||
|
|
||||||
## 拆分范围(默认)
|
|
||||||
从 [doc/wallet.md](doc/wallet.md) 迁移以下“win 主体”内容到 [doc/win.md](doc/win.md):
|
|
||||||
- §12 `Slots 多次派奖与最终结算规则`
|
|
||||||
- §20 中 win 强相关子流程:
|
|
||||||
- `20.4 Round 多次派奖`
|
|
||||||
- `20.5 Round 最终结算`
|
|
||||||
- 与派奖直接相关且需避免重复维护的规则片段(在 `wallet.md` 中改为引用):
|
|
||||||
- §10.3 `派奖归属规则`
|
|
||||||
- §11 中 `Round 总派奖` 分配示例
|
|
||||||
- §16.2 `派奖按比例拆分余数`
|
|
||||||
|
|
||||||
## 文档结构调整
|
|
||||||
- 在 [doc/wallet.md](doc/wallet.md):
|
|
||||||
- 保留“钱包主规则”定位
|
|
||||||
- 将迁移段落替换为简要摘要 + 指向 [doc/win.md](doc/win.md) 的链接
|
|
||||||
- 更新目录,新增 `win` 文档入口
|
|
||||||
- 在 [doc/win.md](doc/win.md):
|
|
||||||
- 新建“win(派奖)需求”专题文档
|
|
||||||
- 按“背景 → 结算时序 → 分配规则 → 边界条件 → 示例流程”重组内容
|
|
||||||
- 明确与 `Lot`、`Task`、`Withdrawable` 的关系,保留必要术语一致性(Round/FinalSettlement/AllocatedPayout)
|
|
||||||
|
|
||||||
## 一致性与可维护性约束
|
|
||||||
- 术语与口径与 [doc/wallet.md](doc/wallet.md) 保持一致(Bonus/Deposit/Withdrawable、Final Settlement、PlayedOut)
|
|
||||||
- 避免双份正文维护:同一规则只在一个文档定义,另一侧做引用
|
|
||||||
- 若保留示例在两文档同时出现,仅保留“简版示意”在 `wallet.md`,完整口径放 `win.md`
|
|
||||||
|
|
||||||
## 验收标准
|
|
||||||
- [doc/win.md](doc/win.md) 可单独说明“派奖”完整业务规则
|
|
||||||
- [doc/wallet.md](doc/wallet.md) 删除/替换原 win 细节后仍可作为钱包总览
|
|
||||||
- 两文档目录和交叉链接可直接跳转,且不存在明显冲突描述
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
name: 测试 register bet win
|
|
||||||
overview: 设计一套不改代码的联调测试方案,覆盖 register、bet、win(含中间派奖+最终结算)主流程、幂等与关键校验。输出可直接执行的请求序列和验收点。
|
|
||||||
todos:
|
|
||||||
- id: collect-endpoints
|
|
||||||
content: 整理 register/bet/win 的可调用入口与参数最小集合
|
|
||||||
status: completed
|
|
||||||
- id: define-test-cases
|
|
||||||
content: 设计主链路、幂等、参数异常三类用例
|
|
||||||
status: completed
|
|
||||||
- id: prepare-request-templates
|
|
||||||
content: 产出按执行顺序排列的请求模板与预期结果
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Register/Bet/Win 测试计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
- 用最小链路验证 `register -> bet -> win` 的资金变更与返回口径。
|
|
||||||
- 覆盖 `win` 的两阶段结算(`is_end=0` 中间派奖、`is_end=1` 最终结算)。
|
|
||||||
- 覆盖关键风控点:幂等(重复 `biz_id`)、参数校验(`round_id`、`is_end`、金额)。
|
|
||||||
|
|
||||||
## 关键实现依据(用于制定用例)
|
|
||||||
- 接口入口与兼容关系:[`app/api/controller/WalletController.php`](app/api/controller/WalletController.php)
|
|
||||||
- `wallet/bet`、`wallet/win` 为独立入口;`wallet/update(type=bet|win)` 复用同链路。
|
|
||||||
- 业务分发与资金处理:[`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php)
|
|
||||||
- `register` 走 `RegisterService::execute`。
|
|
||||||
- `bet` 按 `biz_id` 做幂等。
|
|
||||||
- `win`:`is_end=0` 只累计待结算金额,`is_end=1` 才真正入账并清理缓存。
|
|
||||||
- 注册落账与 Lot 初始化:[`app/service/wallet/RegisterService.php`](app/service/wallet/RegisterService.php)
|
|
||||||
- 注册会初始化钱包、统计行,并按 `fee/bonus` 创建 Deposit/Bonus Lot。
|
|
||||||
- 请求参数约束:[`app/validator/Wallet2Validator.php`](app/validator/Wallet2Validator.php)
|
|
||||||
- bet/win 必传 `round_id`,`is_end` 仅支持 `0/1`。
|
|
||||||
|
|
||||||
## 测试范围与步骤
|
|
||||||
1. **准备阶段**
|
|
||||||
- 选一个全新 `uid`(避免历史账干扰)。
|
|
||||||
- 固定 `currency/source/organization`,并准备唯一 `biz_id` 生成规则(例如带时间戳)。
|
|
||||||
|
|
||||||
2. **主链路测试(Happy Path)**
|
|
||||||
- 调 `wallet/update` 发起 `register`(`type=register`,带 `fee` 与 `bonus`)。
|
|
||||||
- 调 `wallet/wallet` 查询余额基线。
|
|
||||||
- 调 `wallet/bet`(带 `round_id`)验证扣款与返回结构。
|
|
||||||
- 调 `wallet/win` 且 `is_end=0`:确认返回中 `withdraw` 临时增加(待结算展示),但不落最终账。
|
|
||||||
- 调 `wallet/win` 且 `is_end=1`:确认最终入账,并清理该 `round_id` 的待结算累计。
|
|
||||||
|
|
||||||
3. **幂等与异常场景**
|
|
||||||
- 重放同一 `bet.biz_id`:应命中幂等,余额不重复变化。
|
|
||||||
- 重放同一 `win.biz_id`:应命中幂等,余额不重复变化。
|
|
||||||
- 缺失 `round_id` 调 bet/win:应返回参数错误。
|
|
||||||
- 传非法 `is_end`(如 2):应返回参数错误。
|
|
||||||
|
|
||||||
4. **验收口径**
|
|
||||||
- 余额变化符合顺序:下注优先扣 Bonus,再 Deposit,再 Withdraw。
|
|
||||||
- `win` 仅在最终结算时落地资金结果;中间派奖只累计。
|
|
||||||
- 重复请求无重复记账,返回语义稳定。
|
|
||||||
|
|
||||||
## 建议请求流(逻辑顺序图)
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
registerCall[register(update)] --> walletCheck1[wallet_query]
|
|
||||||
walletCheck1 --> betCall[bet]
|
|
||||||
betCall --> winMid[win_is_end_0]
|
|
||||||
winMid --> walletCheck2[wallet_query]
|
|
||||||
walletCheck2 --> winFinal[win_is_end_1]
|
|
||||||
winFinal --> walletCheck3[wallet_query]
|
|
||||||
walletCheck3 --> idemReplay[replay_same_biz_id]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 交付物
|
|
||||||
- 一份可直接在 Postman/curl 执行的请求模板(含示例 body)。
|
|
||||||
- 一份对照清单:每一步的“预期返回 + 余额期望 + 幂等期望”。
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
name: 测试报告可追踪输出
|
|
||||||
overview: 在现有 WalletRegisterBetWinTest 上增加“可追踪测试报告”:每次执行输出并落盘 uid/round_id/biz_id/余额快照,失败时可直接按用户复现。
|
|
||||||
todos:
|
|
||||||
- id: add-reporter-support
|
|
||||||
content: 新增 WalletTestRunContext 与 WalletTestReporter(JSON+Markdown 落盘)
|
|
||||||
status: completed
|
|
||||||
- id: wire-test-class
|
|
||||||
content: 改造 WalletRegisterBetWinTest:记录 uid/round/biz_id,tearDown 输出摘要
|
|
||||||
status: completed
|
|
||||||
- id: config-and-doc
|
|
||||||
content: phpunit.xml 增加 WALLET_TEST_UID/REPORT_DIR;更新 doc 执行说明
|
|
||||||
status: completed
|
|
||||||
- id: verify-run
|
|
||||||
content: 容器执行 phpunit 并确认控制台+报告文件含 uid
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 钱包集成测试可追踪报告方案
|
|
||||||
|
|
||||||
## 问题
|
|
||||||
当前 [tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php) 每个用例都会 `makeUid()` 生成随机用户,PHPUnit 默认只显示 `OK (3 tests, 37 assertions)`,**看不到本次用了哪个 `uid`、`round_id`、`biz_id`**,联调排障和 DB 核对都不方便。
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
- 跑完测试后,**控制台**能看到每个用例的测试用户与关键业务 ID。
|
|
||||||
- **落盘一份报告**(JSON + 可读 Markdown),便于复制 `uid` 去查库或手工 curl 复现。
|
|
||||||
- 可选:通过环境变量固定 `uid`,便于反复验证同一用户。
|
|
||||||
|
|
||||||
## 实现方案
|
|
||||||
|
|
||||||
### 1. 新增测试上下文与报告器
|
|
||||||
新增 [tests/Support/WalletTestRunContext.php](tests/Support/WalletTestRunContext.php):
|
|
||||||
- 字段:`testName`, `uid`, `currency`, `roundId`, `bizIds`(register/bet/win_mid/win_final), `traceId`, `steps[]`(每步 API、code、余额快照), `status`, `errorMsg`。
|
|
||||||
|
|
||||||
新增 [tests/Support/WalletTestReporter.php](tests/Support/WalletTestReporter.php):
|
|
||||||
- `startRun()` / `recordStep()` / `finishTest()` / `writeReport()`
|
|
||||||
- 报告目录:`runtime/test-reports/`(文件名含时间戳,如 `wallet-register-bet-win-20260515-160530.json` 与同名 `.md`)
|
|
||||||
- Markdown 表格示例列:用例名 | uid | round_id | biz_id 列表 | 结果 | 最终余额(deposit/withdraw/b)
|
|
||||||
|
|
||||||
### 2. 改造现有 Feature 测试
|
|
||||||
在 [tests/Feature/WalletRegisterBetWinTest.php](tests/Feature/WalletRegisterBetWinTest.php) 中:
|
|
||||||
- `setUp()`:初始化 reporter;若存在 `WALLET_TEST_UID` 则使用该固定 uid(否则继续随机)。
|
|
||||||
- 每个 `test*` 开始:创建 `WalletTestRunContext` 并记录 uid/round_id/biz_id。
|
|
||||||
- `postJson()` / `wallet()`:成功后把 `code`、关键 `data`、查询余额写入 `steps`。
|
|
||||||
- `tearDown()`:标记 pass/fail,调用 `writeReport()`;**向 STDOUT 打印一行摘要**(PHPUnit 控制台可见),例如:
|
|
||||||
- `[WalletTest] testRegisterBetWinFlow uid=90012345 round=r_... bet=bet_... => PASS`
|
|
||||||
|
|
||||||
失败时 assertion message 附带 context 摘要,便于一眼定位用户。
|
|
||||||
|
|
||||||
### 3. 配置与文档
|
|
||||||
- [phpunit.xml](phpunit.xml) 增加可选 env:
|
|
||||||
- `WALLET_TEST_UID`(空=随机)
|
|
||||||
- `WALLET_TEST_REPORT_DIR`(默认 `runtime/test-reports`)
|
|
||||||
- `WALLET_TEST_VERBOSE`(`1` 时打印每步明细)
|
|
||||||
- 更新 [doc/register-bet-win-test.md](doc/register-bet-win-test.md) §7:
|
|
||||||
- 报告路径说明
|
|
||||||
- 固定 uid 复现示例:`WALLET_TEST_UID=90012345 docker compose exec ... phpunit ...`
|
|
||||||
|
|
||||||
### 4. 执行验证
|
|
||||||
容器内执行:
|
|
||||||
```bash
|
|
||||||
docker compose exec -T -w /app/www/ray/slot-wallet php82 php vendor/bin/phpunit --filter WalletRegisterBetWinTest
|
|
||||||
```
|
|
||||||
验收:
|
|
||||||
- 控制台出现每个用例的 `uid` 摘要行
|
|
||||||
- `runtime/test-reports/` 生成 `.json` + `.md`
|
|
||||||
- 测试仍全部通过(3 tests)
|
|
||||||
|
|
||||||
## 报告结构示意
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
testCase[WalletRegisterBetWinTest] --> context[WalletTestRunContext]
|
|
||||||
context --> reporter[WalletTestReporter]
|
|
||||||
reporter --> stdout[ConsoleSummary]
|
|
||||||
reporter --> jsonFile[runtime/test-reports/*.json]
|
|
||||||
reporter --> mdFile[runtime/test-reports/*.md]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 不在本阶段做的(可选后续)
|
|
||||||
- HTML 可视化报告、CI artifact 上传
|
|
||||||
- DB 直连断言(wallet_log / wallet_fund_lot)
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
name: 玩家任务进度查询
|
|
||||||
overview: 新增一个面向玩家的只读接口,返回当前 Bonus/Deposit 任务及其打码进度与详情,数据口径以 PRD 定义的 `wallet_fund_lot` 为准。保持现有资金主流程不变,仅补充查询链路与文档说明。
|
|
||||||
todos:
|
|
||||||
- id: define-player-task-endpoint
|
|
||||||
content: 新建独立玩家任务查询 Controller(不改 WalletController),并完成入参校验与统一返回
|
|
||||||
status: pending
|
|
||||||
- id: add-lot-query-service
|
|
||||||
content: 在 wallet 域新增只读查询服务,按 PRD 聚合 bonus/deposit 当前任务视图并计算进度字段
|
|
||||||
status: pending
|
|
||||||
- id: extend-fund-lot-model
|
|
||||||
content: 在 WalletFundLotModel 增加面向玩家任务页的查询方法(按币种、lot_type、状态过滤与排序)
|
|
||||||
status: pending
|
|
||||||
- id: document-api-contract
|
|
||||||
content: 更新 doc/wallet.md,补充玩家任务进度查询接口契约与状态语义
|
|
||||||
status: pending
|
|
||||||
- id: verify-key-scenarios
|
|
||||||
content: 按基础与边界场景验证返回结构、进度计算和状态映射
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 玩家查看Bonus/Deposit任务与打码进度计划
|
|
||||||
|
|
||||||
## 目标与口径
|
|
||||||
- 提供玩家侧查询能力:查看“当前仍在生命周期内”的 `Bonus` 与 `Deposit` 任务,以及每条任务的进度与关键详情。
|
|
||||||
- 数据源以 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php) 为准,不再使用旧 `wager_task` 作为玩家任务主视图。
|
|
||||||
- 任务状态遵循 PRD(`Waiting/Active/PendingConversion/PlayedOut` 等),并输出前端可直接消费的进度字段。
|
|
||||||
|
|
||||||
## 接口设计(新增)
|
|
||||||
- 新建独立 Controller 承载玩家查询接口(建议新增 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/PlayerTaskController.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/PlayerTaskController.php) 并提供 `taskProgress` 方法),不在 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/WalletController.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/WalletController.php) 增加行为。
|
|
||||||
- 入参:`uid`、`currency`(可复用 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/validator/WalletValidator.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/validator/WalletValidator.php) 现有校验,或为新 Controller 补充专用 scene/validator)。
|
|
||||||
- 出参建议:
|
|
||||||
- `summary`:当前 `bonus_task_count`、`deposit_task_count`。
|
|
||||||
- `bonus_tasks[]` 与 `deposit_tasks[]`:每条含 `lot_id`、`lot_no`、`status`、`status_text`、`source_type`、`source_id`、`original_amount`、`remaining_amount`、`required_wager`、`current_wager`、`left_wager`、`progress_rate`、`created_at`、`completed_at`、`brief`。
|
|
||||||
- “当前任务”默认筛选为:`status in (Waiting, Active, PendingConversion, PlayedOut)`;不返回 `Completed/Cancelled/Reversed`(避免历史噪音)。
|
|
||||||
|
|
||||||
## 分层落地
|
|
||||||
- Logic/Service 层新增只读查询编排(建议放在 wallet 域 service,控制器不直接拼查询):
|
|
||||||
- 查询指定用户指定币种的 Deposit/Bonus Lots;
|
|
||||||
- 按类型分组并按 `consume_priority_at, id` 排序;
|
|
||||||
- 统一计算衍生字段:
|
|
||||||
- `left_wager = max(required_wager - current_wager, 0)`
|
|
||||||
- `progress_rate = required_wager > 0 ? min(current_wager / required_wager, 1) : 1`
|
|
||||||
- 统一状态文案映射(与 PRD 对齐)。
|
|
||||||
- Model 层在 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php) 补充专用查询方法(如按币种+lot_type+status 列表查询),保持 Controller/Logic 不下沉 SQL 细节。
|
|
||||||
|
|
||||||
## 文档与兼容
|
|
||||||
- 在 [`/Users/ray/Documents/project/www/ray/slot-wallet/doc/wallet.md`](/Users/ray/Documents/project/www/ray/slot-wallet/doc/wallet.md) 的 API 建议章节补充“玩家任务进度查询”示例(字段说明与状态语义)。
|
|
||||||
- 明确该接口是玩家视图;旧 [`/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/TaskController.php`](/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/TaskController.php) 维持兼容,不做破坏式改造。
|
|
||||||
|
|
||||||
## 验证计划
|
|
||||||
- 基础场景:同时存在 Bonus/Deposit Active 任务,返回分组正确、进度计算正确。
|
|
||||||
- 边界场景:
|
|
||||||
- `required_wager=0`(进度应视为 100%);
|
|
||||||
- `current_wager > required_wager`(进度封顶 100%);
|
|
||||||
- 仅有 PlayedOut Bonus(仍应展示,方便玩家理解“已用完未转化”);
|
|
||||||
- 无当前任务(返回空数组与计数 0)。
|
|
||||||
- 一致性检查:字段值与 `wallet_fund_lot` 原始记录一致,且状态解释符合 PRD。
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
---
|
|
||||||
name: 跨服务 slot_sdk 约束
|
|
||||||
overview: 在用户级 Cursor 规则目录新增一条「跨服务通信」约束,用统一、可操作的术语规定:业务服务之间的 HTTP 互调必须经 `slot/sdk`(`slotsdk`)完成,并与现有 `backend-layering` 规则互补而不重复。
|
|
||||||
todos:
|
|
||||||
- id: create-mdc
|
|
||||||
content: 新建 /Users/ray/.cursor/rules/cross-service-sdk.mdc(alwaysApply + 术语与调用规范)
|
|
||||||
status: completed
|
|
||||||
- id: consistency-check
|
|
||||||
content: 对照 backend-layering.mdc 确认交叉引用一致、无重复分层表
|
|
||||||
status: completed
|
|
||||||
- id: optional-readme
|
|
||||||
content: (可选)在 slot_sdk/readme.md 增加简短架构说明
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 跨服务通信 Cursor 用户级约束
|
|
||||||
|
|
||||||
## 背景与目标
|
|
||||||
|
|
||||||
当前用户级规则在 [`/Users/ray/.cursor/rules/`](file:///Users/ray/.cursor/rules/) 已有:
|
|
||||||
|
|
||||||
- [`backend-layering.mdc`](file:///Users/ray/.cursor/rules/backend-layering.mdc) — 单服务内 Controller / Logic / Service 分层
|
|
||||||
- [`dev-environment.mdc`](file:///Users/ray/.cursor/rules/dev-environment.mdc) — Docker 本地开发
|
|
||||||
- [`php-doc.mdc`](file:///Users/ray/.cursor/rules/php-doc.mdc) — PHPDoc 规范
|
|
||||||
|
|
||||||
[`backend-layering.mdc`](file:///Users/ray/.cursor/rules/backend-layering.mdc) 仅在 Service 层职责里顺带提到「sdk」,**没有**规定跨服务 HTTP 的入口、命名与新增 API 的流程。本次新增**独立规则**(单一职责,符合 create-rule 实践),`alwaysApply: true`,与现有三条规则一致。
|
|
||||||
|
|
||||||
代码库事实(供规则用语对齐):
|
|
||||||
|
|
||||||
- 包名:`slot/sdk`,命名空间 `slotsdk\`,仓库 [`slot_sdk`](file:///Users/ray/Documents/project/www/slot/slot_sdk)
|
|
||||||
- 典型消费方:`slot_admin`、`slot_agent`、`slot_console`、`slot_pwa`(composer 依赖 `slot/sdk`)
|
|
||||||
- 典型被调方:`slot_wallet`、`slot_user`、`slot_center` 等,对外暴露 `innerapi/*`、`api/*`
|
|
||||||
- 推荐调用链:`new {Domain}Client($config)->service()->{method}(...)`
|
|
||||||
- 项目内已有表述:[`slot_agent/doc/feature_agent.md`](file:///Users/ray/Documents/project/www/slot/slot_agent/doc/feature_agent.md) —「代理服不直连用户域表,统一通过 `slot_sdk` 调用户服 innerapi」
|
|
||||||
|
|
||||||
按你的选择:**规则只约束新代码走 slot_sdk,不写 InnerCurlService / slot_lib 等 legacy 迁移条款。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 术语优化(写入规则正文)
|
|
||||||
|
|
||||||
| 避免说法 | 推荐说法 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 后端服务之间调用 / 中转 | **跨服务 HTTP 调用** | 明确是进程间 HTTP,不是本地 Logic/Service |
|
|
||||||
| 通过 slot/sdk 中转 | **经 slot_sdk 调用** | `slot_sdk` = 客户端库;被调服务仍直接处理请求,库不做业务中转 |
|
|
||||||
| SDK / 封装 | **slot_sdk(`slot/sdk`)** | 与 composer 包名、仓库目录一致 |
|
|
||||||
| 各服务自己拼 URL | **在 slot_sdk 增加 `{Domain}Service` 方法** | 路径与 DTO 单点维护 |
|
|
||||||
| `app\service\WalletService` | **本地 WalletService** vs **slotsdk WalletService** | 防止与 SDK 类名混淆 |
|
|
||||||
|
|
||||||
核心定义(规则开篇 1 段):
|
|
||||||
|
|
||||||
> **slot_sdk** 是跨服务 HTTP 客户端库(`composer` 包 `slot/sdk`,命名空间 `slotsdk\`),用于**调用方服务**访问**被调服务**的 `innerapi/*` 或 `api/*` 接口。它不是独立部署的微服务。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 拟新增文件
|
|
||||||
|
|
||||||
**路径:** [`/Users/ray/.cursor/rules/cross-service-sdk.mdc`](/Users/ray/.cursor/rules/cross-service-sdk.mdc)
|
|
||||||
|
|
||||||
**Frontmatter:**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
description: 跨服务 HTTP 须经 slot_sdk(slot/sdk)调用,禁止在业务服务内散落直连
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
**正文结构(约 35–45 行,中文为主):**
|
|
||||||
|
|
||||||
### 1. 适用范围
|
|
||||||
|
|
||||||
- 一个 Webman 服务需要 HTTP 访问另一个服务的 `innerapi` / `api` 时适用。
|
|
||||||
- **被调服务**自身实现 Controller/Logic/Model,**不**为「被别人调」而引入 `slot/sdk`。
|
|
||||||
- **调用方**若需调第三个服务,必须通过 `slot/sdk`(不在业务代码里手写 Guzzle/curl/拼 host+path)。
|
|
||||||
|
|
||||||
### 2. 标准调用方式
|
|
||||||
|
|
||||||
```php
|
|
||||||
use slotsdk\Config as SDKConfig;
|
|
||||||
use slotsdk\service\wallet\WalletClient;
|
|
||||||
|
|
||||||
$config = new SDKConfig([
|
|
||||||
'host' => ShareConfigService::get('walletApiHost'),
|
|
||||||
'headers' => ['server-name' => config('app.server_name')],
|
|
||||||
]);
|
|
||||||
$result = (new WalletClient($config))->service()->getStatistics($uids, $currency);
|
|
||||||
```
|
|
||||||
|
|
||||||
要点:
|
|
||||||
|
|
||||||
- Host 来自 center 下发的 `*ApiHost`(如 `walletApiHost`、`userApiHost`)。
|
|
||||||
- 请求头带 `server-name`,值为**当前调用方**服务名。
|
|
||||||
- 非零 `code` 由 `slotsdk\exception\ApiException` 抛出,调用方在 Logic/Gateway 层处理。
|
|
||||||
|
|
||||||
### 3. 新增 / 变更远程接口的流程
|
|
||||||
|
|
||||||
1. 在 [`slot_sdk/src/service/{domain}/`](file:///Users/ray/Documents/project/www/slot/slot_sdk/src/service/) 增加 `{Domain}Service` 方法、路径常量、必要时 `entity/*Entity`。
|
|
||||||
2. 在调用方 Logic 或 `*GatewayService` 中调用;Controller 保持薄。
|
|
||||||
3. **禁止**在 `slot_admin` / `slot_agent` 等多仓库重复写同一路径字符串。
|
|
||||||
|
|
||||||
### 4. 与 backend-layering 的衔接(1–2 句)
|
|
||||||
|
|
||||||
- 跨服务访问在调用方落在 **Service 或 `*GatewayService`**,Logic 编排用例;不把 HTTP 细节散落在 Controller。
|
|
||||||
- 与 [`backend-layering.mdc`](file:///Users/ray/.cursor/rules/backend-layering.mdc) 中「Service 可承载 sdk」一致;本规则专门约束**跨服务边界**,不重复写分层表。
|
|
||||||
|
|
||||||
### 5. 命名与混淆规避
|
|
||||||
|
|
||||||
- SDK:`slotsdk\service\{domain}\{Domain}Client`、`{Domain}Service`、`entity\{Name}Entity`。
|
|
||||||
- 本地:`app\service\*`;若同名,用 `SDKConfig`、完整 namespace 或 import alias。
|
|
||||||
- 消费方封装重复调用:`UserReferralGatewayService` 这类 `*GatewayService` 模式。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 与现有规则的关系
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph userRules [用户级 .cursor/rules]
|
|
||||||
layering[backend-layering]
|
|
||||||
crossSdk[cross-service-sdk 新增]
|
|
||||||
docker[dev-environment]
|
|
||||||
phpdoc[php-doc]
|
|
||||||
end
|
|
||||||
layering -->|"单服务内分层"| Logic
|
|
||||||
crossSdk -->|"服务间 HTTP"| slot_sdk
|
|
||||||
slot_sdk --> innerapi[被调服务 innerapi/api]
|
|
||||||
```
|
|
||||||
|
|
||||||
- **不修改** [`backend-layering.mdc`](file:///Users/ray/.cursor/rules/backend-layering.mdc):避免一条规则过长;仅在 `cross-service-sdk` 末尾用交叉引用衔接。
|
|
||||||
- **不修改** [`dev-environment.mdc`](file:///Users/ray/.cursor/rules/dev-environment.mdc) / [`php-doc.mdc`](file:///Users/ray/.cursor/rules/php-doc.mdc)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实施步骤(确认计划后执行)
|
|
||||||
|
|
||||||
1. 创建 [`cross-service-sdk.mdc`](/Users/ray/.cursor/rules/cross-service-sdk.mdc),填入上述 frontmatter 与正文。
|
|
||||||
2. 通读四条 `alwaysApply` 规则,确认无矛盾表述(尤其 Service 层与 Gateway 分工)。
|
|
||||||
3. (可选)在 [`slot_sdk/readme.md`](file:///Users/ray/Documents/project/www/slot/slot_sdk/readme.md) 补 5–10 行架构说明并链到 Cursor 规则 — **仅当你希望仓库内也有文档镜像**;非本次必需。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收标准
|
|
||||||
|
|
||||||
- 新开 Cursor 会话、编辑任意 slot PHP 文件时,Agent 应自动带上「跨服务须经 slot_sdk、新 API 先改 slot_sdk」约束。
|
|
||||||
- 术语统一使用:**跨服务 HTTP**、**调用方 / 被调方**、**slot_sdk**,避免「中转」「服务间随便 HTTP」等模糊说法。
|
|
||||||
- 规则正文不含 legacy 迁移条款(按你的选择)。
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
---
|
|
||||||
name: 首充定格资金修复
|
|
||||||
overview: 本次仅改 slot_wallet 与 slot_console。wallet 删除 Recharge bus、首充发 free_credit_init;console 消费该事件完成定格。pay 等其它服务不在本次范围。
|
|
||||||
todos:
|
|
||||||
- id: wallet-remove-recharge-bus
|
|
||||||
content: 删除 WalletLogic recharge/rechargeSign 中 sendConsoleBus('Recharge')
|
|
||||||
status: completed
|
|
||||||
- id: wallet-send-free-credit-init
|
|
||||||
content: recharge + rechargeSign 首充时发送 free_credit_init(inc/改账前采 balance_before_qf)
|
|
||||||
status: completed
|
|
||||||
- id: wallet-remove-create-wager-first-recharge
|
|
||||||
content: CreateWagerTask 删除 version1/version2 首充特殊打码分支,首充走普通充值打码
|
|
||||||
status: completed
|
|
||||||
- id: console-event-bus-handler
|
|
||||||
content: EventBus case free_credit_init + FreeCreditInitEvent
|
|
||||||
status: completed
|
|
||||||
- id: console-free-credit-init-logic
|
|
||||||
content: handleFreeCreditInit;RechargeEvent 移除首充定格,保留档位推进
|
|
||||||
status: completed
|
|
||||||
- id: console-safe-freeze-order
|
|
||||||
content: freezeFirstRecharge 先 RPC 后落库;上限与幂等
|
|
||||||
status: completed
|
|
||||||
- id: mq-reliability
|
|
||||||
content: free_credit_init 失败 nack/requeue(仅 console EventBus)
|
|
||||||
status: completed
|
|
||||||
- id: tests-and-repair
|
|
||||||
content: wallet/console 单测与集成测;FreeCreditsFreezeDev 补偿(console)
|
|
||||||
status: completed
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 首充定格资金操作修复方案(修订 v4)
|
|
||||||
|
|
||||||
## 变更范围(硬约束)
|
|
||||||
|
|
||||||
**本次仅编辑以下仓库/服务,不修改任何其它服务(含 slot_pay、slot_lib 等):**
|
|
||||||
|
|
||||||
| 在范围内 | 不在范围内 |
|
|
||||||
|----------|------------|
|
|
||||||
| `slot_wallet` | `slot_pay` |
|
|
||||||
| `slot_console` | `slot_lib`、`slot_agent`、… |
|
|
||||||
|
|
||||||
> `Recharge` 总线消息假定由 **pay 或其它既有链路** 发送;本次不从 wallet 重复发送,也**不改 pay** 去补发。若线上 pay 未发 `Recharge`,统计/档位问题需另开 pay 任务,**不纳入本 PR**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 设计原则
|
|
||||||
|
|
||||||
> **情愿用户定格失败,也不能让系统亏钱。**
|
|
||||||
|
|
||||||
| 服务(本次) | 职责 |
|
|
||||||
|--------------|------|
|
|
||||||
| **slot_wallet** | 入账;首充发 **`free_credit_init`**;**删除** `Recharge` bus;**移除** `CreateWagerTask` 旧首充打码拆分 |
|
|
||||||
| **slot_console** | 消费 `free_credit_init` → 定格扣款 + 活动落库;`RechargeEvent` **不再**做首充定格 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目标架构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Ext as 外部_pay等_本次不改
|
|
||||||
participant Wallet as slot_wallet
|
|
||||||
participant MQ as console_bus
|
|
||||||
participant Console as slot_console
|
|
||||||
|
|
||||||
Ext->>Wallet: recharge / rechargeSign
|
|
||||||
Wallet->>Wallet: 采 balance_before_qf,入账
|
|
||||||
Wallet->>MQ: free_credit_init
|
|
||||||
Ext->>MQ: Recharge(本次不实现)
|
|
||||||
MQ->>Console: FreeCreditInitEvent
|
|
||||||
Console->>Wallet: freeCreditsFreeze RPC
|
|
||||||
Console->>Console: player/package 落库
|
|
||||||
MQ->>Console: RechargeEvent(既有逻辑,无定格)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 1. slot_wallet
|
|
||||||
|
|
||||||
#### 1.1 删除 `Recharge` bus
|
|
||||||
|
|
||||||
从 [`WalletLogic::recharge()`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php)、[`rechargeSign()`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 移除:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$this->sendConsoleBus('Recharge', $this->requestDTO->recharge);
|
|
||||||
```
|
|
||||||
|
|
||||||
保留 `sendConsoleBus('reward', ...)` 及其它非 Recharge 类型(本次不动)。
|
|
||||||
|
|
||||||
#### 1.2 `maybeSendFreeCreditInit()` + 扩展 `sendConsoleBus`
|
|
||||||
|
|
||||||
- 改账/ `inc()` **前**:`balance_before_qf`、`is_first_recharge`(`total_deposit == 0`,在 `statModel->inc` 之前判断)。
|
|
||||||
- 改账成功后:`is_first_recharge && balance_before_qf > 0` 时发送:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$this->sendConsoleBus('free_credit_init', 0, [
|
|
||||||
'balance_before_qf' => $balanceBeforeQf,
|
|
||||||
'recharge_amount' => $this->requestDTO->recharge,
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [`sendConsoleBus()`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 增加可选参数 `array $extraData = []`。
|
|
||||||
|
|
||||||
#### 1.3 `recharge()` 与 `rechargeSign()` 均接入
|
|
||||||
|
|
||||||
签到购买 [`rechargeSign()`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 与普通 [`recharge()`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 使用同一套 `maybeSendFreeCreditInit()` 逻辑。
|
|
||||||
|
|
||||||
#### 1.4 移除 `CreateWagerTask` 旧首充打码逻辑(已废弃)
|
|
||||||
|
|
||||||
[`app/command/CreateWagerTask.php`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/command/CreateWagerTask.php) 在 **Free Credits 上线前** 于首充时本地拆分余额打码,与现「console 定格 + 分档释放」重复且易冲突,**本次删除**。
|
|
||||||
|
|
||||||
**旧逻辑位置**(条件均为 `RechargeExchangeService::total` 累计充值等于本笔 `recharge_amount`,即首充):
|
|
||||||
|
|
||||||
| 方法 | 行号(约) | 行为 |
|
|
||||||
|------|-----------|------|
|
|
||||||
| `version1()` | L87–140 | `SOURCE_TYPE_FIRST_RECHARGE` 任务 + `SOURCE_TYPE_FREE` 免打码任务 + `SOURCE_TYPE_FIRST_LEFT`「首充剩余」打码 |
|
|
||||||
| `version2()` | L186–244 | 首充充值/赠送打码 + `SOURCE_TYPE_FIRST_LEFT`(`first_recharge_left` 系数) |
|
|
||||||
|
|
||||||
**与新方案关系**:
|
|
||||||
|
|
||||||
- 充值前免费余额 → 由 console `free_credit_init` → `freeCreditsFreeze` 扣出活动池(不再在 wallet 侧拆 `FREE` / `FIRST_LEFT` 任务)。
|
|
||||||
- 免打码第一档 / 后续释放 → 由 console `FreeCreditsLogic` + `freeCreditsClaim` 创建 Y1(`WalletLogic::freeCreditsClaim` 内 `createTask`)。
|
|
||||||
- 首充**本笔充值金额**的打码 → 与其它充值相同,走 `elseif ($dto->required_wager > 0)` 通用分支即可。
|
|
||||||
|
|
||||||
**改动要点**:
|
|
||||||
|
|
||||||
1. 删除 `version1` / `version2` 中整段 `if ($entity->recharge > 0 && $entity->recharge == $dto->recharge_amount) { ... }`。
|
|
||||||
2. 首充与普通充值统一落入后续 `elseif ($dto->required_wager > 0)`(`version1` L142+、`version2` L246+)。
|
|
||||||
3. **保留**其中对 `SOURCE_TYPE_BUY_SIGN`(购买签到解锁额度为 0)的处理——该逻辑在 `elseif` 分支内已有,无需首充专用块。
|
|
||||||
4. 删除后确认无引用孤立的 `SOURCE_TYPE_FIRST_RECHARGE` / `SOURCE_TYPE_FIRST_LEFT` 首充专用路径(常量可保留供历史任务读)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. slot_console
|
|
||||||
|
|
||||||
#### 2.1 EventBus 注册 `free_credit_init`
|
|
||||||
|
|
||||||
[`EventBus::deal()`](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php) 增加显式分支(类名不能走 default 动态加载):
|
|
||||||
|
|
||||||
```php
|
|
||||||
case 'free_credit_init':
|
|
||||||
(new FreeCreditInitEvent())->handle($busEntity);
|
|
||||||
break;
|
|
||||||
```
|
|
||||||
|
|
||||||
新建 [`app/command/event/FreeCreditInitEvent.php`](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/event/FreeCreditInitEvent.php)。
|
|
||||||
|
|
||||||
#### 2.2 `FreeCreditsLogic::handleFreeCreditInit`
|
|
||||||
|
|
||||||
- 入参:`uid`、`balance_before_qf`、`wallet_amount`、`orderId`(来自 bus `data`)。
|
|
||||||
- `frozenAmount = max(balance_before_qf, 0)`;**不以**充值后再读余额反推为主路径。
|
|
||||||
- 活动未开启 / 已定格 → 幂等 return。
|
|
||||||
- 调用调整后的 `freezeFirstRecharge()`。
|
|
||||||
|
|
||||||
#### 2.3 调整 `freezeFirstRecharge`(先扣款、后落库)
|
|
||||||
|
|
||||||
[`freezeFirstRecharge()`](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php):
|
|
||||||
|
|
||||||
1. 幂等:已有 `free_credits_freeze:{orderId}` 流水则跳过 RPC。
|
|
||||||
2. `frozenAmount = min(事件金额, RPC 前当前可扣余额)`;≤0 不扣。
|
|
||||||
3. **先** `freeCreditsFreeze` RPC,**后** `Db::transaction` 写 player/packages。
|
|
||||||
4. RPC 失败 → 不落库,**抛异常**。
|
|
||||||
|
|
||||||
#### 2.4 `RechargeEvent` 去掉首充定格
|
|
||||||
|
|
||||||
[`RechargeEvent::handle()`](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php) **删除** L73-82 对 `handleRecharge` 的调用(首充定格改由 `free_credit_init` 触发)。
|
|
||||||
|
|
||||||
保留并可继续调用 **仅档位推进** 的逻辑,例如:
|
|
||||||
|
|
||||||
- 新增 `FreeCreditsLogic::advanceAfterRecharge($uid, $walletAmount)`,或
|
|
||||||
- `handleRecharge` 内去掉首充 `freezeFirstRecharge` 分支,仅保留 `advanceByRecharge`(供既有 `Recharge` 消息使用)。
|
|
||||||
|
|
||||||
统计、黑名单、代理首充等 **RechargeEvent 现有代码不动**(本次范围外行为保持)。
|
|
||||||
|
|
||||||
#### 2.5 MQ 可靠性(仅 console)
|
|
||||||
|
|
||||||
- `FreeCreditInitEvent` / `free_credit_init`:异常上抛;`EventBus` 对该 type 失败时 **nack/requeue**(需对齐现有 consumer)。
|
|
||||||
- `Recharge` 路径统计块仍可独立 try/catch(本次不改 pay 发消息前提)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 测试(仅 wallet + console)
|
|
||||||
|
|
||||||
| 用例 | 位置 |
|
|
||||||
|------|------|
|
|
||||||
| wallet 删除 Recharge bus | slot_wallet |
|
|
||||||
| wallet 首充发 `free_credit_init`(含 rechargeSign) | slot_wallet |
|
|
||||||
| 首充不再走 CreateWagerTask 特殊分支 | slot_wallet CreateWagerTask |
|
|
||||||
| `FreeCreditInitEvent` 定格成功 | slot_console 集成测 |
|
|
||||||
| RPC 失败不落库 / 先扣后落库 / 幂等 | slot_console |
|
|
||||||
| `RechargeEvent` 不再触发定格 | 调整 [`FreeCreditsHandleRechargeTest`](file:///Users/ray/Documents/project/www/slot/slot_console/tests/Unit/FreeCreditsHandleRechargeTest.php) 等 |
|
|
||||||
|
|
||||||
**不新增** pay 侧联调用例。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键改动文件(仅此两份)
|
|
||||||
|
|
||||||
**slot_wallet**
|
|
||||||
|
|
||||||
- [`app/api/logic/WalletLogic.php`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php)
|
|
||||||
- [`app/command/CreateWagerTask.php`](file:///Users/ray/Documents/project/www/slot/slot_wallet/app/command/CreateWagerTask.php)
|
|
||||||
|
|
||||||
**slot_console**
|
|
||||||
|
|
||||||
- [`app/command/EventBus.php`](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php)
|
|
||||||
- `app/command/event/FreeCreditInitEvent.php`(新建)
|
|
||||||
- [`app/api/logic/FreeCreditsLogic.php`](file:///Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php)
|
|
||||||
- [`app/command/event/RechargeEvent.php`](file:///Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php)
|
|
||||||
- 相关 tests(console 仓内)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收标准(本 PR)
|
|
||||||
|
|
||||||
- **wallet**:`recharge` / `rechargeSign` 不再发送 `type=Recharge`;首充且 inc 前有免费余额时发送 `free_credit_init`,`balance_before_qf` 正确。
|
|
||||||
- **wallet**:首充不再触发 `CreateWagerTask` 的 `FIRST_RECHARGE` / `FREE` / `FIRST_LEFT` 拆分,仅按本笔充值/赠送金额走通用打码任务。
|
|
||||||
- **console**:收到 `free_credit_init` 后完成定格扣款与落库;扣款 ≤ 事件金额且 ≤ 可扣余额;幂等。
|
|
||||||
- **console**:`RechargeEvent` 不再执行首充定格;已定格用户经 `Recharge` 仍可 `advanceByRecharge`(依赖外部 pay 发消息,**本 PR 不验证 pay**)。
|
|
||||||
- **范围**:`git diff` 仅涉及 `slot_wallet`、`slot_console` 路径。
|
|
||||||
Reference in New Issue
Block a user