Files
cursor/plans/free_credits_后台统计页_9d6edc45.plan.md
2026-05-21 18:16:26 +08:00

317 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

---
name: 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 新建 FreeCreditsServicebasePath=innerapi/free-credits含 list + statistics
status: completed
- id: slot-admin-controller
content: slot_admin 新建 game/controller/FreeCreditsStatsController.phpindex 一次调两个 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.vuesa-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.status0/1/2/3/4/5/6/7/10
- `first_cash_status` 数组package.status0/1/2/3/4/5限定 `package_no=1`
- `is_completed` 0/1status=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`(千分位整数)传到 VueVue 用 `/ 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'(活动只面向美国 RMGcurrency 字段后续如多币种再扩展)
- 一次性聚合查询,避免 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()` actionV1 不做导出)
---
### 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 校验路径回归