1481 lines
32 KiB
Markdown
Executable File
1481 lines
32 KiB
Markdown
Executable File
# Xóc Đĩa 游戏 - 前端 API 接口文档
|
||
|
||
> **版本**: v1.0.0
|
||
> **更新日期**: 2026-01-08
|
||
> **基础URL**: `http://yourdomain.com`
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
1. [概述](#概述)
|
||
2. [WebSocket 实时通信](#websocket-实时通信)
|
||
3. [HTTP REST API](#http-rest-api)
|
||
4. [数据结构定义](#数据结构定义)
|
||
5. [错误码说明](#错误码说明)
|
||
6. [前端对接流程](#前端对接流程)
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
### 技术栈建议
|
||
|
||
- **WebSocket**: 实时游戏状态、投注更新、开奖推送
|
||
- **HTTP REST API**: 查询房间信息、历史记录、用户余额
|
||
|
||
### 认证方式
|
||
|
||
所有 API 请求需要用户已登录,Session Cookie 会自动携带。
|
||
|
||
```javascript
|
||
// 前端登录后 Session 自动保存
|
||
fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
credentials: 'include', // 重要:携带 Cookie
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username: 'xxx', password: 'xxx' })
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## WebSocket 实时通信
|
||
|
||
### 连接信息
|
||
|
||
```
|
||
WebSocket URL: ws://yourdomain.com/ws/xocdia
|
||
或 (SSL): wss://yourdomain.com/ws/xocdia
|
||
```
|
||
|
||
### 连接示例
|
||
|
||
```javascript
|
||
const ws = new WebSocket('ws://yourdomain.com/ws/xocdia');
|
||
|
||
ws.onopen = () => {
|
||
console.log('WebSocket 连接成功');
|
||
|
||
// 订阅房间
|
||
ws.send(JSON.stringify({
|
||
action: 'SUBSCRIBE_ROOM',
|
||
data: { room_id: 1 }
|
||
}));
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
const message = JSON.parse(event.data);
|
||
handleServerEvent(message);
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('WebSocket 错误:', error);
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
console.log('WebSocket 连接关闭');
|
||
// 实现重连逻辑
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🔽 服务器 → 客户端事件
|
||
|
||
### 1. GAME_START - 期号开始
|
||
|
||
**说明**: 新期号开始,进入下注阶段
|
||
|
||
```json
|
||
{
|
||
"event": "GAME_START",
|
||
"data": {
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"room_id": 1,
|
||
"state": "betting",
|
||
"countdown": 30,
|
||
"server_time": "2026-01-08T10:00:00Z",
|
||
"config": {
|
||
"odds": {
|
||
"even": 1.0,
|
||
"odd": 1.0,
|
||
"four_red": 10.0,
|
||
"four_white": 10.0,
|
||
"three_red": 3.5,
|
||
"three_white": 3.5
|
||
},
|
||
"limits": {
|
||
"min_bet": 10,
|
||
"max_bet": 50000,
|
||
"max_payout": 500000
|
||
},
|
||
"timing": {
|
||
"betting_duration": 30
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleGameStart(data) {
|
||
// 1. 显示期号信息
|
||
document.getElementById('period-number').textContent = data.period_number;
|
||
|
||
// 2. 启动倒计时
|
||
startCountdown(data.countdown);
|
||
|
||
// 3. 开放投注按钮
|
||
enableBettingButtons();
|
||
|
||
// 4. 清空上期投注记录
|
||
clearPreviousBets();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. COUNTDOWN_UPDATE - 倒计时更新
|
||
|
||
**说明**: 每秒推送一次剩余时间
|
||
|
||
```json
|
||
{
|
||
"event": "COUNTDOWN_UPDATE",
|
||
"data": {
|
||
"period_id": 12345,
|
||
"remaining": 25,
|
||
"state": "betting"
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleCountdownUpdate(data) {
|
||
document.getElementById('countdown').textContent = data.remaining + '秒';
|
||
|
||
if (data.remaining <= 5) {
|
||
// 最后 5 秒闪烁提醒
|
||
document.getElementById('countdown').classList.add('urgent');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. BET_SUCCESS - 投注成功
|
||
|
||
**说明**: 用户投注成功的确认
|
||
|
||
```json
|
||
{
|
||
"event": "BET_SUCCESS",
|
||
"data": {
|
||
"bet_id": 67890,
|
||
"user_id": 456,
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"odds": 1.0,
|
||
"potential_win": 100,
|
||
"remaining_balance": 9900,
|
||
"timestamp": "2026-01-08T10:00:15Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleBetSuccess(data) {
|
||
// 1. 更新余额显示
|
||
document.getElementById('balance').textContent = data.remaining_balance;
|
||
|
||
// 2. 添加到投注列表
|
||
addBetToList({
|
||
type: data.bet_type,
|
||
amount: data.bet_amount,
|
||
potentialWin: data.potential_win
|
||
});
|
||
|
||
// 3. 显示成功提示
|
||
showToast('投注成功!', 'success');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. BETTING_CLOSED - 封盘
|
||
|
||
**说明**: 下注阶段结束,不再接受投注
|
||
|
||
```json
|
||
{
|
||
"event": "BETTING_CLOSED",
|
||
"data": {
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"state": "shaking",
|
||
"message": "投注已封盘,等待开奖...",
|
||
"total_bets_count": 150,
|
||
"total_bets_amount": 25000
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleBettingClosed(data) {
|
||
// 1. 禁用投注按钮
|
||
disableBettingButtons();
|
||
|
||
// 2. 停止倒计时
|
||
stopCountdown();
|
||
|
||
// 3. 显示"等待开奖"动画
|
||
showShakingAnimation();
|
||
|
||
// 4. 显示本期投注统计
|
||
showBetStats(data.total_bets_count, data.total_bets_amount);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. GAME_RESULT - 开奖结果
|
||
|
||
**说明**: 开奖结果公布及用户结算信息
|
||
|
||
```json
|
||
{
|
||
"event": "GAME_RESULT",
|
||
"data": {
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"result": {
|
||
"coins": [1, 1, 1, 0],
|
||
"pattern": "3_red_1_white",
|
||
"red_count": 3,
|
||
"white_count": 1,
|
||
"display_text": "3 Đen 1 Trắng"
|
||
},
|
||
"settlement": {
|
||
"your_bets": [
|
||
{
|
||
"bet_id": 67890,
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"odds": 1.0,
|
||
"is_win": true,
|
||
"win_amount": 100,
|
||
"commission": 0,
|
||
"net_profit": 100,
|
||
"status": "win",
|
||
"special_rule_applied": false
|
||
}
|
||
],
|
||
"total_bet_amount": 100,
|
||
"total_win_amount": 100,
|
||
"net_profit": 100,
|
||
"new_balance": 10000
|
||
},
|
||
"statistics": {
|
||
"total_players": 50,
|
||
"total_bets_amount": 50000,
|
||
"big_winners": [
|
||
{
|
||
"username": "player***",
|
||
"win_amount": 5000
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleGameResult(data) {
|
||
// 1. 显示开奖动画
|
||
showResultAnimation(data.result.coins);
|
||
|
||
// 2. 更新余额
|
||
document.getElementById('balance').textContent = data.settlement.new_balance;
|
||
|
||
// 3. 显示输赢结果
|
||
data.settlement.your_bets.forEach(bet => {
|
||
if (bet.is_win) {
|
||
showWinNotification(bet.net_profit);
|
||
} else {
|
||
showLoseNotification(bet.bet_amount);
|
||
}
|
||
});
|
||
|
||
// 4. 更新投注历史
|
||
updateBetHistory(data);
|
||
|
||
// 5. 显示本期统计
|
||
showGameStatistics(data.statistics);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6. BET_STATS_UPDATE - 实时投注统计
|
||
|
||
**说明**: 实时推送当前期号的投注分布(可选功能)
|
||
|
||
```json
|
||
{
|
||
"event": "BET_STATS_UPDATE",
|
||
"data": {
|
||
"period_id": 12345,
|
||
"stats": {
|
||
"even": {
|
||
"amount": 10000,
|
||
"count": 50,
|
||
"percentage": 40
|
||
},
|
||
"odd": {
|
||
"amount": 8000,
|
||
"count": 40,
|
||
"percentage": 32
|
||
},
|
||
"four_red": {
|
||
"amount": 3000,
|
||
"count": 15,
|
||
"percentage": 12
|
||
}
|
||
},
|
||
"total_amount": 25000,
|
||
"total_count": 150
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleBetStatsUpdate(data) {
|
||
// 更新投注分布图表
|
||
updateBetDistributionChart(data.stats);
|
||
|
||
// 显示热门投注项
|
||
highlightPopularBets(data.stats);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7. CONFIG_UPDATED - 配置更新
|
||
|
||
**说明**: 房间配置被管理员修改
|
||
|
||
```json
|
||
{
|
||
"event": "CONFIG_UPDATED",
|
||
"data": {
|
||
"room_id": 1,
|
||
"new_config": {
|
||
"odds": {
|
||
"even": 1.0,
|
||
"odd": 1.0,
|
||
"four_red": 12.0,
|
||
"four_white": 12.0,
|
||
"three_red": 4.0,
|
||
"three_white": 4.0
|
||
},
|
||
"limits": {
|
||
"min_bet": 10,
|
||
"max_bet": 100000
|
||
}
|
||
},
|
||
"effective_from": "next_period",
|
||
"message": "房间配置已更新,下期生效"
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端处理**:
|
||
```javascript
|
||
function handleConfigUpdated(data) {
|
||
// 显示配置更新通知
|
||
showNotification(data.message, 'info');
|
||
|
||
// 可选:提前显示新赔率
|
||
previewNewOdds(data.new_config.odds);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 8. ERROR - 错误通知
|
||
|
||
**说明**: 服务器推送的错误信息
|
||
|
||
```json
|
||
{
|
||
"event": "ERROR",
|
||
"data": {
|
||
"code": "BET_AMOUNT_INVALID",
|
||
"message": "投注金额超出限制",
|
||
"details": {
|
||
"min_bet": 10,
|
||
"max_bet": 50000,
|
||
"your_bet": 100000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔼 客户端 → 服务器事件
|
||
|
||
### 1. SUBSCRIBE_ROOM - 订阅房间
|
||
|
||
**说明**: 连接后立即订阅房间,接收该房间的实时消息
|
||
|
||
```json
|
||
{
|
||
"action": "SUBSCRIBE_ROOM",
|
||
"data": {
|
||
"room_id": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. PLACE_BET - 下注
|
||
|
||
**说明**: 用户下注
|
||
|
||
```json
|
||
{
|
||
"action": "PLACE_BET",
|
||
"data": {
|
||
"room_id": 1,
|
||
"period_id": 12345,
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"client_timestamp": "2026-01-08T10:00:15Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**服务器响应**: `BET_SUCCESS` 或 `ERROR` 事件
|
||
|
||
---
|
||
|
||
### 3. GET_GAME_STATE - 获取当前游戏状态
|
||
|
||
**说明**: 用户刷新页面或重连后,获取当前游戏状态
|
||
|
||
```json
|
||
{
|
||
"action": "GET_GAME_STATE",
|
||
"data": {
|
||
"room_id": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**服务器响应**:
|
||
```json
|
||
{
|
||
"event": "GAME_STATE",
|
||
"data": {
|
||
"room_id": 1,
|
||
"current_period": {
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"state": "betting",
|
||
"countdown": 18,
|
||
"started_at": "2026-01-08T10:00:00Z"
|
||
},
|
||
"your_bets": [
|
||
{
|
||
"bet_id": 67890,
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"status": "pending"
|
||
}
|
||
],
|
||
"config": { /* 房间配置 */ }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. GET_BET_HISTORY - 获取投注历史
|
||
|
||
**说明**: 查询用户历史投注记录
|
||
|
||
```json
|
||
{
|
||
"action": "GET_BET_HISTORY",
|
||
"data": {
|
||
"room_id": 1,
|
||
"limit": 20,
|
||
"offset": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
**服务器响应**:
|
||
```json
|
||
{
|
||
"event": "BET_HISTORY",
|
||
"data": {
|
||
"bets": [
|
||
{
|
||
"period_number": "20260108-001",
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"status": "win",
|
||
"net_profit": 95,
|
||
"result": {
|
||
"coins": [1, 1, 1, 0],
|
||
"pattern": "3_red_1_white"
|
||
},
|
||
"created_at": "2026-01-08T10:00:15Z"
|
||
}
|
||
],
|
||
"total": 150,
|
||
"limit": 20,
|
||
"offset": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## HTTP REST API
|
||
|
||
### 1. 获取房间列表
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/xocdia/rooms
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"room_id": 1,
|
||
"room_name": "Bàn Thu Phế",
|
||
"room_type": "commission",
|
||
"status": 1,
|
||
"odds": {
|
||
"even": 1.0,
|
||
"odd": 1.0,
|
||
"four_red": 10.0,
|
||
"four_white": 10.0,
|
||
"three_red": 3.5,
|
||
"three_white": 3.5
|
||
},
|
||
"limits": {
|
||
"min_bet": 10,
|
||
"max_bet": 50000,
|
||
"max_payout": 500000
|
||
},
|
||
"online_players": 50,
|
||
"current_period": {
|
||
"period_number": "20260108-001",
|
||
"state": "betting",
|
||
"countdown": 25
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**前端调用示例**:
|
||
```javascript
|
||
async function fetchRooms() {
|
||
const response = await fetch('/api/xocdia/rooms', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
renderRoomList(data.data);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 获取房间详情
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/xocdia/rooms/{room_id}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"room_id": 1,
|
||
"room_name": "Bàn Thu Phế",
|
||
"room_type": "commission",
|
||
"commission_rate": 0.05,
|
||
"odds": { /* ... */ },
|
||
"limits": { /* ... */ },
|
||
"timing": {
|
||
"betting_duration": 30,
|
||
"result_display_duration": 8
|
||
},
|
||
"statistics": {
|
||
"today_periods": 50,
|
||
"today_bets": 5000,
|
||
"today_turnover": 500000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 获取当前期号信息
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/xocdia/periods/current?room_id=1
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"period_id": 12345,
|
||
"period_number": "20260108-001",
|
||
"room_id": 1,
|
||
"state": "betting",
|
||
"started_at": "2026-01-08T10:00:00Z",
|
||
"countdown": 25,
|
||
"config_snapshot": {
|
||
"odds": { /* ... */ },
|
||
"limits": { /* ... */ }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端调用示例**:
|
||
```javascript
|
||
async function getCurrentPeriod(roomId) {
|
||
const response = await fetch(`/api/xocdia/periods/current?room_id=${roomId}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data) {
|
||
return data.data;
|
||
}
|
||
return null;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 下注(HTTP 方式,备用)
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/xocdia/bet
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"room_id": 1,
|
||
"period_id": 12345,
|
||
"bet_type": "odd",
|
||
"bet_amount": 100
|
||
}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "投注成功",
|
||
"data": {
|
||
"bet_id": 67890,
|
||
"remaining_balance": 9900,
|
||
"potential_win": 100
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"code": "INSUFFICIENT_BALANCE",
|
||
"message": "余额不足",
|
||
"details": {
|
||
"current_balance": 50,
|
||
"required": 100
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端调用示例**:
|
||
```javascript
|
||
async function placeBet(roomId, periodId, betType, betAmount) {
|
||
try {
|
||
const response = await fetch('/api/xocdia/bet', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
room_id: roomId,
|
||
period_id: periodId,
|
||
bet_type: betType,
|
||
bet_amount: betAmount
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// 更新余额
|
||
updateBalance(data.data.remaining_balance);
|
||
showToast('投注成功', 'success');
|
||
} else {
|
||
showToast(data.message, 'error');
|
||
}
|
||
|
||
return data;
|
||
} catch (error) {
|
||
console.error('投注失败:', error);
|
||
showToast('网络错误,请重试', 'error');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 获取开奖历史
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/xocdia/periods/history?room_id=1&limit=20&offset=0
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"periods": [
|
||
{
|
||
"period_number": "20260108-001",
|
||
"result": {
|
||
"coins": [1, 1, 1, 0],
|
||
"pattern": "3_red_1_white",
|
||
"display_text": "3 Đen 1 Trắng"
|
||
},
|
||
"settled_at": "2026-01-08T10:01:30Z",
|
||
"total_bets": 150,
|
||
"total_amount": 25000
|
||
}
|
||
],
|
||
"total": 500,
|
||
"limit": 20,
|
||
"offset": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6. 获取用户投注记录
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/xocdia/my-bets?room_id=1&limit=20&offset=0
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"bets": [
|
||
{
|
||
"bet_id": 67890,
|
||
"period_number": "20260108-001",
|
||
"bet_type": "odd",
|
||
"bet_amount": 100,
|
||
"odds": 1.0,
|
||
"status": "win",
|
||
"win_amount": 100,
|
||
"commission": 5,
|
||
"net_profit": 95,
|
||
"result": {
|
||
"coins": [1, 1, 1, 0],
|
||
"pattern": "3_red_1_white"
|
||
},
|
||
"created_at": "2026-01-08T10:00:15Z",
|
||
"settled_at": "2026-01-08T10:01:30Z"
|
||
}
|
||
],
|
||
"total": 150,
|
||
"limit": 20,
|
||
"offset": 0,
|
||
"summary": {
|
||
"total_bet_amount": 15000,
|
||
"total_win_amount": 8000,
|
||
"net_profit": -7000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7. 获取用户余额
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/user/balance
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"balance": 10000.50,
|
||
"currency": "VND"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 数据结构定义
|
||
|
||
### GameState(游戏状态)
|
||
|
||
```typescript
|
||
type GameState = 'waiting' | 'betting' | 'shaking' | 'settling' | 'showing';
|
||
```
|
||
|
||
### BetType(投注类型)
|
||
|
||
```typescript
|
||
type BetType =
|
||
| 'even' // 双 (Chẵn)
|
||
| 'odd' // 单 (Lẻ)
|
||
| 'four_red' // 4红 (4 Đen)
|
||
| 'four_white' // 4白 (4 Trắng)
|
||
| 'three_red' // 3红1白 (3 Đen 1 Trắng)
|
||
| 'three_white'; // 1红3白 (1 Đen 3 Trắng)
|
||
```
|
||
|
||
### BetStatus(投注状态)
|
||
|
||
```typescript
|
||
type BetStatus = 'pending' | 'win' | 'lose' | 'refund';
|
||
```
|
||
|
||
### GameResult(开奖结果)
|
||
|
||
```typescript
|
||
interface GameResult {
|
||
coins: [number, number, number, number]; // [1,1,1,0] = 3红1白
|
||
pattern: string; // '3_red_1_white'
|
||
red_count: number; // 3
|
||
white_count: number; // 1
|
||
display_text: string; // '3 Đen 1 Trắng'
|
||
}
|
||
```
|
||
|
||
### RoomConfig(房间配置)
|
||
|
||
```typescript
|
||
interface RoomConfig {
|
||
room_id: number;
|
||
room_name: string;
|
||
room_type: 'commission' | 'special_rule';
|
||
status: 0 | 1; // 0=关闭, 1=开放
|
||
|
||
odds: {
|
||
even: number;
|
||
odd: number;
|
||
four_red: number;
|
||
four_white: number;
|
||
three_red: number;
|
||
three_white: number;
|
||
};
|
||
|
||
limits: {
|
||
min_bet: number;
|
||
max_bet: number;
|
||
max_payout: number;
|
||
};
|
||
|
||
timing: {
|
||
betting_duration: number;
|
||
result_display_duration: number;
|
||
};
|
||
|
||
commission_rate?: number; // 抽水比例(0.05 = 5%)
|
||
|
||
special_rule?: {
|
||
enabled: boolean;
|
||
trigger_result: string;
|
||
affected_bet_types: string[];
|
||
action: 'refund' | 'half_win' | 'lose';
|
||
refund_percentage: number;
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 错误码说明
|
||
|
||
### 客户端错误 (4xx)
|
||
|
||
| 错误码 | 说明 | 解决方案 |
|
||
|-------|------|---------|
|
||
| `NOT_LOGGED_IN` | 用户未登录 | 跳转到登录页 |
|
||
| `INSUFFICIENT_BALANCE` | 余额不足 | 提示用户充值 |
|
||
| `BET_AMOUNT_INVALID` | 投注金额无效 | 检查最小/最大限制 |
|
||
| `BET_TYPE_INVALID` | 投注类型无效 | 检查投注类型是否正确 |
|
||
| `BETTING_CLOSED` | 已封盘 | 提示用户等待下一期 |
|
||
| `PERIOD_NOT_FOUND` | 期号不存在 | 刷新页面获取最新期号 |
|
||
| `DUPLICATE_BET` | 重复投注 | 防止重复提交 |
|
||
|
||
### 服务器错误 (5xx)
|
||
|
||
| 错误码 | 说明 | 解决方案 |
|
||
|-------|------|---------|
|
||
| `SETTLEMENT_FAILED` | 结算失败 | 联系客服 |
|
||
| `DATABASE_ERROR` | 数据库错误 | 稍后重试 |
|
||
| `WEBSOCKET_ERROR` | WebSocket 连接错误 | 尝试重连 |
|
||
|
||
---
|
||
|
||
## 前端对接流程
|
||
|
||
### 第一步:初始化
|
||
|
||
```javascript
|
||
class XocdiaGame {
|
||
constructor(roomId) {
|
||
this.roomId = roomId;
|
||
this.ws = null;
|
||
this.currentPeriod = null;
|
||
this.userBets = [];
|
||
this.balance = 0;
|
||
}
|
||
|
||
async init() {
|
||
// 1. 获取房间配置
|
||
await this.fetchRoomConfig();
|
||
|
||
// 2. 获取用户余额
|
||
await this.fetchBalance();
|
||
|
||
// 3. 连接 WebSocket
|
||
await this.connectWebSocket();
|
||
|
||
// 4. 订阅房间
|
||
this.subscribeRoom();
|
||
|
||
// 5. 获取当前游戏状态
|
||
this.getGameState();
|
||
}
|
||
|
||
async fetchRoomConfig() {
|
||
const response = await fetch(`/api/xocdia/rooms/${this.roomId}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.roomConfig = data.data;
|
||
this.renderRoomInfo();
|
||
}
|
||
}
|
||
|
||
async fetchBalance() {
|
||
const response = await fetch('/api/user/balance', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.balance = data.data.balance;
|
||
this.updateBalanceDisplay();
|
||
}
|
||
}
|
||
|
||
connectWebSocket() {
|
||
return new Promise((resolve, reject) => {
|
||
this.ws = new WebSocket('ws://yourdomain.com/ws/xocdia');
|
||
|
||
this.ws.onopen = () => {
|
||
console.log('WebSocket 连接成功');
|
||
resolve();
|
||
};
|
||
|
||
this.ws.onmessage = (event) => {
|
||
const message = JSON.parse(event.data);
|
||
this.handleServerEvent(message);
|
||
};
|
||
|
||
this.ws.onerror = (error) => {
|
||
console.error('WebSocket 错误:', error);
|
||
reject(error);
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket 断开,尝试重连...');
|
||
setTimeout(() => this.connectWebSocket(), 3000);
|
||
};
|
||
});
|
||
}
|
||
|
||
subscribeRoom() {
|
||
this.ws.send(JSON.stringify({
|
||
action: 'SUBSCRIBE_ROOM',
|
||
data: { room_id: this.roomId }
|
||
}));
|
||
}
|
||
|
||
getGameState() {
|
||
this.ws.send(JSON.stringify({
|
||
action: 'GET_GAME_STATE',
|
||
data: { room_id: this.roomId }
|
||
}));
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 第二步:处理服务器事件
|
||
|
||
```javascript
|
||
class XocdiaGame {
|
||
// ... 上面的代码 ...
|
||
|
||
handleServerEvent(message) {
|
||
switch (message.event) {
|
||
case 'GAME_START':
|
||
this.handleGameStart(message.data);
|
||
break;
|
||
|
||
case 'COUNTDOWN_UPDATE':
|
||
this.handleCountdownUpdate(message.data);
|
||
break;
|
||
|
||
case 'BET_SUCCESS':
|
||
this.handleBetSuccess(message.data);
|
||
break;
|
||
|
||
case 'BETTING_CLOSED':
|
||
this.handleBettingClosed(message.data);
|
||
break;
|
||
|
||
case 'GAME_RESULT':
|
||
this.handleGameResult(message.data);
|
||
break;
|
||
|
||
case 'ERROR':
|
||
this.handleError(message.data);
|
||
break;
|
||
|
||
case 'GAME_STATE':
|
||
this.handleGameState(message.data);
|
||
break;
|
||
}
|
||
}
|
||
|
||
handleGameStart(data) {
|
||
this.currentPeriod = data;
|
||
this.userBets = [];
|
||
|
||
// UI 更新
|
||
document.getElementById('period-number').textContent = data.period_number;
|
||
this.startCountdown(data.countdown);
|
||
this.enableBettingButtons();
|
||
}
|
||
|
||
handleCountdownUpdate(data) {
|
||
document.getElementById('countdown').textContent = data.remaining;
|
||
|
||
if (data.remaining <= 5) {
|
||
document.getElementById('countdown').classList.add('urgent');
|
||
}
|
||
}
|
||
|
||
handleBetSuccess(data) {
|
||
// 更新余额
|
||
this.balance = data.remaining_balance;
|
||
this.updateBalanceDisplay();
|
||
|
||
// 添加到投注列表
|
||
this.userBets.push(data);
|
||
this.renderUserBets();
|
||
|
||
// 显示提示
|
||
this.showToast('投注成功!', 'success');
|
||
}
|
||
|
||
handleBettingClosed(data) {
|
||
this.disableBettingButtons();
|
||
this.stopCountdown();
|
||
this.showShakingAnimation();
|
||
}
|
||
|
||
handleGameResult(data) {
|
||
// 显示开奖动画
|
||
this.showResultAnimation(data.result);
|
||
|
||
// 更新余额
|
||
this.balance = data.settlement.new_balance;
|
||
this.updateBalanceDisplay();
|
||
|
||
// 显示输赢结果
|
||
this.showSettlementResult(data.settlement);
|
||
|
||
// 更新历史记录
|
||
this.updateHistory(data);
|
||
}
|
||
|
||
handleError(data) {
|
||
this.showToast(data.message, 'error');
|
||
}
|
||
|
||
handleGameState(data) {
|
||
// 恢复游戏状态(用于刷新后)
|
||
this.currentPeriod = data.current_period;
|
||
this.userBets = data.your_bets;
|
||
|
||
if (data.current_period.state === 'betting') {
|
||
this.startCountdown(data.current_period.countdown);
|
||
this.enableBettingButtons();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 第三步:投注操作
|
||
|
||
```javascript
|
||
class XocdiaGame {
|
||
// ... 上面的代码 ...
|
||
|
||
async placeBet(betType, betAmount) {
|
||
// 前端验证
|
||
if (!this.currentPeriod || this.currentPeriod.state !== 'betting') {
|
||
this.showToast('当前不可下注', 'error');
|
||
return;
|
||
}
|
||
|
||
if (betAmount < this.roomConfig.limits.min_bet) {
|
||
this.showToast(`最小投注 ${this.roomConfig.limits.min_bet}`, 'error');
|
||
return;
|
||
}
|
||
|
||
if (betAmount > this.roomConfig.limits.max_bet) {
|
||
this.showToast(`最大投注 ${this.roomConfig.limits.max_bet}`, 'error');
|
||
return;
|
||
}
|
||
|
||
if (betAmount > this.balance) {
|
||
this.showToast('余额不足', 'error');
|
||
return;
|
||
}
|
||
|
||
// 通过 WebSocket 发送
|
||
this.ws.send(JSON.stringify({
|
||
action: 'PLACE_BET',
|
||
data: {
|
||
room_id: this.roomId,
|
||
period_id: this.currentPeriod.period_id,
|
||
bet_type: betType,
|
||
bet_amount: betAmount,
|
||
client_timestamp: new Date().toISOString()
|
||
}
|
||
}));
|
||
|
||
// 或通过 HTTP 发送(备用)
|
||
// const response = await fetch('/api/xocdia/bet', { ... });
|
||
}
|
||
|
||
// 快捷投注方法
|
||
betEven(amount) {
|
||
this.placeBet('even', amount);
|
||
}
|
||
|
||
betOdd(amount) {
|
||
this.placeBet('odd', amount);
|
||
}
|
||
|
||
betFourRed(amount) {
|
||
this.placeBet('four_red', amount);
|
||
}
|
||
|
||
betFourWhite(amount) {
|
||
this.placeBet('four_white', amount);
|
||
}
|
||
|
||
betThreeRed(amount) {
|
||
this.placeBet('three_red', amount);
|
||
}
|
||
|
||
betThreeWhite(amount) {
|
||
this.placeBet('three_white', amount);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 第四步:UI 渲染
|
||
|
||
```javascript
|
||
class XocdiaGame {
|
||
// ... 上面的代码 ...
|
||
|
||
renderRoomInfo() {
|
||
document.getElementById('room-name').textContent = this.roomConfig.room_name;
|
||
|
||
// 渲染赔率
|
||
document.getElementById('odds-even').textContent = `1:${this.roomConfig.odds.even}`;
|
||
document.getElementById('odds-odd').textContent = `1:${this.roomConfig.odds.odd}`;
|
||
document.getElementById('odds-four-red').textContent = `1:${this.roomConfig.odds.four_red}`;
|
||
// ...
|
||
}
|
||
|
||
updateBalanceDisplay() {
|
||
document.getElementById('balance').textContent = this.balance.toLocaleString();
|
||
}
|
||
|
||
renderUserBets() {
|
||
const container = document.getElementById('user-bets-list');
|
||
container.innerHTML = '';
|
||
|
||
this.userBets.forEach(bet => {
|
||
const betElement = document.createElement('div');
|
||
betElement.className = 'bet-item';
|
||
betElement.innerHTML = `
|
||
<span class="bet-type">${this.getBetTypeLabel(bet.bet_type)}</span>
|
||
<span class="bet-amount">${bet.bet_amount}</span>
|
||
<span class="potential-win">可赢 ${bet.potential_win}</span>
|
||
`;
|
||
container.appendChild(betElement);
|
||
});
|
||
}
|
||
|
||
showResultAnimation(result) {
|
||
const container = document.getElementById('result-animation');
|
||
container.innerHTML = '';
|
||
|
||
result.coins.forEach(coin => {
|
||
const coinElement = document.createElement('div');
|
||
coinElement.className = coin === 1 ? 'coin red' : 'coin white';
|
||
coinElement.textContent = '●';
|
||
container.appendChild(coinElement);
|
||
});
|
||
|
||
// 显示结果文字
|
||
document.getElementById('result-text').textContent = result.display_text;
|
||
}
|
||
|
||
showSettlementResult(settlement) {
|
||
if (settlement.net_profit > 0) {
|
||
this.showWinAnimation(settlement.net_profit);
|
||
} else if (settlement.net_profit < 0) {
|
||
this.showLoseAnimation(Math.abs(settlement.net_profit));
|
||
}
|
||
}
|
||
|
||
getBetTypeLabel(betType) {
|
||
const labels = {
|
||
'even': '双 (Chẵn)',
|
||
'odd': '单 (Lẻ)',
|
||
'four_red': '4红',
|
||
'four_white': '4白',
|
||
'three_red': '3红1白',
|
||
'three_white': '1红3白'
|
||
};
|
||
return labels[betType] || betType;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 第五步:使用示例
|
||
|
||
```javascript
|
||
// 初始化游戏
|
||
const game = new XocdiaGame(1); // room_id = 1
|
||
|
||
game.init().then(() => {
|
||
console.log('游戏初始化完成');
|
||
|
||
// 绑定投注按钮
|
||
document.getElementById('btn-bet-even').addEventListener('click', () => {
|
||
const amount = parseFloat(document.getElementById('bet-amount').value);
|
||
game.betEven(amount);
|
||
});
|
||
|
||
document.getElementById('btn-bet-odd').addEventListener('click', () => {
|
||
const amount = parseFloat(document.getElementById('bet-amount').value);
|
||
game.betOdd(amount);
|
||
});
|
||
|
||
// 快捷金额按钮
|
||
document.getElementById('btn-bet-100').addEventListener('click', () => {
|
||
document.getElementById('bet-amount').value = 100;
|
||
});
|
||
|
||
document.getElementById('btn-bet-500').addEventListener('click', () => {
|
||
document.getElementById('bet-amount').value = 500;
|
||
});
|
||
}).catch(error => {
|
||
console.error('游戏初始化失败:', error);
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 开发建议
|
||
|
||
### 1. 错误处理
|
||
|
||
```javascript
|
||
// 全局错误处理
|
||
window.addEventListener('error', (event) => {
|
||
console.error('全局错误:', event.error);
|
||
// 上报到错误监控平台
|
||
});
|
||
|
||
// WebSocket 重连策略
|
||
class WebSocketReconnect {
|
||
constructor(url, maxRetries = 5) {
|
||
this.url = url;
|
||
this.maxRetries = maxRetries;
|
||
this.retryCount = 0;
|
||
this.retryDelay = 1000;
|
||
}
|
||
|
||
connect() {
|
||
this.ws = new WebSocket(this.url);
|
||
|
||
this.ws.onclose = () => {
|
||
if (this.retryCount < this.maxRetries) {
|
||
this.retryCount++;
|
||
setTimeout(() => {
|
||
console.log(`重连中... (${this.retryCount}/${this.maxRetries})`);
|
||
this.connect();
|
||
}, this.retryDelay * this.retryCount);
|
||
} else {
|
||
console.error('WebSocket 重连失败');
|
||
// 提示用户刷新页面
|
||
}
|
||
};
|
||
|
||
this.ws.onopen = () => {
|
||
this.retryCount = 0;
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 性能优化
|
||
|
||
```javascript
|
||
// 防抖投注(防止重复点击)
|
||
const debounceBet = debounce((betType, amount) => {
|
||
game.placeBet(betType, amount);
|
||
}, 300);
|
||
|
||
// 虚拟滚动(历史记录)
|
||
function renderHistoryWithVirtualScroll(records) {
|
||
// 使用虚拟滚动库,只渲染可见部分
|
||
}
|
||
|
||
// 图片懒加载
|
||
const lazyImages = document.querySelectorAll('img[data-src]');
|
||
const imageObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
const img = entry.target;
|
||
img.src = img.dataset.src;
|
||
imageObserver.unobserve(img);
|
||
}
|
||
});
|
||
});
|
||
|
||
lazyImages.forEach(img => imageObserver.observe(img));
|
||
```
|
||
|
||
### 3. 测试建议
|
||
|
||
```javascript
|
||
// 模拟 WebSocket 消息(用于测试)
|
||
function mockGameStart() {
|
||
const mockEvent = {
|
||
event: 'GAME_START',
|
||
data: {
|
||
period_id: 99999,
|
||
period_number: 'TEST-001',
|
||
room_id: 1,
|
||
state: 'betting',
|
||
countdown: 30,
|
||
config: { /* ... */ }
|
||
}
|
||
};
|
||
|
||
game.handleServerEvent(mockEvent);
|
||
}
|
||
|
||
function mockGameResult() {
|
||
const mockEvent = {
|
||
event: 'GAME_RESULT',
|
||
data: {
|
||
result: {
|
||
coins: [1, 1, 1, 0],
|
||
pattern: '3_red_1_white'
|
||
},
|
||
settlement: { /* ... */ }
|
||
}
|
||
};
|
||
|
||
game.handleServerEvent(mockEvent);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📞 技术支持
|
||
|
||
如有疑问,请联系后端开发团队。
|
||
|
||
**文档版本**: v1.0.0
|
||
**最后更新**: 2026-01-08
|
||
**维护者**: Backend Team
|