# 36字花移动端接口设计草案(V1) 本文基于 `docs/36字花-数据库与实施计划.md` 与 PRD,先给出移动端可对接的接口清单与字段初设。 口径遵循:**全平台单期号、单开奖结果**;渠道仅用于归属、分润与风控,不拆分对局。 **补充(2026-04)**:§**1.5** 描述服务端 **Redis 热点缓存**(`GameHotDataRedis`),**不改变**各接口 URL、参数与响应字段约定,仅供联调与运维对照。 ## 1. 设计约定 ### 1.1 基础约定 - 协议:HTTPS + JSON - 接口命名规范:`/api/{module}/{action}`,且必须满足正则 `^/api/[a-z]+/[a-z]+[A-Z][a-zA-Z]*$` - **请求方法**:所有移动端业务接口(`/api/*`,不含 `/api/v1/authToken`)一律使用 `POST` 调用;查询类接口同时兼容 `GET`(便于浏览器/调试工具直接访问),客户端统一走 `POST` - `POST` 时请求头 `Content-Type: application/json`,参数放在 JSON body - `GET` 兼容模式下,参数走 URL query string - **例外**:公告模块 `/api/notice/noticeList`、`/api/notice/noticeDetail`、`/api/notice/noticeConfirm` **仅支持 `GET`**,参数一律走 URL query string - 鉴权类接口 `/api/v1/authToken` 仍为 `GET` - 时间:UTC 时间戳(秒) + 服务端时区配置 - 金额:字符串传输(如 `"100.00"`),客户端展示统一保留两位小数(存储仍为 `decimal(18,4)`) - 幂等:关键写接口要求 `idempotency_key` - 请求头(必带): - `auth-token`:通过 `GET /api/v1/authToken` 获取的接口鉴权令牌(含义:接口访问的签名鉴权凭证) - `user-token`:用户登录态令牌;需要登录的接口必带 - 语言请求头: - `lang=zh`:返回中文(默认) - `lang=en`:返回英文 ### 1.2 通用响应结构 ```json { "code": 1, "message": "ok", "data": {} } ``` - `code=1` 表示成功,非 1 为业务错误 - `/api/*` 所有接口返回文案支持中英双语:默认中文;请求头 `lang=en` 返回英文,`lang=zh` 返回中文 - 建议错误码段(按错误性质): - `1000-1099`:参数错误(字段缺失、类型错误、格式错误、超范围) - `1100-1199`:鉴权错误(未登录、token 失效、权限不足) - `2000-2999`:业务错误(余额不足、对局不存在、订单不存在、公告不存在) - `3000-3099`:流程错误(非法流程/状态不允许,如封盘后下注、重复确认、状态跃迁非法) - `5000-5999`:系统错误(服务异常、依赖超时、未知错误) - 推荐基础错误码(首版): - `1`:成功 - `1001`:参数缺失 - `1002`:参数格式错误 - `1003`:参数取值非法 - `1101`:未登录或登录已过期 - `1103`:无权限操作 - `2001`:余额不足 - `2002`:对局不存在 - `2003`:订单不存在 - `2004`:公告不存在 - `3001`:当前流程不允许该操作 - `3002`:已封盘,禁止下注 - `3003`:重复请求(幂等冲突) - `5000`:系统繁忙,请稍后重试 ### 1.3 鉴权方式 - **接口鉴权(auth-token)**:所有移动端业务接口请求时必须携带请求头 `auth-token`(由 `/api/v1/authToken` 签发) - **用户登录鉴权(user-token)**:需要登录的接口携带请求头 `user-token`;token 失效后调用刷新或重新登录 ### 1.4 获取接口鉴权 Token(auth-token) - **GET** `/api/v1/authToken` - 用途:获取 `auth-token`(所有接口请求头必带) 请求示例: `/api/v1/authToken?secret=564d14asdasd113e46542asd6das1a2a×tamp=1776331077&device_id=1&signature=AD84C49880896DBC16C59C7B122D1FF7` 请求参数: - `secret`:string(含义:客户端密钥;服务端从环境变量 `AUTH_TOKEN_SECRET` 校验) - `timestamp`:int(含义:请求时间戳;服务端允许与服务器时间误差 ±300 秒) - `device_id`:string(含义:设备码) - `signature`:string(含义:签名值) 签名算法: - 取参与签名的参数(不含 `signature`):`device_id`、`secret`、`timestamp` - 按参数名 **a-z** 排序后拼接为字符串:`key=value&key=value...` - 计算:`signature = strtoupper(md5(拼接字符串))` 返回参数: - `auth_token`:string(含义:接口鉴权 token;放到请求头 `auth-token`) - `expires_in`:int(含义:有效期秒数) - `server_time`:int(含义:服务器时间戳,用于校时) 可能错误码: - `1001` 参数缺失 - `1002` 参数格式错误 - `1103` 密钥无效/签名错误 - `3001` 时间戳无效 ### 1.5 服务端性能与 Redis 热点缓存(实现说明) > **对客户端无契约变更**:请求路径、参数、响应 JSON 形状与错误码均不因缓存而改变;本节仅说明服务端如何降延迟、读路径与一致性注意点。 **与「框架文件缓存」的区别** | 配置 | 作用域 | |------|--------| | `CACHE_DRIVER`(`config/cache.php`,如 `file`) | Think-ORM / `get_sys_config()` 等**系统参数表 `config`** 的模型缓存,落盘在 `runtime/cache`,**不参与**本游戏业务热点路径。 | | `GAME_HOT_CACHE_*`(`config/game_hot_cache.php`) | 游戏侧 **`user` / `game_config` / `game_record`** 行级 JSON 缓存,走 **`support\Redis`**(`config/redis.php` 连接),键前缀 `dfw:v1:`。 | **服务端缓存覆盖(与移动端直接相关的读路径)** - **用户**:会员鉴权优先读 Redis 中的 `user` 行快照,未命中再查库并回填。**余额、连胜、打码量等变更**落库后,统一经 **`GameHotDataCoordinator::afterUserCommitted($userId)`**:先 **`GameHotDataRedis::userReplaceCacheFromDb`** 与 DB 对齐,再向 Redis 写队列投递幂等刷新任务(见 `GameHotDataWriteQueue` / `GameHotDataQueueConsumer`),用于削峰而非替代同步回源。 - **游戏配置**:`game_config` 按 `config_key` 缓存。后台直连 `Db` 更新时须 **`GameHotDataCoordinator::afterGameConfigKeyCommitted($key)`**(模型 `GameConfig` 事件与独立表单控制器已接入);独立保存接口在写入前对同一 `config_key` 使用 **`GameHotDataLock`(`TYPE_GAME_CONFIG`)** 互斥。勿仅删除缓存键而不回源,否则最长不一致窗口为 TTL。 - **对局**:当前活跃局、按 `id` 的局、最新一条 `game_record` 等;写库后经 **`GameHotDataCoordinator::afterGameRecordCommitted`** 同步刷新相关 Redis 键并入队。开奖/封盘等路径另可按记录 id 使用 **`GameHotDataLock`(`TYPE_GAME_RECORD`)** 串行化。 **环境变量(示例见仓库根目录 `.env-example`)** - `GAME_HOT_CACHE_ENABLED`:是否启用上述 Redis 热点缓存(`false` 时全程回退数据库)。 - `GAME_HOT_CACHE_TTL_GAME_CONFIG` / `GAME_HOT_CACHE_TTL_GAME_RECORD` / `GAME_HOT_CACHE_TTL_USER`:各类缓存 TTL(秒);**以写后同步回源为主**,TTL 仅作兜底。 - `GAME_HOT_CACHE_ENABLE_WRITE_QUEUE` 及队列长度、消费进程间隔等:控制写库后的**幂等刷新任务**是否入队及背压策略(见 `config/game_hot_cache.php`)。 **一致性提示(联调/测试)** - 任何绕过协调入口、只改 DB 不调用 **`GameHotDataCoordinator`** 的手工脚本,都可能与 Redis 短期不一致;生产环境应避免。 - **`POST /api/game/betPlace`** 扣款路径使用与后台钱包加减点相同的 **用户维度 Redis 锁**(`GameHotDataRedis::userAdminMutationLockTry`)及 **`WHERE coin = ?` 条件更新**,与并发派彩/后台调账互斥;失败时返回 **§4.2** 所列中文说明。 - 客户端仍可按 **§3.2 `dictionaryList` 的 `version`** 做本地缓存;服务端字典另有 Redis 加速,二者可同时存在。 --- ## 2. 认证与账户模块(user) ### 2.1 注册 - **POST** `/api/user/register` - 用途:仅手机号注册并绑定邀请归属(admin/channel) 请求参数: - `username`:string,手机号(含义:注册账号,仅支持大陆手机号) - `password`:string,明文经 HTTPS 传输(含义:登录密码,服务端需加盐哈希存储) - `invite_code`:string,必填(含义:子代理邀请码,用于绑定渠道 `channel_id` 与归属) - `device_id`:string,可选(含义:设备标识,用于风控与登录记录) 返回参数: - `user-token`:string(含义:后续接口登录态令牌;用于需要登录的接口请求头) - `refresh_token`:string,可选(含义:用于刷新访问令牌) - `expires_in`:int(秒,含义:令牌有效期) - `user`:object(仅返回非私密信息,不返回 `id`) - `uuid`:string(含义:用户对外唯一标识,10 位) - `username`:string(含义:用户昵称/展示名) - `coin`:string(含义:当前余额) - `channel_id`:int(含义:归属渠道 ID) - `risk_flags`:int(含义:风控状态位) ### 2.2 登录 - **POST** `/api/user/login` 请求参数: - `username`:string(含义:登录账号,当前支持手机号) - `password`:string(含义:登录密码) - `device_id`:string,可选(含义:设备标识,辅助风控) 返回参数: - `user-token`:string(含义:访问令牌;用于需要登录的接口请求头) - `refresh_token`:string,可选(含义:用于刷新访问令牌) - `expires_in`:int(含义:访问令牌剩余有效秒数) - `user`:object(仅返回非私密信息,不返回 `id`) - `uuid`:string(含义:用户对外唯一标识,10 位) - `username`:string(含义:用户昵称/展示名) - `coin`:string(含义:当前余额) - `channel_id`:int(含义:归属渠道 ID) - `risk_flags`:int(含义:风控状态位) ### 2.3 获取当前用户信息 - **POST** `/api/user/profile` 返回参数(金额类字段统一 4 位小数字符串,与 `/api/wallet/balanceSummary` 对齐): **基础档案** - `uuid`:string(含义:用户对外唯一标识,10 位) - `username`:string(含义:昵称) - `head_image`:string(含义:头像地址) - `phone`:string(含义:手机号) - `email`:string(含义:邮箱) - `register_invite_code`:string(含义:注册邀请码快照) - `channel_id`:int(含义:归属渠道 ID) - `risk_flags`:int(含义:风控状态位) - `current_streak`:int(含义:当前连胜次数) - `last_bet_period_no`:string(含义:最近一笔有效下注所在期号) - `create_time`:int(含义:注册时间戳) **资金与提现配额** - `coin` / `coin_balance`:string(含义:当前余额;两字段同值,便于与 `/api/wallet/balanceSummary` 平滑切换) - `frozen_balance`:string(含义:冻结余额;无冻结场景,固定 `0.0000`) - `total_deposit_coin`:string(含义:累计充值) - `total_withdraw_coin`:string(含义:累计提现;受理后累加) - `bet_flow_coin`:string(含义:打码量/流水;开奖结算后按每注 `total_amount` 1:1 累加) - `max_withdrawable`:string(含义:**当前允许发起的单笔最大提现金额** = `min(coin_balance, max_withdraw_by_flow)`) - `withdraw_flow`:object(含义:打码量 / 提现配额诊断快照,结构与 `/api/wallet/balanceSummary.withdraw_flow` 一致,此处额外附 `pending_withdraw`) - `ratio`:string(打码量倍数;`0` 表示不限打码) - `net_deposit`:string(净充值 = max(0, 累计充值 − 累计提现)) - `required_bet_flow`:string(按门槛口径所需打码量,纯展示) - `remaining_bet_flow`:string(按门槛口径还差多少打码量,纯展示) - `eligible`:bool(是否满足整体门槛,纯展示;真正放行以 `max_withdrawable` 为准) - `max_withdraw_by_flow`:string/null(仅按打码量折算的上限;`ratio=0` 时为 `null`) - `flow_unlimited`:bool(是否处于"不限打码"状态) - `pending_withdraw`:object - `count`:int(当前待审核提现订单数) - `max`:int(单用户最多允许的待审核提现数,当前为 `3`;超过 `withdrawCreate` 返回 `code=2004`) ### 2.4 刷新令牌(可选) - **POST** `/api/user/refreshToken` 请求参数: - `refresh_token`:string(含义:续签访问令牌的凭证) 返回参数: - `user-token`:string(含义:新访问令牌) - `expires_in`:int(含义:新令牌有效期) --- ## 3. 游戏大厅与字典模块(game/lobby) ### 3.1 获取首页初始化数据 - **POST** `/api/game/lobbyInit` - 用途:一次返回本局、配置、36字花字典、用户快捷展示 返回参数: - `server_time`:int(含义:服务端当前时间,用于客户端校时) - `period`:object - `period_no`:string(含义:当前全局期号) - `status`:string(`betting`/`locked`/`settling`/`finished`,含义:当前期状态) - `countdown`:int(含义:当前期倒计时秒数) - `lock_at`:int(含义:封盘时间戳) - `open_at`:int(含义:预计开奖时间戳) - `bet_config`:object - `pick_max_number_count`:int(含义:单注最多可选号码数,来自 `game_config.config_key = pick_max_number_count`,缺省与库内种子一致,通常为 10,合法范围 1–36) - `chips`:array[string](如 `["1.00","5.00"]`,含义:快捷筹码面额) - `single_number_max_bet`:string(含义:单号码最大下注额) - `dictionary`:array - `number`:int(1-36,含义:字花编号) - `name`:string(含义:字花名称) - `category`:string(含义:字花分类) - `icon`:string(含义:图标资源地址) - `user_snapshot`:object(`coin`、`current_streak`,含义:用户状态快照) ### 3.2 获取36字花字典(可缓存) - **POST** `/api/game/dictionaryList` 返回参数: - `version`:string(含义:字典版本号,前端可用于缓存比对) - `items`:同 `dictionary`(含义:36字花字典明细) ### 3.3 获取最近开奖记录(近30期) - **POST** `/api/game/periodHistory` 请求参数: - `limit`:int(可选,默认 30,含义:返回最近几期) 返回参数: - `list`:array - `period_no`:string(含义:历史期号) - `result_number`:int(含义:该期开奖结果) - `open_time`:int(含义:开奖时间) --- ## 4. 下注与对局模块(game/bet) ### 4.1 获取当前期详情 - **POST** `/api/game/periodCurrent` 返回参数: - `period_id`:int(含义:当前期主键 ID) - `period_no`:string(含义:当前期号) - `status`:string(含义:当前期状态) - `countdown`:int(含义:当前期剩余秒数) - `bet_close_in`:int(含义:距离封盘剩余秒数) - `result_number`:int/null(未开奖为 null,含义:开奖号码) ### 4.2 提交下注 - **POST** `/api/game/betPlace` - 用途:单期手动下注;玩家只需选择**压注号码**与**本笔压注总金额**。开奖只出一个号码,若该号码 ∈ 所选号码集合即视为**中奖**,派彩按整笔 `bet_amount`(落库为 `total_amount`)× 赔率计算(赔率与连胜倍率见服务端实现)。 请求参数: - `period_no`:string(含义:下注目标期号) - `numbers`:string(含义:本次压注号码集合,**英文逗号分隔**,如 `1,8,16`;每个号码为 1–36 的整数,数量不超过 `pick_max_number_count`(同 `lobbyInit.bet_config`),重复号码会去重) - `bet_amount`:string(含义:**本笔整笔压注金额**,> 0;服务端按此金额从余额扣款并写入注单 `total_amount`,**不再**按「单号金额 × 号码个数」计算) - `idempotency_key`:string(必填,含义:防止重复下单) 返回参数: - `order_no`:string(含义:下注订单号) - `period_no`:string(含义:实际落单期号) - `status`:string(`accepted`/`rejected`,含义:受理结果) - `locked_balance`:string(可选,含义:冻结金额) - `balance_after`:string(含义:下单后余额) - `current_streak`:int(含义:下单后连胜快照) **可能错误码(补充)**(其余见文档头部错误码分段;扣款与缓存一致性强相关): - `5000`:系统繁忙;或 **用户 Redis 互斥锁**未获取(与后台钱包/并发写同一用户串行,文案与后台一致:「该用户正在被其他管理员操作(钱包/并发保存),请稍后再试」);或 **`coin` 条件更新**未命中(并发下注/派彩/后台已改余额:「扣款失败:该用户余额已被其他请求修改(如下注、派彩或其他管理员已保存),请刷新后重试」)。 > 说明:一键重复上一注、自动托管开启/停止均由前端控制,客户端在相应时机调用 `/api/game/betPlace` 即可完成,不再提供独立接口。 ### 4.3 查询我的下注记录(最近1个月) - **POST** `/api/game/betMyOrders` 请求参数: - `page`:int(可选,默认 1) - `page_size`:int(可选,默认 20) 返回参数: - `list`:array - `order_no`:string(含义:下注订单号) - `period_no`:string(含义:所属期号) - `numbers`:array[int](含义:下注号码) - `bet_amount`:string(含义:本笔整笔压注金额,与 `total_amount` 相同) - `total_amount`:string(含义:本笔整笔压注金额) - `result_number`:int/null(含义:开奖号码,未开可空) - `win_amount`:string(含义:中奖金额) - `status`:string(含义:订单状态) - `create_time`:int(含义:下注时间) - `pagination`:object(`page`、`page_size`、`total`,含义:分页信息) --- ## 5. 钱包与资金模块(wallet/finance) ### 5.1 获取钱包摘要 - **POST** `/api/wallet/balanceSummary` 返回参数: - `coin_balance`:string(含义:可用余额,等同于 `user.coin`) - `frozen_balance`:string(含义:冻结余额;当前系统无冻结场景,固定返回 `0.0000`) - `withdrawable_balance`:string(含义:可提现余额;等同于 `coin_balance`,**单笔实际上限以 `max_withdrawable` 为准**) - `max_withdrawable`:string(含义:**当前允许发起的单笔最大提现金额** = min(`coin_balance`, 打码配额余量);客户端直接用于"最大可提 XXX"提示与金额输入上限校验) - `total_deposit_coin`:string(含义:累计充值币额) - `total_withdraw_coin`:string(含义:累计提现币额;提现受理时累加) - `bet_flow_coin`:string(含义:打码量/流水;开奖结算后按每注 `total_amount` 1:1 累加) - `withdraw_flow`:object(含义:打码量 / 提现配额诊断快照,供前端展示说明与上限推导) - `ratio`:string(含义:打码量倍数,来自 `game_config.withdraw_bet_flow_ratio`,默认 `1.0000`;`0` 表示"不限打码") - `net_deposit`:string(含义:净充值 = max(0, 累计充值 - 累计提现)) - `required_bet_flow`:string(含义:按门槛口径所需的打码量 = 净充值 × 倍数,纯展示) - `remaining_bet_flow`:string(含义:按门槛口径还差多少打码量,纯展示;已达标为 `0.0000`) - `eligible`:bool(含义:是否满足整体门槛,纯展示,实际放行以 `max_withdrawable` 为准) - `max_withdraw_by_flow`:string/null(含义:仅按打码量折算的单笔可提上限 = max(0, `bet_flow_coin` / `ratio` - `total_withdraw_coin`);`ratio=0` 不限打码时返回 `null`) - `flow_unlimited`:bool(含义:是否处于"不限打码"状态,`ratio=0` 时为 `true`) 说明(打码量即提现配额模型): - 每笔提现按 `withdraw_coin × ratio` 消耗打码配额;`total_withdraw_coin` 已累积历史消耗。 - **单笔最大可提现 `max_withdrawable = min(coin_balance, max_withdraw_by_flow)`**;`ratio=0` 时退化为仅受余额约束。 - 倍数 `ratio` 由后台「游戏配置 → `withdraw_bet_flow_ratio`」维护,修改后对新请求立即生效。 - 历史累计类字段(`total_deposit_coin` / `total_withdraw_coin` / `bet_flow_coin`)均为累加语义;若后续审核驳回,回冲逻辑由后台审核流程负责。 ### 5.2 钱包流水 - **POST** `/api/wallet/recordList` 请求参数: - `page`:int(可选,默认 1) - `page_size`:int(可选,默认 20) - `type`:string,可选(含义:流水类型筛选,可选值如下;不传表示查询全部) - `deposit`:充值入账(充值订单成功后,金额入账到玩家余额) - `withdraw`:提现出账(提现订单受理/打款后,金额从玩家余额扣除或冻结) - `bet`:下注扣款(提交下注时从玩家余额扣除的投注金额) - `payout`:开奖派彩(中奖后系统将奖金入账到玩家余额) - `adjust`:人工调整(后台管理员加/扣点,对应 `biz_type=admin_credit/admin_deduct`) 返回参数: - `list`:array - `record_id`:int(含义:钱包流水 ID) - `biz_type`:string(含义:业务类型) - `direction`:int(1入2出,含义:资金方向) - `amount`:string(含义:本次变动金额) - `balance_before`:string(含义:变动前余额) - `balance_after`:string(含义:变动后余额) - `ref_type`:string(含义:关联业务单类型) - `ref_id`:string(含义:关联业务单标识) - `create_time`:int(含义:流水时间) 补充约定: - 金额字段(`amount`、`balance_before`、`balance_after` 等)客户端显示统一两位小数。 - 后台管理员加减点会生成 `biz_type=admin_credit/admin_deduct` 的流水记录,备注默认模板:`后台管理员(操作管理员)加点/扣点100(值)`(示例)。 ### 5.3 充值档位列表 - **POST** `/api/finance/depositTierList` 说明: - 由后台「配置管理 → 充值档位」维护,存放在 `game_config.deposit_tier`(JSON 数组)。 - 仅返回启用状态(`status=1`)的档位,按 `sort` 升序;玩家仅能从中选择。 - 档位仅描述"充值规格",不再包含收款账户;具体收款由第三方支付网关返回的 `pay_url` 引导。 - **多语言**:后台保存 `title`(中文名)、`title_en`(英文名)、`desc`(中文描述)、`desc_en`(英文描述)。接口返回的 `title` / `desc` 会根据请求头 `lang` 自动适配: - `lang=zh`(默认):返回 `title` / `desc`,若为空则回退到英文 - `lang=en`:返回 `title_en` / `desc_en`,若为空则回退到中文 - 移动端客户端仅看到单一 `title` / `desc`,无需自行判断语言 请求参数:无(无需 body 与 query) 返回参数: - `list`:list,档位列表;每一项结构: - `id`:string(含义:档位稳定 ID,创建订单时原样回传) - `title`:string(含义:档位名称,已按 `lang` 头切换;例如 `lang=en` 下返回 `"Starter Pack"`,`lang=zh` 下返回 `"新手首充礼包"`) - `amount`:string(4 位小数,含义:玩家本次需支付的充值金额) - `bonus_amount`:string(4 位小数,含义:该档位赠送金额,无赠送为 `0.0000`) - `total_amount`:string(4 位小数,含义:到账总额 = amount + bonus_amount,方便前端直接展示"到账 120") - `desc`:string(含义:档位描述/活动文案,已按 `lang` 头切换;可空) ### 5.4 创建充值订单 - **POST** `/api/finance/depositCreate` - `Content-Type: application/json`(推荐)或 `application/x-www-form-urlencoded` 说明: - **当前版本:mock 支付网关**。玩家在客户端选中档位并点击"立即充值"即视为支付成功:服务端在同一请求内完成订单创建与钱包入账,返回 `paid=true`、`status=paid`、`pay_url=""`。 - **未来第三方支付接入**:保持请求/响应形状不变。接口仅改为创建 `status=0` 订单并返回 `pay_url`(`paid=false`、`status=pending`);实际入账由第三方回调触发,服务端通过 `app\common\library\finance\DepositSettlement::settle` 完成钱包加币,客户端通过 `GET /api/finance/depositDetail` 轮询最终状态。 - 档位取自 `GET /api/finance/depositTierList`,服务端会二次校验档位存在且为启用状态。 请求参数: - `tier_id`:string,必填(含义:玩家选择的充值档位 ID,取自 `/api/finance/depositTierList` 返回) - `idempotency_key`:string,必填,≤64(含义:客户端生成的唯一键,短时间内同 `idempotency_key` 不会重复下单;建议 UUID) 返回参数: - `order_no`:string(含义:充值订单号) - `amount`:string(4 位小数,含义:玩家本次支付的充值金额,与所选档位 `amount` 一致) - `bonus_amount`:string(4 位小数,含义:本次赠送金额,与所选档位 `bonus_amount` 一致,无赠送为 `0.0000`) - `total_amount`:string(4 位小数,含义:实际入账总额 = amount + bonus_amount) - `pay_channel`:string(含义:支付通道标识;当前 mock 模式固定为 `mock_gateway`,未来接入网关会替换为实际通道标识,如 `alipay_h5` / `wechatpay_native`) - `paid`:bool(含义:当前单据是否已到账;`true` 表示钱包已入账、`status=paid`;`false` 表示待玩家在第三方支付页面完成支付) - `pay_url`:string(含义:第三方支付页面地址;`paid=true` 时为空串,`paid=false` 时为客户端需要跳转的支付页 URL) - `status`:string(`pending`/`paid`/`failed`,含义:订单处理状态;mock 模式下始终返回 `paid`) - `create_time`:int(含义:订单创建时间,秒级时间戳) - `pay_time`:int(含义:订单到账时间,未到账为 0) 错误码约定: - `1001`:缺少必填参数(`tier_id`/`idempotency_key` 任一为空) - `1002`:`idempotency_key` 过长,或与其他玩家的订单冲突 - `2000`:订单落库或入账失败(事务回滚后返回原始错误描述) - `2003`:所选 `tier_id` 不存在或已停用 ### 5.5 查看充值订单详情 - **POST** `/api/finance/depositDetail` 请求参数: - `order_no`:string,必填(含义:充值订单号) 返回参数(与 `depositCreate` 统一结构): - `order_no`:string(含义:充值订单号) - `amount`:string(4 位小数,含义:本单充值金额) - `bonus_amount`:string(4 位小数,含义:本单赠送金额,无赠送为 `0.0000`) - `total_amount`:string(4 位小数,含义:入账总额) - `pay_channel`:string(含义:支付通道标识) - `paid`:bool(含义:是否已到账) - `pay_url`:string(含义:第三方支付页面地址,已到账为空串) - `status`:string(`pending`/`paid`/`failed`) - `create_time`:int(含义:订单创建时间) - `pay_time`:int(含义:订单到账时间,未到账为 0) ### 5.6 查询充值订单列表 - **POST** `/api/finance/depositList` 用于我的充值记录页的分页列表;列表含订单状态,到账时间/支付通道等完整字段请再调用 `/api/finance/depositDetail` 获取。 请求参数: - `page`:int,选填,默认 `1`(含义:页码,从 1 开始) - `page_size`:int,选填,默认 `20`,最大 `100`(含义:每页数量,超出范围回退为 `20`) 返回参数: - `list`:array(含义:充值订单列表,按 `id desc` 排序) - `order_no`:string(含义:充值订单号) - `amount`:string(4 位小数,含义:本单充值金额) - `bonus_amount`:string(4 位小数,含义:本单赠送金额,无赠送为 `0.0000`) - `status`:string(含义:订单状态,与 `depositDetail` 一致:`pending`/`paid`/`failed`) - `pagination`:object(含义:分页信息) - `page`:int(含义:当前页码) - `page_size`:int(含义:每页数量) - `total`:int(含义:总记录数) ### 5.7 提现申请 - **POST** `/api/finance/withdrawCreate` 请求参数: - `withdraw_coin`:string(含义:申请提现金额,必须 > 0) - `receive_account`:string(含义:收款账号) - `receive_type`:string(`bank`/`ewallet`/`crypto`,含义:收款类型) - `idempotency_key`:string(含义:防重复提交提现) 返回参数: - `order_no`:string(含义:提现订单号) - `status`:string(`pending_review`/`processing`,含义:提现状态) - `fee_coin`:string(含义:手续费) - `actual_arrival_coin`:string(含义:实到账金额) - `risk_review_required`:bool(含义:是否命中人工审核) 校验顺序(任一失败即返回对应错误码,不再创建订单): 1. 参数完整性与金额合法性(`code=1001`;金额必须为数值且 > 0) 2. **待审核订单数限制**:同一用户 `status=0`(待审核)的 `withdraw_order` 不得超过 3 笔,否则 `code=2004 Too many pending withdraw orders`,`data` 中回传: - `max_pending`:上限值(当前为 `3`) - `pending_count`:当前待审核订单数 3. `coin_balance >= withdraw_coin`,否则 `code=2001 Insufficient balance` 4. **单笔上限校验**:`withdraw_coin <= max_withdrawable`,否则 `code=2002 Withdraw exceeds available bet flow`,`data` 中回传: - `max_withdrawable`:**当前允许的单笔最大提现金额**(= `min(coin_balance, max_withdraw_by_flow)`,前端据此提示"最大可提现金额为 XXX") - `coin_balance`、`bet_flow_coin`、`total_withdraw_coin`、`ratio` - `max_withdraw_by_flow`:仅按打码量折算的上限(= `max(0, bet_flow_coin / ratio - total_withdraw_coin)`);`ratio=0` 时为 `null` 5. 以上全通过后在同一事务内: - `withdraw_order` 写入:`amount` / `fee`(默认 0.5%) / `actual_amount = amount - fee` / `status=0`(待审核) / `channel_id` 取自用户归属渠道快照。 - `user` 表原子更新:`coin -= withdraw_coin` 且 `total_withdraw_coin += withdraw_coin`(WHERE `coin >= withdraw_coin` 防止并发超额扣减)。 - `user_wallet_record` 写入 `biz_type=withdraw`、`direction=2`、`amount=withdraw_coin`、`ref_type=withdraw_order`、`idempotency_key=wd_apply_{order_no}`,代表"冻结"动作。 说明(打码量即提现配额模型): - 单笔最大可提现 `max_withdrawable = min(coin_balance, max_withdraw_by_flow)`;每笔提现按 `withdraw_coin × ratio` 消耗打码配额,已消耗部分累积在 `total_withdraw_coin`。 - `ratio = 0` 时视为"不限打码",单笔上限仅受 `coin_balance` 约束。 - 采用"申请即冻结"语义:提现在移动端提交后立即从 `user.coin` 中扣减并写出金流水;后台审核 **拒绝** 时由管理端在同一事务中回冲余额、`total_withdraw_coin` 与流水,不出现"等待审核期间用户还能把这笔钱再下注"的漏洞。 - 后台审核 **通过** 时不再额外触碰余额;若管理员调整了 `amount` 或 `fee`,按新旧差额再生成一条 `withdraw` / `withdraw_refund` 流水以保持账务平衡,并同步修正 `total_withdraw_coin`。 - `withdraw_bet_flow_ratio` 由后台「游戏配置」维护,默认 `1.0000`,修改后对新请求立即生效。 ### 5.8 查看提现订单详情 - **POST** `/api/finance/withdrawDetail` 请求参数: - `order_no`:string,必填(含义:提现订单号) 返回参数: - `order_no`:string(含义:提现订单号) - `status`:string(`pending_review`/`approved`/`rejected`,含义:审核状态;`status=3 已打款` 暂未对外暴露,合并到 `approved`) - `withdraw_coin`:string(含义:申请提现金额,与后台 `withdraw_order.amount` 对齐) - `fee_coin`:string(含义:手续费,与后台 `withdraw_order.fee` 对齐) - `actual_arrival_coin`:string(含义:实际到账金额 = 申请金额 - 手续费;后台审核调整后会同步刷新) - `reject_reason`:string/null(含义:拒绝原因,`status=rejected` 时取自 `withdraw_order.remark`,否则为 `null`) - `create_time`:int(含义:申请时间) - `review_time`:int/null(含义:审核时间戳,未审核为 `null`) ### 5.9 查询提现订单列表 - **POST** `/api/finance/withdrawList` 用于我的提现记录页的分页列表;列表含审核/打款状态摘要,手续费、实到账、拒绝原因等请再调用 `/api/finance/withdrawDetail` 获取。 请求参数: - `page`:int,选填,默认 `1`(含义:页码,从 1 开始) - `page_size`:int,选填,默认 `20`,最大 `100`(含义:每页数量,超出范围回退为 `20`) 返回参数: - `list`:array(含义:提现订单列表,按 `id desc` 排序) - `order_no`:string(含义:提现订单号) - `amount`:string(4 位小数,含义:申请提现金额,与后台 `withdraw_order.amount` 对齐) - `status`:string(含义:订单状态,与 `withdrawDetail` 一致:`pending_review`/`approved`/`rejected`;后台已打款 `status=3` 合并为 `approved`) - `pagination`:object(含义:分页信息) - `page`:int(含义:当前页码) - `page_size`:int(含义:每页数量) - `total`:int(含义:总记录数) --- ## 6. 公告与消息模块(operation/notice) ### 6.1 拉取公告列表 - **GET** `/api/notice/noticeList` 请求参数(query string): - `page`:int(可选,默认 1) - `page_size`:int(可选,默认 20) 返回参数: - `list`:array - `notice_id`:int(含义:公告 ID) - `title`:string(含义:公告标题) - `notice_type`:string(`silent`/`popout`,含义:公告类型) - `is_read`:bool(含义:当前用户是否已读) - `publish_time`:int(含义:发布时间) ### 6.2 公告详情 - **GET** `/api/notice/noticeDetail` 请求参数(query string): - `id`:int,必填(含义:公告 ID) 返回参数: - `notice_id`:int(含义:公告 ID) - `title`:string(含义:公告标题) - `content`:string(含义:公告正文) - `notice_type`:string(含义:公告类型) - `must_confirm`:bool(含义:是否必须手动确认) - `publish_time`:int(含义:发布时间) ### 6.3 强弹窗确认已读 - **GET** `/api/notice/noticeConfirm` 请求参数(query string): - `notice_id`:int(含义:待确认公告 ID) 返回参数: - `notice_id`:int(含义:已确认公告 ID) - `confirmed`:bool(含义:确认结果) - `confirm_time`:int(含义:确认时间) --- ## 7. 推送模块(webman/push) > 用于移动端实时监听对局状态、开奖结果、余额变更与强公告事件。 > 协议与客户端行为对齐 [Pusher Channels](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/)(webman/push 内置兼容客户端 `push.js`)。 > 参考:[webman/push 官方文档](https://www.workerman.net/doc/webman/plugin/push.html) ### 7.1 频道命名与职责(优化版) | 频道名 | 类型 | 订阅方 | 典型事件 | |--------|------|--------|----------| | `private-user-{user.uuid}` | 私有(`private-` 前缀) | 当前登录用户;`{user.uuid}` 与登录态/档案中的 **10 位 `uuid`** 一致 | `bet.accepted`、`wallet.changed`、`withdraw.review_required`、定向 `notice.popout` 等 | | `public-game-period` | 公共 | 所有在线客户端 | `period.tick`、`period.locked`、`period.opened` | | `public-operation-notice` | 公共 | 所有在线客户端 | 全站/渠道级 `notice.popout`(与私有公告二选一或并存,由实现约定) | 约定说明: - **用户私有频道一律使用对外标识 `uuid`,不使用数据库主键 `user_id`**,避免与后台、日志、多端展示口径不一致,并降低枚举内网 ID 的风险。 - 名称以 `private-` 开头的频道必须通过 **私有频道鉴权**(见 7.2)成功后才能收到服务端推送。 - `public-*` 可直接订阅,无需鉴权 HTTP 步骤。 ### 7.2 连接地址与鉴权流程 **WebSocket 连接 URL(与官方 `push.js` 一致)** - 形如:`{websocket_base}/app/{app_key}` - 示例(本地默认配置见 `config/plugin/webman/push/app.php`):`ws://127.0.0.1:3131/app/{app_key}` - 生产环境请改为 `wss://` 与对外域名,并与网关/证书一致。 **连接建立后的协议步骤(简述)** 1. 客户端建立 WebSocket,服务端下发 `pusher:connection_established`,payload 内含 **`socket_id`**(后续鉴权必填)。 2. 订阅 **公共** 频道:发送 `pusher:subscribe`,`data` 仅含 `channel` 名即可。 3. 订阅 **私有** 频道: - 客户端向 **鉴权接口** 发起 `POST`(`Content-Type: application/x-www-form-urlencoded`),表单字段:`channel_name`、`socket_id`。 - 默认鉴权路径为 **`/plugin/webman/push/auth`**(与 `config/plugin/webman/push/app.php` 中 `auth` 一致,可随部署调整)。 - 服务端校验「当前登录用户是否允许订阅该 `channel_name`」——对 `private-user-{uuid}` 应校验 **`uuid` 与当前用户一致**,否则返回 `403`。 - 鉴权成功返回的 JSON 由 `push.js` 原样作为 `pusher:subscribe` 的 `data` 发送(含 `auth` 等字段)。 **与移动端登录态的关系** - 客户端在调用鉴权接口时,除 `channel_name` / `socket_id` 外,需携带与 REST API 一致的 **`user-token`(及业务所需的 `auth-token`)**,由服务端解析用户身份后再比对 `private-user-{uuid}`。 - **不建议**依赖浏览器 Cookie Session 作为唯一依据(H5 外还有 App 内嵌、小程序等);若仅沿用框架示例中的 Session,需在落地实现中改为 **无状态 token 校验**。 ### 7.3 事件定义(初设) | 事件名 | 建议频道 | 说明 | |--------|----------|------| | `period.tick` | `public-game-period` | 倒计时广播 | | `period.locked` | `public-game-period` | 封盘 | | `period.opened` | `public-game-period` | 开奖完成(中奖号码) | | `bet.accepted` | `private-user-{uuid}` | 下注成功回执 | | `bet.settled` | `private-user-{uuid}` | **每期每用户一条**:该局开奖对该用户全部注单的汇总(`total_win_amount`、`order_count`、`hit_order_count`、`result_number`、`balance_after`;不再按单笔注单重复推送) | | `wallet.changed` | `private-user-{uuid}` | 余额变化(中奖派彩入账等;`reason=payout` 等) | | `notice.popout` | `public-operation-notice` 或 `private-user-{uuid}` | 强公告(按业务选择广播或定向) | | `withdraw.review_required` | `private-user-{uuid}` | 提现进入审核 | ### 7.4 消息形态(客户端解析) 连接上收到的单帧一般为 JSON,常见两类: - 协议类:`event` 为 `pusher:connection_established`、`pusher_internal:subscription_succeeded` 等。 - 业务类:`event` 为业务事件名,`channel` 为频道名,`data` 为负载(可能为字符串化的 JSON,客户端需 `JSON.parse` 一次)。 业务负载示例(与初设一致,字段以实际实现为准): ```json { "event": "period.opened", "channel": "public-game-period", "data": { "period_no": "20260416001", "result_number": 18, "open_time": 1776326400 } } ``` ### 7.5 降级与一致性 - 推送仅作 **体验增强**:断线、弱网时客户端仍应以 **HTTP 轮询/用户主动刷新**(如 `/api/game/periodCurrent`、`/api/wallet/balanceSummary`)为准。 - 同一业务状态以 **服务端落库与接口查询** 为最终一致;推送到达顺序不保证与业务因果严格一致,需客户端幂等与去重(可带 `period_no` / `order_no` / 时间戳)。 ### 7.6 使用 Apipost 调试 WebSocket 与私有频道 Apipost(v7+)支持 **WebSocket**:新建请求 → 选择 **WebSocket** → 类型选 **Raw**。私有频道遵循「先拿 `socket_id` → 再 HTTP 鉴权 → 再发 `pusher:subscribe`」,与 `vendor/webman/push/src/push.js` 行为一致。 **A. 仅调试公共频道(如 `public-game-period`)** 1. 启动 webman 与 push 进程,确认 `config/plugin/webman/push/app.php` 中 `websocket`、`app_key`。 2. 在 Apipost 中 WebSocket URL 填:`ws://127.0.0.1:3131/app/{app_key}`(将 `{app_key}` 换成配置中的真实值)。 3. 点击连接,在消息面板应收到一帧 `pusher:connection_established`,从中取出 `socket_id`(公共订阅可不依赖后续步骤,但便于对照协议)。 4. 在发送框填入一行 JSON(勿带代码块标记)并发送: `{"event":"pusher:subscribe","data":{"channel":"public-game-period"}}` 5. 成功时随后会收到 `pusher_internal:subscription_succeeded`;之后服务端向该频道 `trigger` 的事件会出现在消息列表中。 **B. 调试用户私有频道 `private-user-{uuid}`** 1. 同上先连接,从首帧解析出 **`socket_id`**。 2. 新建 **HTTP** 请求:`POST http://{你的HTTP入口}/plugin/webman/push/auth` - Header:`Content-Type: application/x-www-form-urlencoded` - Body(x-www-form-urlencoded):`channel_name=private-user-{替换为真实uuid}&socket_id={上一步的socket_id}` - 若鉴权已接入 `user-token`,请在 Header 中一并带上与移动端一致的 **`user-token`**(及 `auth-token` 等),否则会得到 `403` 或无效签名。 3. 将接口返回的 **JSON 正文**(整段)作为 `pusher:subscribe` 的 `data`:在 Apipost WebSocket 发送 `{"event":"pusher:subscribe","data": <上一步响应 JSON 对象>}` 注意:`push.js` 会把鉴权返回与 `channel` 字段合并后再发送;若手搓 JSON,需保证与官方协议一致(含 `auth` 字段)。 4. 订阅成功后即可在消息面板等待该私有频道上的业务事件。 **说明**:若仅做协议连通性验证,可暂时使用服务端对鉴权接口的占位实现;**上线前**必须落实「`channel_name` 与当前用户 `uuid` 匹配」校验,避免越权订阅。 --- ## 8. 移动端完整调用流程 ## 8.1 首次进入游戏 1. `GET /api/v1/authToken?secret=xxx×tamp=xxx&device_id=xxx&signature=xxx` 获取 `auth-token` 2. `POST /api/user/login` 登录(请求头带 `auth-token`) 3. `GET /api/game/lobbyInit` 拉首页初始化(请求头带 `auth-token`) 3. 建立 webman/push 连接并订阅: - `public-game-period` - `private-user-{user.uuid}`(`uuid` 取自登录/档案接口,与 7.1 一致) 4. 收到 `period.tick` 实时刷新倒计时 5. 用户下注调用 `POST /api/game/betPlace` 6. 监听 `bet.accepted` + `wallet.changed` 更新下注结果和余额 7. 监听 `period.opened` 渲染开奖动画并刷新开奖记录 ## 8.2 充值到下注到提现闭环 1. 拉取档位:`GET /api/finance/depositTierList`(玩家选择一档) 2. 创建订单:`POST /api/finance/depositCreate`(JSON:`tier_id` + `idempotency_key`) - **当前 mock 模式**:服务端同一请求内入账完成,返回 `paid=true`、`status=paid`、`pay_url=""`,客户端直接刷新钱包 - **未来第三方支付**:返回 `paid=false`、`status=pending`、`pay_url=<网关页面>`;客户端跳转网关页完成支付,后端由第三方回调触发入账(通过 `DepositSettlement::settle`) 3. 客户端可选轮询 `GET /api/finance/depositDetail` 兜底确认状态;入账成功后会收到 `wallet.changed` 4. 下注:`POST /api/game/betPlace` 5. 派彩后收到 `wallet.changed` 6. 查询流水:`GET /api/wallet/recordList` 7. 提现:`POST /api/finance/withdrawCreate`(即时冻结 `user.coin` 与写出 `withdraw` 流水) -> `GET /api/finance/withdrawDetail` - 等待后台 `/admin/order/withdrawOrder` 审核;通过后订单 `status` 变为 `approved`,拒绝则回冲余额并在 `reject_reason` 中回传管理员填写的驳回原因 ## 8.3 公告强触达流程 1. 客户端监听 `notice.popout` 2. 拉取详情 `GET /api/notice/noticeDetail` 3. 用户勾选确认 `GET /api/notice/noticeConfirm?notice_id=...` 4. 未确认前可由前端阻断下注入口 --- ## 9. 游戏时序流程图(接口 + 推送) ```mermaid flowchart TD A[用户登录 /api/user/login] --> B[拉初始化 /api/game/lobbyInit] B --> C[连接webman/push并订阅频道] C --> D[收到 period.tick 倒计时] D --> E{0-20秒下注期?} E -- 是 --> F[提交下注 /api/game/betPlace] F --> G[推送 bet.accepted + wallet.changed] E -- 否 --> H[进入封盘状态 period.locked] H --> I[服务端算票与开奖] I --> J[推送 period.opened] J --> K[客户端开奖动画与结果展示] K --> L[客户端刷新开奖记录 /api/game/periodHistory] L --> D ``` --- ## 10. 后台渠道分红比例配置(管理端补充) > 本节为管理后台 `/admin/channel`「分配比例」弹窗补充口径,用于便于管理员按角色层级设置二次分红比例。 ### 10.1 角色组展示规则 - 表格列顺序调整为:`角色组层级` -> `负责人` -> `状态` -> `分配比例(%)` - `角色组层级` 在 `负责人` 前展示,降低识别与分配成本 - 层级路径使用 `/` 拼接,如:`顶级组 / 运营组 / 一组` - 同一负责人若存在多个角色组,按多标签展示多条路径 - 无角色组时显示 `-` ### 10.2 接口:读取渠道管理员分配配置 - **GET** `/admin/channel/channelAdminShareList?id={channel_id}` 返回参数(`data.list[]`)新增: - `group_paths`:array(负责人所属角色组层级路径列表) - `group_paths_text`:string(层级路径拼接文本,`|` 分隔,用于兼容纯文本场景) 返回示例(节选): ```json { "code": 1, "message": "ok", "data": { "channel_id": 1, "channel_name": "渠道A", "list": [ { "admin_id": 12, "username": "zhuguan1", "group_paths": ["顶级组 / 运营组 / A组"], "group_paths_text": "顶级组 / 运营组 / A组", "status": 1, "share_rate": "30.0000" } ] } } ``` ### 10.3 保存约束(沿用现有) - **POST** `/admin/channel/saveChannelAdminShare` - 仅 `status=1` 的行参与占比汇总 - 启用项分配比例总和必须严格等于 `100.00` --- ## 11. 后台提现审核接口(管理端补充) 对应前端入口:`/admin/order/withdrawOrder`。列表读写沿用 `/admin/order.WithdrawOrder/index`(BuildAdmin CRUD 标准协议)。审核流程另起 2 个动作接口,默认按钮 `add / del` 已在迁移中下线。 ### 11.1 GET 审核详情 - **GET** `/admin/order.WithdrawOrder/edit?id={id}` - 与 CRUD `edit` 协议复用,但对 `POST` 方法直接返回错误,强制走 `approve/reject`。 - 返回 `row` 字段已 `withJoin` 关联:`user.username`、`channel.name`、`reviewAdmin.username`,方便弹窗直接展示"用户 / 渠道 / 审核人"。 ### 11.2 审核通过 - **POST** `/admin/order.WithdrawOrder/approve` - 请求参数: - `id`:int,必填(`withdraw_order.id`) - `amount`:string,必填(审核后申请金额;允许管理员调整) - `fee`:string,必填(审核后手续费;`>=0` 且 `<= amount`) - `remark`:string,可选(为空时自动写入审核摘要) - 事务行为: 1. 订单状态必须为 `0 待审核`,否则返回错误。 2. 对比 `new_amount - old_amount`: - `>0`:用户 `coin -= diff`、`total_withdraw_coin += diff`,再写一条 `withdraw` 流水(direction=2)。 - `<0`:用户 `coin += |diff|`、`total_withdraw_coin -= |diff|`,写一条 `withdraw_refund` 流水(direction=1)。 3. 更新订单:`amount` / `fee` / `actual_amount = amount - fee` / `status=1` / `review_admin_id` / `review_time` / `remark`。 ### 11.3 审核拒绝 - **POST** `/admin/order.WithdrawOrder/reject` - 请求参数: - `id`:int,必填 - `remark`:string,必填(拒绝原因,最多 255 字,玩家将在 `/api/finance/withdrawDetail` 的 `reject_reason` 中看到) - 事务行为: 1. 订单状态必须为 `0 待审核`。 2. 回冲申请时的冻结:用户 `coin += amount`、`total_withdraw_coin -= amount`,写一条 `withdraw_refund` 流水(direction=1,`ref_type=withdraw_order`)。 3. 更新订单:`status=2` / `review_admin_id` / `review_time` / `remark`。 ### 11.4 权限节点 由 `20260418240000_withdraw_order_review_menu.php` 写入: - `order/withdrawOrder/approve`(审核通过) - `order/withdrawOrder/reject`(审核驳回) - `order/withdraw_order/approve` / `order/withdraw_order/reject`(snake 别名,兼容 `snake_case` 路径兜底) - 默认 CRUD 按钮 `order/withdrawOrder/add` / `order/withdrawOrder/del`(含 snake 别名)已置为 `status=0`,审核流程不允许新增/删除订单。 --- ## 12. 需要你确认的实现口径(进入接口开发前) 1. **登录方式**:仅账号密码,还是要短信/邮箱验证码? 2. **提现收款类型**:首版只做银行卡,还是同时支持电子钱包/加密地址? 3. **自动托管**:是否首期上线;若不上线可先隐藏 `auto-bet` 接口。 4. **push事件最小集**:是否先只上 `period.tick`、`period.opened`、`wallet.changed` 三类。 5. **错误码规范**:是否已有公司统一错误码表;若有需对齐替换本草案码段。 确认后可进入下一步:按该文档落地 controller + validate + service + 路由。