# 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/noticeConfirm` **仅支持 `GET`**,参数一律走 URL query string - **公告列表免鉴权**:`GET /api/notice/noticeList` **无需** `auth-token`、`user-token`;若仍携带 `user-token` 则强弹窗项可返回该用户真实的 `is_read` - 鉴权类接口 `/api/v1/authToken` 仍为 `GET` - 时间:UTC 时间戳(秒) + 服务端时区配置 - 金额:数字传输(如 `"100.00"`),客户端展示统一保留两位小数(存储仍为 `decimal(18,2)`) - 幂等:关键写接口要求 `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` 返回参数(金额类字段统一 2 位小数字符串,与钱包展示口径一致): **基础档案** - `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(含义:当前余额;两字段同值) - `frozen_balance`:string(含义:冻结余额;无冻结场景,固定 `0.00`) - `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(含义:打码量 / 提现配额诊断快照,此处额外附 `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(含义:服务端当前时间,用于客户端校时) - `runtime_enabled`:bool(含义:**游戏运行开关**;`false` 时表示后台维护——**禁止下注**,且 idle 时不会自动创建新期、派彩结束后也不会自动创建下一期;**当前已开盘的局仍会开奖、派彩并结算**。移动端应禁用下注入口并提示「维护」类文案) - `period`:object - `period_no`:string(含义:当前全局期号) - `status`:string(`betting`/`locked`/`settling`/`finished`/`void`,含义:当前期状态;`void` 表示该期已作废) - `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`:object(含义:快捷筹码字典,固定 6 个键 `"1"`…`"6"`,值为该档单注面额字符串,两位小数;与后台 `game_config.bet_chips` 语义一致) - `default_bet_chip_id`:int(含义:默认选中的筹码标识,来自 `game_config.default_bet_chip_id`,非法或指向无效档位时服务端回退为首个有效档) - `min_bet_per_number`:string(含义:单号码最小下注额,须 ≤ 所选筹码面额且受后台配置约束) - `max_bet_per_number`:string(含义:单号码最大下注额) - `dictionary`:array - `number`:int(1-36,含义:字花编号) - `name`:string(含义:字花名称) - `category`:string(含义:字花分类) - `icon`:string(含义:图标资源地址) - `user_snapshot`:object(含义:用户状态快照 + **当前玩家本局适用赔率**,不下发 1~10 全表) - `coin`:string(余额) - `current_streak`:int(当前连胜场数) - `streak_level`:int(若本局中奖将使用的连胜档位 1~10,由 `min(current_streak+1, 10)` 推导) - `odds_factor`:int(赔率乘数;中奖派彩 = 本笔 `total_amount` × `odds_factor`) - `is_jackpot`:bool(是否大奖档) ### 3.2 获取36字花字典(可缓存) - **POST** `/api/game/dictionaryList` 返回参数: - `version`:string(含义:字典版本号,前端可用于缓存比对) - `items`:同 `dictionary`(含义:36字花字典明细) ## 4. 下注与对局模块(game/bet) ### 4.1 获取当前期详情 - **POST** `/api/game/periodCurrent` 返回参数: - `runtime_enabled`:bool(含义:同 `lobbyInit.runtime_enabled`) - `period_id`:int(含义:当前期主键 ID) - `period_no`:string(含义:当前期号) - `status`:string(含义:当前期状态,含 `void` 已作废) - `countdown`:int(含义:当前期剩余秒数) - `bet_close_in`:int(含义:距离封盘剩余秒数) - `result_number`:int/null(未开奖为 null,含义:开奖号码) ### 4.2 提交下注 - **POST** `/api/game/placeBet`(兼容旧路径 `/api/game/betPlace`) - 用途:单期手动下注;玩家传入**压注号码**与**筹码标识 `bet_id`(1–6)**。单注金额由后台 `game_config.bet_chips` 解析,服务端按 `单注金额 × numbers数量` 计算本笔总扣款(落库 `total_amount`),开奖只出一个号码,若该号码 ∈ 所选号码集合即视为中奖。 请求参数: - `period_no`:string(含义:下注目标期号) - `numbers`:string(含义:本次压注号码集合,**英文逗号分隔**,如 `1,8,16`;每个号码为 1–36 的整数,数量不超过 `pick_max_number_count`(同 `lobbyInit.bet_config`),重复号码会去重) - `bet_id`:int(含义:**快捷筹码标识**,取值 1–6,须为 `lobbyInit.bet_config.chips` 中存在的键;不再使用 `single_bet_amount` / `bet_amount` 传参) - `idempotency_key`:string(必填,含义:防止重复下单) 返回参数: - `order_no`:string(含义:下注订单号) - `period_no`:string(含义:实际落单期号) - `status`:string(`accepted`/`rejected`,含义:受理结果) - `bet_id`:int(含义:本次使用的筹码标识) - `single_bet_amount`:string(含义:本次单注金额,由 `bet_id` 对应档位解析得到) - `numbers_count`:int(含义:本次号码数量) - `locked_balance`:string(可选,含义:冻结金额) - `balance_after`:string(含义:下单后余额) - `current_streak`:int(含义:下单后连胜快照) **可能错误码(补充)**(其余见文档头部错误码分段;扣款与缓存一致性强相关): - `3001`:游戏已暂停(`runtime_enabled=false`,后台「游戏实时对局」关闭运行开关或作废本局后未重新开启;与非法流程类错误同段) - `5000`:系统繁忙;或 **用户 Redis 互斥锁**未获取(与后台钱包/并发写同一用户串行,文案与后台一致:「该用户正在被其他管理员操作(钱包/并发保存),请稍后再试」);或 **`coin` 条件更新**未命中(并发下注/派彩/后台已改余额:「扣款失败:该用户余额已被其他请求修改(如下注、派彩或其他管理员已保存),请刷新后重试」)。 ### 4.3 自动托管 - **POST** `/api/game/autoSpin` 请求参数: - `action`:string(`start`/`stop`) - `period_no`:string(`action=start` 时必填) - `numbers`:string(`action=start` 时必填,英文逗号分隔) - `bet_id`:int(`action=start` 时必填,含义同 `placeBet`,快捷筹码 1–6) - `rounds`:int(`action=start` 时必填,>=1) 返回参数: - `status`:string(`scheduled`/`stopped`) - `auto_mode`:bool - `bet_id`:int(仅 `start` 返回,本次托管使用的筹码标识) - `single_bet_amount`:string(仅 `start` 返回,由 `bet_id` 解析得到的单注面额) - `remaining_rounds`:int(仅 `start` 返回) ### 4.4 查询我的下注记录(最近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 余额同步口径(已移除独立摘要接口) - 已移除 `/api/wallet/balanceSummary`。 - 余额同步来源调整为: - 下注返回 `placeBet.balance_after` - WebSocket 推送 `wallet.changed` - 充值/提现详情接口(如 `depositDetail` / `withdrawDetail`)作为业务单据维度核对 - 倍数 `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`) - `bet_void`:期次作废退款(后台「游戏实时对局」作废本局时,退回待开奖注单本金) 返回参数: - `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 数组)。 - 后台表单中的「支付货币」下拉来源于 `game_config.finance_cashier.currencies`(不再前端硬编码)。 - 初始化/重建档位时按当前 `finance_cashier` 货币集合生成:**每种货币 6 条档位**(运营可再编辑)。 - 仅返回启用状态(`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,创建订单时作为 `tier_id` 原样回传;与 `tier_key` 同值) - `tier_key`:string(含义:与 `id` 相同,兼容旧字段名) - `title`:string(含义:档位名称,已按 `lang` 头切换;例如 `lang=en` 下返回 `"Starter Pack"`,`lang=zh` 下返回 `"新手首充礼包"`) - `currency`:string(含义:标价币种,如 `CNY`) - `pay_amount`:string(2 位小数,含义:对外标价金额,与业务配置一致;展示用) - `amount`:string(2 位小数,含义:玩家本次需支付的充值金额) - `bonus_amount`:string(2 位小数,含义:该档位赠送金额,无赠送为 `0.00`) - `total_amount`:string(2 位小数,含义:到账总额 = amount + bonus_amount,方便前端直接展示"到账 120") - `desc`:string(含义:档位描述/活动文案,已按 `lang` 头切换;可空) - `channels`:array(含义:可用支付渠道列表,用于 `depositCreate` 的 `channel_code`;渠道与档位不再做单独绑定,所有启用渠道自动兼容全部档位) - 每项:`code`(string,渠道代码,小写,与创建订单时传入的 `channel_code` 一致)、`name`(展示名)、`sort`(排序) ### 5.3A 获取充值/提现配置 - **POST** `/api/finance/depositWithdrawConfig` - 兼容旧接口:`POST /api/finance/cashierConfig`(返回结构一致,建议客户端统一切到 `depositWithdrawConfig`) 用途: - 一次性返回充值与提现页面所需配置:货币列表、汇率、可用充值渠道、提现银行、提现限额与文案配置。 返回参数: - `platform_coin_label`:string(平台币名称,按 `lang` 适配) - `currencies`:array - `code`:string(货币代码) - `label`:string(货币展示名,按 `lang` 适配) - `deposit_coins_per_fiat`:string(充值汇率) - `withdraw_coins_per_fiat`:string(提现汇率) - `rates`:array(兼容字段) - `currency`:string - `diamonds_per_fiat_unit`:string - `pay_channels`:array(充值渠道) - `code`:string(渠道代码) - `name`:string(展示名) - `sort`:int(排序) - `status`:int(启用状态,1=启用) - `tier_ids`:array(兼容字段;当前固定空数组,表示自动兼容全部充值档位) - `withdraw`:object - `pay_channels`:array(提现可选支付渠道,用于 `withdrawCreate` 的 `channel_code`;仅返回后台已启用且服务端已对接出金的渠道) - 每项:`code`(string,渠道代码,小写)、`name`(展示名)、`sort`(排序) - `banks`:array(提现银行) - `min_ewallet`:string(电子钱包最低提现) - `min_bank`:string(银行卡最低提现) - `rate_hint`:string(汇率提示文案) - `processing_note`:string(到账提示文案) - `fee_note`:string(手续费提示文案) - `rate_mode`:string(`fixed` / `live`) - `fields`:object(**与 DDPay / `withdrawCreate` 一致**,不由后台「支付/收款配置」开关维护;用于客户端展示必填项) - `receive_type_bank_only`:bool(当前固定 `true`,仅支持银行卡出金) - `require_channel_code`:bool(固定 `true`,对应 `channel_code`) - `require_receiver_name`:bool(固定 `true`,对应 `receiver_name`) - `require_receive_account`:bool(固定 `true`,对应 `receive_account`) - `require_receiver_email`:bool(固定 `true`,对应 `receiver_email`) - `require_receiver_mobile`:bool(固定 `true`,对应 `receiver_mobile`) - `require_bank_code`:bool(固定 `true`,对应 `bank_code`) - `require_bank_branch`:bool(固定 `false`,`bank_branch` 选填,不传时服务端按 `N/A` 提交 DDPay) ### 5.4 创建充值订单 - **POST** `/api/finance/depositCreate` - `Content-Type: application/json`(推荐)、`application/x-www-form-urlencoded` 或 **`multipart/form-data`**(如 Apifox 的 form-data);字段名与下表一致即可,服务端通过统一参数名读取,**不限制**为某一种 body 类型。 说明: - **模拟支付(推荐联调)**:`channel_code=mock`,无需 DDPay 商户配置;创建后返回 `pay_url`(**模拟收银台页面** `GET /api/finance/mockDepositPage`,浏览器打开;**链接有效期 3 分钟**,过期未支付则订单自动失败)。用户在收银台点击「确认支付」后,订单进入 **`pending_review`(待后台审核)**,不会立即入账;管理员在后台「充值订单」审核通过后才入账。 - 可选 **`GET/POST /api/finance/mockDepositPay`**(需登录,与创建订单同一 `auth-token`):用于 App 内二次确认(与收银台确认二选一,幂等)。 - **DDPay**:`channel_code=ddpay`;先创建待支付单,再调用 DDPay「入金发起」;若成功返回 `payment_url`,则 `pay_url=payment_url`。 - 开关:`FINANCE_MOCK_PAY_ENABLED`(默认开发环境开启);关闭后 `mock` 渠道不可用。 - 入账由 **`POST /api/finance/ddpayDepositNotify`** 验签后结算,或网关同步返回 `transaction_status=completed` 时结算;**`mock` 渠道**在用户确认支付后需 **后台审核通过** 才调用入账结算。 - 档位与渠道取自 `depositTierList`:`channels` 中会出现后台已启用的渠道(包含 `mock` / `ddpay` 等)。 - 同一用户最多 3 笔待支付充值单;**待支付**订单创建后,超过 `DEPOSIT_PENDING_EXPIRE_SECONDS`(默认 **60 秒**,见 `.env`)未支付会自动失败;支付链接倒计时与此一致。 请求参数: **A. 通用必填参数** - `tier_id`:string,必填(充值档位 ID;同义字段:`tier_key`) - `channel_code`:string,必填,`mock`(模拟)或 `ddpay`(真实网关) - `idempotency_key`:string,必填,≤64(客户端幂等键,建议 UUID) **A.1 模拟支付(`channel_code=mock`)** - 无需 `payment_type` / `payer_name` / `payer_bank_name` **B. DDPay 渠道必填参数(`channel_code=ddpay`)** > **依据**:DDPay 官方《Payment Gateway》接口说明(与仓库内 `docs/DDPay Payment Gateway_v1.1.3_zh.md` / `docs/DDPay Payment Gateway_v1.1.3.pdf` 一致;以下「官方」均指该文档)。 - `payment_type`:string,必填(DDPay 字段 **`payment_type`,支付方式选择**) - **取值须为官方枚举字符串**(文档 §3.1):**`01`** = FPX,**`02`** = duitnow,**`03`** = ewallet;**其他取值须向 DDPay 商户支持另行确认**,勿自行臆造。 - 兼容字段:`paymentType` - **注意**:若传入 `FPX`、`duitnow` 等**非官方编码**,网关可能直接拒绝;移动端应传 **`01` / `02` / `03`**。 - `payer_name`:string,必填(**付款账户持有人姓名**,须与银行登记信息一致) - 兼容字段:`payerName` - `payer_bank_name`:string,必填(对应官方字段 **`payer_bank[name]`,付款银行名称**) - 须使用 **DDPay 银行列表中的银行全称**(与官方附录一致):**MYR** 参考官方附录「入金银行列表(MYR)」中 **英文全称**(如 `Public Bank`、`Maybank2U`);**THB** 参考「入金银行列表(THB)」中 **英文全称**(如 `BANGKOK BANK PUBLIC COMPANY LTD.`)。勿使用简称或与列表不一致的拼写,否则可能被网关拒单。 - 兼容字段:`payer_bank[name]`、`payerBankName` **B.1 服务端代传、客户端无需传的 DDPay 字段(供联调对照)** 以下由服务端在调用 DDPay「入金发起」时自动组装(来自环境/配置),移动端**不要**也无法通过本接口覆盖: - `client_id`、`identifier`(若项目环境已配置 `DDPAY_IDENTIFIER`)、`order_id`(即本系统 `order_no`) - `transaction_amount`:取自所选档位的 **`pay_amount`**(法币应付额,类型为 Decimal),币种以商户在 DDPay **onboarding 时约定币种**为准;官方示例表写 **MYR**,若贵司为 THB 等,以 DDPay 后台为准。 - `callback_url`、`redirect_url`:由服务端拼接,根地址优先读取环境变量 **`DDPAY_PUBLIC_BASE_URL`**(见 `.env-example`),未配置时按请求 `Host` 推导;官方要求 **HTTPS**(生产务必配置公网 HTTPS 根地址)。 > **常见 1001 原因(DDPay)**:只传了通用参数,漏传 `payment_type / payer_name / payer_bank_name` 之一;或 `payment_type` 未使用官方 `01/02/03`。 推荐请求示例(DDPay,MYR + FPX): ```json { "tier_id": "t_xxxxxxxx", "channel_code": "ddpay", "idempotency_key": "dp_20260429_xxx", "payment_type": "01", "payer_name": "ZHANG SAN", "payer_bank_name": "Public Bank" } ``` 返回参数: - `order_no`:string(含义:充值订单号) - `amount`:string(2 位小数,含义:玩家本次支付的充值金额,与所选档位 `amount` 一致) - `bonus_amount`:string(2 位小数,含义:本次赠送金额,与所选档位 `bonus_amount` 一致,无赠送为 `0.00`) - `total_amount`:string(2 位小数,含义:实际入账总额 = amount + bonus_amount) - `pay_channel`:string(含义:支付通道标识,与请求中选择的 `channel_code` 一致,落库在订单上) - `paid`:bool(含义:当前单据是否已到账;`true` 表示钱包已入账、`status=paid`;`false` 表示尚未入账;**`mock` 待审核**也为 `false`) - `pay_url`:string(含义:收银台地址;**`pending`(待支付)** 时:`ddpay` 为三方 **`payment_url`(完整 URL)**;**`mock` 为模拟收银台**(`GET /api/finance/mockDepositPage`);**`paid=true` 或已进入待审核**时可能为空串) - `review_required`:bool(含义:是否需要后台审核入账;**仅 `mock` 在用户确认支付后**为 `true`) - `reject_reason`:string 或 null(含义:审核驳回原因;仅失败且存在驳回原因时有值) - `expire_at`:int 或 null(含义:`pay_url` 过期时间戳(秒);主要用于 `mock` 待支付) - `expire_seconds`:int 或 null(含义:与 `expire_at` 配套,待支付时返回,值同 `DEPOSIT_PENDING_EXPIRE_SECONDS`) - `status`:string(`pending` / `pending_review` / `paid` / `failed`,含义:`pending`=待支付;`pending_review`(仅 mock)=用户已确认,等待后台审核;`paid`=已入账;`failed`=失败/超时/驳回) - `create_time`:int(含义:订单创建时间,秒级时间戳) - `pay_time`:int(含义:订单到账时间,未到账为 0) #### 5.4.1 DDPay 回调与状态说明(当前实现) - 回调地址:`POST /api/finance/ddpayDepositNotify`(由服务端在 DDPay 发起请求时作为 **`callback_url`** 传给三方;须 **HTTPS**)。 - 官方 Webhook 负载字段(文档 §3.5,处理前**必须先验签**): | 参数 | 说明 | |---|---| | `client_id` | 商户标识 | | `order_id` | 交易引用号(与本系统充值 `order_no` 对应) | | `transaction_status` | 最终状态:`pending` / `completed` / `failed`(含义见官方 §4.1) | | `timestamp` | 通知时间 | | `transaction_amount` | 支付金额(配置币种下) | | `signature` | 通知校验签名 | - 我方验签通过后: - `transaction_status=completed`:执行入账结算,订单转 `paid` - `transaction_status=failed`:订单转 `failed` - 官方要求:收到通知后应尽快返回 HTTP **200**,响应体为纯文本 **`{"status":"ok"}`**;未收到确认时平台最多重试 **6** 次(以官方文档为准)。 - 若三方暂未提供/未打通回调,`pending` 订单将保持待处理;官方另提供「**入金状态查询**」接口(请求需含 `query_time`,格式 **`YYYY-MM-DD HH:MM:SS`**),当前移动端未单独暴露,由服务端按需扩展。 错误码约定: - `1001`:缺少必填参数(`tier_id`(或 `tier_key`)、`channel_code`、`idempotency_key` 任一未传或为空字符串) - `1001`(DDPay):缺少 `payment_type` / `payer_name` / `payer_bank_name`,或 `payment_type` 非官方允许取值 - `1002`:`idempotency_key` 过长,或与其他玩家的订单冲突 - `2000`:**通用**:订单落库或入账失败(事务回滚等,可能带原始错误描述);**DDPay**:调用「入金发起」失败(网络/HTTP/JSON/验签/`status_code≠0` 等),对外文案为「DDPay 充值发起失败」,**具体原因可查看该笔 `deposit_order.remark`(前缀 `[ddpay]`)或服务端日志** - `2003`:所选 `tier_id` 不存在、已停用或不在启用列表中 - `2004`:不支持的 `channel_code`;`mock` 已关闭;或 `ddpay` 未启用;或当前档位币种与该渠道不允许的组合 - `2005`:待支付充值单超过上限(`data.max_pending`、`data.pending_count`、`data.expire_seconds`) ### 5.4A 模拟支付(浏览器收银台 + 可选 App 确认) - **GET/POST** `/api/finance/mockDepositPage`:模拟收银台 HTML 页面(**无需登录**;`order_no` 必填;仅 `pay_channel=mock` 且 `status=pending` 且未过期可展示支付按钮) - **GET/POST** `/api/finance/mockDepositConfirm`:收银台内确认支付(**无需登录**;将订单标记为 **`pending_review`**,不入账) - **GET/POST** `/api/finance/mockDepositPay`:App 内确认(**需登录**;与上一接口二选一、幂等) 说明:后台管理员在 **「充值订单」** 对待审核单执行审核通过/驳回后,`mock` 订单才会最终成功入账或失败(未接入真实商户前的模拟流程)。 ### 5.5 查看充值订单详情 - **POST** `/api/finance/depositDetail` 请求参数: - `order_no`:string,必填(含义:充值订单号) 返回参数(与 `depositCreate` 统一结构): - `order_no`:string(含义:充值订单号) - `amount`:string(2 位小数,含义:本单充值金额) - `bonus_amount`:string(2 位小数,含义:本单赠送金额,无赠送为 `0.00`) - `total_amount`:string(2 位小数,含义:入账总额) - `pay_channel`:string(含义:支付通道标识) - `paid`:bool(含义:是否已到账) - `pay_url`:string(含义:收银台地址;已到账或无需再支付时可能为空串) - `review_required`:bool(含义:是否等待后台审核入账) - `reject_reason`:string 或 null(含义:驳回原因) - `expire_at`:int 或 null(含义:`pay_url` 过期时间戳(秒)) - `expire_seconds`:int 或 null(含义:与 `expire_at` 配套) - `status`:string(`pending` / `pending_review` / `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(2 位小数,含义:本单充值金额) - `bonus_amount`:string(2 位小数,含义:本单赠送金额,无赠送为 `0.00`) - `status`:string(含义:订单状态,与 `depositDetail` 一致:`pending` / `pending_review` / `paid` / `failed`) - `pagination`:object(含义:分页信息) - `page`:int(含义:当前页码) - `page_size`:int(含义:每页数量) - `total`:int(含义:总记录数) ### 5.7 提现申请 - **POST** `/api/finance/withdrawCreate` 说明: - **仅支持 DDPay 出金**:`channel_code` 必须为 **`ddpay`**(兼容同义字段 `pay_channel`),且该渠道在收银台配置中为启用状态,否则返回 `code=2004`。 - 可选渠道列表见 `depositWithdrawConfig` → `withdraw.pay_channels`。 请求参数: - `channel_code`:string,必填(支付渠道代码;当前仅支持 `ddpay`;兼容字段 `pay_channel`) - `withdraw_coin`:string(含义:申请提现金额,必须 > 0) - `receive_account`:string(含义:收款账号;对接 DDPay 出金时对应官方字段 **`receiver_account`**,为收款账户号/手机号,须与银行登记一致) - `receive_type`:string(含义:收款类型;当前版本仅支持 `bank`) - `idempotency_key`:string(含义:防重复提交提现) - `receiver_name`:string(含义:收款账户持有人姓名;`receive_type=bank` 必填,对应官方 **`receiver_name`**,须与银行登记一致) - `receiver_email`:string(含义:收款人邮箱,必填,须为合法邮箱格式,最长 255) - `receiver_mobile`:string(含义:收款人手机号,必填,5–32 位,仅允许数字与 `+` `-` 空格,至少含 5 位数字) - `bank_code`:string(含义:银行代码;`receive_type=bank` 必填。取值来自收银台配置 `withdraw_banks[].code`;服务端会映射为 DDPay 所需的 **`bank[name]`** 银行全称发起出金) - `bank_branch`:string(含义:银行支行名称;`receive_type=bank` 可选。官方 **`bank_branch`** 为必填项,若客户端不传则服务端按 **`N/A`** 提交,与 DDPay 文档「若缺失则默认值为 `N/A`」一致) **DDPay 出金(Payout)官方请求字段对照(文档 §3.2,由服务端在审核通过后组装)** | 官方参数 | 说明 | |---|---| | `client_id` | 商户账号标识 | | `bill_number` | 出金唯一引用号(与本系统提现 `order_no` 对应) | | `amount` | 出金金额(Decimal,配置币种下) | | `receiver_name` | 收款账户持有人姓名 | | `receiver_account` | 收款账户号/手机号 | | `bank[name]` | 银行全称(须参考官方附录「出金银行列表」) | | `bank_branch` | 支行名称;缺失为 `N/A` | | `callback_url` | 异步通知地址,须 **HTTPS** | | `signature` | MD5 签名(小写) | 出金成功响应中,官方还可能返回 `transaction_fee`、`transaction_total`、`transaction_status`、`remark` 等;最终以 DDPay 文档为准。 返回参数: - `order_no`:string(含义:提现订单号) - `status`:string(`pending_review`/`approved`/`rejected`,含义:提现状态;已打款合并到 `approved`) - `fee_coin`:string(含义:手续费) - `actual_arrival_coin`:string(含义:实到账金额) - `risk_review_required`:bool(含义:是否命中人工审核) 校验顺序(任一失败即返回对应错误码,不再创建订单): 1. 参数完整性与金额合法性(`code=1001`;含 `channel_code`、`withdraw_coin` 等必填项;金额必须为数值且 > 0) 2. **支付渠道**:`channel_code` 非 `ddpay` 返回 `code=2004 Withdraw only supports DDPay`;渠道未启用返回 `code=2004 Pay channel not available` 3. **待审核订单数限制**:同一用户 `status=0`(待审核)的 `withdraw_order` 不得超过 3 笔,否则 `code=2004 Too many pending withdraw orders`,`data` 中回传: - `max_pending`:上限值(当前为 `3`) - `pending_count`:当前待审核订单数 4. `coin_balance >= withdraw_coin`,否则 `code=2001 Insufficient balance` 5. **单笔上限校验**:`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` 6. 以上全通过后在同一事务内: - `withdraw_order` 写入:`pay_channel`(= `channel_code`)/ `amount` / `fee`(默认 0.5%) / `actual_amount = amount - fee` / `status=0`(待审核) / `channel_id` 取自用户归属渠道快照;同时写入收款字段(`receive_type/receive_account/receiver_email/receiver_mobile/receiver_name/bank_code/bank_branch`)。 - `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` 与流水,不出现"等待审核期间用户还能把这笔钱再下注"的漏洞。 - 后台审核 **通过** 时会触发三方出金(DDPay Payout);出金 **失败** 会自动回冲余额、`total_withdraw_coin` 与流水(`withdraw_refund`),并将订单标记为 `rejected`(内部 `status=2`)。 - 后台审核 **通过** 且出金完成后,订单内部 `status=3`(已打款),移动端对外仍合并展示为 `approved`。 - `withdraw_bet_flow_ratio` 由后台「游戏配置」维护,默认 `1.00`,修改后对新请求立即生效。 ### 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(含义:实际到账金额 = 申请金额 - 手续费;后台审核调整后会同步刷新) - `receive_type`:string(含义:收款类型) - `receive_account`:string(含义:收款账号) - `pay_channel`:string(含义:支付渠道代码,与 `channel_code` 一致) - `receiver_email`:string(含义:收款人邮箱) - `receiver_mobile`: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(2 位小数,含义:申请提现金额,与后台 `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` - **鉴权**:无需 `auth-token`、`user-token`(公开接口) 请求参数(query string): - `page`:int(可选,默认 1) - `page_size`:int(可选,默认 20) 返回参数: - `list`:array - `notice_id`:int(含义:公告 ID) - `title`:string(含义:公告标题) - `content`:string(含义:公告正文,原详情接口字段) - `notice_type`:string(`silent`/`popout`,含义:公告类型) - `must_confirm`:bool(含义:是否必须手动确认;强弹窗为 `true`) - `is_read`:bool(含义:当前用户是否已确认;**仅 `popout` 强弹窗有效**,`silent` 恒为 `false`,不写入阅读记录) - `publish_time`:int(含义:发布时间) > **阅读记录口径**:`user_notice_read` 仅用于强弹窗(`notice_type=popout`)的确认已读;静默信箱(`silent`)不生成、不查询阅读记录。 ### 6.2 强弹窗确认已读 - **GET** `/api/notice/noticeConfirm` 请求参数(query string): - `notice_id`:int(含义:待确认公告 ID) 行为说明: - **仅强弹窗**(`notice_type=popout`)可调用;静默公告调用返回业务错误 - 若当前用户对该公告已有阅读记录:仅更新 `read_at`;若尚未确认则同时将 `confirmed` 置为 `1` - 若无阅读记录:创建一条已确认记录 返回参数: - `notice_id`:int(含义:已确认公告 ID) - `confirmed`:bool(含义:确认结果) - `confirm_time`:int(含义:确认时间) --- ## 7. WebSocket(H5)与状态同步 > 本版本已移除 webman/push 频道模式;H5 前端使用原生 WebSocket 直连,HTTP 轮询仅作为弱网兜底。 ### 7.0 部署与连接前置条件(当前实现) 以下与代码 `app/process/GameWebSocketServer.php`、`config/process.php`、`app/common/library/admin/WebSocketConfigHelper.php` 一致,**文档此前未写明的条件**在此补齐: - **独立进程**:需在运行环境中启动 Webman 自定义进程 **`gameWebSocketServer`**(见 `config/process.php`),默认监听 **`H5_WEBSOCKET_LISTEN`**(缺省为 `websocket://0.0.0.0:3131`)。未启动进程则客户端无法建连或立刻断线。 - **连接 URL**:优先使用环境变量 **`H5_WEBSOCKET_URL`**(完整 `ws://` / `wss://` 地址,建议带路径,如 `.env-example` 所示)。**生产环境勿将 `H5_WEBSOCKET_URL` 配成 `127.0.0.1` / `localhost`**:浏览器无法访问服务端本机地址。若 `.env` 中为回环地址且 HTTP 请求来自外网域名,`WebSocketConfigHelper` 会**忽略该配置**,改按当前请求的 `Host` / `X-Forwarded-*` 推导 `ws(s)://{host}/ws/`。未配置时,后台 **`GET /admin/test.GameCurrentStatus/wsConfig`**、**`GET /admin/game.Live/wsConfig`** 等同理;本地兜底 **`ws://127.0.0.1:3131/ws/`**。HTTPS 站点需在 Nginx 等反向代理将 **`/ws/`** 转发至 `gameWebSocketServer` 监听端口(默认 **3131**)。 - **移动端配置缺口**:**`POST /api/game/lobbyInit` 当前不下发 WebSocket 地址**;H5 需与运维约定同一套 `H5_WEBSOCKET_URL`(打包进前端配置、远程配置中心等),与 HTTP API 基址可不同域。 - **混合内容**:若 H5 页面为 **HTTPS**,浏览器要求 WebSocket 使用 **`wss://`**,否则会被拦截。 - **事件投递依赖 Redis**:HTTP 侧业务通过 **`GameWebSocketEventBus`**(Redis 列表)将事件投递到 WebSocket 进程;Redis 不可用或队列异常时,**除 `admin.live.snapshot` 外**的广播类推送可能收不到。后台若订阅了 `admin.live.snapshot`,服务端有**每秒直连构建快照**的兜底,不依赖队列。 - **握手鉴权(2026-05 重构后强制)**:`GameWebSocketServer::onWebSocketConnect` 通过 `GameWebSocketAuthHelper::authorize` 校验 URL Query。两种合法身份: - **mobile(H5/移动端)**:必须同时携带 Query **`auth-token`**、**`user-token`**(与 HTTP 请求头同名,**统一用连字符**)。校验通过后绑定 `user_id`,分发器仅向其推送本人的 user 级主题。服务端仍兼容旧别名 `auth_token` / `user_token` 解析,但**新接入请只用连字符**。 - **admin(后台/运维)**:必须携带 Query **`admin-ws-token`**(由后台 `wsConfig` 签发,写入 Redis,默认 TTL 7200s)。`ws_url` 已自动拼接该参数;admin 模式 `user_id=0`,可观测全量推送。 - 任一身份不通过 → 服务端发送 `{"event":"ws.error","code":1101,"message":"Authentication failed: ..."}` 并立即 `close`。 - **服务端按 user_id 过滤(user 级主题)**:以下 topic 的 `data.user_id` 必须 **等于** 当前连接绑定的 `user_id` 才会下发——**`bet.win` / `user.streak` / `wallet.changed` / `bet.accepted` / `auto.spin.progress`**。其它 topic(`period.tick` / `period.opened` / `jackpot.hit` / `admin.*` 等)按订阅广播。admin 模式不参与此过滤。 - **心跳超时(服务端主动)**:连接 60s 内无任何上行报文(含 `ping`/`subscribe`)即被 server 主动 `close`,触发客户端走重连流程;避免半关闭的僵尸连接长期持有订阅却不能实际送达推送。 - **独立日志通道 `ws`**:`runtime/logs/ws.log`(保留 7 天)。记录维度包含 `publish 入队 / popBatch 异常 / dispatch(topic/candidates/matched/skipped_not_owner/skipped_closed/send_failed)/ handshake_ok | denied / subscribe / pong / close idle / send failed` 等。排查"为什么没收到推送"时优先看此文件。 - **订阅才有业务推送**:建连后仅会收到握手首帧(见下)及本连接已订阅主题的消息;不发送 `subscribe` 则收不到 `period.tick` 等(`admin.live.snapshot` 同上,需显式订阅)。 ### 7.1 WebSocket 连接与消息 - **连接地址**:见 **§7.0**(环境变量 `H5_WEBSOCKET_URL` 或后台 `wsConfig` 返回的 `ws_url`) - **客户端**:浏览器原生 `WebSocket`(`ws://` / `wss://`) - **连接时必带 Query 参数(2026-05 起强制)**: - **H5/移动端**:`auth-token=` + `user-token=`。可选:`device_id`、`lang`。 - **后台**:`admin-ws-token=`(已拼入 `ws_url`)。 - 示例: - H5:`wss://ws.example.com/ws/?auth-token=xxx&user-token=yyy&device_id=ios_001&lang=zh` - 后台:`wss://ws.example.com/ws/?admin-ws-token=zzz` - 缺失任一必填字段或 token 失效 → 服务端回 `{"event":"ws.error","code":1101,...}` 后立即关闭连接。 - **连接成功首帧(当前实现)**: - `event`:`ws.connected` - `message`:固定文案 `WebSocket connected`(便于联调日志) - `connection_id`:连接唯一标识(进程内) - `mode`:`mobile` | `admin`(表明本连接的鉴权身份;**不下发** `user_id`) - `server_time`:服务器时间戳(**秒**,int) - `heartbeat_interval`:建议心跳间隔(**秒**,当前实现固定为 `30`) - `idle_timeout`:服务端主动关闭的空闲秒数(**秒**,当前实现固定为 `60`;客户端 `idle_timeout - 心跳间隔` 内必须发出 `ping`,否则会被 server 主动 `close`) - **连接后错误帧(当前实现,非 HTTP 业务码)**: - JSON 无法解析:`event`=`ws.error`,`message`=`Invalid JSON payload`(无 `code` 或与 HTTP `code` 不同体系) - 未知 `action`:`event`=`ws.error`,`message`=`Unsupported action`,并可能带 `received_action` - 进程级异常:`onError` 回包含 `event`=`ws.error`、`message`=`Server internal error` 及 Workerman 侧 `code`/`detail`(**非** §1.2 的 HTTP API `code` 段) - **建议消息**: - 心跳:`{"action":"ping"}` - 服务端对心跳的当前实现回包:`{"event":"pong","server_time":"YYYY-mm-dd HH:ii:ss"}`(**注意**:此处 `server_time` 为**本地时间字符串**,与业务推送帧里 `server_time` 常用**秒级 int** 不一致,客户端解析时请分支处理) - 订阅状态流:`{"action":"subscribe","topics":["period.tick"]}` - 订阅连胜/赔率(仅当前玩家):`{"action":"subscribe","topics":["user.streak","wallet.changed","bet.accepted"]}` - 订阅资金流:`{"action":"subscribe","topics":["bet.accepted","wallet.changed"]}` - 订阅托管流:`{"action":"subscribe","topics":["auto.spin.progress","wallet.changed"]}` - 移动端推荐合并订阅:`period.tick`、`bet.win`、`user.streak`、`wallet.changed`、`bet.accepted`、`period.opened`、`period.payout`、`jackpot.hit` #### 7.1.1 消息协议字段定义(联调口径) - 客户端 -> 服务端: - `action`:动作名(当前约定 `ping` / `subscribe`) - `topics`:仅 `subscribe` 时必填,表示要订阅的主题列表(数组) - 服务端 -> 客户端: - `event`:事件名(如 `period.tick`、`wallet.changed`、`jackpot.hit`) - `topic`:所属主题(通常与 `event` 一致;用于前端按主题路由) - `data`:业务载荷(对象) - `server_time`:服务端时间戳(秒,倒计时与对时基准) #### 7.1.2 订阅行为说明 - **仅建立连接不会自动下发全部业务消息**;客户端需要发送 `subscribe` 明确订阅主题。 - 成功订阅后服务端返回:`{"event":"ws.subscribed","topics":[...]}`(已去重、按字典序排序,与提交顺序无关)。 - **`subscribe` 覆盖式生效**:每次发送都会**完全替换**该连接的订阅集合(不是累加)。需要追加请把已有列表一并发上来。 - 若未订阅主题,通常只能收到握手首帧(`ws.connected`)和心跳回包(`pong`)。 - **服务端按连接绑定用户过滤**:mobile 模式仅下发本人相关的 user 级主题;**出站 `data` 不含 `user_id`**(及其它敏感字段,见下)。客户端**无需**再按 `user_id` 过滤。 - **出站脱敏字段(2026-05)**:`data` 中移除 `user_id`、`uuid`、`phone`、`balance_before`、`channel_id` 等;`jackpot.hit` 的 `hits[]` 仅保留 `nickname`、`period_no`、`total_win`、`result_number` 等展示字段。 - **不下发** `streak_win_reward` 全表(1~10 档);赔率仅通过 `user.streak` / `wallet.changed` / `bet.accepted` 及 `lobbyInit.user_snapshot` 推送**当前登录玩家**本局适用字段。 #### 7.1.2A 连胜赔率与连胜场次(WebSocket) - **`user.streak`**(开奖结算后推送;载荷为当前玩家本局适用赔率) - `data.current_streak`:int - `data.streak_level`:int - `data.odds_factor`:int - `data.is_jackpot`:bool - **`wallet.changed` / `bet.accepted` / `bet.win`**:在原有字段上合并 **`current_streak`**、**`streak_level`**、**`odds_factor`**、**`is_jackpot`**(**不含** `user_id`)。 - **`bet.accepted` 与 `bet.win` 的 `is_jackpot` 区别**:`bet.accepted` 表示**下注时**本笔适用档位是否大奖档(赔率展示);**开奖中奖通知以 `bet.win` 为准**,其 `is_jackpot` 表示**结算时**该用户中奖注单是否含大奖档(`streak_at_bet` 对应 `streak_win_reward.is_jackpot=true`,通常为第 10 档)。 #### 7.1.3 推送频率与触发规则(当前实现) - `period.tick`:**仅在 `status ∈ {betting, locked}` 时每秒推送**(用于倒计时、状态同步;**不含**赔率全表)。 - **派彩静默期**:`status=payouting` 期间**不推** `period.tick`(避免彩池/倒计时干扰中奖动画)。改为每秒推送 **`period.payout.tick`**,仅含 `payout_remaining_seconds` / `payout_until` 等派彩倒计时字段。中奖展示仍靠 `period.opened` / `period.payout` / **`bet.win`** / `jackpot.hit`(大奖补充)/ `wallet.changed(biz_type=payout)`。 - **边界帧(每期仅一次)**: - `status=finished`:派彩宽限期结束、本期进入收尾时推送一帧,告知前端可以清理本期 UI。 - `status=void`:本期被作废时推送一帧作为通知。 - **恢复推送**:下一期创建并进入 `betting` 后,按新 `period_no` 重新开始每秒推送。 - 服务端去重:边界帧通过 Redis Key `dfw:v1:ws:tick:boundary:{period_no}:{status}`(TTL 300s)保证同一期号同一状态只推一次。 - `user.streak`:每期结算更新用户连胜后按用户推送(未中奖也会推送,`current_streak` 可能归零)。 - `admin.live.snapshot`:**每秒一次**(后台实时对局页全量快照;不受派彩静默期影响)。 - `period.opened` / `period.payout` / `admin.live.opened`:按开奖流程阶段触发(事件触发型,非固定频率)。 - `wallet.changed`:仅在余额发生变更时推送(如下注扣款、充值入账、派彩入账)。派彩时 `biz_type=payout`,并带 `amount`(本次派彩金额)、`period_no`、`period_id`、`result_number`(若有)。 - **`bet.win`(本期中奖,小奖/大奖统一)**:开奖结算后,**凡本期有中奖的用户**均按用户聚合推送一帧(与 `wallet.changed(payout)` 同一结算批次);**个人中奖弹窗/横幅统一监听此主题**,用 `data.is_jackpot` 区分普通档与大奖档样式。**中大奖档用户同样会收到 `bet.win`**,无需仅依赖 `jackpot.hit`。 - `data.period_id` / `data.period_no` / `data.result_number`(**不含** `user_id`) - `data.total_win`:本期该用户派彩合计(已入账部分;若触发**后台大奖审核**(`win_amount >= game_config.jackpot_max_amount`)且注单为待审核,可能尚未入账,但仍会推送本事件) - `data.balance_after`:推送时用户余额(已派彩则为派彩后余额) - `data.bets[]`:`{ bet_id, win_amount }` 明细 - **`data.is_jackpot`**:`bool`,`true` 表示**中大奖**(满足任一:连胜**大奖档**、或派彩金额达 `jackpot_max_amount` 需后台审核)。**客户端用此字段做大奖样式,勿仅依赖 `jackpot.hit`** - **`data.is_win`**:`bool`,固定为 `true`(便于与 `user.streak` 的 `extra.is_win` 对齐) - **`data.payout_pending_review`**:`bool`,`true` 表示已中奖但派彩待后台大奖审核,尚未入账(仍应展示中奖 UI) - **合并赔率字段**(与 §7.1.2A 一致):`current_streak`、`streak_level`、`odds_factor`、`is_jackpot` - `data.server_time`:Unix 秒 - **服务端去重**:Redis Key `dfw:v1:ws:betwin:{period_id}:{user_id}`(TTL 86400s),**入队成功后再写入**,每期每用户至多推送一次;与 `dfw:v1:settle:notify:{period_id}` **分离**。结算推送以库内已结算中奖注单为准重建载荷,避免内存聚合丢失。 - **补偿**:若内存聚合 `bet_wins` 为空但库内已有本期已结算中奖注单,结算服务会从库重建载荷并补发(`buildBetWinPayloadsFromSettledOrders`)。 - **大奖审核通过后**:后台 `approveJackpot` 会再次向该用户推送 `bet.win`(入账后)。 - **`jackpot.hit`(公共大奖广播,补充)**:在 **`bet.win` 之后**(同一结算批次内),若本期存在**大奖档命中**用户,**额外**向公共频道推送一帧,供全站公告/跑马灯;无大奖命中则不推送。**个人弹窗仍以 `bet.win` 为主**;`jackpot.hit` 用于全站展示昵称与金额。 - **推送顺序**:先 `bet.win`(按用户,含 `is_jackpot`)→ 再 `jackpot.hit`(仅大奖档) - **载荷字段**:`period_id` / `period_no` / `result_number` / `hits[]` / `server_time`。 - `hits[]` 数组每项字段: - `nickname`:string(用户昵称,供全站公告展示;**不含** `user_id`) - `period_no`:string - `total_win`:string(本期该用户的命中大奖派彩合计,金额字符串) - `result_number`:int #### 7.1.4 结算推送去重与运维补发(2026-05) | Redis Key | 作用 | |-----------|------| | `dfw:v1:settle:notify:{period_id}` | 整期 `user.streak` / `wallet.changed`(结算批次内)/ `jackpot.hit` 仅推一次 | | `dfw:v1:ws:betwin:{period_id}:{user_id}` | 该用户本期 `bet.win` 仅推一次(与上表独立) | **运维补发**(已结算中奖但客户端未收到 `bet.win` 时): ```bash php scripts/republish_bet_win.php --play-record-id=1370 php scripts/republish_bet_win.php --period-id=123 php scripts/republish_bet_win.php --period-no=20260526-183418-c9c90ef1 # 强制忽略 dedup 再推:加 --force ``` 联调脚本:`php scripts/verify_ws_topic_subscribe.php`(含 `bet.win` 入队/订阅校验)。 ### 7.1A 后台连接方式(管理端联调) - 后台菜单:`连接服务器websocket`(联调)、**`游戏管理` → `游戏实时对局`**(`/admin/game/live`,生产监控) - 后台 WebSocket 配置入口: - `/admin/test.GameCurrentStatus/wsConfig` - `/admin/game.Live/wsConfig`(实时对局页自动拉取并订阅,含 `admin.live.snapshot`、`bet.win` 等) - **HTTP 快照**:`GET /admin/game.Live/snapshot` 为**只读**快照(`buildSnapshot`),**不在此接口执行** `recoverLiveRoundState` / 自动开奖,避免请求超时;对局推进由进程 **`gameLiveTicker`** 与开奖流程负责。 - 后台页面能力: - 读取 `ws_url`、`connect_tip`、`sample_messages` - 手动连接/断开 WebSocket - 手动发送订阅与心跳报文 - 实时查看服务端返回帧内容(用于联调事件格式) ### 7.2 HTTP 兜底接口 - 本版本已移除以下兜底接口:`/api/game/currentStatus`、`/api/game/periodHistory`、`/api/wallet/balanceSummary`。 - 状态与余额统一以 WebSocket 推送为主,HTTP 仅保留业务动作/详情查询接口(如 `placeBet`、`depositDetail`、`withdrawDetail`)。 ### 7.3 一致性规则 - 倒计时以服务端下发时间为准,不信任本地时钟累计。 - 下注成功后以 `placeBet` 返回的 `balance_after` 为准,并等待 `wallet.changed` 同步。 - WebSocket 断线后立即重连并重新订阅主题,不再依赖 `currentStatus/periodHistory/balanceSummary` 回补。 --- ## 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. `POST /api/game/lobbyInit` 拉首页初始化(请求头带 `auth-token`) 4. 取得 WebSocket 基址(**当前非 lobbyInit 下发**:与运维/打包配置中的 `H5_WEBSOCKET_URL` 或自建配置接口一致)后建立 WebSocket 连接,**立即发送 `subscribe`** 监听状态流(见 §7.0 / §7.1;**务必包含 `bet.win`**) 5. 用户下注调用 `POST /api/game/placeBet` 6. 下单后以 `placeBet.balance_after` 与 `wallet.changed` 同步余额;开奖结算后监听 **`bet.win`**(`is_win=true`)展示中奖,大奖档看 `data.is_jackpot`(连接已绑定用户,载荷无 `user_id`) 7. 断线或页面回前台时,重连 WebSocket 并重新订阅主题回补实时状态 ## 8.2 充值到下注到提现闭环 1. 拉取档位:`POST /api/finance/depositTierList`(玩家选择一档,并记下该档 `channels[].code`) 2. 创建订单:`POST /api/finance/depositCreate`(`tier_id` + **`channel_code=ddpay`** + `idempotency_key` + DDPay 入金必填字段;可用 JSON / form-data / x-www-form-urlencoded) - 返回 `paid=false`、`status=pending`、**非空 `pay_url`**:客户端在 WebView/浏览器中打开 **`pay_url`(DDPay `payment_url`)** 完成支付;入账由 **`ddpayDepositNotify`** Webhook 驱动,可轮询 `depositDetail` 或等待 `wallet.changed` 刷新余额 3. 客户端可选轮询 `POST /api/finance/depositDetail` 兜底确认状态;入账成功后会收到 `wallet.changed` 4. 下注:`POST /api/game/placeBet` 5. 监听余额:`wallet.changed`(或按订单详情接口核对) 6. 查询流水:`POST /api/wallet/recordList` 7. 提现:`POST /api/finance/withdrawCreate`(即时冻结 `user.coin` 与写出 `withdraw` 流水) -> `POST /api/finance/withdrawDetail` ## 8.3 公告强触达流程 1. 客户端监听 `notice.popout` 2. 拉取列表 `GET /api/notice/noticeList`(列表项已含 `content`、`must_confirm`) 3. 用户勾选确认 `GET /api/notice/noticeConfirm?notice_id=...`(若已有阅读记录则仅更新 `read_at`) 4. 未确认前可由前端阻断下注入口 --- ## 9. 游戏时序流程图(WebSocket + HTTP兜底) ```mermaid flowchart TD A[用户登录 /api/user/login] --> B[拉初始化 /api/game/lobbyInit] B --> C[连接 WebSocket 并订阅主题] C --> D{0-20秒下注期?} D -- 是 --> E[提交下注 /api/game/placeBet] E --> F[等待 wallet.changed 同步余额] D -- 否 --> G[进入封盘与开奖阶段] G --> H[服务端算票与开奖] H --> I[WebSocket: period.opened / bet.win / wallet.changed 等] I --> J[断线重连并重新订阅] J --> C ``` --- ## 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.00" } ] } } ``` ### 10.3 保存约束(沿用现有) - **POST** `/admin/channel/saveChannelAdminShare` - 仅 `status=1` 的行参与占比汇总 - 启用项分配比例总和必须严格等于 `100.00` --- ## 11. 需要你确认的实现口径(进入接口开发前) 1. **登录方式**:仅账号密码,还是要短信/邮箱验证码? 2. **提现收款类型**:首版只做银行卡,还是同时支持电子钱包/加密地址? 3. **自动托管**:是否首期上线;若不上线可先隐藏 `auto-bet` 接口。 4. **WebSocket 主题定义**:状态流、资金流、托管流的 topic 与消息体是否按本文固定。 5. **错误码规范**:是否已有公司统一错误码表;若有需对齐替换本草案码段。 确认后可进入下一步:按该文档落地 controller + validate + service + 路由。