diff --git a/.env-example b/.env-example index 34d6099..0033c29 100644 --- a/.env-example +++ b/.env-example @@ -40,9 +40,10 @@ GAME_HOT_CACHE_QUEUE_CONSUMER_BATCH = 80 # 移动端接口鉴权(/api/v1/authToken) AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a -# Webman Push:浏览器实际连接的 WS 基址(不含 /app/)。HTTPS 后台须用 wss://,与 Nginx 反代 3131 一致 -# 示例:PUSH_WEBSOCKET_CLIENT_URL = wss://zihua-api.yuliao666.top -PUSH_WEBSOCKET_CLIENT_URL = +# H5/后台联调共用:WebSocket 连接地址(建议带 /ws/ 路径) +# HTTPS 域名请使用 wss:// +# 示例:H5_WEBSOCKET_URL = wss://zihua-api.yuliao666.top/ws/ +H5_WEBSOCKET_URL = wss://zihua-api.yuliao666.top/ws/ # 充值支付渠道:在代码注册表之外追加渠道(JSON 数组,每项含 code / name / name_en / sort) # 示例:DEPOSIT_CHANNELS_REGISTRY_JSON = [{"code":"bank_a","name":"银行转账A","name_en":"Bank A","sort":20}] diff --git a/app/admin/controller/game/Live.php b/app/admin/controller/game/Live.php index 142acf9..24c794c 100644 --- a/app/admin/controller/game/Live.php +++ b/app/admin/controller/game/Live.php @@ -10,7 +10,7 @@ use support\Response; use Webman\Http\Request as WebmanRequest; /** - * 游戏实时对局 + * Game live control. */ class Live extends Backend { @@ -40,7 +40,7 @@ class Live extends Backend } /** - * 后台实时对局 WebSocket 配置(管理员联调专用)。 + * WebSocket config for admin live page. */ public function wsConfig(WebmanRequest $request): Response { @@ -64,8 +64,8 @@ class Live extends Backend return $this->success('', [ 'name' => 'ws.admin.live', - 'ws_url' => WebSocketConfigHelper::wsUrl(), - 'connect_tip' => '后台实时对局页将自动订阅管理员全量主题(含本局下注、候选号、开奖与派彩信息)。', + 'ws_url' => WebSocketConfigHelper::wsUrl($request), + 'connect_tip' => 'The admin live page auto-subscribes topics for status, draw result and payout events.', 'subscribe_topics' => $topics, 'sample_messages' => [ '{"action":"ping"}', @@ -98,7 +98,7 @@ class Live extends Backend } /** - * 预约本期开奖号码(倒计时结束后自动开奖,不立即开奖)。 + * Schedule draw number for current period. */ public function draw(WebmanRequest $request): Response { @@ -126,7 +126,7 @@ class Live extends Backend } /** - * 游戏运行开关:关闭时禁止下注、派彩结束后不自动开新期,但当局仍自动开奖并结算;重新开启且无进行中局时立即创建新一期。 + * Runtime switch for game period generation and betting. */ public function runtime(WebmanRequest $request): Response { @@ -147,7 +147,7 @@ class Live extends Backend } /** - * 作废当前对局(下注/封盘阶段),填写原因后退款待开奖注单并关闭运行开关。 + * Void current period and refund waiting orders. */ public function voidPeriod(WebmanRequest $request): Response { diff --git a/app/admin/controller/test/GameCurrentStatus.php b/app/admin/controller/test/GameCurrentStatus.php index cc9819f..4c5bc89 100644 --- a/app/admin/controller/test/GameCurrentStatus.php +++ b/app/admin/controller/test/GameCurrentStatus.php @@ -10,7 +10,7 @@ use support\Response; use Webman\Http\Request as WebmanRequest; /** - * WebSocket 测试:状态流(period.tick / period.opened) + * WebSocket test for period status stream. */ class GameCurrentStatus extends Backend { @@ -35,8 +35,8 @@ class GameCurrentStatus extends Backend return $this->success('', [ 'name' => 'ws.period', - 'ws_url' => WebSocketConfigHelper::wsUrl(), - 'connect_tip' => '连接成功后会自动订阅下列主题;也可在「发送消息」中手动改订阅。未订阅时不会收到业务推送。', + 'ws_url' => WebSocketConfigHelper::wsUrl($request), + 'connect_tip' => 'After connected, topics are auto-subscribed. You can also send subscribe manually.', 'subscribe_topics' => $subscribeTopics, 'sample_messages' => [ '{"action":"ping"}', diff --git a/app/api/controller/Account.php b/app/api/controller/Account.php index ace499d..ca8b71b 100644 --- a/app/api/controller/Account.php +++ b/app/api/controller/Account.php @@ -76,7 +76,7 @@ class Account extends Frontend 'last_bet_period_no' => $user->last_bet_period_no ?? '', 'create_time' => $user->create_time ?? 0, - // 资金字段(4 位小数字符串,与 /api/wallet/balanceSummary 对齐) + // 资金字段(4 位小数字符串,与钱包展示口径一致) 'coin' => floatval($coinBalance), 'coin_balance' => floatval($coinBalance), 'frozen_balance' => 0.00, diff --git a/app/api/controller/Game.php b/app/api/controller/Game.php index 5ff3e2e..61db76f 100644 --- a/app/api/controller/Game.php +++ b/app/api/controller/Game.php @@ -18,7 +18,7 @@ use support\Response; class Game extends MobileBase { - protected array $noNeedLogin = ['dictionaryList', 'periodHistory']; + protected array $noNeedLogin = ['dictionaryList']; public function lobbyInit(Request $request): Response { @@ -93,28 +93,6 @@ class Game extends MobileBase ]); } - public function periodHistory(Request $request): Response - { - $response = $this->initializeMobile($request); - if ($response !== null) { - return $response; - } - $limit = $this->intValue($request->input('limit', 30)); - if ($limit < 1) { - $limit = 30; - } - $list = GameRecord::whereNotNull('result_number')->order('id', 'desc')->limit($limit)->select(); - $rows = []; - foreach ($list as $item) { - $rows[] = [ - 'period_no' => $item->period_no, - 'result_number' => $item->result_number, - 'open_time' => $item->update_time, - ]; - } - return $this->mobileSuccess(['list' => $rows]); - } - public function periodCurrent(Request $request): Response { $response = $this->initializeMobile($request); @@ -138,14 +116,6 @@ class Game extends MobileBase ]); } - /** - * 与前端文档对齐:/api/game/current_status - */ - public function currentStatus(Request $request): Response - { - return $this->periodCurrent($request); - } - /** * 兼容旧路由:/api/game/betPlace * 新语义与 place_bet 一致:bet_amount 作为“单注金额”。 diff --git a/app/api/controller/Wallet.php b/app/api/controller/Wallet.php index 1db1513..03f5913 100644 --- a/app/api/controller/Wallet.php +++ b/app/api/controller/Wallet.php @@ -4,47 +4,12 @@ declare(strict_types=1); namespace app\api\controller; -use app\common\library\finance\WithdrawFlow; use app\common\model\UserWalletRecord; use Webman\Http\Request; use support\Response; class Wallet extends MobileBase { - public function balanceSummary(Request $request): Response - { - $response = $this->initializeMobile($request); - if ($response !== null) { - return $response; - } - $user = $this->auth->getUser(); - $coinBalance = WithdrawFlow::amountString($user->coin ?? '0'); - $flow = WithdrawFlow::status(intval($user->id), [ - 'total_deposit_coin' => $user->total_deposit_coin ?? '0', - 'total_withdraw_coin' => $user->total_withdraw_coin ?? '0', - 'bet_flow_coin' => $user->bet_flow_coin ?? '0', - ]); - $maxWithdrawable = WithdrawFlow::maxWithdrawable($coinBalance, $flow); - return $this->mobileSuccess([ - 'coin_balance' => floatval($coinBalance), - 'frozen_balance' => 0.00, - 'withdrawable_balance' => floatval($coinBalance), - 'max_withdrawable' => floatval($maxWithdrawable), - 'total_deposit_coin' => floatval(WithdrawFlow::amountString($user->total_deposit_coin ?? '0')), - 'total_withdraw_coin' => floatval(WithdrawFlow::amountString($user->total_withdraw_coin ?? '0')), - 'bet_flow_coin' => floatval($flow['bet_flow_coin']), - 'withdraw_flow' => [ - 'ratio' => floatval($flow['ratio']), - 'net_deposit' => floatval($flow['net_deposit']), - 'required_bet_flow' => floatval($flow['required_bet_flow']), - 'remaining_bet_flow' => floatval($flow['remaining_bet_flow']), - 'eligible' => $flow['eligible'], - 'max_withdraw_by_flow' => $flow['flow_unlimited'] ? null : floatval($flow['max_withdraw_by_flow']), - 'flow_unlimited' => $flow['flow_unlimited'], - ], - ]); - } - public function recordList(Request $request): Response { $response = $this->initializeMobile($request); diff --git a/app/common/library/admin/WebSocketConfigHelper.php b/app/common/library/admin/WebSocketConfigHelper.php index fff4ad6..6888dfd 100644 --- a/app/common/library/admin/WebSocketConfigHelper.php +++ b/app/common/library/admin/WebSocketConfigHelper.php @@ -4,15 +4,37 @@ declare(strict_types=1); namespace app\common\library\admin; +use Webman\Http\Request; + final class WebSocketConfigHelper { - public static function wsUrl(): string + public static function wsUrl(?Request $request = null): string { $url = trim((string) env('H5_WEBSOCKET_URL', '')); if ($url !== '') { return $url; } - return 'ws://127.0.0.1:3131'; + + if ($request !== null) { + $proto = strtolower((string) $request->header('x-forwarded-proto', '')); + if ($proto === '') { + $proto = strtolower((string) $request->header('x-forwarded-protocol', '')); + } + $isHttps = $proto === 'https' + || strtolower((string) $request->header('x-forwarded-ssl', '')) === 'on' + || strtolower((string) $request->header('x-scheme', '')) === 'https'; + $scheme = $isHttps ? 'wss' : 'ws'; + + $host = trim((string) $request->header('host', '')); + if ($host === '') { + $host = trim((string) $request->header('x-forwarded-host', '')); + } + if ($host !== '') { + return $scheme . '://' . $host . '/ws/'; + } + } + + return 'ws://127.0.0.1:3131/ws/'; } } diff --git a/config/route.php b/config/route.php index 400548f..50db0a3 100644 --- a/config/route.php +++ b/config/route.php @@ -127,15 +127,12 @@ Route::add(['GET', 'POST'], '/api/account/userProfile', [\app\api\controller\Acc Route::add(['GET', 'POST'], '/api/game/lobbyInit', [\app\api\controller\Game::class, 'lobbyInit']); Route::add(['GET', 'POST'], '/api/game/dictionaryList', [\app\api\controller\Game::class, 'dictionaryList']); -Route::add(['GET', 'POST'], '/api/game/periodHistory', [\app\api\controller\Game::class, 'periodHistory']); Route::add(['GET', 'POST'], '/api/game/periodCurrent', [\app\api\controller\Game::class, 'periodCurrent']); Route::post('/api/game/betPlace', [\app\api\controller\Game::class, 'betPlace']); Route::add(['GET', 'POST'], '/api/game/betMyOrders', [\app\api\controller\Game::class, 'betMyOrders']); -Route::add(['GET', 'POST'], '/api/game/currentStatus', [\app\api\controller\Game::class, 'currentStatus']); Route::post('/api/game/placeBet', [\app\api\controller\Game::class, 'placeBet']); Route::post('/api/game/autoSpin', [\app\api\controller\Game::class, 'autoSpin']); -Route::add(['GET', 'POST'], '/api/wallet/balanceSummary', [\app\api\controller\Wallet::class, 'balanceSummary']); Route::add(['GET', 'POST'], '/api/wallet/recordList', [\app\api\controller\Wallet::class, 'recordList']); Route::add(['GET', 'POST'], '/api/finance/depositTierList', [\app\api\controller\Finance::class, 'depositTierList']); diff --git a/docs/36字花-移动端接口设计草案.md b/docs/36字花-移动端接口设计草案.md index 06536c2..4b48442 100644 --- a/docs/36字花-移动端接口设计草案.md +++ b/docs/36字花-移动端接口设计草案.md @@ -168,7 +168,7 @@ ### 2.3 获取当前用户信息 - **POST** `/api/user/profile` -返回参数(金额类字段统一 2 位小数字符串,与 `/api/wallet/balanceSummary` 对齐): +返回参数(金额类字段统一 2 位小数字符串,与钱包展示口径一致): **基础档案** - `uuid`:string(含义:用户对外唯一标识,10 位) @@ -184,13 +184,13 @@ - `create_time`:int(含义:注册时间戳) **资金与提现配额** -- `coin` / `coin_balance`:string(含义:当前余额;两字段同值,便于与 `/api/wallet/balanceSummary` 平滑切换) +- `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(含义:打码量 / 提现配额诊断快照,结构与 `/api/wallet/balanceSummary.withdraw_flow` 一致,此处额外附 `pending_withdraw`) +- `withdraw_flow`:object(含义:打码量 / 提现配额诊断快照,此处额外附 `pending_withdraw`) - `ratio`:string(打码量倍数;`0` 表示不限打码) - `net_deposit`:string(净充值 = max(0, 累计充值 − 累计提现)) - `required_bet_flow`:string(按门槛口径所需打码量,纯展示) @@ -247,24 +247,10 @@ - `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/currentStatus`(兼容旧路径 `/api/game/periodCurrent`) +- **POST** `/api/game/periodCurrent` 返回参数: - `runtime_enabled`:bool(含义:同 `lobbyInit.runtime_enabled`) @@ -340,29 +326,12 @@ ## 5. 钱包与资金模块(wallet/finance) -### 5.1 获取钱包摘要 -- **POST** `/api/wallet/balanceSummary` - -返回参数: -- `coin_balance`:string(含义:可用余额,等同于 `user.coin`) -- `frozen_balance`:string(含义:冻结余额;当前系统无冻结场景,固定返回 `0.00`) -- `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.00`;`0` 表示"不限打码") - - `net_deposit`:string(含义:净充值 = max(0, 累计充值 - 累计提现)) - - `required_bet_flow`:string(含义:按门槛口径所需的打码量 = 净充值 × 倍数,纯展示) - - `remaining_bet_flow`:string(含义:按门槛口径还差多少打码量,纯展示;已达标为 `0.00`) - - `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` 时退化为仅受余额约束。 +### 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`)均为累加语义;若后续审核驳回,回冲逻辑由后台审核流程负责。 @@ -735,15 +704,14 @@ ### 7.2 HTTP 兜底接口 -- **当前期状态**:`POST /api/game/currentStatus`(建议 1 秒/次兜底) -- **开奖记录**:`POST /api/game/periodHistory`(建议 3~5 秒/次兜底) -- **余额快照**:`POST /api/wallet/balanceSummary`(下注后主动刷新) +- 本版本已移除以下兜底接口:`/api/game/currentStatus`、`/api/game/periodHistory`、`/api/wallet/balanceSummary`。 +- 状态与余额统一以 WebSocket 推送为主,HTTP 仅保留业务动作/详情查询接口(如 `placeBet`、`depositDetail`、`withdrawDetail`)。 ### 7.3 一致性规则 - 倒计时以服务端下发时间为准,不信任本地时钟累计。 -- 下注成功后以 `placeBet` 返回的 `balance_after` 为准,再调用钱包接口兜底。 -- WebSocket 断线后立即重连,并并发触发 `currentStatus + balanceSummary` 全量回补。 +- 下注成功后以 `placeBet` 返回的 `balance_after` 为准,并等待 `wallet.changed` 同步。 +- WebSocket 断线后立即重连并重新订阅主题,不再依赖 `currentStatus/periodHistory/balanceSummary` 回补。 --- @@ -755,8 +723,8 @@ 3. `POST /api/game/lobbyInit` 拉首页初始化(请求头带 `auth-token`) 4. 建立 WebSocket(H5)连接,发送订阅消息监听状态流 5. 用户下注调用 `POST /api/game/placeBet` -6. 下单后调用 `POST /api/wallet/balanceSummary` 刷新余额(并等待 WebSocket 消息) -7. 断线或页面回前台时,兜底调用 `currentStatus + periodHistory` 回补状态 +6. 下单后以 `placeBet.balance_after` 与 `wallet.changed` 同步余额 +7. 断线或页面回前台时,重连 WebSocket 并重新订阅主题回补实时状态 ## 8.2 充值到下注到提现闭环 1. 拉取档位:`POST /api/finance/depositTierList`(玩家选择一档,并记下该档 `channels[].code`) @@ -765,7 +733,7 @@ - 未来接真实第三方:将 `pay_url` 换为真网关,入账仅在支付平台 **异步通知** 中调用 `DepositSettlement::settle`(与当前 `depositMockNotify` 路径一致) 3. 客户端可选轮询 `POST /api/finance/depositDetail` 兜底确认状态;入账成功后会收到 `wallet.changed` 4. 下注:`POST /api/game/placeBet` -5. 轮询余额:`POST /api/wallet/balanceSummary` +5. 监听余额:`wallet.changed`(或按订单详情接口核对) 6. 查询流水:`POST /api/wallet/recordList` 7. 提现:`POST /api/finance/withdrawCreate`(即时冻结 `user.coin` 与写出 `withdraw` 流水) -> `POST /api/finance/withdrawDetail` @@ -785,11 +753,11 @@ flowchart TD B --> C[连接 WebSocket 并订阅主题] C --> D{0-20秒下注期?} D -- 是 --> E[提交下注 /api/game/placeBet] - E --> F[刷新余额 /api/wallet/balanceSummary] + E --> F[等待 wallet.changed 同步余额] D -- 否 --> G[进入封盘与开奖阶段] G --> H[服务端算票与开奖] H --> I[WebSocket 推送状态变化] - I --> J[断线兜底 /api/game/currentStatus] + I --> J[断线重连并重新订阅] J --> C ``` diff --git a/web/src/lang/backend/en/game/live.ts b/web/src/lang/backend/en/game/live.ts index 3f3d79b..6d82449 100644 --- a/web/src/lang/backend/en/game/live.ts +++ b/web/src/lang/backend/en/game/live.ts @@ -7,7 +7,7 @@ export default { bet_countdown: 'Bet left', draw_countdown: 'Draw left', payout_countdown: 'Payout left', - payout_na: '—', + payout_na: '?', payout_phase: 'Payout in progress', action_panel: 'Actions', manual_draw_number: 'Scheduled draw', @@ -18,7 +18,7 @@ export default { push_connected: 'Realtime connection established', push_disconnected: 'Polling mode enabled (push removed)', ws_connected: 'Connected to real-time match', - ws_disconnected: 'WebSocket disconnected (HTTP polling fallback only)', + ws_disconnected: 'Service unavailable, please check backend service', ws_panel_title: 'Admin WebSocket (vs. mobile lightweight stream)', ws_reload_config: 'Load WS config', ws_connect: 'Connect WS', diff --git a/web/src/lang/backend/zh-cn/game/live.ts b/web/src/lang/backend/zh-cn/game/live.ts index 340980e..109477d 100644 --- a/web/src/lang/backend/zh-cn/game/live.ts +++ b/web/src/lang/backend/zh-cn/game/live.ts @@ -1,5 +1,5 @@ export default { - tip: '实时监听压注记录;封盘后 AI 默认号码会锁定,本期倒计时结束按该号码(或您预约的号码)开奖;开奖后约 3 秒派彩再进入下一期。', + tip: '实时监听下注记录;封盘后 AI 默认号码会锁定,本期倒计时结束按该号码(或您预约的号码)开奖;开奖后约 3 秒派彩再进入下一期。', current_record: '当前对局', ai_default_number: 'AI默认开奖号码', pending_draw: '已预约开奖号码', @@ -18,7 +18,7 @@ export default { push_connected: '实时连接已建立', push_disconnected: '已切换为轮询模式(无推送)', ws_connected: '已连接实时对局', - ws_disconnected: 'WebSocket 未连接(仅 HTTP 轮询兜底)', + ws_disconnected: '服务异常,请检查服务端', ws_panel_title: '后台 WebSocket 连接(区别于前端轻量流)', ws_reload_config: '加载WS配置', ws_connect: '连接WS', @@ -28,16 +28,15 @@ export default { candidate_title: '候选号码赔付预估', number: '号码', estimated_loss: '预估赔付', - bet_stream_title: '实时压注记录', + bet_stream_title: '实时下注记录', bet_id: '注单ID', user_id: '玩家ID', - pick_numbers: '压注号码', - total_amount: '压注总额', + pick_numbers: '下注号码', + total_amount: '下注总额', streak_at_bet: '下注时连胜', runtime_switch: '游戏运行', countdown_maintenance: '维护中', - runtime_draining_banner: - '已关闭游戏:当前局将正常进行至开奖、结算并完成派彩;全部结束后进入维护模式(倒计时与操作区将切换为维护中)。', + runtime_draining_banner: '已关闭游戏:当前局将正常进行至开奖、结算并完成派彩;全部结束后进入维护模式(倒计时与操作区将切换为维护中)。', runtime_maintenance_banner: '维护中:玩家端已禁止下注。请开启「游戏运行」恢复;若无进行中的局将自动创建新一期。', runtime_off_tip: '开启「游戏运行」后,若无进行中的局将立即创建新一期。', void_btn: '作废本局',