first commit
This commit is contained in:
317
plans/free_credits_后台统计页_9d6edc45.plan.md
Normal file
317
plans/free_credits_后台统计页_9d6edc45.plan.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
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 校验路径回归
|
||||
Reference in New Issue
Block a user