sam-gong-game

API 文件

API — API 規格文件

<!-- SDLC API Specification — Layer 3:API Design Document -->


Document Control

欄位內容
DOC-IDAPI-SAM-GONG-GAME-20260422
專案名稱三公遊戲(Sam Gong 3-Card Poker)即時多人線上平台
文件版本v1.1
狀態DRAFT(STEP-11 Review 完成,12 findings 修復;依 EDD v1.4-draft)
作者Evans Tseng(由 STEP-09 自動生成)
日期2026-04-22
來源 EDDEDD-SAM-GONG-GAME-20260422 v1.4-draft §4

Change Log

版本日期作者變更摘要
v1.02026-04-22STEP-09初稿;依 EDD v1.4-draft §4 生成;涵蓋所有 REST API 端點、WebSocket 協議、錯誤碼、速率限制
v1.12026-04-22STEP-11 Review修復 12 findings:F1 rescue_chip 錯誤碼區分(400=rescue_unavailable/403=rescue_chip_claimed);F2 KYC submit 改 201;F3 rate limit table 補 L2b rescue-chip 與 L1b otp/send;F4 CORS 設定 §1.6;F5 audit-log 分頁完整化;F6 error code enum 補 rescue_unavailable;F7 nullable JSON schema 表示法修正;F8 KYC content-type 說明補充;F9 audit-log query param 正式表格;F10 my_rank null 說明;F11 banned_until nullable 說明;F12 otp/send rate limit 改 per-IP;F13 rescue_chips/rescue_not_available WS 描述補充

1. API Overview

1.1 基礎資訊

項目
Base URL(REST)https://api.samgong.io/api/v1
Base URL(WebSocket)wss://ws.samgong.io
協議HTTPS(TLS 1.2+)/ WSS(WebSocket Secure)
內容類型application/json(REST)/ Colyseus Binary Protocol(WS)
OpenAPI Spechttps://api.samgong.io/api/docs(Swagger UI)
版本策略URL Path 版本控制(/api/v1/

1.2 API 版本策略

1.3 認證方式

所有需要認證的端點使用 Bearer JWT(RS256)

Authorization: Bearer {access_token}

1.4 共用 Request Headers

Header必填說明
Authorization視端點Bearer {access_token}
Content-Type是(POST/PUT)application/json
X-Request-ID分散式追蹤 ID(UUID v4)

1.5 共用 Response Headers

Header說明
X-Request-ID回傳請求 ID(來自 request 或自動生成)
Retry-After429 回應時,剩餘秒數
DeprecationAPI 廢棄日期(若已廢棄)
SunsetAPI 下線日期(若已廢棄)

1.6 CORS 設定

REST API 端點採用嚴格 CORS 策略(Express cors 套件):

設定項目
Access-Control-Allow-Origin白名單(https://samgong.io, https://www.samgong.io);非白名單 Origin 回傳 CORS 錯誤
Access-Control-Allow-MethodsGET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-HeadersAuthorization, Content-Type, X-Request-ID
Access-Control-Allow-Credentialsfalse(Bearer Token 模式不需要 Credentials)
Access-Control-Max-Age86400(Preflight Cache 24h)

2. Authentication

2.1 JWT RS256 認證流程

sequenceDiagram
    participant C as Client
    participant A as Auth Service
    participant DB as PostgreSQL
    participant R as Redis

    C->>A: POST /auth/login { credential }
    A->>DB: 查詢帳號 + 驗證密碼/OAuth
    DB-->>A: 帳號資料(age_verified, is_banned)
    alt 帳號封禁
        A-->>C: 401 { error: 'account_banned' }
    else 驗證成功
        A->>A: 簽發 Access Token(RS256, TTL=1h)
        A->>DB: 儲存 Refresh Token hash(TTL=7d)
        A-->>C: { access_token, refresh_token, expires_in: 3600 }
    end

    C->>A: 一般 API 請求(Authorization: Bearer {access_token})
    A->>A: 驗證 RS256 簽名 + TTL
    A->>R: EXISTS block:{player_id}(封號黑名單,60s TTL)
    alt Token 在黑名單
        A-->>C: 401 { error: 'token_revoked' }
    else 正常
        A-->>C: API 回應
    end

    C->>A: POST /auth/refresh { refresh_token }
    A->>DB: 查詢 Refresh Token(one-time use)
    A->>DB: 廢棄舊 Refresh Token
    A->>A: 簽發新 Access Token + 新 Refresh Token
    A->>DB: 儲存新 Refresh Token hash
    A-->>C: { access_token, refresh_token }

2.2 封號後 60s 失效機制(NFR-17)

封號時:
  1. UPDATE users SET is_banned = true WHERE id = {player_id}
  2. SETEX block:{player_id} 60 "1"        ← Redis 60s TTL
  3. DEL session:{player_id}                ← 強制 Session Cache 失效
  4. PUBLISH sam-gong:admin:force_disconnect "{player_id}"

Token 驗證時:
  1. 驗證 JWT RS256 簽名 + TTL
  2. EXISTS block:{player_id} → 命中 → HTTP 401(token_revoked)

2.3 Refresh Token Rotation


3. REST API Endpoints

3.1 Auth(認證)


POST /api/v1/auth/register

描述:新帳號註冊(Guest / OAuth)

Auth:不需要

Rate Limit:30 次/min/IP

Request Body

{
  "username": "string",       // 可選(Guest 模式省略)
  "password": "string",       // 可選(OAuth 模式省略)
  "oauth_provider": "google|facebook",  // 可選
  "oauth_token": "string"     // OAuth Access Token
}

Response 201

{
  "player_id": "uuid",
  "display_name": "string",
  "access_token": "string",
  "refresh_token": "string",
  "expires_in": 3600
}

Response Codes201 成功 / 400 格式錯誤 / 409 帳號已存在


POST /api/v1/auth/login

描述:登入,取得 Access Token + Refresh Token

Auth:不需要

Rate Limit:30 次/min/IP

Request Body

{
  "username": "string",
  "password": "string"
}

Response 200

{
  "access_token": "string",
  "refresh_token": "string",
  "expires_in": 3600,
  "player_id": "uuid"
}

Response Codes200 / 400 格式錯誤 / 401 帳號或密碼錯誤 / 429 Rate Limit


POST /api/v1/auth/refresh

描述:Refresh Token Rotation,取得新 Access Token

Auth:Refresh Token(Bearer)

Rate Limit:30 次/min/IP

Request Body

{
  "refresh_token": "string"
}

Response 200

{
  "access_token": "string",
  "refresh_token": "string",
  "expires_in": 3600
}

Response Codes200 / 401 Token 無效或已撤銷 / 429 Rate Limit


POST /api/v1/auth/logout

描述:登出,撤銷 Refresh Token

Auth:JWT Bearer

Rate Limit:60 次/min/user

Request Body

{
  "refresh_token": "string"
}

Response 200

{
  "message": "Logged out successfully"
}

Response Codes200 / 401 Token 無效


POST /api/v1/auth/otp/send

描述:發送 OTP 簡訊(年齡驗證 REQ-014)

Auth:不需要

Rate Limit:5 次/min/IP(未登入端點以 IP 計量);每手機號每日 ≤ 5 次(Redis otp:daily:{phone_hash}:{date}

Request Body

{
  "phone_number": "string",   // 台灣格式 09xxxxxxxx
  "birth_year": 2000          // 出生年份(整數)
}

Response 200

{
  "message": "OTP sent",
  "expires_in_seconds": 300   // OTP 有效期 5 分鐘
}

Response Codes200 / 400 手機格式錯誤 / 429 Rate Limit


POST /api/v1/auth/otp/verify

描述:驗證 OTP + 啟用帳號年齡驗證

Auth:不需要

Rate Limit:30 次/min/IP;3 次錯誤 OTP 失效

Request Body

{
  "phone_number": "string",
  "otp_code": "123456",       // 6 碼 OTP
  "birth_year": 2000
}

Response 200

{
  "age_verified": true,
  "is_minor": false,
  "message": "Age verification successful"
}

Response Codes200 / 400 OTP 格式錯誤 / 401 OTP 無效或過期 / 429 Rate Limit


3.2 Player(玩家)


GET /api/v1/player/me

描述:取得玩家個人資料 + 籌碼餘額

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

讀取來源:Read Replica(排行榜過濾後)

Response 200

{
  "player_id": "uuid",
  "display_name": "string",
  "avatar_url": "string | null",
  "chip_balance": 100000,
  "age_verified": true,
  "is_minor": false,
  "tutorial_completed": false,
  "show_in_leaderboard": true,
  "music_volume": 70,
  "sfx_volume": 80,
  "vibration": true,
  "daily_chip_claimed_at": "2026-04-22 | null",
  "created_at": "2026-04-22T00:00:00Z",
  "last_login_at": "2026-04-22T10:00:00Z"
}

Schema 說明avatar_urldaily_chip_claimed_at 為 nullable(string | null);未設定時為 null

Response Codes200 / 401 Token 無效


PUT /api/v1/player/settings

描述:更新玩家設定(音量、動畫、排行榜顯示、Cookie 同意)

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

Request Body(所有欄位可選,只更新傳入的欄位):

{
  "display_name": "string",        // 最長 24 字元(可選)
  "avatar_url": "string",          // URL(可選)
  "music_volume": 70,              // 0-100(可選)
  "sfx_volume": 80,                // 0-100(可選)
  "vibration": true,               // boolean(可選)
  "show_in_leaderboard": true      // 關閉後同步 ZREM(可選)
}

Response 200

{
  "message": "Settings updated",
  "updated_fields": ["music_volume", "show_in_leaderboard"]
}

Response Codes200 / 400 格式錯誤 / 401 Token 無效


DELETE /api/v1/player/me

描述:申請帳號刪除(7 工作日內完成;個資刪除,財務記錄匿名化保留 7 年)

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

Response 202

{
  "message": "Account deletion requested",
  "deletion_scheduled_at": "2026-04-29T00:00:00Z",
  "note": "Financial records will be anonymized and retained for 7 years per regulation"
}

Response Codes202 已接受(非同步刪除)/ 401 Token 無效


POST /api/v1/player/daily-chip

描述:每日籌碼領取(冪等;每日重置依 UTC+8)

Auth:JWT Bearer(必填)

Rate Limit:5 次/min/user

Response 200

{
  "chips_awarded": 1000,
  "new_balance": 101000,
  "next_claim_available_at": "2026-04-23T00:00:00+08:00"
}

Response 400(已領取)

{
  "error": "daily_task_limit",
  "message": "Daily chips already claimed today",
  "next_claim_available_at": "2026-04-23T00:00:00+08:00"
}

Response Codes200 成功 / 400 今日已領取 / 401 Token 無效 / 429 Rate Limit


POST /api/v1/player/rescue-chip

描述:申請救援籌碼(chip_balance < 500 時可申請,每日 1 次,冪等)

Auth:JWT Bearer(必填)

Rate Limit:≤ 1 次/day/user

業務規則

Response 200

{
  "chips_awarded": 1000,
  "new_balance": 1000
}

Response 400(餘額不符條件:chip_balance ≥ 500)

{
  "error": "rescue_unavailable",
  "message": "Chip balance is sufficient; rescue chip threshold not met (balance >= 500)"
}

Response 403(今日已領取)

{
  "error": "rescue_chip_claimed",
  "message": "Rescue chip already claimed today",
  "next_claim_available_at": "2026-04-23T00:00:00+08:00"
}

Response Codes200 / 400 餘額 ≥ 500 不符申請條件 / 401 Token 無效 / 403 今日已領取(rescue_chip_claimed)/ 429 Rate Limit


GET /api/v1/player/chip-transactions

描述:籌碼交易記錄查詢(分頁)

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

讀取來源:Read Replica

Query Parameters

參數類型必填說明
pageint頁碼(預設 1)
limitint每頁筆數(預設 20,最大 100)
tx_typestring篩選類型(game_win\game_lose\daily_gift 等)
fromdate起始日期(ISO 8601)
todate結束日期(ISO 8601)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "tx_type": "game_win",
      "amount": 500,
      "balance_before": 99500,
      "balance_after": 100000,
      "created_at": "2026-04-22T10:00:00Z",
      "game_session_id": "uuid | null",
      "metadata": {}
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "total_pages": 8
  }
}

Response Codes200 / 401 Token 無效


POST /api/v1/player/ad-reward

描述:AdMob 廣告觀看獎勵(REQ-020a),冪等 by ad_view_token

Auth:JWT Bearer(必填)

Rate Limit:≤ 5 次/min/user

Request Body

{
  "ad_view_token": "string"   // AdMob 廣告觀看憑證(唯一)
}

Response 200

{
  "chips_awarded": 100,
  "new_balance": 100100
}

Response Codes


POST /api/v1/player/cookie-consent

描述:提交 Cookie 同意記錄(REQ-016 GDPR/個資法)

Auth:JWT 可選(未登入前也可提交)

Rate Limit:無限制

Request Body

{
  "analytics_consent": true,
  "marketing_consent": false,
  "consent_version": "1.0.0",
  "session_id": "string"     // 未登入時的會話識別
}

Response 200

{
  "consent_id": "uuid",
  "consented_at": "2026-04-22T10:00:00Z"
}

Response Codes200 / 400 格式錯誤


3.3 Leaderboard(排行榜)


GET /api/v1/leaderboard

描述:取得週排行榜(weekly / chip type)

Auth:JWT Bearer(可選)

Rate Limit:60 次/min/user

讀取來源:Redis ZREVRANGE(lb:weekly:{week_key})+ DB 過濾 show_in_leaderboard

Query Parameters

參數類型必填說明
typestringweekly(預設)
weekstringISO 週格式如 2026-W17(預設當週)
limitint排名數量(預設 100,最大 200)

Response 200

{
  "week_key": "2026-W17",
  "updated_at": "2026-04-22T10:00:00Z",
  "data": [
    {
      "rank": 1,
      "player_id": "uuid",
      "display_name": "string",
      "avatar_url": "string | null",
      "net_chips": 50000,
      "first_win_at": "2026-04-22T08:00:00Z"
    }
  ],
  "my_rank": {
    "rank": 42,
    "net_chips": 1200
  }
}

Schema 說明my_rank 未登入時為 null;已登入但未上榜時,rank 仍回傳實際名次(排行榜過濾後)。

Response Codes200 / 400 week 格式錯誤


3.4 Tasks(每日任務)


GET /api/v1/tasks

描述:取得今日每日任務列表

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

Response 200

{
  "date": "2026-04-22",
  "tasks": [
    {
      "task_id": "complete_3_games",
      "name": "完成 3 場遊戲",
      "reward_chips": 500,
      "completed": false,
      "completed_at": null
    }
  ]
}

Response Codes200 / 401 Token 無效


POST /api/v1/tasks/:id/complete

描述:完成任務 + 發放獎勵(冪等)

Auth:JWT Bearer(必填)

Rate Limit:5 次/min/user

Path Parametersid = task_id(如 complete_3_games

Response 200

{
  "task_id": "complete_3_games",
  "reward_chips": 500,
  "new_balance": 100500,
  "completed_at": "2026-04-22T10:00:00Z"
}

Response Codes200 / 400 任務條件未達成 / 401 Token 無效 / 404 任務不存在 / 429 Rate Limit


3.5 KYC(身份驗證)


POST /api/v1/kyc/submit

描述:提交 KYC 身份文件(REQ-018;文件加密存儲)

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

Request HeadersContent-Type: application/json(文件 URL 預先上傳至 S3,此端點僅接收 URL 參照)

Request Body

{
  "kyc_type": "full_kyc",
  "document_type": "national_id|passport",
  "document_front_url": "string",   // 預先加密上傳至 S3 後的 URL
  "document_back_url": "string"     // 預先加密上傳至 S3 後的 URL
}

Response 201

{
  "kyc_id": "uuid",
  "status": "pending",
  "submitted_at": "2026-04-22T10:00:00Z",
  "estimated_review_time": "1-3 business days"
}

Response Codes201 提交成功(資源已建立)/ 400 格式錯誤 / 401 Token 無效


GET /api/v1/kyc/status

描述:查詢 KYC 審核狀態

Auth:JWT Bearer(必填)

Rate Limit:60 次/min/user

Response 200

{
  "kyc_type": "full_kyc",
  "status": "pending | approved | rejected",
  "submitted_at": "2026-04-22T10:00:00Z",
  "reviewed_at": "2026-04-23T10:00:00Z | null"
}

Schema 說明status 枚舉值為 pendingapprovedrejectedreviewed_at 為 nullable,未審核時為 null

Response Codes200 / 401 Token 無效


3.6 Rooms(私人房間)


POST /api/v1/rooms/private

描述:建立私人房間(返回 room_code 供其他玩家加入)

Auth:JWT Bearer(必填)

Rate Limit:一般 API(60 次/min/user)

Request Body

{
  "tier": "青銅廳|白銀廳|黃金廳|鉑金廳|鑽石廳"
}

Response 201

{
  "room_code": "ABC123",
  "colyseus_room_id": "string",
  "tier": "青銅廳",
  "expires_at": "2026-04-22T11:00:00Z"
}

Response Codes201 成功 / 400 tier 無效或餘額不足


GET /api/v1/rooms/private/{room_code}

描述:查詢私人房間資訊(給 Colyseus joinById 使用)

Auth:JWT Bearer(必填)

Rate Limit:一般 API

Path Parametersroom_code = 6 位大寫英數字

Response 200

{
  "room_code": "ABC123",
  "colyseus_room_id": "string",
  "tier": "青銅廳",
  "current_players": 3,
  "max_players": 6,
  "status": "active"
}

Response Codes200 / 404 房間不存在或已關閉


3.7 Admin API(內部 VPN Only)


POST /api/v1/admin/player/:id/ban

描述:封鎖帳號(Admin Only;內網 VPN 存取)

Auth:Admin JWT(role=admin,獨立私鑰)

Rate Limit:內網 VPN Only

Path Parametersid = player_id(UUID)

Request Body

{
  "reason": "string",          // 封號原因
  "duration_days": 7           // null = 永久封號
}

Response 200

{
  "player_id": "uuid",
  "banned": true,
  "ban_reason": "string",
  "banned_at": "2026-04-22T10:00:00Z",
  "banned_until": "2026-04-29T10:00:00Z | null"
}

Schema 說明banned_until 為 nullable;null 代表永久封號(duration_days: null)。

封號附帶效果(Server 端):

1. UPDATE users SET is_banned = true
2. SETEX block:{player_id} 60 "1"
3. DEL session:{player_id}
4. PUBLISH sam-gong:admin:force_disconnect "{player_id}" → client.leave(4001)

Response Codes200 / 400 格式錯誤 / 401 Admin JWT 無效 / 403 非 Admin 角色


GET /api/v1/admin/audit-log

描述:稽核日誌查詢(Admin Only)

Auth:Admin JWT(role=admin)

Rate Limit:內網 VPN Only

Query Parameters

參數類型必填說明
fromdate起始日期(ISO 8601,如 2026-04-01
todate結束日期(ISO 8601,如 2026-04-30
player_iduuid篩選特定玩家的稽核記錄
actionstring篩選操作類型(如 `ban_player\adjust_chips`)
pageint頁碼(預設 1)
limitint每頁筆數(預設 20,最大 100)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "admin_id": "uuid",
      "action": "ban_player|adjust_chips|...",
      "target_player_id": "uuid",
      "payload": {},
      "created_at": "2026-04-22T10:00:00Z"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 50, "total_pages": 3 }
}

Response Codes200 / 401 Token 無效 / 403 非 Admin 角色


3.8 System(系統)


GET /api/v1/health

描述:健康檢查(k8s liveness probe)

Auth:不需要

Rate Limit:無限制

Response 200

{
  "status": "ok",
  "timestamp": "2026-04-22T10:00:00Z"
}

GET /api/v1/health/ready

描述:就緒探針(DB + Redis 連線確認;k8s readiness probe)

Auth:不需要

Rate Limit:無限制

Response 200

{
  "status": "ready",
  "checks": {
    "postgresql": "ok",
    "redis": "ok"
  }
}

Response 503(任一依賴不健康)

{
  "status": "not_ready",
  "checks": {
    "postgresql": "ok",
    "redis": "error: connection timeout"
  }
}

GET /api/v1/config

描述:用戶端設定(伺服器域名、廳別設定、Feature Flags)

Auth:不需要

Rate Limit:300 次/min/IP

Response 200

{
  "ws_server_url": "wss://ws.samgong.io",
  "tiers": [
    {
      "tier_name": "青銅廳",
      "entry_chips": 1000,
      "min_bet": 100,
      "max_bet": 500,
      "quick_bet_amounts": [100, 200, 300, 500]
    },
    {
      "tier_name": "白銀廳",
      "entry_chips": 10000,
      "min_bet": 1000,
      "max_bet": 5000,
      "quick_bet_amounts": [1000, 2000, 3000, 5000]
    },
    {
      "tier_name": "黃金廳",
      "entry_chips": 100000,
      "min_bet": 10000,
      "max_bet": 50000,
      "quick_bet_amounts": [10000, 20000, 30000, 50000]
    },
    {
      "tier_name": "鉑金廳",
      "entry_chips": 1000000,
      "min_bet": 100000,
      "max_bet": 500000,
      "quick_bet_amounts": [100000, 200000, 300000, 500000]
    },
    {
      "tier_name": "鑽石廳",
      "entry_chips": 10000000,
      "min_bet": 1000000,
      "max_bet": 5000000,
      "quick_bet_amounts": [1000000, 2000000, 3000000, 5000000]
    }
  ],
  "feature_flags": {
    "iap_enabled": false,
    "tutorial_enabled": true
  },
  "server_version": "1.0.0"
}

4. WebSocket Protocol

4.1 Colyseus 連線建立

連線端點wss://ws.samgong.io

Colyseus Room 名稱sam_gong(正式)/ sam_gong_tutorial(教學)

Client SDK(Cocos Creator):

import { Client } from 'colyseus.js';

const client = new Client('wss://ws.samgong.io');

// 配對加入
const room = await client.joinOrCreate('sam_gong', {
  tier: '青銅廳',
  token: access_token    // JWT Access Token(Bearer)
});

// 加入私人房間
const room = await client.joinById(colyseus_room_id, {
  token: access_token
});

// 連線成功後儲存重連憑證
const reconnectInfo = { roomId: room.id, sessionId: room.sessionId };

4.2 Client → Server 訊息

訊息類型Payload驗證規則說明
banker_bet{ amount: number }phase === 'banker-bet';is_banker;min_bet ≤ amount ≤ max_bet;amount ≤ chip_balance莊家下注
call{}phase === 'player-bet';本人輪次;chip_balance ≥ banker_bet_amount閒家跟注(Call)
fold{}phase === 'player-bet';本人輪次閒家棄牌
see_cards{}phase === 'banker-bet';is_banker莊家查看手牌
send_chat{ text: string }text.length ≤ 200;rate limit ≤ 2/s房間內聊天
report_player{ target_id: string, message_id: string, reason: string }target_id 存在房間;reason 合法枚舉舉報玩家
confirm_anti_addiction{ type: "adult" }is_adult;type=adult成人確認防沉迷 2h 提醒

Client → Server 訊息速率限制:每連線每秒 ≤ 10 條訊息(NFR-15)

4.3 Server → Client 訊息

訊息類型Payload說明
Room State SyncSchema diff(自動)Colyseus 自動推送 SamGongState diff
my_session_info{ session_id: string, player_id: string }玩家加入後 Server 推送(sendTo client),Client 識別自身 session
myHand{ cards: Card[] }推送玩家手牌(私人訊息,dealing phase 後)
showdown_reveal{ hands: { [seat_index]: Card[] } }所有未 Fold 玩家手牌(廣播,showdown phase)
anti_addiction_warning{ type: "adult", session_minutes: number }成人 2h 連續遊玩提醒(需確認)
anti_addiction_signal{ type: "underage", daily_minutes_remaining: number, midnight_timestamp: number }未成年每日 2h 硬停訊號
rescue_chips{ amount: 1000, new_balance: number }結算後偵測 chip_balance < 500 且今日未領取時自動觸發補發(DB 原子 UPDATE 防 Race Condition)
rescue_not_available`{ reason: "already_claimed" \"balance_sufficient" }`結算後偵測餘額 < 500 但今日已領取(already_claimed)或餘額 ≥ 500(balance_sufficient)時推送;Client 顯示對應提示
rate_limit{ error: "rate_limit", retry_after_ms: number }訊息速率超限回應
error{ code: string, message: string }一般錯誤回應(非法操作、驗證失敗)
matchmaking_expanded{ expanded_tiers: string[] }配對擴展至相鄰廳別(30s 後)
send_message_rejected`{ reason: "content_filter" \"rate_limit" }`聊天訊息被拒絕

4.4 Room State Schema(公開欄位)

SamGongState(Colyseus Schema,自動同步至所有 Client):

// 公開欄位(所有 Client 可見)
{
  players: MapSchema<PlayerState>,  // key = seat_index.toString()
  phase: string,                    // 'waiting'|'dealing'|'banker-bet'|'player-bet'|'showdown'|'settled'
  banker_seat_index: number,
  banker_bet_amount: number,
  min_bet: number,
  max_bet: number,
  current_pot: number,
  action_deadline_timestamp: number, // Server Unix ms(Client 計算倒計時)
  round_number: number,
  current_player_turn_seat: number,
  settlement: SettlementState,
  tier_config: TierConfig,
  is_tutorial: boolean,
  room_id: string,                  // 私人房間 room_code
  room_type: string,                // 'matchmaking'|'private'
  matchmaking_status: MatchmakingStatus
}

// PlayerState(每個玩家的公開狀態)
{
  player_id: string,
  seat_index: number,
  chip_balance: number,      // 開放籌碼設計(所有玩家可見)
  bet_amount: number,
  is_connected: boolean,
  is_folded: boolean,
  has_acted: boolean,
  is_banker: boolean,
  display_name: string,
  avatar_url: string
  // 手牌不在此 Schema(僅透過私人訊息 myHand 推送)
}

4.5 斷線重連流程

Client 端(Cocos Creator):

room.onLeave(async (code) => {
  if (code === 4000) return; // 被踢出,不重連
  const stored = { roomId: room.id, sessionId: room.sessionId };
  cc.sys.localStorage.setItem('reconnect_info', JSON.stringify(stored));
  try {
    const reconnInfo = JSON.parse(cc.sys.localStorage.getItem('reconnect_info'));
    room = await client.reconnect(reconnInfo.roomId, reconnInfo.sessionId);
    // 重連成功後 Server 重新推送 myHand
  } catch (e) {
    cc.sys.localStorage.removeItem('reconnect_info');
    UIManager.navigateTo('LobbyScene');
  }
});

Server 端(onLeave)


5. Error Codes Reference

5.1 REST API 統一錯誤格式

interface ErrorResponse {
  error: string;        // Error code(機器可讀)
  message: string;      // 人類可讀說明(英文,用於 debug)
  detail?: object;      // 額外上下文(可選)
  request_id?: string;  // X-Request-ID
}

範例

{
  "error": "insufficient_chips",
  "message": "Player chip balance is below the required amount for this tier",
  "detail": {
    "required": 10000,
    "current_balance": 5000
  },
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}

5.2 WebSocket 錯誤格式

interface WsErrorMessage {
  code: string;
  message: string;
}

5.3 完整 Error Code 枚舉

認證/授權

CodeHTTP Status說明
token_expired401Access Token 已過期
token_invalid401Token 格式錯誤或簽名無效
token_revoked401Token 已撤銷(封號黑名單)
account_banned401帳號已封禁
account_not_found401帳號不存在

遊戲邏輯

Code說明
invalid_phase操作不符當前 phase
insufficient_chips籌碼不足
invalid_bet_amount下注金額無效
bet_out_of_range下注金額超出廳別限制
already_called玩家已 Call
already_folded玩家已 Fold
banker_insolvent莊家破產
room_full房間已滿
room_not_found房間不存在

速率限制

CodeHTTP Status說明
rate_limit_exceeded429超過速率限制
daily_task_limit400每日任務已完成
rescue_unavailable400救援籌碼不符申請條件(chip_balance ≥ 500)
rescue_chip_claimed403今日已領取救援籌碼(每日 1 次上限)

KYC/合規

Code說明
age_verification_required需要年齡驗證
underage_session_expired未成年每日遊戲時間已達上限
otp_invalidOTP 代碼無效
otp_expiredOTP 已過期

系統

CodeHTTP Status說明
server_maintenance503伺服器維護中
internal_error500內部伺服器錯誤
request_timeout504請求超時

Client 端根據 code(即 i18n key error.{code})本地化顯示。


6. Rate Limiting

6.1 四層速率限制(NFR-19)

層次適用端點Redis Key限制超限回應
L1 認證/auth/*(除 OTP)rl:auth:{ip}30 次/min/IPHTTP 429
L1b OTP 發送POST /auth/otp/sendrl:otp_send:{ip}5 次/min/IPHTTP 429
L2 高敏感POST /player/daily-chip, POST /tasks/:id/completerl:sensitive:{player_id}5 次/min/userHTTP 429
L2b 每日唯一POST /player/rescue-chiprl:rescue:{player_id}:{date}≤ 1 次/day/user(UTC+8 日期 key)HTTP 429
L3 一般所有其他 /api/v1/*rl:general:{player_id}60 次/min/userHTTP 429
L4 IP 全局所有端點rl:global:{ip}300 次/min/IPHTTP 429

6.2 超限回應格式

{
  "error": "rate_limit_exceeded",
  "message": "Too many requests",
  "retry_after_seconds": 30,
  "limit": 30,
  "window_seconds": 60
}

Response Headers:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714000000

6.3 Redis Lua Script(原子性計數)

-- rate_limit.lua(原子 GET + INCR + EXPIRE)
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
  redis.call('EXPIRE', key, window)
end
if current > limit then
  return redis.call('TTL', key)  -- 返回剩餘秒數
else
  return 0  -- 0 = 未超限
end

6.4 WebSocket 訊息速率限制

每連線每秒 ≤ 10 條訊息(NFR-15):


7. WebSocket Close Codes

Code含義觸發條件Client 處理
4000被系統踢出(禁止重連)帳號封禁且 Redis Pub/Sub 觸發踢出顯示封號訊息,清除 token,不重連
4001Token 失效或帳號封禁JWT 驗證失敗或封號黑名單命中清除 token,跳轉登入頁
4002伺服器維護Server 計劃性維護顯示維護中訊息,5 分鐘後重試
4003未成年每日遊戲時間上限AntiAddictionManager 達 2h 每日上限顯示下線提示,次日台灣午夜後可重連
4004重連逾時(reconnect window 過期)30s 重連視窗過期清除 reconnect_info,返回大廳
4005重複登入(同帳號其他裝置已登入)Redis Pub/Sub force_disconnect_device顯示「其他裝置已登入」,清除本地 token
1001伺服器正常關閉(Going Away)Pod 優雅關閉(preStop 30s)嘗試重連,最多 3 次

Appendix A:廳別設定速查

廳別Entry 籌碼min_betmax_betquick_bet_amounts
青銅廳1,000100500[100, 200, 300, 500]
白銀廳10,0001,0005,000[1000, 2000, 3000, 5000]
黃金廳100,00010,00050,000[10000, 20000, 30000, 50000]
鉑金廳1,000,000100,000500,000[100000, 200000, 300000, 500000]
鑽石廳10,000,0001,000,0005,000,000[1000000, 2000000, 3000000, 5000000]

Appendix B:賠率表(PRD §5.4)

閒家牌型賠率(N)閒家勝 net_chips說明
三公3x+3 × banker_bet3 張 10 點牌
9 點2x+2 × banker_betsum mod 10 = 9
1-8 點(非三公)1x+1 × banker_bet一般比點
平手0x0(退注)D8 比牌後仍平手
-1x-called_bet閒家點數低於莊家
Fold-0不扣款
Insolvent Win--called_bet莊家破產後排隊中的贏家

文件版本 v1.1 — 依 EDD v1.4-draft §4 生成 — 2026-04-22 — STEP-11 Review 完成