1.优化websocket连接

2.修复游戏实施对局显示错误
This commit is contained in:
2026-04-24 16:53:00 +08:00
parent 203e478b65
commit d9b574676b
11 changed files with 67 additions and 145 deletions

View File

@@ -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}]

View File

@@ -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
{

View File

@@ -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"}',

View File

@@ -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,

View File

@@ -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 作为“单注金额”。

View File

@@ -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);

View File

@@ -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/';
}
}

View File

@@ -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']);

View File

@@ -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<object>
- `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. 建立 WebSocketH5连接发送订阅消息监听状态流
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
```

View File

@@ -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',

View File

@@ -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: '作废本局',