Skip to content

C9 Platform — 全端專案規格書

本文件整合後端架構、API 規格、資料庫結構、前端串接指南、功能規格與開發理念。 供前端與後端工程師共用,作為開發、串接、維護的唯一真相來源 (Single Source of Truth)。


目錄


1. 專案概覽

項目
專案名稱C9 Platform
後端版本c9-be 0.0.1
功能定位線上博弈平台(遊戲、入金、VIP、代理推廣)
端點總數142(GET 50 / POST 57 / PATCH 9 / DELETE 8 + 新增 18),含 11 遊戲商 S2S 回調
Base URLhttp://localhost:8080/api (開發環境)
Swagger UI{BASE_URL}/api/docs
認證方式JWT Bearer Token,Authorization: Bearer <token>
多語系Header: locales: zh-TW / en-US / zh-CN
Content-Typeapplication/json(銀行卡新增為 multipart/form-data
幣別系統統一使用 USD,入金時依即時匯率自動轉換

核心模組

┌──────────────────────────────────────────────────────────────┐
│                        C9 Platform                           │
├──────────┬───────────┬──────────┬───────────┬───────────────┤
│   Auth   │   Game    │ Deposit  │    VIP    │   Affiliate   │
│  認證系統 │  遊戲系統  │ 入金系統  │ VIP 系統  │  代理推廣     │
├──────────┼───────────┼──────────┼───────────┼───────────────┤
│  Wallet  │   Promo   │ Ranking  │ BetRecord │   Common      │
│  錢包管理 │  活動促銷  │  排行榜   │  投注紀錄  │  共用列舉     │
├──────────┼───────────┼──────────┼───────────┼───────────────┤
│  Inbox   │SiteConfig │Withdrawal│ LiveSports│   Mission     │
│  站內信   │  站點設定  │  提領系統  │ 即時體育   │  任務系統      │
├──────────┼───────────┼──────────┼───────────┼───────────────┤
│  Admin   │AdminGroup │ AdminLog │           │               │
│ 後台管理員│ 管理員群組 │ 操作紀錄  │           │               │
└──────────┴───────────┴──────────┴───────────┴───────────────┘

2. 技術架構

後端技術棧

層級技術版本
RuntimeNode.jsES2023
FrameworkNestJSv11
LanguageTypeScript5.7 (strict)
DatabaseMySQLutf8mb4, TZ +08:00
ORMTypeORM0.3.28
CacheRedisvia @keyv/redis
AuthJWT + Passport7 天過期
2FAspeakeasy (TOTP)Google Authenticator
OAuthgoogle-auth-libraryGoogle 登入
Telegram Logincrypto (HMAC-SHA256)Telegram Login Widget 驗證
File StorageCloudflare R2S3-compatible
EmailResend API驗證信
i18nnestjs-i18n3 語系
Scheduler@nestjs/scheduleCron 排程
Exchange Ratetw-exchange台灣銀行即時匯率

環境變數

變數用途
PORT伺服器 Port (預設 8080)
JWT_SECRET / JWT_EXPIRESJWT 簽名密鑰 / 過期時間 (7d)
DB_HOST/PORT/USER/PASSWORD/DATABASEMySQL 連線
REDIS_URLRedis 連線字串
RESEND_API_KEY / RESEND_FROMEmail 發送
GOOGLE_CLIENT_ID/SECRET/REDIRECT_URIGoogle OAuth
TELEGRAM_BOT_TOKEN / TELEGRAM_BOT_USERNAMETelegram Login Widget 驗證
R2_BUCKET_NAME/ENDPOINT/ACCESS_KEY_ID/SECRET_ACCESS_KEY/PUBLIC_URLCloudflare R2
API_DOMAIN自身域名(用於回調 URL)
RSG_*RSG 遊戲商 API 設定
BS_*BetSolutions 遊戲商 API 設定

專案結構

src/
├── main.ts                    # Bootstrap: port 8080, /api prefix, CORS, pipes, filters
├── app.module.ts              # Root module: 匯入所有模組
├── enum/                      # 共用列舉
│   ├── auth/index.ts          # AUTH_ENUM (登入紀錄 action)
│   ├── game/index.ts          # GameType, TURNOVER_WEIGHT, BetOrderStatus
│   └── error-codes/index.ts   # I18N_PATH_MAP: i18n key → API path 映射
├── utils/
│   ├── http-exception.filter.ts       # 統一錯誤回應格式
│   ├── success-response.interceptor.ts # 統一成功回應格式
│   ├── i18n.ts                        # resolveText(), resolveGameTypeLabel()
│   └── helper.ts                      # toNum() 數字工具
├── i18n/{zh-TW,en-US,zh-CN}/         # 翻譯 JSON 檔
└── modules/
    ├── auth/         # 認證、註冊、Google OAuth、Telegram Login、2FA
    ├── game/         # 遊戲商管理、啟動、模擬、回調
    ├── common/       # 共用列舉 + 錯誤碼 API
    ├── deposit/      # 存款訂單、匯率
    ├── wallet/       # 銀行卡 / 信用卡 / 加密錢包
    ├── vendor/       # 金流通道、萬通、USDT
    ├── promo/        # 活動促銷、領取追蹤
    ├── vip/          # VIP 等級、反水、保級
    ├── ranking/      # 排行榜
    ├── bet-record/   # 投注紀錄
    ├── affiliate/    # 代理推廣、佣金、結算
    ├── mission/      # 任務系統(每日/週/月存款、投注任務)
    ├── inbox/        # 站內信、通知已讀
    ├── site-config/  # 站點設定、主題、吉祥物管理
    ├── withdrawal/   # 提領訂單、審核流程
    ├── live-sports/  # 即時體育賽事(API-Football + Redis 快取)
    ├── r2/           # Cloudflare R2 檔案上傳
    └── admin/        # 後台管理(管理員、群組、操作紀錄)

scripts/
├── seed-all.ts                  # 全資料表假資料 (37 表)
├── generate-promo-images.ts     # 活動橫幅圖片生成 + R2 上傳 (50×2 = 100 張)
└── generate-mascot-avatars.ts   # 吉祥物頭像生成 + R2 上傳 (10 張 512×512)

3. 開發理念與設計原則

3.1 統一幣別:一切以 USD 為核心

系統內部所有金額一律使用 USD (decimal(18,6))。使用者以 TWD、USDT 等幣別入金時,由後端於收到付款確認的當下,透過台灣銀行即時匯率 (tw-exchange) 換算為 USD 後上分。

為什麼: 多幣別入金若不統一,VIP 流水、反水計算、代理佣金結算將需要各自維護匯率快照,複雜度倍增。統一 USD 讓所有後續計算都在同一基準上進行。

3.2 多語系:DB 欄位 + i18n 翻譯雙軌制

場景方案範例
使用者可編輯的內容DB json 欄位VIP 等級名稱、活動標題、金流通道名稱
系統固定的錯誤訊息nestjs-i18n JSON 檔「帳號已存在」「查無此遊戲商」
系統固定的 UI 標籤nestjs-i18n JSON 檔遊戲類型名稱「老虎機」「真人」

DB json 欄位格式:

json
{"zh-TW": "青銅 I", "en-US": "Bronze I", "zh-CN": "青铜 I"}

後端使用 resolveText() 根據請求的 locales header 自動解析為對應語言字串回傳。

3.3 錯誤碼集中管理

所有業務錯誤碼透過 i18n JSON 檔案定義。GET /common/enums 回傳完整的 ERROR_CODES 列舉,前端載入後依 path + code 查表顯示錯誤訊息。

前端不需硬寫任何錯誤文字,切換語系只需重新呼叫 /common/enums 即可取得對應語言的錯誤碼。

3.4 回應格式零歧義

  • 成功: HTTP 200,body 固定為 { code: 200, message: "ok", result, timestamp, path }
  • 業務錯誤: HTTP 200,code ≠ 200message 為當前語系的錯誤訊息
  • 未授權: HTTP 401,code: 401message: "Unauthorized"

前端只需判斷 HTTP status 是否 401、以及 body.code 是否為 200,即可決定後續行為。

3.5 投注後連鎖觸發 (Event-Driven)

每筆投注結算完成後,系統同步觸發兩項操作(對前端透明):

  1. VIP 等級重算 — 累計有效投注達門檻自動升級
  2. 優惠打碼量累計 — 已領取的活動獎勵打碼進度自動更新

3.6 金流商抽象層

所有金流商(萬通、USDT、未來可擴充的第三方)統一透過 VendorService 路由。前端呼叫 POST /deposit 時只需帶 channelId + paymentMethod,後端自動判斷應走哪家金流商。

3.7 遊戲商 Provider 模式

遊戲商以 providerCode 區分(目前 betsolutionsrsg),每個 provider 各自實作 callback 端點。遊戲大廳以 gameCode 為唯一識別,一個 provider 可有多個 gameCode(如 slot-betsolutions、crypto-betsolutions)。

3.8 資料精度規範

用途型別精度
金額(餘額、投注、佣金)decimal(18,6)小數 6 位
匯率decimal(18,10)小數 10 位
百分比(反水率、佣金率)decimal(5,2)小數 2 位
倍率decimal(10,2)小數 2 位

USD 金額截斷規則:Math.floor(value * 1e6) / 1e6(無條件捨去到 6 位)。


4. 統一回應格式與錯誤處理

成功回應 (HTTP 200)

json
{
  "code": 200,
  "message": "ok",
  "result": { ... },
  "timestamp": 1708588800000,
  "path": "/api/auth/login"
}

業務錯誤 (HTTP 200, code ≠ 200)

json
{
  "code": 2001,
  "message": "帳號已存在",
  "data": null,
  "timestamp": 1708588800000,
  "path": "/api/auth/register"
}

未授權 (HTTP 401)

json
{
  "code": 401,
  "message": "Unauthorized",
  "data": null,
  "timestamp": 1771899908020,
  "path": "/api/vendor/channels"
}

例外:遊戲商回調

遊戲商 callback 回傳 { StatusCode, Data } 格式時,直接 passthrough,不套用 wrapper。

錯誤回應實作邏輯

AllExceptionsFilter:
  ├─ HTTP 401 → 回 HTTP 401, code=401, message="Unauthorized"
  └─ 其他所有 → 回 HTTP 200, code=業務碼, message=i18n訊息

Service 層拋出錯誤的方式:

typescript
throw new HttpException(
  { code: 2001, message: this.i18n.t('authError.register.2001') },
  HttpStatus.BAD_REQUEST,
);
// AllExceptionsFilter 會攔截,統一回 HTTP 200 + code 2001

5. 多語系 (i18n) 系統

支援語系

Code語言
zh-TW繁體中文(預設)
en-US英文
zh-CN簡體中文

語系判定優先順序

  1. Request header locales
  2. Request header Accept-Language
  3. Fallback: zh-TW

i18n 檔案結構

src/i18n/
├── zh-TW/
│   ├── authError.json         # 認證相關錯誤碼
│   ├── walletError.json       # 錢包相關錯誤碼
│   ├── vendorError.json       # 金流商相關錯誤碼
│   ├── depositError.json      # 存款相關錯誤碼
│   ├── gameError.json         # 遊戲相關錯誤碼
│   ├── promoError.json        # 活動相關錯誤碼
│   ├── vipError.json          # VIP 相關錯誤碼
│   ├── affiliateError.json    # 代理相關錯誤碼
│   ├── betRecordError.json    # 投注紀錄相關錯誤碼
│   ├── game.json              # 遊戲類型標籤(一般翻譯)
│   └── ranking.json           # 排行榜標籤(一般翻譯)
├── en-US/                     # 同結構
└── zh-CN/                     # 同結構

錯誤碼 Key 對應 API Path

I18N_PATH_MAP 定義了 i18n key prefix 到 API path 的映射:

i18n key prefix                  →  API path
─────────────────────────────────────────────────────
authError.register               →  /api/auth/register
authError.loginGoogle             →  /api/auth/login-google
walletError.bankCard.add          →  /api/wallet/bank-card/add
vendorError.channels              →  /api/vendor/channels
vendorError.wantong               →  [/api/vendor/wantong/add-atm, /api/vendor/wantong/add-card]
depositError.exchangeRate         →  /api/deposit/exchange-rate
gameError.launch                  →  /api/game/launch
vipError.levels                   →  [/api/vip/levels, /api/vip/levels/:id]
affiliateError.withdrawals.request →  /api/affiliate/withdrawals/request
...

CommonService.scanI18nCodes() 在啟動時遞迴掃描所有 i18n JSON 檔案,找出數字 key(錯誤碼),透過 I18N_PATH_MAP 映射後組成 ERROR_CODES 物件。


6. 前端串接指南

6.1 錯誤碼讀取規則

┌─────────────────────────────────────────────────────────────┐
│  1. 啟動時呼叫 GET /common/enums 取得 ERROR_CODES           │
│     └→ 依 locales header 回傳對應語系的錯誤訊息             │
│     └→ 切換語系後重新呼叫一次即可                           │
│                                                             │
│  2. API 錯誤回應格式固定為:                                 │
│     { code, message, data, timestamp, path }                │
│                                                             │
│  3. 錯誤處理邏輯:                                          │
│     ├─ HTTP 401 → 直接跳轉登入頁(不查 enum)              │
│     └─ HTTP 200 + code ≠ 200 → 查 ERROR_CODES 顯示訊息    │
│        └→ ERROR_CODES[path][code]                           │
│        └→ 例: ERROR_CODES["/api/auth/register"][2001]       │
│           = "帳號已存在"                                    │
│                                                             │
│  4. path 含動態參數時的匹配規則:                           │
│     回應 path = "/api/vip/levels/3"                         │
│     ERROR_CODES key = "/api/vip/levels/:id"                 │
│     前端需自行做 path pattern matching                      │
│     (將 path 中的數字段替換為 :param 後再查)              │
└─────────────────────────────────────────────────────────────┘

6.2 Axios 建議設定

typescript
import axios from 'axios';

// ── 錯誤碼 enum(啟動時 / 切換語系時載入)──
let ERROR_CODES: Record<string, Record<number, string>> = {};

export async function loadErrorCodes() {
  const enums = await api.get('/common/enums');
  ERROR_CODES = enums.ERROR_CODES || {};
}

// ── path pattern matching(處理動態路由參數)──
function matchErrorPath(path: string): string {
  if (ERROR_CODES[path]) return path;
  const pattern = path.replace(/\/\d+/g, '/:id');
  if (ERROR_CODES[pattern]) return pattern;
  return path;
}

// ── Axios instance ──
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api',
  headers: { 'Content-Type': 'application/json' },
});

// 請求攔截器:自動加 token + 語系
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  config.headers['locales'] = localStorage.getItem('locale') || 'zh-TW';
  return config;
});

// 回應攔截器:統一取出 result / 處理錯誤
api.interceptors.response.use(
  (res) => {
    const body = res.data;
    if (body.code === 200) return body.result;

    // 業務錯誤 (code ≠ 200):查 ERROR_CODES 取得 i18n 訊息
    const matched = matchErrorPath(body.path);
    const i18nMsg = ERROR_CODES[matched]?.[body.code];
    return Promise.reject({ ...body, message: i18nMsg || body.message });
  },
  (err) => {
    if (err.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(err.response?.data || err);
  },
);

export default api;

6.3 多語系欄位讀取

後端已自動處理,前端直接使用 API 回傳的字串即可:

typescript
// 後端回傳已解析好的字串
const channel = await api.get('/vendor/channels');
// channel.groupName = "預設"      ← 依 locales header 自動解析
// channel.channels[0].name = "萬通" ← 同上

// 切換語系:改 localStorage + 重打 API 即可
localStorage.setItem('locale', 'en-US');
loadErrorCodes(); // 重載錯誤碼

6.4 認證流程

┌─────────────────────────────────────────┐
│  1. 一般登入                             │
│     POST /auth/login                     │
│     → 存 token 到 localStorage           │
│     → 後續 API 自動帶 Authorization       │
│                                         │
│  2. Google 登入                          │
│     GET /auth/login-config → 取得 URL    │
│     → 跳轉 Google OAuth 頁面             │
│     → Google 回調帶 code + state          │
│     → POST /auth/login-google            │
│     → 存 token                           │
│                                         │
│  3. 註冊(可帶推廣碼)                    │
│     POST /auth/register                  │
│     { account, password, name, refCode } │
│     → refCode 來自推廣連結 ?ref=XXX       │
│     → 自動建立 3 層代理關係               │
│                                         │
│  4. Token 過期                           │
│     HTTP 401 → 清 token → 跳轉登入頁     │
└─────────────────────────────────────────┘

6.5 檔案上傳 (R2)

僅銀行卡新增使用 multipart/form-data,其餘所有端點皆為 application/json

圖片欄位在 list/detail API 回傳時自動轉為完整 R2 公開 URL:

新增時回傳: "bank-card/xxx.jpg"
列表時回傳: "https://pub-xxx.r2.dev/bank-card/xxx.jpg"

7. 資料庫結構 (Database Schema)

總覽:36 張資料表

資料表用途主要關聯
auth-user用戶帳號vendorGroupId → vendor-group
auth-user-login-log登入紀錄userId → auth-user
vendor-group金流群組
vendor-channel金流通道vendorGroupId → vendor-group
deposit-order存款訂單userId, channelId
bank-card銀行卡userId → auth-user
credit-card信用卡userId → auth-user
crypto-address加密錢包userId → auth-user
game-provider遊戲商
game-transaction遊戲錢包帳本userId
bet-order投注訂單userId
bet-detail注單明細orderId → bet-order
vip-levelVIP 等級定義
vip-rebate反水規則level
vip-rebate-log反水發放紀錄userId
promo活動促銷
promo-claim活動領取紀錄promoId, userId
rank-list排行榜userId
affiliate-commission佣金明細agentId, memberId, betOrderId, settlementId
affiliate-settlement佣金結算agentId
affiliate-balance代理佣金餘額agentId (unique)
affiliate-withdrawal代理提款agentId
affiliate-click推廣點擊追蹤agentId
affiliate-bind-log綁定審計紀錄memberId, agentId
affiliate-risk-log風控紀錄settlementId, agentId
notification站內信通知userId → auth-user (nullable)
notification-read通知已讀紀錄userId, notificationId
site-config站點設定
site-theme站點主題siteConfigId → site-config
withdrawal-order提領訂單userId → auth-user
mission任務定義unique(category, periodType, tier)
mission-progress任務進度userId, unique(userId, periodType, periodKey)
mission-claim任務領取紀錄missionId, userId, unique(missionId, userId, periodKey)
admin-user後台管理員帳號groupId → admin-group
admin-group管理員群組 (RBAC)
admin-operation-log管理員操作紀錄adminId → admin-user

auth-user

欄位型別說明
idint PK auto
accountvarchar(50)帳號
passwordvarchar(255)bcrypt hash
namevarchar(50)暱稱
emailvarchar(50) unique nullable
mobilevarchar(30) unique nullable
telegramvarchar(50) unique nullable
googlevarchar(50) unique nullableGoogle sub ID
vipLevelvarchar(4) default '1'當前 VIP 等級
totalEffectiveBetdecimal(18,6)累計有效投注 (USD)
relegationMissCounttinyint連續未達保級月數
vipHoldtinyint保級鎖定 (VIP 5+)
googleAuthSecretvarchar(32) nullableTOTP secret
googleAuthEnabledtinyint default 0
tokenVersionint default 0JWT 版本(強制登出用)
balancedecimal(18,6)USD 餘額
frozenBalancedecimal(18,6) default 0凍結中金額(提領審核中)
withdrawalVerifyCodevarchar(6) nullable提領用郵箱驗證碼
vendorGroupIdint nullable金流群組 ID
agentCodevarchar(20) unique nullable代理推廣碼 (null=非代理)
level1AgentIdint nullable直屬代理 user ID
level2AgentIdint nullable上 2 層代理
level3AgentIdint nullable上 3 層代理

deposit-order

欄位型別說明
idint PK auto
userIdint
channelIdint金流通道 ID
channelNamevarchar(30)通道名稱快照
currencyvarchar(10)TWD / USDT / ETH
subOrdervarchar(24) unique商戶訂單號
orderAmountint原幣金額
paymentMethodvarchar(10)fiat / credit / crypto
statusvarchar(20)pending → created → paid / failed
payAmountint實付金額
payTimevarchar(50) nullable
usdAmountdecimal(18,6)換算 USD
exchangeRatedecimal(18,10)當時匯率
callbackDatajson nullable回調原始資料

vendor-channel

欄位型別說明
idint PK auto
namejson多語系名稱
storeCodevarchar(30)金流商代碼
secret1~4varchar(100)金流商認證資訊
currencyvarchar(10)TWD / USDT / ETH
paymentMethodssimple-arrayfiat / credit / crypto
paymentAddressvarchar(255) nullable加密貨幣繳費地址
enabledtinyint default 1
vendorGroupIdint nullableFK to vendor-group

game-provider

欄位型別說明
idint PK auto
gameCodevarchar unique遊戲商識別碼 (e.g. slot-betsolutions)
providerCodevarchar(20)後端路由 (rsg / betsolutions)
gameTypeint1=體育 2=電子 ... 10=捕魚
areaBlocktinyint default 0地區封鎖
maintaintinyint default 0維護中
enabletinyint default 1

bet-order

欄位型別說明
idint PK auto
userIdint
gameTypevarchar(20)"1"~"10" (存數字字串)
gamePlatformvarchar(20)rsg / betsolutions / simulate
gameNumbervarchar(100) unique訂單號
totalBetCountint總注單數
betAmountdecimal(18,6)投注金額
betEffectivedecimal(18,6)有效投注 = betAmount × 權重
winLosedecimal(18,6)輸贏
statusvarchar(20)valid / invalid / cancelled
oddsdecimal(10,4)
invalidReasonvarchar(50) nullabledraw / cancelled / low_odds / arbitrage
gameNamevarchar(100)
betDatetimedatetime

vip-level

欄位型別說明
idint PK auto
levelint unique1~15
namejson多語系名稱
tiervarchar(20)bronze / gold / platinum / diamond
minChipdecimal(18,6)升級門檻 (累計有效投注 USD)
relegationChipdecimal(18,6)月保級門檻 (USD)
sortOrderint
enabledtinyint

promo

欄位型別說明
idint PK auto
titlejson多語系標題
contentjson多語系 HTML 內容
imgPc / imgMobilevarchar(255) nullableR2 圖片 key
startTime / endTimedatetime活動期間
tagvarchar(30)新手/回歸/VIP/節日/限時/每日/週末/月度
conditionTypevarchar(20)deposit_threshold / vip_level / first_deposit
conditionValuevarchar(50)門檻值
rewardAmountdecimal(18,6)獎勵金額 (USD)
turnoverMultiplierdecimal(10,2)打碼倍數 (0=無要求)
maxClaimsint最大領取人數 (0=無限)
claimedCountint已領取人數

affiliate 相關

資料表主要欄位說明
affiliate-commissionagentId, memberId, agentLevel, netLoss, commissionRate, commissionAmount, weekStart佣金明細(每週結算)
affiliate-settlementagentId, weekStart, totalCommission, status, riskFlagged結算紀錄
affiliate-balanceagentId (unique), available, frozen, totalEarned, totalWithdrawn佣金餘額
affiliate-withdrawalagentId, amount, method, status提款紀錄
affiliate-clickagentId, refCode, ip, userAgent, converted推廣點擊
affiliate-bind-logmemberId, agentId, action, source綁定審計
affiliate-risk-logsettlementId, riskType, detail風控紀錄

mission

欄位型別說明
idint PK auto
categoryvarchar(20)任務類別: deposit / bet
periodTypevarchar(20)週期類型: daily / weekly / monthly
tierint階級 (1-5)
thresholddecimal(18,6)門檻金額 (USD)
rewardAmountdecimal(18,6)獎勵金額 (USD)
vipRequiredint default 0VIP 最低等級要求 (0=無)
turnoverMultiplierdecimal(10,2) default 0打碼量倍數
enabledtinyint(1) default 1是否啟用
createdAtdatetime
updatedAtdatetime

UNIQUE(category, periodType, tier)

mission-progress

欄位型別說明
idint PK auto
userIdint index用戶 ID
periodTypevarchar(20)週期類型
periodKeyvarchar(20)週期 key (e.g. 2026-02-24 / 2026-W09 / 2026-02)
depositTotaldecimal(18,6) default 0累計存款 (USD)
betTotaldecimal(18,6) default 0累計有效投注 (USD)
createdAtdatetime
updatedAtdatetime

UNIQUE(userId, periodType, periodKey) 存款確認時 ON DUPLICATE KEY UPDATE depositTotal += amount 投注結算時 ON DUPLICATE KEY UPDATE betTotal += betEffective

mission-claim

欄位型別說明
idint PK auto
missionIdint index任務定義 ID
userIdint index用戶 ID
periodKeyvarchar(20)領取時的週期 key
rewardAmountdecimal(18,6)實際發放金額 (USD)
requiredTurnoverdecimal(18,6) default 0需完成的打碼量
completedTurnoverdecimal(18,6) default 0已完成的打碼量
turnoverCompletedtinyint(1) default 0打碼量是否已完成
claimedAtdatetime領取時間

UNIQUE(missionId, userId, periodKey) — 同一任務同一期只能領一次


8. 功能規格

8.1 認證系統 (Auth)

端點: 17 個

功能端點Auth
註冊POST /auth/register-
登入POST /auth/login-
用戶資料GET /auth/user-detailJWT
國碼列表GET /auth/country-codesJWT
發送驗證信POST /auth/send-verify-emailJWT
驗證 EmailPOST /auth/check-verify-emailJWT
產生 Google AuthPOST /auth/generate-google-authJWT
啟用 Google AuthPOST /auth/enable-google-authJWT
修改密碼POST /auth/edit-passwordJWT
登出POST /auth/logoutJWT
更新語系偏好PATCH /auth/localeJWT
取得登入設定GET /auth/login-config-
Google 登入POST /auth/login-google-
Telegram 登入POST /auth/login-telegram-
吉祥物列表GET /auth/mascots-
切換頭像PATCH /auth/avatarJWT

規格要點:

  • 密碼使用 bcryptjs (salt rounds: 10) 雜湊
  • JWT 有效期 7 天,tokenVersion 用於強制登出
  • Google 登入:使用 OAuth 2.0 code flow,回傳 token + user + google profile
  • Telegram 登入:使用 Telegram Login Widget,HMAC-SHA256(SHA256(BOT_TOKEN), data-check-string) 驗證 hash,auth_date 須在 5 分鐘內。首次登入自動建立帳號(account: tg_{telegramId}
  • GET /auth/login-config 回傳 Google OAuth URL + telegram: { botUsername } 供前端初始化 Login Widget
  • 2FA:基於 TOTP (speakeasy),需先 generate 再 enable
  • 登入紀錄自動記錄 IP、UserAgent、時間
  • 註冊時可帶 refCode 自動綁定代理關係
  • 註冊時依語系自動分配金流群組(zh-TW→TWD, en-US→USD, zh-CN→CNY),找第一個啟用且含該幣別通道的群組
  • 登出時:從 locales header 覆寫語系 → 重新匹配金流群組 → tokenVersion + 1 使 token 失效
  • 吉祥物頭像:10 組 SVG 生成的 512×512 PNG(dragon / phoenix / angel / wizard / knight / mermaid / tiger / fox / owl / wolf),存放於 R2 avatars/mascots/PATCH /auth/avatarmascotId 切換,更新 auth-user.avatar

8.2 遊戲系統 (Game)

端點: 5 個 + 11 個遊戲商回調

功能端點Auth
遊戲商列表GET /game/provider-
啟動遊戲POST /game/launchJWT
試玩 (Demo)POST /game/demo-
模擬遊戲POST /game/simulateJWT
遊戲列表GET /game/listJWT

遊戲類型與流水權重

gameType名稱標籤流水權重說明
1體育sports1.0 (100%)
2電子slot1.0 (100%)
3真人live1.0 (100%)
4彩票lottery1.0 (100%)
5棋牌chess1.0 (100%)
8電競esports1.0 (100%)
9加密貨幣crypto0.5 (50%)半權重:防止高勝率遊戲快速刷流水
10捕魚fish0.5 (50%)半權重:同上

有效投注公式: betEffective = betAmount × TURNOVER_WEIGHT[gameType]

有效投注在下注時即計算(非結算時),影響 VIP 升級、反水計算、打碼量累計。

遊戲商 Provider

gameCodeProvidergameType說明
slot-betsolutionsbetsolutions2 (電子)BetSolutions Slots
crypto-betsolutionsbetsolutions9 (加密)BetSolutions ProvablyFair
slot-rsgrsg2 (電子)RSG 瑞盛電子

啟動遊戲流程

前端呼叫 POST /game/launch { gameCode, productId }

  game-provider 查表 → 取得 providerCode

  ┌─────┴─────┐
  │           │
 RSG      BetSolutions
  │           │
 DES-CBC      生成 publicToken (5min TTL)
 加密 + MD5    → 組裝 URL 含 Token
 → 回傳 URL    → 回傳 URL
  • RSG:DES-CBC 加密 JSON payload + MD5 簽名(clientId + clientSecret + timestamp + encrypted),userId 格式為 c9_{userId}
  • BetSolutions:生成 publicToken 存 cache(5min TTL),遊戲端首次回調時交換為 privateToken(2hr TTL),後續所有回調使用 privateToken + SHA256 hash 驗證

試玩模式 (Demo)

  • 僅支援 BetSolutionsIsFreeplay=1),RSG 不支援試玩
  • 免登入,不需 JWT

模擬遊戲 (Simulate) — RTP 97%

投注金額限制:0.01 ~ 10,000 USD

完整流程:

  1. 扣除 betAmount,建立 game-transaction (type=bet)
  2. 隨機開獎(累積機率閾值):
result倍率累積機率區間
lose0x60%0 ~ 60%
small0.5x82%60 ~ 82%
medium2x92%82 ~ 92%
good4x97%92 ~ 97%
big8x99%97 ~ 99%
huge20x99.8%99 ~ 99.8%
mega70x100%99.8 ~ 100%
  1. 派彩 winAmount = betAmount × 倍率,建立 game-transaction (type=win)
  2. 計算 betEffective = betAmount × getTurnoverWeight(gameType)
  3. 建立 bet-order + bet-detail(winLose = winAmount - betAmount
  4. 觸發 afterBetSettle()

投注結算後連鎖觸發 (afterBetSettle)

每筆投注結算完成後(模擬 / RSG 回調 / BS 回調),同步執行:

afterBetSettle(userId, betEffective)
  ├─ VipService.recalculateUserVip(userId)     → 重算 VIP 等級(只升不降)
  └─ PromoService.updatePromoTurnover(userId, betEffective) → 累計優惠打碼量

注意: RSG 和 BS 的 afterBetSettle 在 handleBet() 時觸發(下注時),不是在 handleBetResult/handleWin() 時。

遊戲錢包帳本 (game-transaction)

每一筆遊戲資金異動記錄一筆 game-transaction:

type說明餘額影響
bet下注扣款balance -= amount
win派彩入帳balance += amount
cancel取消退款balance += amount
jackpotJackpot 獎勵balance += amount
  • transactionId 以 provider 前綴區分:rsg_{id}bs_{id}sim_{uuid}
  • 重複 transactionId 檢查防止 callback 重放(冪等性)

RSG 回調流程

RSG → POST /game/rsg/Bet       → 下注扣款 + 建 bet-order + afterBetSettle
RSG → POST /game/rsg/BetResult → 派彩入帳 + 更新 bet-order (winLose, odds)
RSG → POST /game/rsg/CancelBet → 退款 (type=cancel)
RSG → POST /game/rsg/JackpotResult → Jackpot 入帳 (type=jackpot)
RSG → POST /game/rsg/GetBalance → 查詢餘額

bet-order 匹配規則:gameNumber LIKE 'rsg_{SequenceNumber}_%'

BetSolutions 回調流程

BS → POST /game/betsolutions/auth        → publicToken 交換 privateToken
BS → POST /game/betsolutions/getBalance   → 查詢餘額 (SHA256 驗證)
BS → POST /game/betsolutions/bet          → 下注扣款 + 建 bet-order + afterBetSettle
BS → POST /game/betsolutions/win          → 派彩入帳 + 更新 bet-order (winLose, odds)
BS → POST /game/betsolutions/cancelBet    → 退款
BS → POST /game/betsolutions/getPlayerInfo → 回傳玩家資訊
  • gameType 判斷:productId === 3 → crypto (gameType=9),其他 → slot (gameType=2)
  • bet-order 匹配規則:gameNumber LIKE 'bs_{roundId}_%'
  • SHA256 驗證:所有參數以 | 串接 + privateKey,計算 SHA256 比對

注單狀態

status說明
valid有效注單(參與流水、反水計算)
invalid無效(原因:draw / cancelled / low_odds / arbitrage)
cancelled已取消

前端遊戲整合流程

1. GET /game/provider        → 取得遊戲商列表,依 gameType 分頁
2. GET /game/list?gameCode=  → 取得該商的遊戲清單
3. POST /game/launch         → 正式遊戲(iframe/window.open)
3. POST /game/demo           → 試玩(免登入,僅 BetSolutions)
3. POST /game/simulate       → 模擬遊戲(一鍵扣款→開獎→派彩→觸發 VIP+打碼量)
4. GET /bet-record            → 查看投注紀錄

8.3 金流系統 (Vendor + Deposit)

金流通道端點: 1 (vendor) + 4 (wantong) + 1 (usdt) + 4 (deposit)

金流架構

                    用戶

              POST /deposit
             (channelId + paymentMethod)

       建立 deposit-order (status: pending)

         依 channel.paymentMethods 路由

              ┌──────┴──────┐
              │              │
         fiat/credit      crypto
              │              │
         萬通金流         USDT 入金
    (wantong.service)  (usdt.service)
              │              │
    ┌────────┴────────┐     └→ 回傳繳費地址 + 鏈路
    │                 │        用戶自行轉帳
  ATM建單          信用卡建單      │
    │                 │       pay callback
  萬通API            萬通API    (pending → paid)
    │                 │
  add callback      add callback    ← S2S: pending → created
  pay callback      pay callback    ← S2S: created → paid
    │                 │
  匯率轉換 → creditBalance() → user.balance += USD
                                 └→ recalculateUserVip()

法幣存款 vs USDT 存款比較

面向法幣 (萬通)USDT (加密)
建單呼叫萬通 API 取得繳費資訊直接回傳預設錢包地址
狀態流轉pending → created → paid(2 次 callback)pending → paid(1 次 callback)
驗證方式MD5 checksum外部區塊鏈確認
匯率轉換TWD → USD(台灣銀行即時匯率)USDT → USD(匯率 ≈ 1:1)
callbackData{add: dto, pay: dto}{pay: dto}

存款訂單生命週期

狀態觸發說明
pendingPOST /deposit本地訂單建立
createdwantong callback/add金流商確認建單(僅萬通)
paidcallback/pay付款成功,已上分至 balance
failed超時/異常訂單失敗

匯率系統

  • 來源: 台灣銀行即時匯率 (tw-exchange)
  • 基底: USD(API 回傳所有幣種對 USD 的匯率)
  • 快取: cache-manager,TTL 60 秒,key 格式 deposit:exchange-rate:{bankCode}:{base}:{currency}:v1
  • 去重: inFlight Promise 防止並發時重複呼叫外部 API
  • 匯率優先序: sellbksellcashsell(賣出方向)

USD 轉換公式:

usdAmount = payAmount / exchangeRate
精度截斷:Math.floor(usdAmount * 1e6) / 1e6

creditBalance 原子操作

付款回調確認後的上分流程:

1. 取得幣別匯率(cache → tw-exchange API)
2. usdAmount = Math.floor(payAmount / rate * 1e6) / 1e6
3. 原子更新:UPDATE auth-user SET balance = balance + {usdAmount} WHERE id = {userId}
4. 更新 deposit-order:status='paid', usdAmount, exchangeRate, payAmount, payTime
5. 觸發 VipService.recalculateUserVip(userId) → 重算 VIP 等級

萬通 Checksum 驗證

raw = `${subOrder}${secret1}${storeCode}`.toUpperCase()
checkSum = MD5(raw).toUpperCase()
  • 用於 add/pay callback 的來源驗證
  • checksum 不符時 log 警告但不阻擋處理(容錯)

金流通道分配

  • 用戶註冊時依語系自動分配 vendorGroupId(zh-TW→TWD, en-US→USD, zh-CN→CNY)
  • 僅顯示用戶所屬 vendor-group 中 enabled=1 的通道
  • USDT 通道的 secret1 欄位儲存鏈路名稱(如 TRC-20)

冪等性保護

  • add callback:訂單不存在時靜默成功(防止重試失敗)
  • pay callback:已為 paid 狀態時直接回傳成功(不重複上分)

8.4 錢包系統 (Wallet)

功能端點特殊
銀行卡/wallet/bank-card/add, list, :idmultipart/form-data,含圖片上傳
信用卡/wallet/credit-card/add, list, :idJSON
加密錢包/wallet/crypto-address/add, list, :idJSON

錢包狀態: 0=待審核1=已通過2=已拒絕

  • 代理提款時的 targetId 需對應 status=1(已通過審核)的錢包

8.5 VIP 等級系統

端點: 12 個

資料表

vip-level:

欄位型別說明
idint PK auto
levelint, UNIQUEVIP 等級編號 (1~15)
namejson多語系名稱 {"zh-TW":"青銅 I","en-US":"Bronze I","zh-CN":"青铜 I"}
tiervarchar(20)階層標籤:bronze / gold / platinum / diamond
minChipdecimal(18,6) default 0升級門檻:累計有效投注 (USD)
relegationChipdecimal(18,6) default 0月保級門檻:當月有效投注 (USD)
sortOrderint default 1前端顯示排序
enabledtinyint(1) default 1是否啟用
createdAt / updatedAtdatetime

vip-rebate:

欄位型別說明
idint PK auto
levelint, INDEXVIP 等級
gameTypevarchar(20)遊戲類型標籤 (sports/slot/live/lottery/chess/esports/crypto/fish)
rebateRatedecimal(5,2) default 0反水率 (%),如 0.50 = 0.50%
createdAtdatetime

UNIQUE(level, gameType) — 每等級每遊戲類型一筆反水率

vip-rebate-log:

欄位型別說明
idint PK auto
userIdint
settleDatedate結算日期 (如 2026-02-23)
vipLevelint結算當時的 VIP 等級
gameTypevarchar(20)遊戲類型標籤
dailyEffectivedecimal(18,6)當日該遊戲類型有效投注
rebateRatedecimal(5,2)適用反水率 (%)
rebateAmountdecimal(18,6)發放的反水金額 (USD)
createdAtdatetime

INDEX(userId, settleDate)

15 級制

等級名稱Tier升級門檻 (USD)月保級門檻 (USD)
1青銅 Ibronze00
2青銅 IIbronze3,600200
3青銅 IIIbronze12,000600
4青銅 IVbronze36,0001,800
5青銅 Vbronze120,0006,000
6青銅 VIbronze360,00018,000
7黃金 Igold1,200,00060,000
8黃金 IIgold2,400,000120,000
9黃金 IIIgold5,000,000250,000
10鉑金 Iplatinum12,000,000600,000
11鉑金 IIplatinum30,000,0001,500,000
12鉑金 IIIplatinum60,000,0003,000,000
13鑽石 Idiamond120,000,0006,000,000
14鑽石 IIdiamond480,000,00024,000,000
15鑽石 IIIdiamond960,000,00048,000,000

等級門檻數值儲存於 vip-level 資料表,管理員可透過 API 調整。上表為預設初始值。

VIP 升級演算法 — recalculateUserVip(userId)

觸發時機: 每次投注結算完成後同步呼叫(非排程)

1. 取得所有啟用的 VipLevel,按 level ASC 排序
2. 查詢 SUM(betEffective) FROM bet_order WHERE userId AND status='valid'
   → totalEffective(全歷史累計有效投注)
3. 由低到高遍歷等級表,取最後一個 totalEffective >= minChip 的等級
   → matchedLevel
4. 「只升不降」: newLevel = MAX(matchedLevel.level, user.currentVipLevel)
5. 計算升級進度:
   - nextLevel = 下一個等級
   - 若有 nextLevel: progress = MIN(totalEffective / nextLevel.minChip, 1.0)
   - 若已滿級: progress = 1.0
6. 寫入 auth_user:
   - vipLevel = String(newLevel)
   - vipProgress = String(Math.floor(progress × 100))  // 整數 0~100
   - totalEffectiveBet = totalEffective.toFixed(6)

重點:此函數永遠不會降級,降級僅由月度保級檢查(checkMonthlyRelegation)執行。

月度保級檢查 — checkMonthlyRelegation()

排程: 0 0 1 1 * * → 每月 1 號 01:00:00

狀態機:

通過本月保級  → missCount = 0       (歸零)
第 1 個月未達  → missCount = 1       (警告,不降級)
連續第 2 月未達 → 降 1 級,missCount = 0
VIP 5+ 且 vipHold = 1 → 完全跳過檢查

演算法:

1. 取得所有啟用的 VipLevel → levelMap<level, VipLevel>
2. 計算上月日期範圍: lastMonthStart ~ lastMonthEnd (23:59:59)
3. 查詢 auth_user WHERE CAST(vipLevel AS UNSIGNED) >= 2
4. 逐一檢查:
   a. 若 user.vipHold === 1 且 vipLevel >= 5 → 跳過
   b. 查表 levelConfig = levelMap[userLevel],若 relegationChip <= 0 → 跳過
   c. 計算 SUM(betEffective) FROM bet_order WHERE userId AND 上月範圍 AND status='valid'
      → monthlyEffective
   d. 判定:
      - monthlyEffective >= relegationChip → 通過,missCount 歸零
      - monthlyEffective < relegationChip:
        - newMissCount = missCount + 1
        - newMissCount >= 2 → 降級: vipLevel = MAX(level - 1, 1), missCount = 0
        - newMissCount == 1 → 警告: missCount = 1(不降級)
5. 回傳 { checked, warned, demoted }

VIP Hold 保級鎖定

  • 僅限 VIP 5 以上 的用戶可設定
  • 管理員透過 PATCH /vip/users/:userId/hold 設定 { hold: 0|1 }
  • vipHold = 1 的用戶在月度保級檢查時完全跳過,不會被降級
  • 錯誤碼:2001 用戶不存在、2002 用戶 VIP 等級低於 5

每日反水結算 — settleDailyRebate(targetDate?)

排程: 0 5 0 * * * → 每日 00:05:00

演算法:

1. 確定結算日期(預設昨日): dayStart = 'YYYY-MM-DD 00:00:00', dayEnd = 'YYYY-MM-DD 23:59:59'

2. 查詢所有有效投注:
   SELECT userId, gameType, SUM(betEffective) as dailyEffective
   FROM bet_order
   WHERE settledAt BETWEEN dayStart AND dayEnd AND status = 'valid'
   GROUP BY userId, gameType

3. 建立反水率查詢表:
   rebateMap = Map<"${level}-${gameTypeLabel}", rebateRate%>
   (從 vip-rebate 全表載入)

4. 逐用戶處理:
   a. 取得 user.vipLevel
   b. 對每個 (gameType, dailyEffective):
      - gameTypeLabel = GAME_TYPE_LABELS[gameType]  // 數字→文字
      - rate = rebateMap["${level}-${gameTypeLabel}"] || 0
      - ★ rebateAmount = Math.floor(dailyEffective × (rate / 100) × 1e6) / 1e6
        ↑ 截斷至 6 位小數(非四捨五入)
      - 若 rebateAmount > 0 → 累計到 userRebate,建立 rebate-log 記錄
   c. 若 userRebate > 0:
      - rounded = Math.floor(userRebate × 1e6) / 1e6
      - UPDATE auth_user SET balance = balance + rounded WHERE id = userId
      - INSERT INTO vip-rebate-log (批量寫入)

5. 回傳 { usersProcessed, totalRebate }

精度規則: 所有中間值與最終值均使用 Math.floor(x × 1e6) / 1e6 截斷至 6 位小數,絕不四捨五入。

反水率範例 (%):

Levelsportsslotlivecrypto
10.200.500.500.50
70.500.800.700.70
150.901.501.001.10

反水率儲存於 vip-rebate 資料表,管理員可透過 API 調整。每等級每遊戲類型一筆。

VIP 狀態查詢 — GET /vip/status

回傳結構:

json
{
  "level": 1,
  "name": "Bronze I",
  "tier": "bronze",
  "totalEffectiveBet": "1200.000000",
  "currentChip": "0.000000",
  "nextLevelMinChip": "3600.000000",
  "progress": "0.333333",
  "relegationChip": "0.000000",
  "monthlyEffective": "450.000000",
  "relegationMissCount": 0,
  "vipHold": 0,
  "rebates": [
    { "gameType": "sports", "rebateRate": "0.50" }
  ],
  "allLevels": [
    { "level": 1, "name": "Bronze I", "tier": "bronze",
      "minChip": "0.000000", "relegationChip": "0.000000" }
  ]
}

端點

MethodPathAuth說明
GET/vip/levels取得所有啟用的 VIP 等級表 (sortOrder ASC)
GET/vip/rebates取得所有反水率規則 (level ASC, gameType ASC)
GET/vip/statusJWT取得當前用戶完整 VIP 狀態
POST/vip/settlement/daily-rebateJWT手動觸發每日反水結算
POST/vip/settlement/monthly-relegationJWT手動觸發月度保級檢查
PATCH/vip/users/:userId/holdJWT設定/取消 VIP Hold
POST/vip/levelsJWT新增 VIP 等級
PATCH/vip/levels/:idJWT修改 VIP 等級
DELETE/vip/levels/:idJWT刪除 VIP 等級
POST/vip/rebatesJWT新增反水率規則
PATCH/vip/rebates/:idJWT修改反水率規則
DELETE/vip/rebates/:idJWT刪除反水率規則

8.6 活動促銷系統 (Promo)

端點: 7 個

資料表

promo:

欄位型別說明
idint PK auto
titlejson多語系標題 {"zh-TW":"...","en-US":"...","zh-CN":"..."}
imgPcvarchar(255) nullable電腦版圖片 (R2 key)
imgMobilevarchar(255) nullable手機版圖片 (R2 key)
contentjson多語系 HTML 內容
actionHtmltext nullable渲染連結/按鈕 (HTML)
startTimedatetime活動開始時間
endTimedatetime活動結束時間
tagvarchar(30)活動標籤(篩選用)
enabledtinyint(1) default 10=關 1=開
conditionTypevarchar(20)領取條件類型
conditionValuevarchar(50) default '0'門檻值
rewardAmountdecimal(18,6)獎勵金額 (USD)
turnoverMultiplierdecimal(10,2) default 0打碼量倍數 (0=無要求)
maxClaimsint default 0最大領取總數 (0=無限)
claimedCountint default 0已領取次數
createdAt / updatedAtdatetime

promo-claim:

欄位型別說明
idint PK auto
promoIdint, INDEX對應 promo.id
userIdint, INDEX對應 auth_user.id
rewardAmountdecimal(18,6)實際發放金額
requiredTurnoverdecimal(18,6) default 0需完成的打碼量 (USD)
completedTurnoverdecimal(18,6) default 0已完成的打碼量
turnoverCompletedtinyint(1) default 00=進行中 1=已完成
claimedAtdatetime auto領取時間

UNIQUE(promoId, userId) — 每用戶每活動只能領取一次

領取條件類型 — checkCondition(userId, promo)

conditionType判定邏輯conditionValue 用途
deposit_thresholdSUM(deposit_order.payAmount) WHERE userId AND status='paid' >= conditionValue門檻金額 (USD)
vip_leveluser.vipLevel >= conditionValue最低 VIP 等級
first_deposit用戶首筆 paid 存單的 createdAt[promo.startTime, promo.endTime] 範圍內不使用(設 0)

未知的 conditionType 一律回傳 false(不可領取)

領取流程 — claimPromo(userId, promoId)

1. 查詢 promo → 不存在 → 2001
2. 檢查活動有效: enabled=1 且 now 在 [startTime, endTime] 內 → 否 → 2001
3. 檢查重複領取: promo-claim(promoId, userId) 已存在 → 2001
4. 檢查領取額度: maxClaims > 0 且 claimedCount >= maxClaims → 2002
5. 檢查領取條件: checkCondition(userId, promo) → false → 2003

6. 發放獎勵:
   reward = Number(promo.rewardAmount)
   UPDATE auth_user SET balance = balance + reward WHERE id = userId

7. 計算打碼量:
   multiplier = Number(promo.turnoverMultiplier || 0)
   requiredTurnover = multiplier > 0 ? reward × multiplier : 0

8. 建立領取紀錄:
   INSERT INTO promo-claim {
     promoId, userId,
     rewardAmount: promo.rewardAmount,
     requiredTurnover: requiredTurnover.toFixed(6),
     completedTurnover: '0.000000',
     turnoverCompleted: (requiredTurnover <= 0) ? 1 : 0  // 無打碼要求直接完成
   }

9. 累計已領取: UPDATE promo SET claimedCount = claimedCount + 1

10. 回傳 { rewardAmount, newBalance }

打碼量累計 — updatePromoTurnover(userId, betAmount)

觸發時機: 每次投注結算完成後同步呼叫(非排程)

1. 若 betAmount <= 0 → 直接返回(no-op)
2. 查詢 promo-claim WHERE userId AND turnoverCompleted = 0(所有未完成的領取紀錄)
3. 逐筆更新:
   - required = Number(claim.requiredTurnover)
   - 若 required <= 0 → 跳過
   - completed = MIN(claim.completedTurnover + betAmount, claim.requiredTurnover)
     ↑ 封頂於 requiredTurnover,不會超過
   - UPDATE: completedTurnover = completed.toFixed(6)
   - 若 completed >= required → turnoverCompleted = 1

重點: 單次投注的 betAmount 同時套用到所有未完成的領取紀錄。每筆紀錄獨立追蹤進度,各自封頂於 requiredTurnover。

前端計算欄位(列表 API 回傳)

欄位計算邏輯
isActivepromo.enabled === 1 AND startTime <= now AND endTime >= now
isClaimed用戶已登入且 promo-claim(promoId, userId) 存在
isClaimable用戶已登入 AND isActive AND !isClaimed AND checkCondition() === true

未登入時 isClaimedisClaimable 一律為 false

圖片上傳

  • Content-Type: multipart/form-data
  • 欄位: imgPc(電腦版)、imgMobile(手機版),各最多 1 張
  • 限制: 僅 image/* MIME,單檔 5 MB
  • 儲存: Cloudflare R2,上傳後回傳 key,前端透過公開 URL 存取
  • 更新時上傳新圖會先刪除舊 R2 物件;刪除活動時兩張圖同步刪除

端點

MethodPathAuth說明
GET/promo可選 JWT活動列表(分頁,?tag, ?activeOnly=1)
GET/promo/claimsJWT用戶領取紀錄(?tab=all/pending/completed, ?startDate, ?endDate)
GET/promo/:id可選 JWT單一活動詳情(含計算欄位)
POST/promoJWT新增活動 (multipart/form-data)
PATCH/promo/:idJWT修改活動 (部分更新,可上傳新圖)
DELETE/promo/:idJWT刪除活動(同步刪除 R2 圖片)
POST/promo/:id/claimJWT領取活動獎勵

8.7 代理推廣系統 (Affiliate + Alliance)

端點: 39 個(原 21 + 新增 18)

3 層代理架構

代理 A (level1AgentId)
├── 會員 X → A 的直接下線 (Level 1)
│   ├── 會員 Y → A 的 Level 2
│   │   └── 會員 Z → A 的 Level 3

代理階層制度

階層tierCode升級門檻 (累計佣金)最低直屬下線
青銅bronze$00
白銀silver$1,00010
黃金gold$5,00050
鉑金platinum$20,000200

佣金比例(DB 可配置,依遊戲類型 + 代理階層 + 代理層級)

查詢邏輯:精確匹配 (agentTier, agentLevel, gameType) → 找不到時 fallback (agentTier, agentLevel, null)

預設佣金比例(Bronze 階層為例):

層級預設比例計算基礎
Level 130%直接下線淨虧損
Level 210%間接下線淨虧損
Level 35%間接下線淨虧損

可依遊戲類型設定差異化比例(如 crypto 遊戲較低、sports 較高)。

淨虧損計算: netLoss = max(0, -winLose)(只有下線虧損時代理才有佣金)

VIP 里程碑獎勵

當被推薦會員升級 VIP 等級時,L1 代理獲得額外獎金:

VIP 等級獎勵 (USD)
VIP 2$0.50
VIP 3$1
VIP 4$2
VIP 5$5
VIP 6$10
VIP 7$20
VIP 8$50
VIP 9$100
VIP 10$200
VIP 11~15$300~$1,000
  • 每位會員每個 VIP 等級只發放一次(unique constraint 冪等保護)
  • 獎金直接入帳 AffiliateBalance.available
  • VipService 重算後自動觸發(best-effort,不阻擋 VIP 重算)

多推廣碼

  • 每位代理最多 10 個自訂推廣碼(3-30 英數字)
  • 可標記渠道標籤(YouTube、Telegram、Twitter 等)
  • 自動追蹤每個碼的轉換人數
  • 推廣碼全域唯一(同時檢查 auth_user.agentCode + alliance-referral-code)
  • 註冊綁定流程:先查 alliance-referral-code,再查 auth_user.agentCode

佣金結算

雙軌結算:

  • 週結算:每週一 03:00 自動結算上一週(與現有一致)
  • 日結算:每日 03:30 自動結算前一日
結算流程:
  1. 載入 rateMap(所有佣金比例 → Map)
  2. 查詢注單(含 gameType)
  3. Pre-load 所有代理的 agentTier
  4. 逐單計算: resolve gameType → lookup rate → calc commission
  5. 寫入 settlement (含 gameTypeBreakdown JSON)
  6. 寫入 commission 明細 (含 gameType)
  7. 風控檢測

結算生命週期:

結算 Cron → 計算佣金明細 → 風控檢測
  ├─ 無風控 → status: pending
  └─ 有風控 → status: pendingReview
Admin 審核 ──┬─ approved → 佣金入帳 available
             └─ rejected → 不發放

風控規則: same_ip / same_device / agent_member_ip / agent_member_device

代理佣金餘額流轉

totalEarned ← 累計所有已核准佣金 + VIP 里程碑獎勵
available   ← 可提款餘額
frozen      ← 提款審核中凍結金額
totalWithdrawn ← 歷史已提款總額
agentTier   ← 代理階層 (bronze|silver|gold|platinum)

available → (提款申請) → frozen → (出款完成) → totalWithdrawn
                          └→ (審核拒絕) → available

前端推廣流程

1. 用戶進入 /?ref=MYCODE2026
2. 前端呼叫 POST /affiliate/track-click { refCode: "MYCODE2026" }
   → resolveRefCode 解析(alliance-referral-code 優先 → auth_user.agentCode fallback)
3. 用戶註冊時帶入 POST /auth/register { ..., refCode: "MYCODE2026" }
4. 自動建立 3 層代理關係 + increment convertCount(best-effort)

資料表

alliance-commission-rate:

欄位型態說明
idint PK
agentTiervarchar(20)bronze|silver|gold|platinum
agentLeveltinyint1=直屬 2=二級 3=三級
gameTypevarchar(20) nullablesports|slot|live|...,null=全部
commissionRatedecimal(5,2)佣金比例 (%)
enabledtinyint(1)
UNIQUE(agentTier, agentLevel, gameType)

alliance-agent-tier:

欄位型態說明
idint PK
tierCodevarchar(20) unique階層碼
tierNamevarchar(50)顯示名稱
minTotalEarneddecimal(18,6)升級門檻
minActiveMembersint最低直屬下線
sortOrderint

alliance-vip-milestone:

欄位型態說明
idint PK
vipLevelint unique觸發 VIP 等級
bonusAmountdecimal(18,6)獎勵金額 (USD)
descriptionvarchar(100) nullable
enabledtinyint(1)

alliance-vip-milestone-log:

欄位型態說明
idint PK
agentIdint代理 ID
memberIdint會員 ID
vipLevelint觸發等級
bonusAmountdecimal(18,6)發放金額
milestoneIdintFK
UNIQUE(agentId, memberId, vipLevel)冪等

alliance-referral-code:

欄位型態說明
idint PK
agentIdint代理 ID
codevarchar(30) unique推廣碼
labelvarchar(50) nullable渠道標籤
enabledtinyint(1)
convertCountint default 0轉換人數

affiliate-commission (新增欄位):

新增欄位型態說明
gameTypevarchar(20) nullable遊戲類型

affiliate-settlement (新增欄位):

新增欄位型態說明
gameTypeBreakdownjson nullable各遊戲類型佣金分解
periodTypevarchar(10) default 'weekly'weekly|daily

affiliate-balance (新增欄位):

新增欄位型態說明
agentTiervarchar(20) default 'bronze'代理階層

8.8 投注紀錄 (BetRecord)

端點: 2 個

功能端點
列表 (含匯總)GET /bet-record
小注單明細GET /bet-record/:orderId/details

匯總欄位 (totalBetCount, betAmount, betEffective, winLose) 為該用戶全量統計(status=valid),不受分頁影響。

注單狀態:

  • valid:有效
  • invalid:無效(原因:draw/cancelled/low_odds/arbitrage)
  • cancelled:已取消

8.9 排行榜 (Ranking)

端點: 1 個 — GET /ranking

type說明排序
realtime最新投注時間倒序
daily今日支付金額
weekly本週支付金額
monthly本月支付金額
total累積提領排名總金額

匿名顯示: isAnonymous=true 時 playerName 顯示為 i18n 翻譯的「隱身」文字。


8.10 站內信 (Inbox)

端點: 7 個 — User 4 + Admin 3

支援「個人通知」(userId 指定) 與「全站通知」(userId=NULL),分類為 system / promo。 全站通知不複製到每個用戶,使用獨立 notification-read 表追蹤已讀。 title / content 為多語系 JSON,User 端回傳會經 resolveText() 轉為當前語系字串。

資料表

notification:

欄位型態說明
idint PK auto
userIdint nullable, indexNULL = 全站通知
titlejson{"zh-TW":"...","en-US":"...","zh-CN":"..."}
contentjson多語系 HTML
categoryvarchar(20)system / promo
createdAtdatetime
updatedAtdatetime

notification-read:

欄位型態說明
idint PK auto
userIdint, index
notificationIdint, index
readAtdatetime (auto)
UNIQUE(userId, notificationId)

User 端點

MethodPath說明
GET/inbox站內信列表(個人+全站,LEFT JOIN 已讀,分頁)
GET/inbox/unread-count未讀數量
POST/inbox/:id/read標記單則已讀
POST/inbox/read-all全部已讀

Admin 端點

MethodPath說明
POST/inbox/admin/send發送通知(userId 不傳 = 全站)
GET/inbox/admin/list通知列表(原始多語系 JSON)
DELETE/inbox/admin/:id刪除通知(一併清除已讀紀錄)

核心查詢

sql
SELECT n.*, r.id AS readId
FROM notification n
LEFT JOIN `notification-read` r
  ON r.notificationId = n.id AND r.userId = ?
WHERE (n.userId = ? OR n.userId IS NULL)
ORDER BY n.createdAt DESC
  • isRead = !!readId
  • 未讀數量: 同上 + AND r.id IS NULL + COUNT
  • 全部已讀: 查出未讀 IDs → 批次 INSERT notification-read

8.11 站點設定 (SiteConfig)

端點: 8 個 — Public 1 + Admin 7

支援多站點架構,透過 SITE_CODE 環境變數區分當前站點(預設 C9)。 站點名稱 / 介紹為多語系 JSON,Public 端回傳經 resolveText() 轉為當前語系字串。 每個站點可設定多組主題色(site-theme),透過 activeThemeId 指定當前使用的主題。

資料表

site-config:

欄位型態說明
idint PK auto
siteCodevarchar(30) unique站點代碼 (e.g. C9)
siteNamejson{"zh-TW":"...","en-US":"...","zh-CN":"..."}
siteDescriptionjson多語系站點介紹
supportedLocalesjson["zh-TW","en-US","zh-CN"]
activeThemeIdint nullable當前使用的主題 (FK site-theme)
enabledtinyint(1)0=關 1=開
createdAtdatetime
updatedAtdatetime

site-theme:

欄位型態說明
idint PK auto
themeIdvarchar(50) unique主題識別碼 (e.g. default-emerald)
themeNamejson多語系主題名稱
primaryjson主色系 {base, dark, light, glow}
accentjson強調色 {gold, info, violet, cyan, error}
surfacejson表面色 {page, navbar, card, modal, sidebar}
textjson文字色 {primary, secondary, muted, hint}
borderjson邊框色 {subtle, default, strong}
enabledtinyint(1)0=關 1=開
siteConfigIdint FK所屬站點設定
createdAtdatetime
updatedAtdatetime

Public 端點

MethodPath說明
GET/site-config取得當前站點設定(含 activeTheme 完整色號 + availableThemes 列表)

Admin 端點

MethodPath說明
GET/site-config/admin/list取得所有站點設定(含主題列表)
PATCH/site-config/admin/:id更新站點設定(含 activeThemeId)
GET/site-config/admin/:siteConfigId/themes主題列表
POST/site-config/admin/:siteConfigId/themes新增主題
PATCH/site-config/admin/themes/:id更新主題
DELETE/site-config/admin/themes/:id刪除主題
PATCH/site-config/admin/:siteConfigId/mascots更新吉祥物列表(全量替換)

8.12 提領系統 (Withdrawal)

USDT 提領模組。用戶需已綁定信箱與手機,擁有已審核通過的加密錢包,並透過郵箱驗證碼確認身份後方可提交提領。

資料表

withdrawal-order:

欄位型別說明
idint PK auto
userIdint, INDEX
amountdecimal(18,6)提領金額 (USD)
cryptoAddressIdint提領目標錢包 ID
addressvarchar(255)快照:錢包地址(提交時快照,防後續修改)
networkvarchar(20)快照:鏈路 (如 TRC-20)
statusvarchar(20) default 'pending'pending / approved / rejected / completed
rejectReasonvarchar(255) nullable拒絕原因
reviewedByvarchar(50) nullable審核人帳號
reviewedAtdatetime nullable審核時間
completedAtdatetime nullable完成時間
createdAt / updatedAtdatetime

INDEX(userId, createdAt)

auth-user 相關欄位:

  • frozenBalance decimal(18,6) default 0 — 凍結中金額(提領審核中)
  • withdrawalVerifyCode varchar(6) nullable — 提領用郵箱驗證碼

提領狀態機

pending ──┬── approve ──→ approved ──→ complete ──→ completed
          │                                         (frozenBalance 扣除,金額離開系統)
          └── reject  ──→ rejected
                          (frozenBalance 退回 balance)

餘額帳務變化

事件balancefrozenBalance
提交提領-amount+amount
審核拒絕 (reject)+amount-amount
確認出款 (complete)不變-amount

所有金額操作使用 Math.floor(value × 1e6) / 1e6 截斷至 6 位小數

流程一:發送驗證碼 — POST /withdrawal/send-code

1. 檢查 user.email 不為 null → 否 → 2001 (信箱未驗證)
2. 檢查 user.mobile 不為 null → 否 → 2002 (手機未驗證)
3. 產生 6 位驗證碼(排除全同數字如 111111、連續遞增/遞減如 123456/654321)
4. 透過 Resend 發送郵件到 user.email
5. 儲存至 auth_user.withdrawalVerifyCode

驗證碼產生規則:

  • 隨機產生 6 位數字 (000000~999999)
  • 排除黑名單: 000000
  • 排除全同: 111111, 222222... 等
  • 排除連續遞增/遞減: 123456, 654321
  • 最多嘗試 30 次,fallback 僅排除黑名單(保證終止)

流程二:提交提領 — POST /withdrawal/request

DTO: { amount: number, cryptoAddressId: number, verifyCode: string }

驗證管線(依序執行):

Step 1 — 驗證碼檢查
  ├ user.withdrawalVerifyCode 為 null → 2001 (驗證碼未發送)
  └ dto.verifyCode !== withdrawalVerifyCode → 2002 (驗證碼錯誤)

Step 2 — 綁定檢查
  ├ user.email 為 null → 2003 (信箱未驗證)
  └ user.mobile 為 null → 2004 (手機未驗證)

Step 3 — 錢包檢查
  SELECT * FROM crypto_address
  WHERE id = cryptoAddressId AND userId = userId AND status = 1
  → 無結果 → 2005 (錢包不存在或未審核)

Step 4 — 金額檢查
  dto.amount <= 0 → 2006 (金額需大於 0)

Step 5 — 優惠打碼量檢查
  SELECT COUNT(*) FROM promo_claim
  WHERE userId AND turnoverCompleted = 0 AND requiredTurnover > 0
  → count > 0 → 2008 (優惠打碼量未完成)

Step 6 — 存款打碼量檢查
  totalEffectiveBet = user.totalEffectiveBet
  totalDeposits = SUM(deposit_order.usdAmount) WHERE userId AND status='paid'
  requiredTurnover = totalDeposits × DEPOSIT_TURNOVER_MULTIPLIER (=1)
  → totalEffectiveBet < requiredTurnover → 2009 (存款打碼量不足)

Step 7 — 原子凍結餘額
  rounded = Math.floor(dto.amount × 1e6) / 1e6
  UPDATE auth_user
  SET balance = balance - rounded, frozenBalance = frozenBalance + rounded
  WHERE id = userId AND balance >= rounded
  → affected = 0 → 2007 (餘額不足)

Step 8 — 建立提領單
  INSERT withdrawal-order {
    userId, amount: rounded.toFixed(6),
    cryptoAddressId: wallet.id,
    address: wallet.address,   // 快照
    network: wallet.network,   // 快照
    status: 'pending'
  }

Step 9 — 清除驗證碼
  UPDATE auth_user SET withdrawalVerifyCode = NULL

流程三:Admin 審核 — POST /withdrawal/admin/:id/review

DTO: { action: 'approve' | 'reject', rejectReason?: string }

前置: 查單 → 不存在 → 2001;status !== 'pending' → 2002

approve:
  UPDATE withdrawal-order SET status='approved', reviewedBy, reviewedAt=NOW()

reject:
  rounded = Math.floor(order.amount × 1e6) / 1e6
  UPDATE auth_user SET frozenBalance = frozenBalance - rounded, balance = balance + rounded  // 解凍退回
  UPDATE withdrawal-order SET status='rejected', rejectReason, reviewedBy, reviewedAt=NOW()

流程四:Admin 確認出款 — POST /withdrawal/admin/:id/complete

前置: 查單 → 不存在 → 2001;status !== 'approved' → 2002

rounded = Math.floor(order.amount × 1e6) / 1e6
UPDATE auth_user SET frozenBalance = frozenBalance - rounded  // 金額已離開系統
UPDATE withdrawal-order SET status='completed', completedAt=NOW()

打碼量檢查規則

提領前系統自動檢查兩項打碼量條件,任一未通過即拒絕提領

1. 優惠打碼量(Promo Turnover)

條件: 所有已領取優惠的打碼量需全部完成 (turnoverCompleted = 1)
檢查: COUNT(promo_claim) WHERE userId AND turnoverCompleted=0 AND requiredTurnover>0
通過: count === 0

打碼量累計由 PromoService.updatePromoTurnover() 在每次投注結算時同步更新。

2. 存款打碼量(Deposit Turnover)

公式: totalEffectiveBet >= totalDeposits × DEPOSIT_TURNOVER_MULTIPLIER
常數: DEPOSIT_TURNOVER_MULTIPLIER = 1  (1 倍打碼)

totalEffectiveBet = auth_user.totalEffectiveBet (全歷史累計有效投注)
totalDeposits = SUM(deposit_order.usdAmount WHERE status='paid')

有效投注權重:

遊戲類型TURNOVER_WEIGHT說明
sports1.0100% 計入
slot1.0100% 計入
live1.0100% 計入
lottery1.0100% 計入
chess1.0100% 計入
esports1.0100% 計入
crypto0.550% 計入
fish0.550% 計入

betEffective = betAmount × TURNOVER_WEIGHT[gameType],存於 bet_order.betEffective

打碼量狀態查詢 — GET /withdrawal/turnover-status

供前端查詢當前用戶的打碼量完成進度,依據 canWithdraw 控制提領按鈕狀態。

回傳結構:

json
{
  "canWithdraw": false,
  "deposit": {
    "totalDeposits": "1000.000000",
    "multiplier": 1,
    "requiredTurnover": "1000.000000",
    "completedTurnover": "750.000000",
    "remaining": "250.000000",
    "completed": false
  },
  "promo": {
    "pendingCount": 1,
    "completed": false,
    "items": [
      {
        "promoId": 1,
        "promoTitle": "新手首存禮",
        "rewardAmount": "10.000000",
        "requiredTurnover": "50.000000",
        "completedTurnover": "30.000000",
        "remaining": "20.000000"
      }
    ]
  }
}
  • canWithdraw = deposit.completed && promo.completed
  • deposit.completedTurnover 封頂於 requiredTurnover(不超過目標值)
  • promo.items 僅列出未完成的優惠打碼量(turnoverCompleted = 0),JOIN promo 表取多語系標題

端點

MethodPathAuth說明
POST/withdrawal/send-codeJWT發送提領驗證碼到用戶信箱
POST/withdrawal/requestJWT提交提領申請(含驗證碼、金額、錢包 ID)
GET/withdrawal/listJWT用戶提領紀錄(?page, ?pageSize, ?status, ?startDate, ?endDate)
GET/withdrawal/turnover-statusJWT查詢打碼量狀態
GET/withdrawal/admin/listJWT[Admin] 提領列表(?page, ?pageSize, ?status, ?startDate, ?endDate)
POST/withdrawal/admin/:id/reviewJWT[Admin] 審核提領 (approve/reject)
POST/withdrawal/admin/:id/completeJWT[Admin] 確認出款完成

8.13 即時體育賽事 (LiveSports)

端點: 1 個

資料來源: API-Football (api-sports.io),Free plan 每日 100 次請求

快取策略:

  • Cron 每 30 分鐘抓取一次(0 */30 * * * *
  • Redis 快取 TTL 35 分鐘
  • 每次抓取:2 requests(live fixtures + today's upcoming)+ 最多 5 requests(odds)
  • Quota 感知:當 x-ratelimit-requests-remaining < 10 時跳過 odds 抓取

回傳格式:

每個 BannerItem 包含:fixtureId, kickoffAt, status(short/long/elapsed), league(id/name/country/logo/round), home(id/name/logo/score), away(id/name/logo/score), odds(home/draw/away/extraCount) 或 null

前端顯示邏輯:

  • 進行中的賽事(status.short ∈ [1H, HT, 2H, ET, P])顯示「現場」Badge
  • 排序:進行中優先 → 按開賽時間
  • 最多回傳 20 場賽事

端點:

MethodPathAuth說明
GET/live-sports取得即時體育賽事 Banner(公開)

8.14 任務系統 (Mission)

端點: 3 個

功能端點Auth
取得任務列表GET /missionOptionalJWT
取得領取紀錄GET /mission/claimsJWT
領取任務獎勵POST /mission/:id/claimJWT

任務分類:

categoryperiodTypetier 1-5說明
depositdaily / weekly / monthly5 階存款達門檻即可領取
betdaily / weekly / monthly5 階投注有效投注額達門檻即可領取

規格要點:

  • 每個分類 × 週期類型共 30 個任務(2 類 × 3 週期 × 5 階)
  • 進度在存款確認 (updateDepositProgress) 和投注結算 (updateBetProgress) 時自動累加
  • 每期(日/週/月)自動重置(透過 periodKey 區分,如 2026-02-242026-W092026-02
  • 同一任務同一期只能領取一次(UNIQUE constraint on missionId + userId + periodKey
  • 領取時檢查:!isClaimed && currentProgress >= threshold && meetsVip
  • vipRequired > 0 時需 VIP 等級達標才可領取
  • 領取獎勵直接加入用戶 USD 餘額
  • 領取時依 turnoverMultiplier 計算打碼量(requiredTurnover = rewardAmount × turnoverMultiplier

前端顯示邏輯:

  • 未登入時:currentProgress = nullisClaimed = falseisClaimable = false
  • isClaimable = true 時顯示「領取」按鈕
  • isClaimed = true 時顯示「已領取」
  • meetsVip = false 時顯示 VIP 等級不足提示

8.15 吉祥物頭像系統 (Avatar)

端點: 3 個(Auth 2 + SiteConfig Admin 1)

功能端點Auth
取得吉祥物列表GET /auth/mascots-
切換頭像PATCH /auth/avatarJWT
[Admin] 更新吉祥物列表PATCH /site-config/admin/:siteConfigId/mascotsJWT

10 組預設吉祥物:

IDLabel主題
c9-dragon翡翠龍翡翠綠漸層
c9-phoenix鳳凰火紅漸層
c9-angel天使天藍漸層
c9-wizard巫師紫色漸層
c9-knight騎士銀灰漸層
c9-mermaid美人魚海藍漸層
c9-tiger白虎金橘漸層
c9-fox狐狸橘紅漸層
c9-owl貓頭鷹深藍漸層
c9-wolf灰狼暗灰漸層

規格要點:

  • 頭像為 512×512 PNG,SVG 生成後經 sharp 轉檔
  • 存放路徑:R2 avatars/mascots/{mascotId}.png
  • GET /auth/mascots 從 SiteConfig 讀取當前站點的 mascots 列表
  • PATCH /auth/avatarmascotId 找到對應 URL,更新 auth-user.avatar 欄位
  • Admin 可透過 PATCH /site-config/admin/:siteConfigId/mascots 全量替換吉祥物列表
  • 生成腳本:npx ts-node scripts/generate-mascot-avatars.ts

8.16 後台管理系統 (Admin)

端點: 13 個

功能端點Auth
管理員登入POST /admin/login-
取得個人資料GET /admin/profileAdminJWT
管理員列表GET /admin/listAdminJWT
取得單一管理員GET /admin/:idAdminJWT
建立管理員POST /admin/createAdminJWT
更新管理員PATCH /admin/:idAdminJWT
刪除管理員DELETE /admin/:idAdminJWT
群組列表GET /admin/groups/listAdminJWT
取得單一群組GET /admin/groups/:idAdminJWT
建立群組POST /admin/groups/createAdminJWT
更新群組PATCH /admin/groups/:idAdminJWT
刪除群組DELETE /admin/groups/:idAdminJWT
操作紀錄列表GET /admin/logs/listAdminJWT

架構設計

  • 獨立認證系統:管理員帳號存儲於 admin-user 表,與前台 auth-user 完全獨立
  • 獨立 JWT 策略:使用 admin-jwt Passport 策略,JWT payload 包含 role: 'admin' 標識
  • AdminJwtAuthGuard:專屬 Guard,確保只有管理員 token 可存取後台 API
  • 操作紀錄全記錄:所有增刪改操作自動記錄至 admin-operation-log,含操作者 IP、User-Agent、變更詳情

資料表結構

admin-user (管理員帳號)

欄位型別說明
idint PK auto
accountvarchar(50) unique管理員帳號
passwordvarchar(255)密碼 (bcrypt)
namevarchar(50)管理員名稱
groupIdint nullable FK所屬群組 → admin-group.id
statustinyint(1) default 1啟用狀態 (1=啟用, 0=停用)
lastLoginIpvarchar(45) nullable最後登入 IP
lastLoginAtdatetime nullable最後登入時間
tokenVersionint default 0Token 版本 (變更密碼時遞增)
createdAtdatetime建立時間
updatedAtdatetime更新時間

admin-group (管理員群組)

欄位型別說明
idint PK auto
namevarchar(50)群組名稱
permissionsjson nullable權限列表 (e.g. ["admin:read", "admin:write"])
descriptionvarchar(200) nullable群組描述
statustinyint(1) default 1啟用狀態
createdAtdatetime建立時間
updatedAtdatetime更新時間

admin-operation-log (管理員操作紀錄)

欄位型別說明
idint PK auto
adminIdint FK操作者 → admin-user.id
modulevarchar(50)操作模組 (admin / admin-group / deposit / ...)
actionvarchar(50)操作動作 (login / create / update / delete)
targetIdint nullable被操作對象 ID
ipvarchar(45)操作者 IP (IPv4/IPv6)
userAgentvarchar(500) nullable瀏覽器 User-Agent
methodvarchar(20)HTTP Method
pathvarchar(255)請求路徑
detailjson nullable操作詳情 / 變更內容
summaryvarchar(500) nullable操作摘要
createdAtdatetime操作時間

權限系統 (RBAC)

權限使用 module:action 格式,存儲於群組的 permissions JSON 欄位:

權限 Key說明
admin:read查看管理員列表
admin:write新增/編輯/刪除管理員
admin-group:read查看群組列表
admin-group:write新增/編輯/刪除群組
admin-log:read查看操作紀錄
deposit:read查看存款列表
deposit:write存款操作
withdrawal:read查看提領列表
withdrawal:write審核/出款操作
user:read查看用戶列表
user:write編輯用戶
promo:read查看活動列表
promo:write新增/編輯活動
vip:read查看 VIP 設定
vip:write編輯 VIP 設定
game:read查看遊戲設定
game:write編輯遊戲設定
affiliate:read查看代理資料
affiliate:write代理操作
inbox:read查看站內信
inbox:write發送/刪除站內信
site-config:read查看站點設定
site-config:write編輯站點設定
report:read查看報表

9. 系統自動排程

排程時間服務說明
每日反水結算00:05VipService結算昨日各用戶各遊戲類型的有效投注反水,發放到餘額
月度保級檢查每月 1 號 01:00VipService檢查 VIP 2+ 用戶上月投注是否達保級門檻
代理佣金週結每週一 03:00AffiliateSettlementService結算上週各代理佣金,含風控檢測
代理佣金日結算每日 03:30AffiliateSettlementService.handleDailySettlementCron代理佣金日結算
即時賽事快取更新每 30 分鐘LiveSportsService抓取 API-Football 即時/今日賽事 + 賠率,快取至 Redis

手動觸發端點(測試/補結算用):

  • POST /vip/settlement/daily-rebate
  • POST /vip/settlement/monthly-relegation
  • POST /affiliate/admin/trigger-settlement

投注後同步觸發(非排程,即時):

投注結算完成
  ├─ VipService.recalculateUserVip() → 自動升級
  ├─ PromoService.updatePromoTurnover() → 累計打碼量
  └─ MissionService.updateBetProgress() → 累計任務投注進度

存款確認完成
  └─ MissionService.updateDepositProgress() → 累計任務存款進度

10. 錯誤碼完整對照表

所有錯誤碼由 GET /common/enumsERROR_CODES 動態回傳,以下為完整參考。

Auth

API PathCodezh-TW
/api/auth/register2001帳號已存在
/api/auth/register2002推廣碼不存在
/api/auth/login2001帳號或密碼錯誤
/api/auth/send-verify-email2001此信箱已被其他帳號使用
/api/auth/check-verify-email2001查無驗證資訊
/api/auth/check-verify-email2002驗證碼錯誤
/api/auth/enable-google-auth2001查無驗證資訊
/api/auth/enable-google-auth20026 位數密碼輸入錯誤
/api/auth/edit-password2001當前密碼錯誤
/api/auth/edit-password2002舊密碼與新密碼不符合
/api/auth/locale2001不支援的語系
/api/auth/login-config2001尚未設置 GOOGLE_CLIENT_ID
/api/auth/login-google2001尚未設置 GOOGLE_CLIENT_ID
/api/auth/login-google2002GOOGLE_CLIENT_ID 驗證失敗
/api/auth/login-google2003Google Payload 加密失敗
/api/auth/login-google2004Google Payload 解析失敗
/api/auth/login-google2005本次操作時效已過期, 請重新登入
/api/auth/login-google2006Google Code 驗證錯誤
/api/auth/login-google2007Google Api 響應過程錯誤
/api/auth/login-google2008Google Ticket 解析失敗
/api/auth/login-google2009Google Ticket Payload 解析失敗
/api/auth/login-telegram2001尚未設置 TELEGRAM_BOT_TOKEN
/api/auth/login-telegram2002Telegram 驗證失敗
/api/auth/login-telegram2003本次操作時效已過期, 請重新登入
/api/auth/avatar2001無效的頭像 ID

Mission

API PathCodezh-TW
/api/mission/:id/claim3001查無此任務
/api/mission/:id/claim3002此任務本期已領取過
/api/mission/:id/claim3003VIP 等級不足
/api/mission/:id/claim3004尚未達成任務目標

Wallet

API PathCodezh-TW
/api/wallet/bank-card/add2001請上傳身分證正面
/api/wallet/bank-card/add2002請上傳身分證反面
/api/wallet/bank-card/add2003請上傳銀行存摺封面
/api/wallet/bank-card/add2004此銀行卡已存在
/api/wallet/bank-card/:id2001查無此銀行卡
/api/wallet/credit-card/add2001此信用卡已綁定
/api/wallet/credit-card/:id2001查無此信用卡
/api/wallet/crypto-address/add2001此錢包地址已綁定
/api/wallet/crypto-address/:id2001查無此錢包地址

Vendor

API PathCodezh-TW
/api/vendor/channels2001用戶未分配金流群組
/api/vendor/channels2002金流群組不存在或已停用
/api/vendor/channels2003金流通道不存在或不屬於此群組
/api/vendor/wantong/add-atm2001查無可用的萬通金流通道
/api/vendor/wantong/add-atm2002建立 ATM 訂單失敗
/api/vendor/wantong/add-atm2003ATM 訂單請求異常
/api/vendor/wantong/add-card2001查無可用的萬通金流通道
/api/vendor/wantong/add-card2002建立信用卡訂單失敗
/api/vendor/wantong/add-card2003信用卡訂單請求異常

Deposit

API PathCodezh-TW
/api/deposit/exchange-rate2001銀行代碼格式錯誤
/api/deposit/exchange-rate2002匯率解析失敗
/api/deposit2001查無可用的 USDT 金流通道
/api/deposit2002該通道尚未設定繳費地址
/api/deposit2004不支援的支付方式
/api/deposit2005匯率轉換失敗,無法取得有效匯率
/api/deposit2006金流通道尚未建置完成

Game

API PathCodezh-TW
/api/game/launch5001查無此遊戲商
/api/game/launch5002遊戲商已停用
/api/game/launch5003遊戲商維護中
/api/game/launch5004遊戲商 API 回傳錯誤
/api/game/launch5005未設定遊戲商 API
/api/game/demo5001查無此遊戲商
/api/game/demo5006該遊戲商不支援試玩模式
/api/game/simulate5001查無此遊戲商
/api/game/simulate5010投注金額需在 0.01 ~ 10000 之間
/api/game/simulate5011餘額不足
/api/game/simulate2001用戶不存在
/api/game/list5001查無此遊戲商

Promo

API PathCodezh-TW
/api/promo/:id2001查無此活動
/api/promo/:id/claim2001此活動已領取過
/api/promo/:id/claim2002此活動領取名額已滿
/api/promo/:id/claim2003尚未滿足領取條件

VIP

API PathCodezh-TW
/api/vip/levels2001查無此 VIP 等級
/api/vip/levels2003此 VIP 等級已存在
/api/vip/levels/:id2001查無此 VIP 等級
/api/vip/levels/:id2003此 VIP 等級已存在
/api/vip/rebates2002查無此返水規則
/api/vip/rebates2004此等級的遊戲類型返水規則已存在
/api/vip/rebates/:id2002查無此返水規則
/api/vip/rebates/:id2004此等級的遊戲類型返水規則已存在
/api/vip/users/:userId/hold2001用戶不存在
/api/vip/users/:userId/hold2002VIP 等級未達 5,無法設定保級鎖定

BetRecord

API PathCodezh-TW
/api/bet-record/:orderId/details2001訂單不存在或不屬於該用戶

Affiliate

API PathCodezh-TW
/api/affiliate/track-click2001推廣碼不存在
/api/affiliate/dashboard2001您不是代理身份
/api/affiliate/downline2001您不是代理身份
/api/affiliate/settlements/:id2001結算紀錄不存在
/api/affiliate/withdrawals/request2001提款金額須大於零
/api/affiliate/withdrawals/request2002提款方式無效或尚未審核通過
/api/affiliate/withdrawals/request2003提款冷卻中,請稍後再試
/api/affiliate/withdrawals/request2004佣金餘額不足
/api/affiliate/withdrawals/request2005您不是代理身份
/api/affiliate/admin/create-agent2001用戶不存在
/api/affiliate/admin/create-agent2002推廣碼已被使用
/api/affiliate/admin/create-agent2003此用戶已是代理
/api/affiliate/admin/settlements/:id/review2001結算紀錄不存在
/api/affiliate/admin/settlements/:id/review2002該結算已審核完成
/api/affiliate/admin/withdrawals/:id/review2001提款紀錄不存在
/api/affiliate/admin/withdrawals/:id/review2002該提款已審核完成
/api/affiliate/admin/withdrawals/:id/complete2001提款紀錄不存在
/api/affiliate/admin/withdrawals/:id/complete2002狀態須為已審核通過
/api/affiliate/admin/bind2001會員不存在
/api/affiliate/admin/bind2002代理不存在

Inbox

API PathCodezh-TW
/api/inbox/:id/read2001查無此通知
/api/inbox/admin/:id2001查無此通知

SiteConfig

API PathCodezh-TW
/api/site-config2001查無此站點設定
/api/site-config/admin/:id2001查無此站點設定
/api/site-config/admin/:siteConfigId/themes2001查無此站點設定
/api/site-config/admin/themes/:id2002查無此主題

Withdrawal

API PathCodezh-TW
/api/withdrawal/send-code2001用戶信箱未驗證
/api/withdrawal/send-code2002用戶手機未驗證
/api/withdrawal/request2001驗證碼未發送
/api/withdrawal/request2002驗證碼錯誤
/api/withdrawal/request2003用戶信箱未驗證
/api/withdrawal/request2004用戶手機未驗證
/api/withdrawal/request2005該錢包不存在或未通過審核
/api/withdrawal/request2006提領金額需大於 0
/api/withdrawal/request2007餘額不足
/api/withdrawal/request2008優惠打碼量未完成
/api/withdrawal/request2009存款打碼量不足
/api/withdrawal/admin/:id/review2001查無此提領單
/api/withdrawal/admin/:id/review2002該提領單狀態不允許審核
/api/withdrawal/admin/:id/complete2001查無此提領單
/api/withdrawal/admin/:id/complete2002該提領單狀態不允許完成

Admin

API PathCodezh-TW
/api/admin/login2001帳號或密碼錯誤
/api/admin/login2002帳號已停用
/api/admin/profile2003管理員不存在
/api/admin/create2001帳號已存在
/api/admin/create2002群組不存在
/api/admin/:id (PATCH)2001管理員不存在
/api/admin/:id (DELETE)2001管理員不存在
/api/admin/:id (DELETE)2002不可刪除自己
/api/admin/groups/:id2001群組不存在
/api/admin/groups/:id (DELETE)2002群組下仍有管理員,無法刪除

11. 端點速查表

MethodPathAuth說明
GET/-Health check
POST/auth/register-註冊
POST/auth/login-登入
GET/auth/user-detailJWT取得用戶資料
GET/auth/country-codesJWT取得國碼列表
POST/auth/send-verify-emailJWT發送驗證信
POST/auth/check-verify-emailJWT驗證 Email
POST/auth/generate-google-authJWT產生 Google Auth
POST/auth/enable-google-authJWT啟用 Google Auth
POST/auth/edit-passwordJWT修改密碼
POST/auth/logoutJWT登出 (語系 + 金流群組重匹配 + token 失效)
PATCH/auth/localeJWT更新語系偏好
GET/auth/login-config-取得登入設定 (Google + Telegram)
POST/auth/login-google-Google 登入
POST/auth/login-telegram-Telegram 登入
GET/auth/mascots-取得吉祥物頭像列表
PATCH/auth/avatarJWT切換用戶頭像
GET/game/provider-取得遊戲商列表
POST/game/launchJWT啟動遊戲
POST/game/demo-試玩遊戲
POST/game/simulateJWT模擬遊戲一輪
GET/game/listJWT取得遊戲列表
POST/game/rsg/GetBalanceS2S[RSG] 查詢餘額
POST/game/rsg/BetS2S[RSG] 下注扣款
POST/game/rsg/BetResultS2S[RSG] 結算派彩
POST/game/rsg/CancelBetS2S[RSG] 取消注單
POST/game/rsg/JackpotResultS2S[RSG] Jackpot 派彩
POST/game/betsolutions/authS2S[BetSolutions] 驗證 token
POST/game/betsolutions/getBalanceS2S[BetSolutions] 查詢餘額
POST/game/betsolutions/betS2S[BetSolutions] 下注扣款
POST/game/betsolutions/winS2S[BetSolutions] 派彩
POST/game/betsolutions/cancelBetS2S[BetSolutions] 取消注單
POST/game/betsolutions/getPlayerInfoS2S[BetSolutions] 取得玩家資訊
GET/common/enums-取得共用列舉 + 錯誤碼
POST/wallet/bank-card/addJWT新增銀行卡
GET/wallet/bank-card/listJWT取得銀行卡列表
DELETE/wallet/bank-card/:idJWT刪除銀行卡
POST/wallet/credit-card/addJWT新增信用卡
GET/wallet/credit-card/listJWT取得信用卡列表
DELETE/wallet/credit-card/:idJWT刪除信用卡
POST/wallet/crypto-address/addJWT新增加密錢包
GET/wallet/crypto-address/listJWT取得加密錢包列表
DELETE/wallet/crypto-address/:idJWT刪除加密錢包
GET/vendor/channelsJWT取得用戶金流通道列表
POST/vendor/wantong/add-atmJWT萬通 ATM 建單
POST/vendor/wantong/add-cardJWT萬通信用卡建單
POST/vendor/wantong/callback/addS2S萬通建單回調
POST/vendor/wantong/callback/payS2S萬通銷案回調
POST/vendor/usdt/callback/payS2SUSDT 付款確認回調
GET/deposit/exchange-rate-取得匯率
GET/deposit/channelsJWT取得金流通道 (含匯率)
GET/deposit/ordersJWT取得存款訂單列表
POST/depositJWT建立存款訂單
GET/promo-取得活動列表
GET/promo/claimsJWT取得領取紀錄(?tab, ?startDate, ?endDate)
GET/promo/:id-取得活動詳情
POST/promoJWT建立活動
PATCH/promo/:idJWT更新活動
DELETE/promo/:idJWT刪除活動
POST/promo/:id/claimJWT領取活動獎勵
GET/vip/levels-取得 VIP 等級列表
GET/vip/rebates-取得返水規則
GET/vip/statusJWT取得用戶 VIP 狀態
POST/vip/levelsJWT建立 VIP 等級
PATCH/vip/levels/:idJWT更新 VIP 等級
DELETE/vip/levels/:idJWT刪除 VIP 等級
POST/vip/rebatesJWT建立返水規則
PATCH/vip/rebates/:idJWT更新返水規則
DELETE/vip/rebates/:idJWT刪除返水規則
POST/vip/settlement/daily-rebateJWT手動觸發每日反水結算
POST/vip/settlement/monthly-relegationJWT手動觸發月度保級檢查
PATCH/vip/users/:userId/holdJWT設定 VIP 保級鎖定
GET/ranking-取得排行榜
GET/bet-recordJWT投注紀錄列表
GET/bet-record/:orderId/detailsJWT訂單小注單明細
POST/affiliate/track-click-記錄推廣連結點擊
GET/affiliate/dashboardJWT代理儀表板
GET/affiliate/promo-linkJWT取得推廣連結
GET/affiliate/downlineJWT下線列表
GET/affiliate/click-statsJWT點擊統計
GET/affiliate/commissionsJWT佣金明細
GET/affiliate/settlementsJWT結算紀錄
GET/affiliate/settlements/:idJWT結算詳情
GET/affiliate/balanceJWT佣金餘額
GET/affiliate/withdrawalsJWT提款紀錄
POST/affiliate/withdrawals/requestJWT發起提款
POST/affiliate/admin/create-agentJWT[Admin] 建立代理
GET/affiliate/admin/settlementsJWT[Admin] 全部結算列表
POST/affiliate/admin/settlements/:id/reviewJWT[Admin] 審核結算
GET/affiliate/admin/settlements/:id/risk-logsJWT[Admin] 結算風控紀錄
GET/affiliate/admin/withdrawalsJWT[Admin] 全部提款列表
POST/affiliate/admin/withdrawals/:id/reviewJWT[Admin] 審核提款
POST/affiliate/admin/withdrawals/:id/completeJWT[Admin] 確認出款完成
POST/affiliate/admin/bindJWT[Admin] 人工綁定/解綁/轉移
GET/affiliate/admin/bind-logsJWT[Admin] 綁定審計紀錄
POST/affiliate/admin/trigger-settlementJWT[Admin] 手動觸發週結算
GET/inboxJWT取得站內信列表
GET/inbox/unread-countJWT取得未讀通知數量
POST/inbox/:id/readJWT標記單則通知為已讀
POST/inbox/read-allJWT全部標記為已讀
POST/inbox/admin/sendJWT[Admin] 發送通知
GET/inbox/admin/listJWT[Admin] 通知列表
DELETE/inbox/admin/:idJWT[Admin] 刪除通知
GET/site-config-取得當前站點設定 (含主題)
GET/site-config/admin/listJWT[Admin] 取得所有站點設定 (含主題)
PATCH/site-config/admin/:idJWT[Admin] 更新站點設定
GET/site-config/admin/:siteConfigId/themesJWT[Admin] 主題列表
POST/site-config/admin/:siteConfigId/themesJWT[Admin] 新增主題
PATCH/site-config/admin/themes/:idJWT[Admin] 更新主題
DELETE/site-config/admin/themes/:idJWT[Admin] 刪除主題
PATCH/site-config/admin/:siteConfigId/mascotsJWT[Admin] 更新吉祥物列表
POST/withdrawal/send-codeJWT發送提領驗證碼
POST/withdrawal/requestJWT提交提領申請
GET/withdrawal/listJWT用戶提領紀錄
GET/withdrawal/turnover-statusJWT查詢打碼量狀態
GET/withdrawal/admin/listJWT[Admin] 提領列表
POST/withdrawal/admin/:id/reviewJWT[Admin] 審核提領
POST/withdrawal/admin/:id/completeJWT[Admin] 確認出款完成
GET/live-sports-取得即時體育賽事 Banner
GET/missionOptionalJWT取得任務列表(含進度)
GET/mission/claimsJWT取得任務領取紀錄
POST/mission/:id/claimJWT領取任務獎勵
POST/admin/login-管理員登入
GET/admin/profileAdminJWT取得管理員個人資料
GET/admin/listAdminJWT管理員列表
GET/admin/:idAdminJWT取得單一管理員
POST/admin/createAdminJWT建立管理員
PATCH/admin/:idAdminJWT更新管理員
DELETE/admin/:idAdminJWT刪除管理員
GET/admin/groups/listAdminJWT管理員群組列表
GET/admin/groups/:idAdminJWT取得單一群組
POST/admin/groups/createAdminJWT建立群組
PATCH/admin/groups/:idAdminJWT更新群組
DELETE/admin/groups/:idAdminJWT刪除群組
GET/admin/logs/listAdminJWT管理員操作紀錄列表

JWT = 需要 Authorization: Bearer <token> / AdminJWT = 需要管理員 JWT Token / OptionalJWT = 有 token 時回傳個人化資料 / S2S = Server-to-Server 回調 (不需 JWT)