first commit

This commit is contained in:
ray zhou
2026-05-21 18:16:26 +08:00
commit 033aa0ae69
324 changed files with 24705 additions and 0 deletions

View 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 新建 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 校验路径回归