14 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| free credits 后台统计页 | 实现需求 §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 模式。 |
|
false |
数据流
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]→whereBetweenstatus数组(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_completed0/1(status=10 与否)page,limit,orderBy,orderType
排序
- DB 可排序:
frozen_amount_qf、first_recharge_time(默认first_recharge_time desc) - 其它字段(进度、剩余、累计充值)派生,前端不开启排序
响应
{
"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
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.statuswhereplayer_id=? AND package_no=1progress_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_idfirst_cashout_amount_qf:SUM(amount_qf) WHERE package_type=1 AND status=3 GROUP BY player_idremaining_amount_qf:frozen_amount_qf - first_cashout_amount_qf - claimed_amount_qftotal_recharge_amount_qf:调用WalletService::statistics(uids, currency='USD'),把total_deposit映射进来。currency 默认 'USD'(活动只面向美国 RMG,currency 字段后续如多币种再扩展)
- 一次性聚合查询,避免 N+1:
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(或直接挂在已有 api/logic/FreeCreditsLogic.php,建议新建避免膨胀),Controller 只负责接参 / 调 Logic / 返回。
2. slot_lib — 新增 FreeCreditsService + 补 WalletService
2.1 slot_lib/src/services/FreeCreditsService.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 补 statistics
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
只做接参 + Service 调用 + 合并:
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 新建
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 新建
骨架仿 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:
<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 表(不新建字典):
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):
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既有FreeCreditsLogicC 端逻辑、free_credits_player/free_credits_package表结构、ext_config配置读取等不动s_recharge_gift_config不需要新增列slot_pay不新增 innerapi(不需要订单表精确口径,wallettotal_deposit已足够)- 现有
ActivityController编辑表单(type=11 分支)已落地,不动 RechargeOrderController/withdrawal等无关模块不动
验收(手测)
按 dev-environment 在 docker 内调试:
- 准备 3 个测试账号:A 未定格、B 已定格未提现首档、C 全部完成;各 source 至少一个
- 访问
/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实际行数
- 不传参 → 默认按
- 筛选:
- 选
source=us_01列表与统计同步收敛 - 选
first_recharge_time范围,统计 7 项随之变化 - 选
is_completed=1,列表只剩 C - 选
first_cash_status=3,列表只剩第一档已成功的玩家
- 选
- 排序:分别点击「定格金额」「定格时间」列头,order 切换;其它字段不可排序
- 权限:用未授权账号访问 → 403;授权后正常
- 仅改活动状态时(已存在的活动编辑场景)回归一遍,确认未触发 type=11 ext 校验路径回归