Skip to content

C9 Platform — RD 技術規格書

目標讀者:前端工程師、後端工程師、全端工程師 文件版本:2026-03-02 涵蓋範圍:c9-ec(前台)、c9-ims(後台)、c9-be(後端)三個子專案的完整技術規格


目錄


第 1 章:技術架構總覽

1.1 三專案技術棧

C9 Entertainment City 平台採用前後端分離的三專案架構,每個專案獨立開發、獨立部署、獨立版本控制。以下為三個專案的完整技術棧比較:

項目c9-ec(前台)c9-ims(後台)c9-be(後端)
用途面向用戶的遊戲平台內部管理儀表板核心業務 API
框架Nuxt 4.2 (Vue 3.5)Next.js 16.1.6 (React 19.2.3)NestJS v11
語言TypeScript 5.6TypeScript 5 (strict)TypeScript 5.7 (strict)
UI 元件庫Nuxt UI v4shadcn/ui new-york (Radix UI)-
CSSTailwind CSS v4Tailwind CSS v4-
狀態管理Pinia v3Zustand v5 + TanStack Query v5-
i18n@nuxtjs/i18n 10.2.1next-intl v4.8.3nestjs-i18n v10.6
驗證Zod v4React Hook Form + Zod v4class-validator + DTOs
認證JWT cookie (7 天)NextAuth 5 beta (JWT)JWT + Passport
HTTP 客戶端原生 fetch (useHttp)Axios 1.13.5 (apiClient)Axios (外部呼叫)
測試Vitest + PlaywrightTypeScript checkJest
套件管理yarnpnpmyarn
開發 Port301030118080
路由檔案路由 (20 頁)App Router (68 頁)Controller (25 個)
元件數量77 個 Vue 元件41 個 React 元件-
API Hook12 個 composables7 個 hooks-
頁面數量20 頁68 頁205+ 端點

前台 (c9-ec) 關鍵依賴

nuxt               ^4.2.2
vue                ^3.5.26
@nuxt/ui           ^4.4.0
@nuxtjs/i18n       10.2.1
pinia              ^3.0.4
@pinia/nuxt        ^0.11.3
zod                ^4.3.5
@fingerprintjs/fingerprintjs  ^5.0.1
moment-timezone    ^0.6.0
qrcode.vue         ^3.8.0
@nuxt/content      ^3.11.0
@nuxt/image         2.0.0
@nuxt/scripts       0.13.2
@nuxt/test-utils    3.23.0
typescript          ^5.6.3

後台 (c9-ims) 關鍵依賴

next               16.1.6
react / react-dom  19.2.3
next-auth          5.0.0-beta.30
next-intl          ^4.8.3
@tanstack/react-query  ^5.90.21
@tanstack/react-table  ^8.21.3
zustand            ^5.0.11
axios              ^1.13.5
@tiptap/react      ^3.20.0
recharts           ^3.7.0
zod                ^4.3.6
tailwindcss        v4
react-hook-form    ^7.71.2
sonner             ^2.0.7
date-fns           ^4.1.0
radix-ui           ^1.4.3

後端 (c9-be) 關鍵依賴

@nestjs/common, core      ^11.0.1
@nestjs/jwt               ^11.0.2
@nestjs/passport          ^11.0.5
@nestjs/swagger           ^11.2.6
@nestjs/typeorm           ^11.0.0
@nestjs/schedule          ^6.1.1
@nestjs/cache-manager     ^3.1.0
typeorm                    ^0.3.28
mysql2                     ^3.16.0
nestjs-i18n                ^10.6.0
speakeasy                  ^2.0.0
sharp                      ^0.34.5
resend                     ^6.8.0
twilio                     ^5.12.2
typescript                 ^5.7.3
bcryptjs                   ^3.0.3
axios                      ^1.13.5
@aws-sdk/client-s3         ^3.995.0
google-auth-library        ^10.5.0
qrcode                     ^1.5.4
moment-timezone            ^0.6.0
request-ip                 ^3.3.0
libphonenumber-js          ^1.12.35

1.2 系統架構圖

以下為 C9 平台的完整系統架構圖,展示各個元件之間的通訊關係:

                                    +-----------------+
                                    |   CDN / R2      |
                                    | (Cloudflare R2) |
                                    | 靜態資源 / 圖片  |
                                    +--------+--------+
                                             |
                                             | HTTPS (圖片)
                                             |
+----------+       HTTP/HTTPS        +-------+--------+       HTTP/HTTPS       +-----------+
|          |  ----------------------> |                | <-------------------   |           |
|  用戶    |  Port 3010              |   c9-be        |      Port 3011        |  管理員    |
| (瀏覽器) |  locales header         |  (NestJS v11)  |   x-site-code header  | (瀏覽器)   |
|          |  site-name header       |   Port 8080    |   Authorization       |           |
|          |  Authorization Bearer   |                |                       |           |
+----+-----+                        +---+---+---+----+                       +-----+-----+
     |                                   |   |   |                                  |
     | HTTP                              |   |   |                                  | HTTP
     v                                   |   |   |                                  v
+----+-----+                             |   |   |                           +------+------+
|          |                             |   |   |                           |             |
|  c9-ec   |                             |   |   |                           |   c9-ims    |
| (Nuxt 4) |                             |   |   |                           | (Next.js 16)|
| 前台平台  |                             |   |   |                           |  後台管理    |
|          |                             |   |   |                           |             |
+----------+                             |   |   |                           +-------------+
                                         |   |   |
                          +--------------+   |   +----------------+
                          |                  |                    |
                          v                  v                    v
                   +------+------+   +------+------+    +--------+--------+
                   |             |   |             |    |                 |
                   |   MySQL     |   |   Redis     |    |  外部服務        |
                   |  (utf8mb4)  |   | (快取/排程)  |    |                 |
                   |  TZ +08:00  |   |             |    | - BetSolutions  |
                   |  49 張資料表 |   +-------------+    | - RSG           |
                   |             |                      | - 萬通金流       |
                   +-------------+                      | - USDT          |
                                                        | - API-Football  |
                                                        | - 台灣銀行匯率   |
                                                        | - Resend (Email)|
                                                        | - Twilio (SMS)  |
                                                        | - Google OAuth  |
                                                        | - Telegram      |
                                                        +-----------------+

架構要點

  1. 前後端分離:c9-ec 和 c9-ims 各自獨立部署,透過 HTTP API 與 c9-be 溝通
  2. 白牌架構:透過 site-name / x-site-code header 區分站點,同一套後端服務支援多個品牌
  3. 統一 API 前綴:所有後端 API 以 /api 為前綴,Swagger 文件在 /api/docs
  4. 圖片儲存:使用 Cloudflare R2 (S3-compatible),前端透過公開 URL 存取
  5. 快取策略:Redis 用於 JWT token version、權限快取、錯誤碼快取、賽事快取等

1.3 部署架構

Port 分配

服務開發 Port正式 Port說明
c9-ec3010動態Nuxt SSR 伺服器
c9-ims30118080Next.js 伺服器
c9-be80808080NestJS API 伺服器
MySQL33063306資料庫
Redis63796379快取

環境變數總覽

每個專案都有獨立的環境變數配置:

專案環境變數檔案主要變數數量
c9-ec無(使用 domainConfig 靜態配置)-
c9-ims.env.local5 個
c9-be.env.local30+ 個

c9-ims 環境變數

env
NEXT_PUBLIC_SITE_ID=a1                           # 站點 ID
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api  # 後端 API URL
NEXTAUTH_SECRET=your-secret-key                  # NextAuth JWT Secret
NEXTAUTH_URL=http://localhost:3011               # NextAuth URL
NEXT_PUBLIC_AUTH_URL=http://localhost:3011        # 公開認證 URL

c9-be 環境變數(完整模板)

env
# ==================== 資料庫 ====================
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=password
DB_DATABASE=c9

# ==================== Redis ====================
REDIS_URL=redis://localhost:6379

# ==================== JWT ====================
JWT_SECRET=your-jwt-secret
ADMIN_JWT_SECRET=your-admin-jwt-secret

# ==================== 站點 ====================
SITE_CODE=C9

# ==================== R2 儲存 ====================
R2_BUCKET_NAME=c9-storage
R2_ENDPOINT=https://xxxx.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=your-access-key
R2_SECRET_ACCESS_KEY=your-secret-key
R2_PUBLIC_URL=https://cdn.example.com

# ==================== 應用程式 ====================
PORT=8080
NODE_ENV=development
APP_NAME=C9
API_DOMAIN=http://localhost:8080
FRONTEND_URL=http://localhost:3000
TZ=Asia/Seoul

# ==================== Admin 預設帳號 ====================
ADMIN_DEFAULT_PASSWORD=root
ADMIN_DEFAULT_EMAIL=root
ADMIN_DEFAULT_NAME=Root

# ==================== OAuth (選填) ====================
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
TELEGRAM_BOT_USERNAME=your-telegram-bot-username

# ==================== Email / SMS (選填) ====================
RESEND_API_KEY=your-resend-api-key
RESEND_FROM=noreply@example.com
TWILIO_ACCOUNT_SID=your-account-sid
TWILIO_AUTH_TOKEN=your-auth-token
TWILIO_VERIFY_SERVICE_SID=your-verify-sid

# ==================== 遊戲商 (選填) ====================
# BetSolutions
BS_API_URL=https://api.betsolutions.com
BS_AUTH_URL=https://auth.betsolutions.com
BS_MERCHANT_ID=12345
BS_PRIVATE_KEY=your-private-key

# RSG
RSG_API_URL=https://api.rsg.com
RSG_CLIENT_ID=your-client-id
RSG_CLIENT_SECRET=your-client-secret
RSG_DES_KEY=your-des-key
RSG_DES_IV=your-des-iv
RSG_SYSTEM_CODE=your-system-code
RSG_WEB_ID=your-web-id

# ==================== 其他 (選填) ====================
LIVE_SPORTS_API_KEY=your-api-football-key

1.4 專案間通訊

c9-ec (前台) → c9-be (後端)

項目說明
傳輸協定HTTP/HTTPS
客戶端原生 fetch API,封裝在 useHttp composable
Base URLdomainConfig[hostname].baseUrl 動態解析
API 前綴/api
認證 HeaderAuthorization: Bearer <JWT token>
語系 Headerlocales: zh-TW (從 i18n_redirected cookie 讀取)
站點 Headersite-name: a1 (從 domainConfig[hostname].siteId 讀取)
Cookie 轉發SSR 時自動轉發 cookieauthorization headers

請求流程

元件 → useApi() [Facade]
  → use{Module}Api() [領域模組]
    → useHttp() / useHttpAsync() [HTTP 原語]
      → fetch() [原生 API]
        → c9-be (NestJS)

c9-ims (後台) → c9-be (後端)

項目說明
傳輸協定HTTP/HTTPS
客戶端Axios 實例 (apiClient)
Base URLdomainConfig[hostname].baseUrl + /api 動態解析
認證 HeaderAuthorization: Bearer <JWT token> (SessionSync 同步)
語系 Headerlocales: zh-TW (從 NEXT_LOCALE cookie 讀取)
站點 Header (白牌)site-name: a1 (從 domainConfig[hostname].siteId 讀取)
站點 Header (多站篩選)x-site-code: C9 (從 siteFilterStore.selectedSiteCode 自動注入)
401 處理清 token → 重取 session → retry → 失敗導向 /login?expired=1

請求流程

頁面元件
  → useApi() / use{Domain}Api() [領域 hooks]
    → httpRequest() [三層錯誤碼映射 + toast]
      → apiClient [Axios 攔截器]
        ├── 動態 baseURL (域名解析 domainConfig)
        ├── site-name header (白牌路由)
        ├── locales header (多語系)
        ├── x-site-code header (多站篩選)
        ├── Authorization: Bearer JWT
        └── 401 Retry 機制

c9-be (後端) → 外部服務

外部服務通訊方式用途
BetSolutionsS2S HTTP 回調遊戲投注/派彩結算
RSGS2S HTTP 回調 + DES 加解密遊戲投注/派彩結算
萬通金流HTTP API + 回調ATM/信用卡入金
USDTHTTP API + 回調加密貨幣入金
API-FootballHTTP API (每 30 分鐘 Cron)即時賽事資料
台灣銀行tw-exchange 套件即時匯率查詢
ResendHTTP APIEmail 發送
TwilioHTTP APISMS 發送
Google OAuthHTTP API第三方登入
TelegramBot API第三方登入
Cloudflare R2S3 API (AWS SDK)檔案上傳/刪除/列表

統一回應格式

所有 c9-be API 回應遵循統一格式:

typescript
// 成功回應
{
  code: 200,
  message: "ok",
  result: T,           // 業務資料
  timestamp: number,   // Unix timestamp
  path: string         // API 路徑
}

// 業務錯誤(HTTP 200,但 code 非 200)
{
  code: 2001,          // 業務錯誤碼
  message: "帳號已存在", // 當前語系錯誤訊息
  result: null,
  timestamp: number,
  path: string
}

// 未授權
{
  code: 401,
  message: "Unauthorized"
}

錯誤碼查表機制

前端不硬寫任何錯誤文字,一律透過 ERROR_CODES 查表:

  1. 後端啟動時,CommonService.onModuleInit() 掃描 i18n/zh-TW/*.json,建構所有數字 key 的錯誤碼 map
  2. 前端呼叫 GET /common/enums,取得當前語系的 ERROR_CODES(以 API path 為索引)
  3. 前端收到非 200 回應時,以 ERROR_CODES[path][code] 查詢可讀錯誤訊息
  4. 支援 :id 動態路由匹配(如 /api/promo/:id
typescript
// 前端查表流程(useHttp / httpRequest 內部)
const mapped = errorCodes[data.path]?.[code]
            || errorCodes[data.path.replace(/\/[^/]+$/, '/:id')]?.[code];
if (mapped) data.message = mapped;

第 2 章:開發環境建置

2.1 前置需求

必要工具

工具最低版本建議版本安裝方式
Node.js20.0+20 LTSnvm install 20
yarn1.22+1.22+npm i -g yarn
pnpm8.0+9.xnpm i -g pnpm
MySQL8.0+8.0+brew install mysql
Redis7.0+7.xbrew install redis
gh (GitHub CLI)-latestbrew install gh
git-cz-latestnpm i -g git-cz

可選工具

工具用途
Docker資料庫容器化
tmux分屏開發 (yarn dev:split)
PlaywrightE2E 測試瀏覽器

2.2 安裝步驟

第一步:Clone 與安裝根目錄

bash
# Clone 主倉庫(含 git submodules)
git clone <repo-url> c9
cd c9

# 安裝根目錄依賴(concurrently 等)
yarn install

第二步:安裝各子專案依賴

bash
# 前台 (c9-ec) — 使用 yarn
cd c9-ec && yarn install && cd ..

# 後台 (c9-ims) — 使用 pnpm
cd c9-ims && pnpm install && cd ..

# 後端 (c9-be) — 使用 yarn
cd c9-be && yarn install && cd ..

第三步:設定環境變數

bash
# 後端 — 建立 .env.local
cp c9-be/.env.example c9-be/.env.local   # 若有範本
# 或手動建立,填入 DB、Redis、JWT 等設定(參見 1.3 節完整模板)

# 後台 — 建立 .env.local
cp c9-ims/.env.example c9-ims/.env.local # 若有範本
# 或手動建立,填入 NEXTAUTH_SECRET 等設定

第四步:建立資料庫

bash
# 登入 MySQL
mysql -u root -p

# 建立資料庫
CREATE DATABASE c9 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

第五步:啟動開發伺服器

bash
# 方式一:一鍵啟動所有服務
cd c9
yarn dev

# 方式二:個別啟動
yarn dev:ec   # 前台 → http://localhost:3010
yarn dev:ims  # 後台 → http://localhost:3011
yarn dev:be   # 後端 → http://localhost:8080/api

第六步:Seed 資料庫(選填)

bash
cd c9-be
npx ts-node scripts/seed-all.ts

2.3 環境變數詳解

c9-be 環境變數分類

必要變數(不設定無法啟動)

變數預設值說明
DB_HOST-MySQL 主機
DB_PORT-MySQL Port
DB_USER-MySQL 帳號
DB_PASSWORD-MySQL 密碼
DB_DATABASE-MySQL 資料庫名稱
JWT_SECRET'c9-secret'前台 JWT 簽名密鑰
SITE_CODE'C9'預設站點代碼

基礎建設變數(建議設定)

變數預設值說明
REDIS_URL-Redis 連線 URL,未設定則使用記憶體快取
PORT8080伺服器監聽 Port
NODE_ENV'development'環境模式,影響 TypeORM synchronize
TZ'Asia/Seoul'TimeService 時區
ADMIN_JWT_SECRETfallback JWT_SECRET後台管理員 JWT 密鑰

R2 儲存變數

變數說明
R2_BUCKET_NAMER2 Bucket 名稱
R2_ENDPOINTR2 API 端點
R2_ACCESS_KEY_IDR2 存取金鑰
R2_SECRET_ACCESS_KEYR2 密鑰
R2_PUBLIC_URLR2 公開存取 URL

Admin 預設帳號

變數預設值說明
ADMIN_DEFAULT_EMAIL'root'預設管理員 Email
ADMIN_DEFAULT_PASSWORD'root'預設管理員密碼
ADMIN_DEFAULT_NAME'Root'預設管理員名稱

AdminModule 啟動時(OnModuleInit)會檢查是否已有管理員,若無則自動建立預設管理員。

OAuth 變數(選填)

變數說明
GOOGLE_CLIENT_IDGoogle OAuth Client ID
GOOGLE_CLIENT_SECRETGoogle OAuth Secret
GOOGLE_REDIRECT_URIGoogle OAuth 回調 URL
TELEGRAM_BOT_TOKENTelegram Bot Token
TELEGRAM_BOT_USERNAMETelegram Bot Username

Email / SMS 變數(選填)

變數說明
RESEND_API_KEYResend Email API Key
RESEND_FROM寄件者 Email
TWILIO_ACCOUNT_SIDTwilio Account SID
TWILIO_AUTH_TOKENTwilio Auth Token
TWILIO_VERIFY_SERVICE_SIDTwilio Verify Service SID

遊戲商變數(選填)

變數說明
BS_API_URLBetSolutions API URL
BS_AUTH_URLBetSolutions Auth URL
BS_MERCHANT_IDBetSolutions 商戶 ID
BS_PRIVATE_KEYBetSolutions 私鑰
RSG_API_URLRSG API URL
RSG_CLIENT_IDRSG Client ID
RSG_CLIENT_SECRETRSG Client Secret
RSG_DES_KEYRSG DES 加密金鑰
RSG_DES_IVRSG DES IV
RSG_SYSTEM_CODERSG System Code
RSG_WEB_IDRSG Web ID

2.4 一鍵啟動指令

根目錄指令 (c9/)

指令說明底層行為
yarn dev同時啟動三個專案concurrently -n ec,ims,be 彩色標籤輸出
yarn dev:splittmux 分屏啟動bash scripts/dev-tmux.sh
yarn dev:ec只啟動前台cd c9-ec && yarn dev
yarn dev:ims只啟動後台cd c9-ims && yarn dev
yarn dev:be只啟動後端cd c9-be && yarn dev
yarn commit互動式 commit 三專案bash scripts/commit-all.sh
yarn pushcommit + push 三專案bash scripts/push-all.sh
yarn push:all只 push (不 commit)bash scripts/push-only-all.sh
yarn push:ec只 push 前台cd c9-ec && git add . && npx git-cz && git push
yarn push:ims只 push 後台cd c9-ims && git add . && npx git-cz && git push
yarn push:be只 push 後端cd c9-be && git add . && npx git-cz && git push
yarn check:imsTypeScript 型別檢查cd c9-ims && npx tsc --noEmit

yarn dev 輸出會以彩色標籤區分:

  • [ec] 藍色 — 前台
  • [ims] 綠色 — 後台
  • [be] 黃色 — 後端

各子專案獨立指令

c9-ec(前台)

指令說明
yarn dev開發模式 (http://localhost:3010)
yarn build正式建置
yarn preview預覽正式版
yarn generate靜態生成
yarn test全部測試 (Vitest)
yarn test:unit只跑單元測試
yarn test:nuxt只跑元件測試
yarn test:e2eE2E 測試 (Playwright)
yarn test:e2e:uiE2E 測試 UI 模式

c9-ims(後台)

指令說明
yarn dev / yarn dev:a1開發模式 (Turbopack, http://localhost:3011)
yarn build / yarn build:a1建置
yarn start / yarn start:a1啟動正式版 (port 8080)
yarn typecheckTypeScript 型別檢查
yarn lintESLint 檢查
yarn formatPrettier 格式化

c9-be(後端)

指令說明
yarn dev開發模式 --watch (http://localhost:8080/api)
yarn build正式建置
yarn start:prod正式啟動
yarn test單元測試 (Jest)
yarn test:e2eE2E 測試
yarn lintESLint 檢查
yarn formatPrettier 格式化

2.5 Seed 資料

後端提供 23+ 個 seed 腳本,可一鍵或個別執行:

一鍵 Seed

bash
cd c9-be
npx ts-node scripts/seed-all.ts

seed-all.ts 依序呼叫所有 seed 腳本,建立完整的測試資料集(5 個站點、30 個用戶、49 張資料表)。

個別 Seed 腳本

腳本說明
seed-site-config.ts站點設定(5 個站點 + 主題)
seed-vendor.ts金流群組/通道
seed-deposit.ts存款訂單
seed-deposit-order.ts存款訂單(獨立版本)
seed-vip.tsVIP 等級 / 反水規則
seed-bet-record.ts投注紀錄
seed-ranking.ts排行榜
seed-ranking-users.ts排行榜用戶
seed-inbox.ts站內信
seed-mission.ts任務系統
seed-withdrawal.ts提領訂單
seed-learn-more.ts了解更多 FAQ
seed-layout-defaults.ts前台佈局預設配置
seed-agent-promo.ts代理活動
seed-merchants.ts商戶資料
assign-all-channels.ts分配所有金流通道
generate-promo-images.ts活動橫幅圖片生成 + R2 上傳
generate-mascot-avatars.ts吉祥物頭像生成 + R2 上傳

執行方式

bash
cd c9-be
npx ts-node scripts/{script-name}.ts

清除資料

腳本說明
clear-deposit.ts清除存款資料
cleanup-promo.sql清理活動資料 (SQL)

第 3 章:前台 (c9-ec) 技術規格

第三章:前台 c9-ec 技術規格(擴展版)

本章完整記錄 c9-ec 前台專案的所有元件、Composable、型別定義、插件、主題系統、i18n 與域名設定。 框架:Nuxt 4.2 (Vue 3.5 + TypeScript),Port:3010


3.1 專案總覽與技術棧

3.1.1 核心框架版本

項目版本說明
Nuxt^4.2.2全端 Vue 框架,SSR/CSR 混合模式
Vue^3.5.26Composition API,<script setup> 語法
TypeScript^5.6.3嚴格模式型別檢查
@nuxt/ui^4.4.0基於 Tailwind CSS v4 的 UI 元件庫
@nuxtjs/i18n10.2.1多語系(5 語系,no_prefix 策略)
Pinia^3.0.4狀態管理(setup store 語法)
Zod^4.3.5表單驗證
FingerprintJS^5.0.1裝置指紋(開源版)
moment-timezone^0.6.0日期時間格式化

3.1.2 Nuxt 模組(9 個)

ts
modules: [
  '@nuxt/eslint',       // ESLint 整合
  '@nuxt/hints',        // 效能提示
  '@nuxt/image',        // 圖片最佳化(WebP、響應式)
  '@nuxt/scripts',      // 外部腳本管理
  '@nuxt/test-utils',   // 測試工具
  '@nuxt/ui',           // UI 元件庫(Tailwind CSS v4)
  '@nuxtjs/i18n',       // 國際化
  '@nuxt/content',      // 內容管理
  '@pinia/nuxt',        // Pinia 狀態管理整合
]

3.1.3 專案統計

類別數量說明
頁面20檔案路由
Vue 元件140+A1 (75) + A2 (65) + Common (2)
Composables4520 業務邏輯 + 12 API + 13 型別
型別定義檔13composables/types/ 下
Plugins42 通用 + 2 Client-only
Pinia Stores54 Feature + 1 Facade
工具函式7utils/ 下
主題預設12A1 (6 寶石色系) + A2 (6 金屬色系)
佈局2a1 (主要) + a2 (奢華)
中介層1auth.global.ts
i18n 語系5zh-TW, en-US, zh-CN, th-TH, vi-VN

3.1.4 App 初始化流程

app.vue onMounted():
  ┌─── 阻塞 SplashScreen ───────────────────────┐
  │ 1. Promise.all([                              │
  │      getEnumsCsr(),      → store.setEnums     │
  │      getLoginConfigCsr() → store.setLoginConfig│
  │    ])                                          │
  │ 2. if (isLogin) Promise.all([                  │
  │      getUserDetailCsr()  → store.setUserDetail │
  │      getCountryCodesCsr()→ store.setCountryCodes│
  │    ])                                          │
  │ 3. store.setIsReady(true) → SplashScreen 淡出  │
  └────────────────────────────────────────────────┘
  ┌─── 非阻塞(fire-and-forget)──────────────────┐
  │ 4. fetchGameProvider()   → 遊戲商列表          │
  │ 5. fetchSiteConfig()     → 站點主題/設定       │
  │ 6. checkTourStatus()     → 代理導覽檢查        │
  └────────────────────────────────────────────────┘

3.2 目錄結構

c9-ec/
├── app/
│   ├── app.vue                    # 根元件
│   ├── app.config.ts              # Nuxt UI 設定 (primary=emerald)
│   ├── router.options.ts          # 自定滾動行為
│   ├── assets/
│   │   ├── css/
│   │   │   ├── main.css           # Tailwind v4 + CSS 變數橋接
│   │   │   └── global.scss        # 全域 SCSS
│   │   ├── fonts/                 # 字型檔
│   │   └── game/childGame/        # 靜態遊戲圖片
│   ├── components/
│   │   ├── Common/                # 共用元件 (2)
│   │   │   ├── SplashScreen.vue
│   │   │   └── ConfirmDialog.vue
│   │   ├── A1/                    # 佈局 a1 元件 (75)
│   │   │   ├── Home/             # 首頁 (4)
│   │   │   ├── Layout/           # 佈局 (5)
│   │   │   ├── Game/             # 遊戲 (10)
│   │   │   ├── Modal/            # 彈窗 (16)
│   │   │   ├── User/             # 用戶 (40)
│   │   │   ├── Promo/            # 活動 (2)
│   │   │   ├── Alliance/         # 聯盟 (1)
│   │   │   ├── Mission/          # 任務 (1)
│   │   │   ├── Help/             # 幫助 (1)
│   │   │   └── PromoLinkCard.vue # 活動連結卡片
│   │   └── A2/                    # 佈局 a2 元件 (65)
│   │       ├── Home/             # 首頁
│   │       ├── Layout/           # 佈局(Navbar 取代 TitleBar+Sidebar)
│   │       ├── Game/             # 遊戲
│   │       ├── Modal/            # 彈窗
│   │       ├── User/             # 用戶
│   │       ├── Promo/            # 活動
│   │       ├── Alliance/         # 聯盟
│   │       ├── Mission/          # 任務
│   │       └── Help/             # 幫助
│   ├── composables/
│   │   ├── api/                   # API composables (12)
│   │   ├── types/                 # TypeScript 型別 (13)
│   │   └── *.ts                   # 業務邏輯 composables (20)
│   ├── config/
│   │   └── domainConfig/          # 域名設定
│   │       ├── index.ts           # DomainConfigEntry + resolveDomainConfig
│   │       ├── a1.ts              # a1 站點映射
│   │       └── a2.ts              # a2 站點映射(預留)
│   ├── middleware/
│   │   └── auth.global.ts         # 全域路由守衛
│   ├── layouts/
│   │   ├── a1.vue                 # Dashboard 佈局
│   │   └── a2.vue                 # 奢華精品佈局
│   ├── pages/                     # 20 頁檔案路由
│   ├── plugins/                   # 4 個插件
│   ├── stores/                    # Pinia stores (5)
│   └── utils/                     # 工具函式 (7)
├── i18n/locales/                  # 5 語系 JSON
├── test/                          # Vitest 測試
├── tests/                         # Playwright E2E
├── nuxt.config.ts
├── vitest.config.ts
└── playwright.config.ts

3.3 元件完整文件

3.3.1 元件命名規則

  • SFC 格式<script setup lang="ts">(僅 Composition API)
  • 檔案命名:PascalCase,依領域分組
  • 自動匯入:巢狀資料夾自動生成名稱,如 A1/Layout/Sidebar.vue = <A1LayoutSidebar />
  • 佈局切換:頁面透過 vueVersion() 動態載入 A1 或 A2 版本

3.3.2 Common/ 共用元件(2 個)

CommonSplashScreen

項目說明
檔案app/components/Common/SplashScreen.vue
用途App 啟動載入畫面,阻塞直到關鍵資料載入完成

Props:無

行為說明

  • 監聽 store.getIsReady,為 true 時播放淡出動畫後移除
  • 包含粒子特效動畫背景
  • 使用 R2 URL 顯示站點 Logo
  • SSR 安全:動畫邏輯僅在 client 端執行

使用範例

vue
<!-- app.vue -->
<A2LayoutSplashScreen v-if="layoutName === 'a2'" />
<CommonSplashScreen v-else />

CommonConfirmDialog

項目說明
檔案app/components/Common/ConfirmDialog.vue
用途通用確認對話框,搭配 useOverlay().create() 使用

Props

Prop型別預設值說明
titlestring''對話框標題
descriptionstring''對話框描述文字
confirmLabelstring'確認'確認按鈕文字
cancelLabelstring'取消'取消按鈕文字
confirmColor'error' | 'primary' | 'warning''primary'確認按鈕顏色
onSuccess() => void確認後的回呼函式

使用範例

ts
const overlay = useOverlay();
const modal = overlay.create(CommonConfirmDialog, {
  props: {
    title: t('deposit.verifyRequired'),
    description: t('deposit.verifyDesc'),
    confirmLabel: t('deposit.goSetting'),
    confirmColor: 'primary',
    onSuccess: () => router.push('/user/setting'),
  },
});
modal.open();

3.3.3 A1/ 佈局元件群組(75 個)

A1 為主要佈局模板,設計風格為深色科技遊戲風,使用寶石色系主題。

A1/Home/ 首頁元件(4 個)

A1HomeIndex
項目說明
檔案app/components/A1/Home/index.vue
用途首頁組合元件,整合 Banner、遊戲大廳、即時賽事、活動推薦、了解更多
子元件A1HomeBanner, A1GameLobby, A1HomeLiveSports, A1HomePromo

行為說明

  • 頁面入口元件,負責組合首頁各區塊
  • 遊戲大廳區塊包含最近遊玩(登入用戶)
  • 了解更多區塊從 useTheme().learnMoreItems 動態載入

A1HomeBanner
項目說明
檔案app/components/A1/Home/Banner.vue
用途首頁輪播 Banner,展示站點活動與公告

行為說明

  • 使用 UCarousel 元件實現自動輪播
  • 圖片來源為活動的 imgPc / imgMobile(依裝置切換)
  • 點擊導向活動詳情頁 /promo/[id]

A1HomeLiveSports
項目說明
檔案app/components/A1/Home/LiveSports.vue
用途首頁即時體育賽事區塊

行為說明

  • 呼叫 getLiveSports() API 取得即時賽事資料
  • 顯示比賽狀態、隊伍 Logo、比分、賠率
  • 支援 LiveSportsFixture 型別的完整資料結構
  • 每 30 分鐘自動更新(對應後端 Cron 排程)

A1HomePromo
項目說明
檔案app/components/A1/Home/Promo.vue
用途首頁活動推薦區塊

行為說明

  • 載入啟用中的活動清單(activeOnly: 1
  • 使用卡片式佈局展示活動縮圖和標題
  • 顯示活動標籤(從 utsPromo().getTagLabel() 取得多語系名稱)
  • 點擊導向活動詳情頁

A1/Layout/ 佈局元件(5 個)

A1LayoutTitleBar
項目說明
檔案app/components/A1/Layout/TitleBar.vue
用途頂部標題列(行動版顯示)

行為說明

  • 顯示站點 Logo(從 domainAssets.logoSmall 載入)
  • 包含漢堡選單按鈕(切換 Sidebar)
  • 行動版主要導航入口

A1LayoutSidebar
項目說明
檔案app/components/A1/Layout/Sidebar.vue
用途左側側邊欄導航,包含所有主要功能入口

行為說明

  • 使用 UDashboardSidebar 元件
  • 導航項目依 gameTypeConfigs 動態生成遊戲分類
  • 包含用戶資訊(餘額、VIP 等級)
  • 支援鍵盤快捷鍵(defineShortcuts()
  • 代理導覽 highlight 階段會高亮「聯盟計劃」項目
  • 包含語系切換、主題切換入口

A1LayoutFooter
項目說明
檔案app/components/A1/Layout/Footer.vue
用途頁尾元件

行為說明

  • 動態載入 useTheme().footerSections 設定
  • 每個 section 包含多語系 title 和 links 陣列
  • 顯示版權資訊
  • enabledsortOrder 過濾排序

A1LayoutBottomBar
項目說明
檔案app/components/A1/Layout/BottomBar.vue
用途行動版底部導航列

行為說明

  • 動態載入 useTheme().bottomBarItems 設定
  • bottomBarEnabled 控制顯示/隱藏
  • 每個 item 包含 icon、多語系 label、link
  • 當前路由高亮對應項目
  • 僅行動版顯示

A1LayoutLiveChat
項目說明
檔案app/components/A1/Layout/LiveChat.vue
用途LiveChat 即時客服嵌入腳本

行為說明

  • useTheme().customerServiceConfig 取得 LiveChat 設定
  • liveChatEnabledtrue 時注入 liveChatScript
  • 使用 useScriptTag 動態載入外部腳本
  • Client-only 渲染

A1/Game/ 遊戲元件(10 個)

A1GameIndex
項目說明
檔案app/components/A1/Game/index.vue
用途遊戲模組入口,路由至遊戲大廳或啟動頁

A1GameLobby
項目說明
檔案app/components/A1/Game/Lobby.vue
用途遊戲大廳主元件

行為說明

  • 整合遊戲類型切換(從 store.getGameTypeConfigs 動態取得)
  • 登入用戶顯示「最近遊玩」區塊(getRecentGamesCsr(10)
  • 代表遊戲模式:子遊戲類型(slot/chess/crypto/fish)每個遊戲商只顯示一款代表遊戲
  • 使用 pickRepresentativeItems() 篩選代表遊戲
  • 非子遊戲類型(live/sports/esports/lottery)顯示 provider 封面卡片
  • 遊戲圖片 URL:{imgUrl}/games/{gameChild|gameCover}/{gameCode}/{locale}/{productId|gameCode}.webp

A1GamePlay
項目說明
檔案app/components/A1/Game/Play.vue
用途遊戲啟動頁,包含 iframe 嵌入遊戲

行為說明

  • 未登入直接導回首頁
  • 呼叫 gameLaunch() API 取得遊戲 URL
  • iframe 嵌入遊戲內容
  • 離開遊戲彈出「模擬結束遊戲」Modal(含投注金額輸入 + 結果展示)
  • 支援 GameSimulateResult 型別的結果展示(betAmount, multiplier, winAmount, profit)

A1GameSearch
項目說明
檔案app/components/A1/Game/Search.vue
用途遊戲搜尋元件

行為說明

  • 搜尋遊戲名稱或供應商名稱
  • 即時過濾遊戲列表
  • 支援多語系遊戲名稱搜尋

A1GameProvider
項目說明
檔案app/components/A1/Game/Provider.vue
用途遊戲供應商卡片

行為說明

  • 顯示供應商 Logo(從 PROVIDER_LOGOS 映射)
  • 顯示供應商名稱(多語系 getProviderLabel()
  • 顯示隨機遊玩人數(getRandomPlayers(),session 內穩定)
  • 點擊進入該供應商的遊戲列表

A1GameRankList
項目說明
檔案app/components/A1/Game/RankList.vue
用途遊戲排行榜

行為說明

  • 支援多種排行類型:realtime, daily, weekly, monthly, total
  • 顯示玩家名稱、遊戲名稱、投注金額、倍率、派彩
  • 使用 RankingItem 型別

A1GameListBar
項目說明
檔案app/components/A1/Game/ListBar.vue
用途水平捲動遊戲列表

行為說明

  • 支援左右捲動箭頭
  • 應用 scrollbar-hide CSS utility 隱藏捲軸
  • 用於「最近遊玩」等水平排列場景

A1GameLoadMore
項目說明
檔案app/components/A1/Game/LoadMore.vue
用途載入更多按鈕

Props

Prop型別說明
prevCountnumber目前顯示數量
nextCountnumber下次載入數量
totalnumber總數量
stepnumber每次載入步長

EmitsloadMore


A1GameEmpty
項目說明
檔案app/components/A1/Game/Empty.vue
用途遊戲列表空狀態提示

A1/Modal/ 彈窗元件(16 個)

A1ModalLogin
項目說明
檔案app/components/A1/Modal/Login.vue
用途登入彈窗

行為說明

  • 表單欄位:帳號、密碼
  • 整合 Google OAuth 登入(讀取 store.getLoginConfig.google 取得 client ID)
  • 整合 Telegram 登入(讀取 store.getLoginConfig.telegram 取得 bot 設定)
  • 登入成功後自動帶入 device(FingerprintJS visitorId)
  • 登入成功後檢查 redirectTo cookie,有值則導向原始目標頁
  • 表單驗證使用 Zod schema
  • 支援切換至註冊彈窗

A1ModalRegister
項目說明
檔案app/components/A1/Modal/Register.vue
用途註冊彈窗

行為說明

  • 表單欄位:帳號、密碼、確認密碼、名稱
  • 自動帶入 refCode(從 useRefCode().getStoredRefCode() 取得推廣碼)
  • 自動帶入 device(FingerprintJS visitorId)
  • 註冊成功後自動登入
  • 註冊成功後觸發代理導覽(triggerTour()
  • 表單驗證使用 Zod schema

A1ModalEditPassword
項目說明
檔案app/components/A1/Modal/EditPassword.vue
用途修改密碼彈窗

行為說明

  • 表單欄位:原密碼、新密碼、確認新密碼
  • 呼叫 editPassword() API
  • 使用 EditPasswordPayload 型別

A1ModalSetPassword
項目說明
檔案app/components/A1/Modal/SetPassword.vue
用途設定密碼彈窗(OAuth 用戶首次設定)

行為說明

  • 適用 Google/Telegram 登入用戶尚未設定密碼的情況
  • app.vue 監聽 store.getUserDetail.hasPassword,為 false 時自動彈出
  • 設定成功後更新 userDetail

A1ModalTheme
項目說明
檔案app/components/A1/Modal/Theme.vue
用途主題切換彈窗

行為說明

  • 顯示所有可用主題預設(useTheme().presets
  • 每個主題顯示預覽色塊和圖示
  • 選擇後呼叫 setTheme(themeId) 即時套用
  • 主題選擇存入 cookie(1 年有效期)

A1ModalLocale
項目說明
檔案app/components/A1/Modal/Locale.vue
用途語系切換彈窗

行為說明

  • 顯示站點支援的語系列表(從 useTheme().supportedLocales 過濾)
  • 選擇後呼叫 i18n.setLocale() 切換
  • 若用戶已登入,同步呼叫 updateLocale() API 更新後端偏好

A1ModalVerifyUserInfo
項目說明
檔案app/components/A1/Modal/VerifyUserInfo.vue
用途用戶資訊驗證彈窗(Email / 手機綁定)

行為說明

  • 支援 Email 驗證流程:發送驗證碼 → 輸入驗證碼 → 確認
  • 支援手機驗證流程:選擇國碼 → 發送 OTP → 輸入驗證碼 → 確認
  • OTP 冷卻倒數(使用 useAuth().resendOtp(),預設 60 秒)
  • 國碼列表從 store.getCountryCodes 取得

A1ModalContactSupport
項目說明
檔案app/components/A1/Modal/ContactSupport.vue
用途聯繫客服彈窗

行為說明

  • 顯示所有啟用的客服管道(從 useTheme().customerServiceConfig.channels 取得)
  • 支援 8 種管道:line, telegram, wechat, facebook, instagram, twitter, discord, custom
  • 每個管道顯示圖示、多語系標籤、連結

A1ModalBuyCrypto
項目說明
檔案app/components/A1/Modal/BuyCrypto.vue
用途購買加密貨幣引導彈窗

行為說明

  • 引導用戶到外部交易所購買 USDT
  • 透過 useLayout().buyCryptoModalOpen 控制開關

A1ModalBindGoogleAuth
項目說明
檔案app/components/A1/Modal/BindGoogleAuth.vue
用途Google Authenticator 2FA 綁定彈窗

行為說明

  • 步驟 1:呼叫 generateGoogleAuth() API 取得 QR Code 和 Secret
  • 步驟 2:用戶掃描 QR Code 後輸入 6 位驗證碼
  • 步驟 3:呼叫 enableGoogleAuth() API 確認綁定
  • 使用 GoogleAuthResultEnableGoogleAuthPayload 型別

A1ModalAddBankCard
項目說明
檔案app/components/A1/Modal/AddBankCard.vue
用途新增銀行卡彈窗

行為說明

  • 表單欄位:銀行代碼、帳號、分行、持卡人姓名
  • 上傳身分證正反面、存摺封面(FormData)
  • 呼叫 addBankCard() API
  • 銀行代碼從 /bankCode.json 靜態檔取得

A1ModalAddCreditCard
項目說明
檔案app/components/A1/Modal/AddCreditCard.vue
用途新增信用卡彈窗

行為說明

  • 表單欄位:卡號、持卡人姓名、CVV、有效期限
  • 使用 AddCreditCardPayload 型別

A1ModalAddCryptoAddress
項目說明
檔案app/components/A1/Modal/AddCryptoAddress.vue
用途新增加密錢包地址彈窗

行為說明

  • 表單欄位:錢包名稱、幣種、網路、地址
  • 使用 AddCryptoAddressPayload 型別

A1ModalBankCardDetail
項目說明
檔案app/components/A1/Modal/BankCardDetail.vue
用途銀行卡詳情彈窗

行為說明

  • 顯示銀行卡完整資訊
  • 顯示審核狀態(使用 utsBankCard().STATUS_MAP
  • 支援刪除操作

A1ModalAgentTour
項目說明
檔案app/components/A1/Modal/AgentTour.vue
用途代理推廣新手導覽彈窗

行為說明

  • 多步驟說明彈窗(modal 階段)
  • 步驟介紹代理推廣計劃和佣金結構
  • 「立即加入」按鈕呼叫 applyFromTour()(一鍵成為代理 + 領取 5 USDT)
  • 「稍後再說」按鈕呼叫 dismissTour()(記錄時間,7 天後再顯示)
  • 透過 useAgentTour() 管理狀態

A1/User/ 用戶元件(40 個)

A1UserSetting
項目說明
檔案app/components/A1/User/Setting.vue
用途個人設定頁面

行為說明

  • 顯示用戶基本資訊(帳號、名稱、VIP 等級、餘額)
  • Email 綁定/驗證狀態
  • 手機綁定/驗證狀態
  • Google 帳號綁定/解綁
  • Telegram 帳號綁定/解綁
  • Google Authenticator 2FA 啟用/停用
  • 修改密碼入口
  • 頭像選擇(吉祥物系統)
  • 登入裝置紀錄列表

A1UserBetRecord/index
項目說明
檔案app/components/A1/User/BetRecord/index.vue
用途投注紀錄頁面

行為說明

  • 支援篩選:狀態(valid/invalid/cancelled)、遊戲類型、日期範圍
  • 顯示統計摘要:總投注次數、投注金額、有效投注、輸贏
  • 分頁列表顯示個別投注紀錄
  • 使用 BetRecordParamsBetRecordResult 型別

A1UserDeposit/ 存款元件(4 個)

A1UserDepositIndex (app/components/A1/User/Deposit/index.vue)

  • 存款頁面主入口,依 depositMethods 設定顯示可用的存款方式 Tab
  • Tab 包含:法幣(ATM)、信用卡、加密貨幣
  • 使用 useCash() composable 管理存款邏輯

A1UserDepositFiat (app/components/A1/User/Deposit/Fiat.vue)

  • ATM 法幣存款表單
  • 表單欄位:金額、銀行卡選擇
  • 顯示即時匯率(useExchangeRate().showRate()
  • 存款前檢查用戶驗證狀態(checkUserVerification()
  • 呼叫 vendor.wantong.deposit() 完成存款

A1UserDepositCredit (app/components/A1/User/Deposit/Credit.vue)

  • 信用卡存款表單
  • 表單欄位:金額、信用卡選擇
  • 使用 vendor.wantong.buildCardParams() 建構參數

A1UserDepositCrypto (app/components/A1/User/Deposit/Crypto.vue)

  • USDT 加密貨幣存款
  • 顯示加密貨幣即時價格(getCryptoPrice()
  • 存款後顯示付款地址和 QR Code
  • 使用 vendor.usdt.deposit() 完成存款
  • 回傳 DepositUsdtResult(paymentAddress, network, currency, orderAmount)

A1UserWithdrawal/index
項目說明
檔案app/components/A1/User/Withdrawal/index.vue
用途提領頁面

行為說明

  • 提領前檢查打碼量狀態(getTurnoverStatus()
  • 顯示存款打碼量和活動打碼量的完成進度
  • 表單欄位:提領金額、加密錢包選擇、驗證碼
  • 使用 Email OTP 驗證
  • 使用 WithdrawalRequestPayload 型別

A1UserWallet/ 錢包元件(4 個)

A1UserWalletIndex (app/components/A1/User/Wallet/index.vue)

  • 錢包管理主頁,Tab 切換銀行卡/信用卡/加密錢包

A1UserWalletFiat (app/components/A1/User/Wallet/Fiat.vue)

  • 銀行卡列表(已審核/待審核/已拒絕)
  • 新增銀行卡按鈕
  • 點擊卡片查看詳情

A1UserWalletCredit (app/components/A1/User/Wallet/Credit.vue)

  • 信用卡列表
  • 卡號遮罩顯示

A1UserWalletCrypto (app/components/A1/User/Wallet/Crypto.vue)

  • 加密錢包地址列表
  • 顯示網路和幣種

A1UserTransaction/ 交易紀錄元件(5 個)

A1UserTransactionIndex (app/components/A1/User/Transaction/index.vue)

  • 交易紀錄主頁,Tab 切換各類型交易

A1UserTransactionDeposit (app/components/A1/User/Transaction/Deposit.vue)

  • 存款紀錄列表(使用 DepositOrdersParams 篩選)

A1UserTransactionWithdrawal (app/components/A1/User/Transaction/Withdrawal.vue)

  • 提領紀錄列表(使用 WithdrawalListParams 篩選)

A1UserTransactionDividend (app/components/A1/User/Transaction/Dividend.vue)

  • 反水紀錄列表

A1UserTransactionPromo (app/components/A1/User/Transaction/Promo.vue)

  • 活動領取紀錄列表(使用 PromoClaimsParams 篩選)

A1UserVip/ VIP 元件(6 個)

A1UserVipIndex (app/components/A1/User/Vip/index.vue)

  • VIP 中心主頁,整合所有 VIP 子元件

A1UserVipStatusCard (app/components/A1/User/Vip/StatusCard.vue)

  • VIP 狀態卡片:當前等級、進度條、下一等級門檻
  • 使用 VipStatusResult 型別
  • 樣式依 tier 動態切換(utsVipTier().getStyle(tier)

A1UserVipLevelList (app/components/A1/User/Vip/LevelList.vue)

  • 所有 VIP 等級列表(動態取得,不硬編碼等級數量)
  • 顯示各等級門檻和特權

A1UserVipBenefits (app/components/A1/User/Vip/Benefits.vue)

  • VIP 特權說明

A1UserVipMyRebates (app/components/A1/User/Vip/MyRebates.vue)

  • 當前等級的反水率列表(依 8 種遊戲類型)

A1UserVipRebateTable (app/components/A1/User/Vip/RebateTable.vue)

  • 完整反水率對照表(所有等級 x 所有遊戲類型)

A1UserAffiliate/ 代理元件(7 個)

A1UserAffiliateIndex (app/components/A1/User/Affiliate/index.vue)

  • 代理推廣主頁
  • 非代理用戶顯示「申請成為代理」按鈕
  • 已是代理用戶顯示 Tab 切換子頁面

A1UserAffiliateDashboard (app/components/A1/User/Affiliate/Dashboard.vue)

  • 代理儀表板:推廣連結、本週統計、餘額
  • 使用 AffiliateDashboard 型別

A1UserAffiliateDownline (app/components/A1/User/Affiliate/Downline.vue)

  • 下線列表(三層級結構)
  • 使用 AffiliateDownlineParams 篩選

A1UserAffiliateCommission (app/components/A1/User/Affiliate/Commission.vue)

  • 佣金紀錄列表
  • 使用 AffiliateCommissionParams 篩選

A1UserAffiliateSettlement (app/components/A1/User/Affiliate/Settlement.vue)

  • 結算紀錄列表
  • 支援查看結算詳情

A1UserAffiliateWithdrawal (app/components/A1/User/Affiliate/Withdrawal.vue)

  • 代理提款功能
  • 支援銀行卡和加密貨幣兩種方式
  • 使用 AffiliateWithdrawalRequestPayload 型別

A1UserAffiliateAlliance (app/components/A1/User/Affiliate/Alliance.vue)

  • 聯盟系統資訊
  • 推廣碼管理(最多 10 個)
  • 代理等級和佣金費率展示

A1UserInbox/index
項目說明
檔案app/components/A1/User/Inbox/index.vue
用途站內信收件匣

行為說明

  • Tab 切換:個人訊息 / 系統訊息
  • 顯示未讀數量(useInbox().unreadCount
  • 點擊訊息標記為已讀
  • 支援「全部已讀」操作

A1UserKyc/ KYC 元件(6 個)

A1UserKycIndex (app/components/A1/User/Kyc/index.vue)

  • KYC 認證主頁,依狀態顯示不同內容
  • 狀態:unverifiedpendingverified / rejected

A1UserKycStatusCard (app/components/A1/User/Kyc/StatusCard.vue)

  • KYC 狀態卡片

A1UserKycStepBasicInfo (app/components/A1/User/Kyc/StepBasicInfo.vue)

  • 步驟 1:基本資料(姓名、生日、國籍、證件類型、證件號碼)

A1UserKycStepDocUpload (app/components/A1/User/Kyc/StepDocUpload.vue)

  • 步驟 2:文件上傳(身分證正面、反面、自拍照)

A1UserKycStepLiveness (app/components/A1/User/Kyc/StepLiveness.vue)

  • 步驟 3:活體驗證

A1UserKycStepReview (app/components/A1/User/Kyc/StepReview.vue)

  • 步驟 4:審核中/結果

注意:KYC 系統目前使用 cookie 模擬狀態(c9-kyc-mock),尚未對接後端 API。


A1/Promo/ 活動元件(2 個)+ PromoLinkCard

A1PromoCenter
項目說明
檔案app/components/A1/Promo/Center.vue
用途活動中心列表頁

行為說明

  • 支援活動標籤篩選
  • 顯示活動卡片(縮圖、標題、標籤、時間)
  • 活動標籤動態載入(utsPromo().fetchPromoTags()

A1PromoDetail
項目說明
檔案app/components/A1/Promo/Detail.vue
用途活動詳情頁

行為說明

  • 顯示活動完整內容(HTML 渲染)
  • 顯示活動條件(CONDITION_MAP 映射)
  • 顯示領取狀態和按鈕
  • 呼叫 claimPromo() API 領取活動獎勵

A1PromoLinkCard
項目說明
檔案app/components/A1/PromoLinkCard.vue
用途活動連結卡片(通用小元件)

A1/Alliance/ 聯盟元件(1 個)

A1AllianceIndex
項目說明
檔案app/components/A1/Alliance/index.vue
用途聯盟計劃頁面

行為說明

  • 展示聯盟計劃完整介紹
  • 顯示代理等級體系(bronze/silver/gold/platinum)
  • 顯示佣金費率結構
  • 顯示 VIP 里程碑獎勵
  • 代理導覽 modal 階段在此頁觸發(advanceToModal()

A1/Mission/ 任務元件(1 個)

A1MissionIndex
項目說明
檔案app/components/A1/Mission/index.vue
用途任務系統列表

行為說明

  • 每日/週/月任務分類顯示
  • 任務類型:存款任務 + 投注任務
  • 顯示任務進度和領取狀態
  • VIP 等級門檻檢查(meetsVip
  • 使用 MissionItem 型別

A1/Help/ 幫助元件(1 個)

A1HelpCenter
項目說明
檔案app/components/A1/Help/Center.vue
用途幫助中心頁面

3.3.4 A2/ 佈局元件群組(65 個)

A2 為奢華精品風格佈局,使用金屬色系主題(champagne, roseGold, platinum, onyx, sapphire, burgundy)。 A2 元件架構與 A1 鏡像,功能邏輯相同,主要差異在於視覺設計和部分佈局結構。

A2 與 A1 的結構差異

差異點A1A2
頂部導航TitleBar + SidebarNavbar(整合式導航列)
設計風格深色科技遊戲風奢華精品風
主題色系寶石色系(emerald, amber, sky, violet, rose, cyan)金屬色系(champagne, roseGold, platinum, onyx, sapphire, burgundy)
SplashScreenCommonSplashScreenA2LayoutSplashScreen(獨立版本)

A2 元件對照表

A1 元件A2 對應元件差異說明
A1/Layout/TitleBar + SidebarA2/Layout/Navbar合併為單一導航元件
A1/Layout/FooterA2/Layout/Footer樣式差異
A1/Layout/BottomBarA2/Layout/BottomBar樣式差異
A1/Layout/LiveChatA2/Layout/LiveChat功能相同
A1/Home/*A2/Home/*樣式差異
A1/Game/*A2/Game/*樣式差異
A1/Modal/*A2/Modal/*樣式差異
A1/User/*A2/User/*樣式差異
A1/Promo/*A2/Promo/*樣式差異
A1/Alliance/*A2/Alliance/*樣式差異
A1/Mission/*A2/Mission/*樣式差異
A1/Help/*A2/Help/*樣式差異

所有 A2 元件的業務邏輯與 A1 完全相同,僅視覺設計和 CSS 樣式不同。 頁面透過 vueVersion() 插件根據 domainConfig.layout 自動選擇載入 A1 或 A2 版本。


3.3.5 頁面路由(20 頁)

路由檔案元件載入需認證
/pages/index.vueA1/Home/indexA2/Home/index
/alliancepages/alliance.vueA1/Alliance/indexA2/Alliance/index
/challengespages/challenges.vue挑戰頁面(mock data)
/missionpages/mission.vueA1/Mission/indexA2/Mission/index
/helppages/help/index.vueA1/Help/CenterA2/Help/Center
/gamepages/game/index.vueA1/Game/LobbyA2/Game/Lobby
/game/playpages/game/play.vueA1/Game/PlayA2/Game/Play
/promopages/promo/index.vueA1/Promo/CenterA2/Promo/Center
/promo/[id]pages/promo/[id].vueA1/Promo/DetailA2/Promo/Detail
/redirect/[action]pages/redirect/[action].vueOAuth callback 處理
/user/affiliatepages/user/affiliate.vueA1/User/Affiliate/index
/user/bet-recordpages/user/bet-record.vueA1/User/BetRecord/index
/user/depositpages/user/deposit.vueA1/User/Deposit/index
/user/inboxpages/user/inbox.vueA1/User/Inbox/index
/user/kycpages/user/kyc.vueA1/User/Kyc/index
/user/settingpages/user/setting.vueA1/User/Setting
/user/transactionpages/user/transaction.vueA1/User/Transaction/index
/user/vippages/user/vip.vueA1/User/Vip/index
/user/walletpages/user/wallet.vueA1/User/Wallet/index
/user/withdrawalpages/user/withdrawal.vueA1/User/Withdrawal/index

/user/* 路徑由 auth.global.ts 中介層保護,未登入用戶自動攔截並開啟登入 Modal。


3.4 Composables 完整文件

3.4.1 架構總覽

composables/
├── useApi.ts              ← Facade:展開所有 API composable 成扁平物件
├── useApiTypes.ts         ← Re-export from ./types(便捷別名)
├── useHttp.ts             ← 底層 HTTP 客戶端(useHttp + useHttpAsync)
├── useAuth.ts             ← 認證狀態管理
├── useConfig.ts           ← 域名設定解析
├── useTheme.ts            ← 主題系統 + 站點設定
├── useLayout.ts           ← Modal 開關狀態
├── useGame.ts             ← 遊戲邏輯
├── useDevice.ts           ← 裝置偵測
├── useCash.ts             ← 金流支付
├── usePaymentChannels.ts  ← 金流通道
├── useExchangeRate.ts     ← 匯率查詢
├── useAffiliate.ts        ← 代理推廣
├── useInbox.ts            ← 站內信
├── useFingerprint.ts      ← 裝置指紋
├── useAgentTour.ts        ← 代理導覽
├── useKyc.ts              ← KYC 認證
├── useRefCode.ts          ← 推廣碼讀取
├── useR2Url.ts            ← R2 URL 轉換
├── usePreventZoom.ts      ← 防止縮放
└── api/                   ← 12 個 API composable
    ├── useAuthApi.ts
    ├── useGameApi.ts
    ├── useWalletApi.ts
    ├── useDepositApi.ts
    ├── usePromoApi.ts
    ├── useVipApi.ts
    ├── useAffiliateApi.ts
    ├── useInboxApi.ts
    ├── useSiteConfigApi.ts
    ├── useWithdrawalApi.ts
    ├── useMissionApi.ts
    └── index.ts

3.4.2 API 層架構

元件 → useApi()(Facade,自動匯入)
  └── use{Module}Api()(領域模組)
        └── useHttp() / useHttpAsync()(HTTP 原語)
              └── fetch()(原生 fetch API)

雙版本 API 模式:每個 API 端點提供 SSR 和 CSR 兩個版本:

  • xxxSsr()useHttpAsync()useAsyncData({ server: true, lazy: false })
  • xxxCsr()useHttp()Promise<ApiResponse<T>>

3.4.3 useHttp — 底層 HTTP 客戶端

項目說明
檔案app/composables/useHttp.ts
用途封裝 fetch API,提供統一的請求/回應處理

匯出型別

ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

interface HttpMiddlewareContext {
  url: string;
  options: RequestInit;
}

interface HttpMiddlewares {
  before?: (ctx: HttpMiddlewareContext) => void | Promise<void>;
  after?: (data: any, ctx: HttpMiddlewareContext) => any;
  onError?: (error: any, ctx: HttpMiddlewareContext) => any;
}

interface UseHttpOptions<T = any> {
  method?: HttpMethod;              // 預設 'GET'
  params?: Record<string, any>;     // URL query params
  body?: any;                       // 請求主體
  headers?: HeadersInit;            // 額外 headers
  middlewares?: HttpMiddlewares;     // 中介層 hooks
  json?: boolean;                   // 是否 JSON(預設 true)
  fetchOptions?: RequestInit;       // 原生 fetch 選項
  auth?: boolean;                   // 是否帶 JWT(預設 true)
  tokenCookieKey?: string;          // Token cookie 名稱(預設 'token')
  autoRedirectOn401?: boolean;      // 401 時自動導回首頁(預設 true)
  redirectCookieKey?: string;       // 重導向 cookie 名稱(預設 'redirectTo')
  redirectTo?: string;              // 重導向目標(預設 '/')
  errorToast?: boolean;             // 錯誤時顯示 toast(預設 true)
  errorMessage?: string | Record<string, string>;  // 自訂錯誤文案
}

匯出函式

useHttp<T>(url, options) — CSR 版本,回傳 Promise<T>

內部處理流程:

  1. 組合 URL:baseUrl + url + buildQuery(body)
  2. 注入 Headers:localessite-nameAuthorization、SSR forward
  3. 執行 middlewares.before hook
  4. fetch() 發送請求
  5. HTTP 401 → handle401()(清 token → 存 callback → 導回首頁 + toast)
  6. 非 200 HTTP → 拋出 Error
  7. 解析 JSON → 錯誤碼攔截(ERROR_CODES[path][code] 映射 → 自訂 errorMessage 覆蓋 → toast)
  8. 執行 middlewares.after hook
  9. 回傳 data

useHttpAsync<T>(key, url, options, asyncOptions) — SSR 版本,回傳 AsyncData<T>

內部使用 useAsyncData(key, () => useHttp(url, options), { server: true, lazy: false })

Header 注入機制

Header來源說明
localesi18n_redirected cookie當前語系(預設 zh-TW
site-namedomainConfig.siteId白牌站點 ID
Content-Typejson: trueapplication/json
Authorizationtoken cookieBearer <JWT>
cookieSSR forwardSSR 時轉發 request cookies
authorizationSSR forwardSSR 時轉發 authorization header

錯誤碼三層映射

  1. Store 查表store.getEnums.ERROR_CODES[data.path][code](含 :id 萬用匹配)
  2. 自訂文案覆蓋errorMessage(string 全覆蓋,Record 按 code 覆蓋)
  3. Toast 顯示useToast().add({ description, color: 'error' })

3.4.4 useAuth — 認證狀態管理

項目說明
檔案app/composables/useAuth.ts
依賴useStore(), useApi(), @vueuse/core

回傳值

名稱型別說明
tokenRef<string | null>JWT Token(cookie 持久化,48 小時)
isLoginComputedRef<boolean>是否已登入(!!token.value
setToken(v: string | null) => Promise<void>設定/清除 Token
refreshUserData() => Promise<void>重新取得用戶資料
logout() => Promise<void>登出(清 token + 清 userDetail + 導回首頁)
resendOtp(options?) => OtpTimerOTP 冷卻倒數計時器

resendOtp 回傳物件

ts
interface OtpTimer {
  remaining: Ref<number>;           // 剩餘秒數
  isRunning: ComputedRef<boolean>;   // 是否正在倒數
  canResend: ComputedRef<boolean>;   // 是否可以重發
  format: ComputedRef<string>;       // 格式化顯示(MM:SS)
  start: (sec?: number) => void;     // 開始倒數
  stop: () => void;                  // 停止倒數
}

3.4.5 useConfig — 域名設定解析

項目說明
檔案app/composables/useConfig.ts
用途根據 hostname 解析域名設定

匯出

ts
export const LAYOUT_ENUM = { a1: 'a1', a2: 'a2' };

export default function (customHostName?: string): DomainConfigEntry;

行為說明

  • SSR 時讀取 useRequestHeaders().host
  • CSR 時讀取 window.location.hostname
  • 呼叫 resolveDomainConfig(hostname) 查找對應設定
  • 找不到時 fallback 至 DEFAULT_DOMAIN (localhost)

DomainConfigEntry

ts
interface DomainConfigEntry {
  baseUrl: string;      // 後端 API base URL
  imgUrl: string;       // R2 圖片公開 URL
  socketUrl?: string;   // WebSocket URL
  siteId: string;       // 白牌站點 ID
  layout: string;       // 佈局代碼(a1, a2)
}

3.4.6 useTheme — 主題系統與站點設定

項目說明
檔案app/composables/useTheme.ts
依賴useAuth(), useApi(), useConfig(), themePresets

回傳值 — 狀態

名稱型別說明
currentThemeIdRef<string>當前主題 ID(預設 emerald
currentThemeComputedRef<ThemePreset>當前主題完整物件
presetsRef<ThemePreset[]>所有可用主題預設
siteCodeRef<string>站點代碼
sitePrefixRef<string>站點前綴
siteLayoutRef<string>站點佈局
siteDisplayNameRef<string>站點顯示名稱
siteDisplayDescriptionRef<string>站點描述
supportedLocalesRef<string[]>支援的語系列表
agentTourEnabledRef<boolean>代理導覽開關
agentTourIntervalSecRef<number>導覽間隔秒數(預設 604800 = 7 天)
depositMethodsRef<{fiat, credit, crypto}>存款方式開關
domainAssetsRef<DomainEntry | null>域名素材(Logo、Favicon)
bottomBarEnabledRef<boolean>底部導航列開關
bottomBarItemsRef<BottomBarItem[]>底部導航項目
footerSectionsRef<FooterSection[]>頁尾區段
learnMoreItemsRef<LearnMoreItem[]>了解更多 FAQ 項目
templateVariablesRef<TemplateVariable[]>模板變數
customerServiceConfigRef<CustomerServiceConfig | null>客服設定

回傳值 — 方法

方法參數說明
setThemethemeId: string切換主題(套用 CSS 變數 + 存 cookie + 更新 appConfig)
initTheme初始化主題(從 cookie 讀取或使用預設)
fetchSiteConfig從後端取得站點設定(主題、語系、佈局、客服等)

fetchSiteConfig 處理邏輯

  1. 呼叫 getSiteConfig() API
  2. 解析回傳的 SiteConfigResult
  3. 更新所有 useState 狀態
  4. 語系不一致時自動切換至第一個支援語系
  5. 匹配 hostname 設定域名素材(Logo、Favicon、Browser Title)
  6. 將 API 回傳的 activeTheme 轉為 ThemePreset 格式並加入預設列表

3.4.7 useLayout — Modal 狀態管理

項目說明
檔案app/composables/useLayout.ts
用途跨元件共享 Modal 開關狀態(使用 useState

回傳值

名稱型別說明
loginModalOpenRef<boolean>登入 Modal 開關
openLoginModal() => void開啟登入 Modal
buyCryptoModalOpenRef<boolean>購買加密貨幣 Modal 開關
openBuyCryptoModal() => void開啟購買加密貨幣 Modal
setPasswordModalOpenRef<boolean>設定密碼 Modal 開關
setPasswordAccountRef<string>設定密碼的目標帳號
openSetPasswordModal(account: string) => void開啟設定密碼 Modal
contactModalOpenRef<boolean>聯繫客服 Modal 開關
openContactModal() => void開啟聯繫客服 Modal

3.4.8 useGame — 遊戲邏輯

項目說明
檔案app/composables/useGame.ts
依賴useStore(), useI18n(), useConfig(), useApi(), gameConstants, childGame

匯出型別

ts
type ProviderItem = {
  gameCode: string;
  providerCode: string;
  gameType: number;
  gameTypeLabel: string;
  label?: Record<string, string>;
  enable: boolean;
  areaBlock: boolean;
  maintain: boolean;
  childGame?: ChildGameValue;
};

type GameItem = Partial<ProviderItem> & Partial<ChildGameItem>;
type GameTypeKey = keyof typeof GAME_TYPE_VALUE_ENUM;

type GameListResult = {
  mapping: Partial<Record<GameTypeKey, ProviderItem[]>>;
  areaBlock: string[];
  maintain: string[];
  enable: string[];
  provider: ProviderItem[];
};

回傳值

名稱型別說明
GAME_TYPE_VALUE_ENUMobject遊戲類型值列舉(sports:1, slot:2, live:3, ...)
GAME_TYPE_KEY_ENUMRecord<number, string>遊戲類型值→鍵反查
providerProviderLogoItem[]63 個供應商 Logo 映射
mapGameList(list, config) => any遊戲列表篩選
getGameMappingImg(item: GameItem) => string取得遊戲圖片 URL
isChildGameType(gameType: number) => boolean是否為子遊戲類型
buildGameList(providers) => GameListResult建構遊戲列表結構
fetchGameProvider() => Promise<void>取得遊戲商列表並存入 store
fetchGameTypeConfigs() => Promise<void>取得遊戲分類設定並存入 store
pickRepresentativeItems(providers) => GameItem[]每個供應商挑選一款代表遊戲
getGameName(item: ChildGameItem) => string取得多語系遊戲名稱
getProviderLabel(p: ProviderItem) => string取得多語系供應商名稱
getRandomPlayers(key: string) => number隨機遊玩人數(session 內穩定)
errImgobject圖片載入錯誤追蹤

3.4.9 useDevice — 裝置偵測

項目說明
檔案app/composables/useDevice.ts
依賴@vueuse/core(useMediaQuery, useWindowSize)

型別定義

ts
type Brand = 'apple' | 'samsung' | 'google' | 'huawei' | 'xiaomi'
           | 'oppo' | 'vivo' | 'oneplus' | 'sony' | 'lg' | 'motorola' | 'unknown';
type Browser = 'safari' | 'chrome' | 'edge' | 'firefox' | 'opera' | 'unknown';
type OS = 'ios' | 'android' | 'macos' | 'windows' | 'linux' | 'unknown';

參數

參數型別預設值說明
desktopMinWidthnumber768桌面最小寬度門檻

回傳值

名稱型別說明
uaComputedRef<string>User-Agent(SSR 安全)
widthRef<number>視窗寬度
isMobileComputedRef<boolean>手機裝置
isTabletComputedRef<boolean>平板裝置
isDesktopComputedRef<boolean>桌面裝置
isTouchDeviceComputedRef<boolean>觸控裝置
isLandscapeRef<boolean>橫向
isPortraitRef<boolean>直向
osComputedRef<OS>作業系統
isIOS, isAndroid, isMac, isWindows, isLinuxComputedRef<boolean>OS 旗標
browserComputedRef<Browser>瀏覽器
isSafari, isChrome, isEdge, isFirefoxComputedRef<boolean>瀏覽器旗標
brandComputedRef<Brand>裝置品牌
isIPhone, isIPad, isSamsung, isPixel, isHuawei, isXiaomi, isOppo, isVivoComputedRef<boolean>品牌旗標

3.4.10 useCash — 金流支付

項目說明
檔案app/composables/useCash.ts
依賴usePaymentChannels(), useExchangeRate(), useApi(), useOverlay()

匯出型別

ts
type WantongDepositParams = {
  channelId: number;
  paymentMethod: 'fiat' | 'credit';
  orderAmount: number;
  expectedCode?: string;
  expectedAccount?: string;
  userCardLastValue?: string;
  payerName?: string; payerMobile?: string; payerEmail?: string;
  productDes?: string; msg?: string;
};

type WantongAtmInput = {
  channelId: number; orderAmount: number;
  bankCard: { bankCode: string; bankAccount: string; holderName: string };
  mobile: string; email: string;
};

type WantongCardInput = {
  channelId: number; orderAmount: number;
  creditCard: { cardNumber: string; holderName: string };
  mobile: string; email: string;
};

type UsdtDepositParams = { channelId: number; paymentMethod: 'crypto'; orderAmount: number };
type UsdtCryptoInput = { channelId: number; orderAmount: number };
type CryptoDepositResult = DepositUsdtResult;

回傳值

名稱說明
...usePaymentChannels()展開金流通道所有屬性
...useExchangeRate()展開匯率所有屬性
refreshAll / init重新取得所有金流資料
checkUserVerification檢查用戶是否已驗證 Email + 手機
vendor.wantong.buildAtmParams()建構 ATM 存款參數
vendor.wantong.buildCardParams()建構信用卡存款參數
vendor.wantong.deposit()執行法幣/信用卡存款
vendor.usdt.buildCryptoParams()建構 USDT 存款參數
vendor.usdt.deposit()執行 USDT 存款
getVendor(channelName)依通道名稱取得金流處理器
getVendorByChannelId(id)依通道 ID 取得金流處理器

3.4.11 usePaymentChannels — 金流通道

項目說明
檔案app/composables/usePaymentChannels.ts
狀態共享模組級共享(module-level ref,非 useState

回傳值

名稱型別說明
channelsRef<VendorChannel[]>存款通道列表
loadingChannelsRef<boolean>載入中
selectedCurrencyRef<string | undefined>選中幣種
currencyOptionsComputedRef<SelectItem[]>幣種選項
getCurrencyOptions(paymentMethod?) => SelectItem[]依支付方式過濾幣種
fetchDepositChannels() => Promise<void>取得存款通道
fetchVendorChannels() => Promise<void>取得金流商通道
bankCardsRef<BankCard[]>銀行卡列表
fetchBankCards(force?) => Promise<void>取得銀行卡(含快取)
bankCodeDataRef<BankCodeItem[]>銀行代碼列表
fetchBankCodeData() => Promise<void>取得銀行代碼
creditCardsRef<CreditCard[]>信用卡列表
fetchCreditCards(force?) => Promise<void>取得信用卡(含快取)
vendorChannelsRef<VendorChannel[]>金流商通道

3.4.12 useExchangeRate — 匯率查詢

項目說明
檔案app/composables/useExchangeRate.ts
狀態共享模組級共享

回傳值

名稱型別說明
exchangeRateDataRef<Record<string, any>>法幣匯率資料
loadingRateRef<boolean>載入中
quotedAtTextComputedRef<string>報價時間文字
rateLookupKeyComputedRef<string | null>匯率查找鍵
hasRateForCurrencyComputedRef<boolean>是否有該幣種匯率
toFixedRate(v) => string匯率格式化(4 位小數)
showRate(field: string) => string顯示特定欄位匯率
fetchExchangeRate() => Promise<void>取得法幣匯率
cryptoRateDataRef<Record<string, any>>加密貨幣匯率
loadingCryptoRateRef<boolean>載入中
fetchCryptoRate() => Promise<void>取得加密貨幣匯率
getCryptoPrice(coin: string) => string取得加密貨幣格式化價格
getCryptoRawPrice(coin: string) => number取得加密貨幣原始價格(計算用)

3.4.13 useAffiliate — 代理推廣

項目說明
檔案app/composables/useAffiliate.ts
依賴useStore(), useApi()

回傳值

名稱參數回傳說明
isAgentComputedRef<boolean>是否為代理
applyAsAgentPromise<{agentCode}>申請成為代理
fetchDashboardPromise<AffiliateDashboard>取得代理儀表板
fetchPromoLinkPromise<AffiliatePromoLink>取得推廣連結
fetchDownlineAffiliateDownlineParams?Promise<PaginatedResult>取得下線列表
fetchCommissionsAffiliateCommissionParams?Promise<PaginatedResult>取得佣金紀錄
fetchSettlementsAffiliateSettlementParams?Promise<PaginatedResult>取得結算紀錄
fetchSettlementDetailid: numberPromise<AffiliateSettlementDetail>取得結算詳情
fetchBalancePromise<AffiliateBalance>取得代理餘額
fetchWithdrawalsAffiliateWithdrawalParams?Promise<PaginatedResult>取得提款紀錄
requestWithdrawalAffiliateWithdrawalRequestPayloadPromise<ApiResponse>申請提款
fetchClickStatsAffiliateClickStatsParams?Promise<AffiliateClickStats>取得點擊統計
fetchAllianceInfoPromise<AllianceInfoResult>取得聯盟資訊
fetchReferralCodesPromise<ReferralCode[]>取得推廣碼列表
createReferralCodeCreateReferralCodePayloadPromise<ApiResponse>建立推廣碼
deleteReferralCodeid: numberPromise<ApiResponse>刪除推廣碼
fetchVipMilestonesPromise<VipMilestoneLog[]>取得 VIP 里程碑紀錄
fetchTierInfoPromise<TierInfoResult>取得代理等級資訊

3.4.14 useInbox — 站內信

項目說明
檔案app/composables/useInbox.ts
依賴useApi()

回傳值

名稱型別說明
unreadCountRef<number>總未讀數(useState 共享)
personalUnreadCountRef<number>個人訊息未讀數
systemUnreadCountRef<number>系統訊息未讀數
fetchInbox(params?) => Promise取得收件匣列表
fetchUnreadCount() => Promise<number>取得總未讀數
fetchScopeUnreadCounts() => Promise<void>取得各分類未讀數
markAsRead(id: number) => Promise<boolean>標記為已讀
markAllAsRead() => Promise<boolean>全部標記已讀

3.4.15 useFingerprint — 裝置指紋

項目說明
檔案app/composables/useFingerprint.ts
依賴@fingerprintjs/fingerprintjs

回傳值

名稱型別說明
getVisitorId() => Promise<string>取得裝置指紋 ID(模組級快取)

行為說明

  • 首次呼叫時初始化 FingerprintJS agent 並產生 visitorId
  • 後續呼叫直接回傳快取值
  • 失敗時回傳空字串

3.4.16 useAgentTour — 代理導覽

項目說明
檔案app/composables/useAgentTour.ts
依賴useApi(), useStore(), useTheme()

導覽流程

checkTourStatus() → GET /affiliate/tour-status
  ↓ shouldShow = true
triggerTour() → tourPhase = 'highlight'
  ↓ 用戶點擊 sidebar「聯盟計劃」
advanceToModal() → tourPhase = 'modal', showTour = true
  ↓ 彈出 AgentTour Modal
applyFromTour() → POST /affiliate/apply-from-tour(一鍵成為代理 + 5 USDT)

dismissTour() → POST /affiliate/dismiss-tour(記錄時間,7 天後再顯示)

回傳值

名稱型別說明
tourPhaseRef<'highlight' | 'modal' | null>當前導覽階段
showTourRef<boolean>Modal 是否顯示
checkTourStatus() => Promise<void>檢查是否顯示導覽
applyFromTour() => Promise<result>從導覽申請代理
dismissTour() => Promise<void>跳過導覽
triggerTour() => void手動觸發導覽
advanceToModal() => void推進至 modal 階段

3.4.17 useKyc — KYC 認證

項目說明
檔案app/composables/useKyc.ts
狀態持久化cookie(c9-kyc-mock,30 天)

注意:目前為 mock 實作,使用 cookie 持久化狀態,尚未對接後端 API。

匯出型別

ts
type KycStatus = 'unverified' | 'pending' | 'verified' | 'rejected';

interface KycBasicInfo {
  fullName: string; dateOfBirth: string; country: string;
  idType: string; idNumber: string;
}

interface KycState {
  status: KycStatus;
  basicInfo: KycBasicInfo;
  documentPreviews: { idFront: string; idBack: string; selfie: string };
  livenessVerified: boolean;
  submittedAt: string | null;
  verifiedAt: string | null;
  rejectedReason: string | null;
}

回傳值

名稱說明
kycStateRef<KycState>(useState 共享)
submitKyc提交 KYC(設為 pending)
fetchKycStatus從 cookie 載入狀態
mockApproveMock 通過
mockRejectMock 拒絕
resetKyc重設狀態
save存入 cookie
load從 cookie 載入

3.4.18 其他 Composables

useRefCode

項目說明
檔案app/composables/useRefCode.ts
回傳{ getStoredRefCode: () => string }
說明affiliateTrackingV1 cookie 讀取推廣碼

useR2Url

項目說明
檔案app/composables/useR2Url.ts
回傳{ toR2Url: (key) => string | undefined, imgUrl: string }
說明R2 key → 完整公開 URL。已是 http 開頭的原樣回傳(向後相容)

usePreventZoom

項目說明
檔案app/composables/usePreventZoom.ts
回傳void
說明iOS Safari 防止雙擊放大和雙指縮放。在 app.vue 呼叫一次。

3.4.19 API Composables(12 個)

useAuthApi — 認證 API(24 方法)

方法HTTP路徑說明
loginPOST/auth/login帳密登入
registerPOST/auth/register註冊
getUserDetailSsrGET/auth/user-detail用戶資料(SSR)
getUserDetailCsrGET/auth/user-detail用戶資料(CSR)
getCountryCodesSsrGET/auth/country-codes國碼列表(SSR)
getCountryCodesCsrGET/auth/country-codes國碼列表(CSR)
getLoginConfigSsrGET/auth/login-config登入設定(SSR)
getLoginConfigCsrGET/auth/login-config登入設定(CSR)
sendVerifyEmailPOST/auth/send-verify-email發送 Email 驗證碼
checkVerifyEmailPOST/auth/check-verify-email確認 Email 驗證碼
sendVerifyMobilePOST/auth/send-verify-mobile發送手機 OTP
checkVerifyMobilePOST/auth/check-verify-mobile確認手機 OTP
generateGoogleAuthGET/auth/google-auth/generate產生 2FA QR Code
enableGoogleAuthPOST/auth/google-auth/enable啟用 2FA
editPasswordPOST/auth/edit-password修改密碼
setPasswordPOST/auth/set-password設定密碼(OAuth)
loginGooglePOST/auth/login-googleGoogle OAuth 登入
loginTelegramPOST/auth/login-telegramTelegram 登入
logoutPOST/auth/logout登出
updateLocalePATCH/auth/locale更新語系偏好
getMascotsGET/auth/mascots取得吉祥物列表
updateAvatarPATCH/auth/avatar更新頭像
bindGooglePOST/auth/bind-google綁定 Google
bindTelegramPOST/auth/bind-telegram綁定 Telegram

useGameApi — 遊戲 API(10 方法)

方法HTTP路徑說明
getGameProviderSsrGET/game/providers遊戲商列表(SSR)
getGameProviderCsrGET/game/providers遊戲商列表(CSR)
gameLaunchPOST/game/launch啟動遊戲
gameSimulatePOST/game/simulate模擬遊戲結果
getGameTypeConfigsCsrGET/game/type-configs遊戲分類設定
getRecentGamesCsrGET/game/recent最近遊玩
getEnumsSsrGET/common/enums枚舉(SSR)
getEnumsCsrGET/common/enums枚舉(CSR)
getRankingGET/ranking排行榜
getBetRecordsGET/bet-record投注紀錄

useWalletApi — 錢包 API(12 方法)

方法HTTP路徑說明
getBankCardsGET/wallet/bank-cards銀行卡列表
addBankCardPOST/wallet/bank-cards新增銀行卡(FormData)
deleteBankCardDELETE/wallet/bank-cards/:id刪除銀行卡
getCreditCardsGET/wallet/credit-cards信用卡列表
addCreditCardPOST/wallet/credit-cards新增信用卡
deleteCreditCardDELETE/wallet/credit-cards/:id刪除信用卡
getCryptoAddressesGET/wallet/crypto-addresses加密錢包列表
addCryptoAddressPOST/wallet/crypto-addresses新增加密錢包
deleteCryptoAddressDELETE/wallet/crypto-addresses/:id刪除加密錢包
getVendorChannelsGET/vendor/channels金流商通道
wantongAddAtmPOST/vendor/wantong/atm萬通 ATM 預登錄
wantongAddCardPOST/vendor/wantong/card萬通信用卡預登錄

useDepositApi — 存款 API(6 方法)

方法HTTP路徑說明
getExchangeRateGET/deposit/exchange-rate法幣匯率
getCryptoRateGET/deposit/crypto-rate加密貨幣匯率
getDepositChannelsGET/deposit/channels存款通道
depositPOST/deposit建立存款訂單
getDepositOrdersGET/deposit/orders存款紀錄
confirmDepositPOST/deposit/confirm確認存款

usePromoApi — 活動 API(8 方法)

方法HTTP路徑說明
getPromosGET/promo活動列表
getPromoByIdGET/promo/:id活動詳情
claimPromoPOST/promo/:id/claim領取活動
getPromoClaimsGET/promo/claims領取紀錄
getPromoTagsCsrGET/promo/tags活動標籤
createPromoPOST/promo建立活動(Admin)
updatePromoPATCH/promo/:id更新活動(Admin)
deletePromoDELETE/promo/:id刪除活動(Admin)

useVipApi — VIP API(11 方法)

方法HTTP路徑說明
getVipLevelsGET/vip/levelsVIP 等級列表
getVipRebatesGET/vip/rebates反水率列表
getVipStatusGET/vip/statusVIP 狀態
createVipLevelPOST/vip/admin/levels建立等級(Admin)
updateVipLevelPATCH/vip/admin/levels/:id更新等級(Admin)
deleteVipLevelDELETE/vip/admin/levels/:id刪除等級(Admin)
createVipRebatePOST/vip/admin/rebates建立反水規則(Admin)
updateVipRebatePATCH/vip/admin/rebates/:id更新反水規則(Admin)
deleteVipRebateDELETE/vip/admin/rebates/:id刪除反水規則(Admin)
triggerDailyRebatePOST/vip/admin/trigger-daily觸發每日反水(Admin)
triggerMonthlyRelegationPOST/vip/admin/trigger-monthly觸發月度保級(Admin)

useAffiliateApi — 代理 API(35 方法)

涵蓋:追蹤點擊、代理管理、儀表板、下線、佣金、結算、提款、聯盟(推廣碼/VIP 里程碑/代理等級/佣金費率)、Admin 操作。

useInboxApi — 站內信 API(7 方法)

方法HTTP路徑說明
getInboxGET/inbox收件匣列表
getInboxUnreadCountGET/inbox/unread-count未讀數量
markInboxReadPATCH/inbox/:id/read標記已讀
markInboxReadAllPATCH/inbox/read-all全部已讀
adminSendInboxPOST/inbox/admin/send發送站內信(Admin)
adminGetInboxGET/inbox/admin/list站內信列表(Admin)
adminDeleteInboxDELETE/inbox/admin/:id刪除站內信(Admin)

useSiteConfigApi — 站點設定 API(7 方法)

方法HTTP路徑說明
getSiteConfigGET/site-config取得站點設定
adminGetSiteConfigsGET/site-config/admin/list站點列表(Admin)
adminUpdateSiteConfigPATCH/site-config/admin/:id更新站點(Admin)
adminGetSiteThemesGET/site-config/admin/:id/themes主題列表(Admin)
adminCreateSiteThemePOST/site-config/admin/:id/themes建立主題(Admin)
adminUpdateSiteThemePATCH/site-config/admin/themes/:id更新主題(Admin)
adminDeleteSiteThemeDELETE/site-config/admin/themes/:id刪除主題(Admin)

useWithdrawalApi — 提領 API(7 方法)

方法HTTP路徑說明
withdrawalSendCodePOST/withdrawal/send-code發送提領驗證碼
withdrawalRequestPOST/withdrawal/request建立提領申請
getWithdrawalListGET/withdrawal/list提領紀錄
getTurnoverStatusGET/withdrawal/turnover-status打碼量狀態
adminGetWithdrawalListGET/withdrawal/admin/list提領列表(Admin)
adminReviewWithdrawalOrderPATCH/withdrawal/admin/:id/review審核提領(Admin)
adminCompleteWithdrawalOrderPATCH/withdrawal/admin/:id/complete完成提領(Admin)

useMissionApi — 任務 API(3 方法)

方法HTTP路徑說明
getMissionsGET/mission任務列表
getMissionClaimsGET/mission/claims領取紀錄
claimMissionPOST/mission/:id/claim領取任務獎勵

3.5 型別定義完整文件

所有型別定義位於 app/composables/types/,共 13 個檔案。 index.ts re-export 所有模組,useApiTypes.ts 提供便捷別名 re-export。

3.5.1 common.ts — 共用型別

ts
/** 統一 API 回應 */
interface ApiResponse<T = any> {
  redirectAfter: string;
  code: number;            // 200=成功,其他=業務錯誤
  message?: string;        // 錯誤訊息(當前語系)
  result?: T;              // 回傳資料
  timestamp?: number;
  path?: string;           // API 路徑(用於 ERROR_CODES 查表)
}

/** 分頁參數 */
interface PaginationParams {
  page?: number;
  pageSize?: number;
}

/** 分頁回應 */
interface Pagination {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

/** 分頁列表回應 */
interface PaginatedResult<T> {
  items: T[];
  pagination: Pagination;
}

/** 枚舉結果(GET /common/enums) */
interface EnumsResult {
  AUTH_ENUM: {
    RELATED: Record<string, string>;
    LOGIN_LOG: { ACTION: Record<string, string> };
  };
  ERROR_CODES: Record<string, Record<string, string>>;
}

/** 多語系文字 */
type MultiLangText = Record<string, string>;

/** 排行榜參數 */
interface RankingParams {
  type?: 'realtime' | 'daily' | 'weekly' | 'monthly' | 'total';
  limit?: number;
}

/** 排行榜項目 */
interface RankingItem {
  id?: number; gameName?: string; playerName?: string;
  time?: string; betAmount?: string; multiplier?: string;
  payout?: string; isAnonymous?: boolean;
  playerAccount?: string; totalPayout?: string;
}

/** 投注紀錄參數 */
interface BetRecordParams extends PaginationParams {
  status?: 'valid' | 'invalid' | 'cancelled';
  gameType?: string; startDate?: string; endDate?: string;
}

/** 投注紀錄摘要 */
interface BetRecordSummary {
  totalBetCount: number; betAmount: string;
  betEffective: string; winLose: string;
}

/** 投注紀錄項目 */
interface BetRecordItem {
  id: number; gameType: string; gamePlatform: string;
  gameNumber: string; totalBetCount: number; betAmount: string;
  betEffective: string; winLose: string;
  status: 'valid' | 'invalid' | 'cancelled';
  odds: string; invalidReason: string | null;
  betDatetime: string; gameName: string;
}

/** 投注紀錄結果(含摘要 + 分頁列表) */
interface BetRecordResult extends BetRecordSummary {
  items: BetRecordItem[];
  pagination: Pagination;
}

/** 投注紀錄詳情 */
interface BetRecordDetail {
  id: number; roundNo: number; betAmount: string;
  winLose: string; createdAt: string;
}

3.5.2 auth.ts — 認證型別(25 個介面)

ts
interface LoginPayload { account: string; password: string; device?: string; }
interface RegisterPayload { account: string; password: string; name: string; refCode?: string; device?: string; }
interface LoginResult { token: string; user: UserBasic; }

interface UserBasic {
  id: number; account: string; name: string;
  email: string | null; mobile: string | null;
  avatar: string | null; vipLevel: string; balance: string;
  vendorGroupId: number | null; googleAuthEnabled: number;
  tokenVersion: number; createdAt: string;
}

interface LoginLog {
  id: number; userId: number; ip: string;
  device: string; action: string; lastUse: string;
}

interface UserDetail {
  id: number; account: string; name: string;
  email: string | null; mobile: string | null;
  avatar: string | null; telegram: string | null; google?: string | null;
  vipLevel: string; vipProgress: string; totalEffectiveBet: string;
  relegationMissCount: number; vipHold: number;
  googleAuthEnabled: number; balance: string;
  vendorGroupId: number | null; hasPassword?: boolean;
  agentCode?: string | null; locale?: string | null;
  createdAt: string; loginLogs?: LoginLog[];
}

interface UserDetailParams { RELATED?: string | string[]; }
interface CountryCode { country: string; callingCode: string; name: string; }
interface SendVerifyEmailPayload { email: string; subject?: string; }
interface SendVerifyMobilePayload { mobile: string; country: { label: string; icon: string }; }
interface CheckVerifyEmailPayload { code: string; email: string; }
interface CheckVerifyMobilePayload { mobile: string; code: string; }
interface EnableGoogleAuthPayload { code: string; }
interface GoogleAuthResult { secret: string; qrCode: string; }
interface EditPasswordPayload { password: string; newPassword: string; confirmPassword: string; }
interface LoginConfigResult { google: string; telegram?: LoginConfigTelegram; }
interface LoginGooglePayload { code: string; state: string; refCode?: string; }
interface LoginGoogleResult { token: string; user: UserBasic; isNewUser: boolean; google: { sub: string; email: string; name: string; picture: string }; }
interface LoginTelegramPayload { id: number; first_name: string; last_name?: string; username?: string; photo_url?: string; auth_date: number; hash: string; refCode?: string; }
interface LoginTelegramResult { token: string; user: UserBasic; isNewUser: boolean; }
interface LoginConfigTelegram { botUsername: string; botId: string; }
interface MascotItem { id: string; label: string; url: string; }
interface UpdateAvatarPayload { mascotId: string; }
interface UpdateAvatarResult { avatar: string; }
interface BindGooglePayload { code: string; state: string; }
interface BindGoogleResult { google: string; email: string; name: string; }
type BindTelegramPayload = Omit<LoginTelegramPayload, 'refCode'>;
interface BindTelegramResult { telegram: string; }

3.5.3 game.ts — 遊戲型別(11 個介面)

ts
interface GameProviderParams { gameType?: number; }
interface GameProvider {
  id: number; gameCode: string; providerCode: string;
  gameType: number; gameTypeLabel: string;
  areaBlock: boolean; maintain: boolean; enable: boolean; createdAt: string;
}
interface GameLaunchPayload { device: 'desktop' | 'mobile'; gameCode: string; productId: number; }
interface GameLaunchResult { url: string; }
interface GameSimulatePayload { gameCode: string; productId: number; betAmount: number; }
interface GameSimulateResult {
  roundId: string; orderId: number; betAmount: number;
  multiplier: number; winAmount: number; profit: number;
  result: 'lose' | 'small' | 'medium' | 'good' | 'big' | 'huge' | 'mega';
  balanceBefore: number; balanceAfter: number;
}
interface GameTypeConfig {
  id: number; gameType: number; typeKey: string;
  label: Record<string, string>; icon: string;
  sortOrder: number; enabled: boolean;
}
interface RecentGameItem { gameCode: string; productId: string; lastPlayedAt: string; }
interface LiveSportsStatus { short: string; long: string; elapsed: number | null; }
interface LiveSportsLeague { id: number; name: string; country: string; logo: string; round: string; }
interface LiveSportsTeam { id: number; name: string; logo: string; score: number | null; }
interface LiveSportsOdds { home: string; draw: string; away: string; extraCount: number; }
interface LiveSportsFixture {
  fixtureId: number; kickoffAt: string; status: LiveSportsStatus;
  league: LiveSportsLeague; home: LiveSportsTeam; away: LiveSportsTeam;
  odds: LiveSportsOdds | null;
}

3.5.4 wallet.ts — 錢包型別

ts
interface BankCard {
  id: number; userId: number; bankCode: string; bankAccount: string;
  branch: string; holderName: string; idCardFront: string; idCardBack: string;
  passbookCover: string; status: number; // 0=待審核, 1=已通過, 2=已拒絕
  createdAt: string; updatedAt: string;
}
interface AddCreditCardPayload { cardNumber: string; holderName: string; cvv: string; expiryDate: string; }
interface CreditCard {
  id: number; userId: number; cardNumber: string; holderName: string;
  cvv: string; expiryDate: string; status: number;
  createdAt: string; updatedAt: string;
}
interface AddCryptoAddressPayload { walletName: string; currency?: string; network?: string; address: string; }
interface CryptoAddress {
  id: number; userId: number; walletName: string; currency: string;
  network: string; address: string; status: number;
  createdAt: string; updatedAt: string;
}

3.5.5 deposit.ts — 存款型別

ts
interface VendorChannel {
  id: number; name: string; currency: string;
  paymentMethods: string[]; paymentAddress: string | null;
  network?: string; enabled: number;
  exchangeRate?: { buy: number; sell: number } | null;
}
interface VendorChannelsResult { groupId: number; groupName: string; channels: VendorChannel[]; }
interface WantongAtmPayload { subOrder: string; orderAmount: number; expectedCode: string; expectedAccount: string; productDes?: string; msg?: string; payerName?: string; payerMobile?: string; payerEmail?: string; }
interface WantongCardPayload { subOrder: string; orderAmount: number; userCardLastValue?: string; productDes?: string; msg?: string; payerName?: string; payerMobile?: string; payerEmail?: string; }
interface WantongResult { status: boolean; sub_order: string; }
interface ExchangeRateParams { currency?: string; bankCode?: string; }
interface ExchangeRateResult { time: string; base: string; [currency: string]: any; }
interface CryptoRateParams { symbol?: string; }
interface CryptoRateResult { base: string; time: string; [coin: string]: any; }
interface DepositPayload {
  channelId: number; paymentMethod: 'fiat' | 'credit' | 'crypto';
  subOrder: string; orderAmount: number;
  expectedCode?: string; expectedAccount?: string; userCardLastValue?: string;
  productDes?: string; msg?: string; payerName?: string; payerMobile?: string; payerEmail?: string;
}
interface DepositWantongResult { status: boolean; sub_order: string; data?: { result_url?: string }; }
interface DepositUsdtResult { paymentAddress: string; network: string; currency: string; orderAmount: number; subOrder: string; orderId: number; }
interface DepositOrdersParams extends PaginationParams { status?: 'pending' | 'created' | 'paid' | 'failed'; startDate?: string; endDate?: string; }
interface DepositOrder {
  id: number; channelName: string; currency: string; subOrder: string;
  orderAmount: number; vendorAmount: number; paymentMethod: string;
  status: string; payAmount: number; payTime: string | null;
  usdAmount: string; exchangeRate: string; createdAt: string; updatedAt: string;
}

3.5.6 promo.ts — 活動型別

ts
interface PromoTag { id: number; name: string; label: Record<string, string>; color: string; sortOrder: number; enabled: number; }
interface PromoListParams extends PaginationParams { tag?: string; activeOnly?: string | number; limit?: number; }
interface PromoItem {
  id: number; title: string; imgPc: Record<string, string> | null; imgMobile: Record<string, string> | null;
  content: string; actionHtml: string; startTime: string; endTime: string;
  tag: string; enabled: number; conditionType: string; conditionValue: string;
  rewardAmount: string; turnoverMultiplier: string; maxClaims: number; claimedCount: number;
  createdAt: string; updatedAt: string; isActive: boolean; isClaimed: boolean; isClaimable: boolean;
}
interface ClaimPromoResult { rewardAmount: string; newBalance: string; }
interface PromoClaimsParams extends PaginationParams { tab?: 'all' | 'pending' | 'completed'; startDate?: string; endDate?: string; }
interface PromoClaim {
  id: number; promoId: number; promoTitle: string; promoTag: string;
  rewardAmount: string; requiredTurnover: string; completedTurnover: string;
  turnoverCompleted: number; claimedAt: string;
}

3.5.7 vip.ts — VIP 型別

ts
interface VipLevel { id: number; level: number; name: string; tier: string; minChip: string; relegationChip: string; sortOrder: number; enabled: number; createdAt: string; updatedAt: string; }
interface VipRebate { id: number; level: number; gameType: string; rebateRate: string; createdAt: string; updatedAt: string; }
interface VipStatusResult {
  level: number; name: string; tier: string;
  totalEffectiveBet: string; currentChip: string; nextLevelMinChip: string;
  progress: string; relegationChip: string; monthlyEffective: string;
  relegationMissCount: number; vipHold: number;
  rebates: { gameType: string; rebateRate: string }[];
  allLevels: { level: number; name: string; tier: string; minChip: string; relegationChip: string }[];
}
interface CreateVipLevelPayload { level: number; name: MultiLangText; tier: string; minChip: number; relegationChip: number; sortOrder?: number; enabled?: number; }
interface CreateVipRebatePayload { level: number; gameType: string; rebateRate: number; }
interface VipDailyRebateResult { usersProcessed: number; totalRebate: string; }
interface VipMonthlyRelegationResult { checked: number; warned: number; demoted: number; }
interface VipHoldPayload { hold: 0 | 1; }
interface VipHoldResult { userId: number; vipHold: number; }

3.5.8 affiliate.ts — 代理型別(35+ 個介面)

涵蓋以下分類:

  • 追蹤TrackClickPayload
  • 儀表板AffiliateDashboard, AffiliatePromoLink, AffiliateBalance
  • 下線AffiliateDownlineParams, AffiliateDownlineItem
  • 點擊統計AffiliateClickStatsParams, AffiliateClickStatsDaily, AffiliateClickStats
  • 佣金AffiliateCommissionParams, AffiliateCommissionItem
  • 結算AffiliateSettlementParams, AffiliateSettlement, AffiliateSettlementDetail
  • 提款AffiliateWithdrawalParams, AffiliateWithdrawalItem, AffiliateWithdrawalRequestPayload
  • 聯盟AllianceTierInfo, AllianceCommissionRateInfo, AllianceVipMilestoneInfo, AllianceInfoResult, ReferralCode, CreateReferralCodePayload, VipMilestoneLog, TierInfoResult
  • Admin 費率CommissionRateItem, UpsertCommissionRatePayload
  • Admin 里程碑AdminVipMilestone, UpsertVipMilestonePayload
  • Admin 等級AgentTier, UpsertAgentTierPayload, SetAgentTierPayload
  • Admin 結算TriggerDailySettlementPayload, TriggerDailySettlementResult, TriggerSettlementResult
  • Admin 管理CreateAgentPayload, CreateAgentResult, AdminSettlementParams, ReviewSettlementPayload, RiskLog, AdminWithdrawalParams, ReviewWithdrawalPayload, BindPayload, BindLogParams, BindLog

3.5.9 mission.ts — 任務型別

ts
interface MissionItem {
  id: number; category: 'deposit' | 'bet';
  periodType: 'daily' | 'weekly' | 'monthly';
  tier: number; threshold: string; rewardAmount: string;
  vipRequired: number; turnoverMultiplier: string;
  currentProgress: string | null; isClaimed: boolean;
  isClaimable: boolean; meetsVip: boolean;
}
interface MissionClaimsParams extends PaginationParams { tab?: 'pending' | 'completed'; startDate?: string; endDate?: string; }
interface MissionClaim {
  id: number; missionId: number; category: 'deposit' | 'bet';
  periodType: 'daily' | 'weekly' | 'monthly'; tier: number;
  periodKey: string; rewardAmount: string; requiredTurnover: string;
  completedTurnover: string; turnoverCompleted: number; claimedAt: string;
}
interface MissionClaimResult { rewardAmount: string; requiredTurnover: string; newBalance: string; }

3.5.10 inbox.ts — 站內信型別

ts
interface InboxParams extends PaginationParams { category?: 'system' | 'promo'; scope?: 'personal' | 'system'; }
interface InboxItem { id: number; title: string; content: string; category: 'system' | 'promo'; isRead: boolean; createdAt: string; }
interface InboxUnreadCountResult { unreadCount: number; }
interface SendInboxPayload { userId?: number; title: MultiLangText; content: MultiLangText; category: 'system' | 'promo'; }
interface AdminInboxItem { id: number; userId: number | null; title: MultiLangText; content: MultiLangText; category: 'system' | 'promo'; createdAt: string; }

3.5.11 site-config.ts — 站點設定型別(15 個介面)

ts
interface SiteTheme {
  themeId: string; themeName: string;
  primary: { base: string; dark: string; light: string; glow: string };
  accent: { gold: string; info: string; violet: string; cyan: string; error: string };
  surface: { page: string; navbar: string; card: string; modal: string; sidebar: string[] };
  text: { primary: string; secondary: string; muted: string; hint: string };
  border: { subtle: string; default: string; strong: string };
}
interface SiteThemeSummary { themeId: string; themeName: string; }
interface DomainEntry {
  hostname: string; browserTitle: string; browserDescription: string;
  logoSmall: string | null; logoBig: string | null; favicon: string | null;
  supportedLocales?: string[];
}
interface BottomBarItem { icon: string; label: Record<string, string>; link: string; sortOrder: number; enabled: boolean; }
interface FooterLink { label: Record<string, string>; link: string; icon: string; sortOrder: number; }
interface FooterSection { title: Record<string, string>; icon: string; sortOrder: number; enabled: boolean; links: FooterLink[]; }
interface LearnMoreItem { question: Record<string, string>; answer: Record<string, string>; sortOrder: number; enabled: boolean; }
interface TemplateVariable { key: string; value: Record<string, string>; isSystem: boolean; }
interface CustomerServiceChannel {
  type: 'line' | 'telegram' | 'wechat' | 'facebook' | 'instagram' | 'twitter' | 'discord' | 'custom';
  label: Record<string, string>; icon: string; link: string; sortOrder: number; enabled: boolean;
}
interface CustomerServiceConfig { channels: CustomerServiceChannel[]; liveChatScript: string | null; liveChatEnabled: boolean; }
interface SiteConfigResult {
  siteCode: string; prefix: string; layout: string;
  siteName: string; siteDescription: string; supportedLocales: string[];
  activeTheme: SiteTheme; availableThemes: SiteThemeSummary[];
  agentTourEnabled: boolean; agentTourIntervalSec: number;
  depositMethods?: { fiat: boolean; credit: boolean; crypto: boolean };
  domains?: DomainEntry[] | null;
  bottomBarEnabled?: boolean; bottomBarConfig?: { mobile: BottomBarItem[]; desktop: BottomBarItem[] } | null;
  footerConfig?: FooterSection[] | null; learnMoreConfig?: LearnMoreItem[] | null;
  customerServiceConfig?: CustomerServiceConfig | null;
  templateVariables?: TemplateVariable[] | null;
}
interface AdminSiteTheme { id: number; siteConfigId: number; themeId: string; themeName: MultiLangText; primary: SiteTheme['primary']; accent: SiteTheme['accent']; surface: SiteTheme['surface']; text: SiteTheme['text']; border: SiteTheme['border']; enabled: number; createdAt: string; updatedAt: string; }
interface AdminSiteConfig { id: number; siteCode: string; siteName: MultiLangText; siteDescription: MultiLangText; supportedLocales: string[]; activeThemeId: number; enabled: number; themes: AdminSiteTheme[]; createdAt: string; updatedAt: string; }
interface UpdateSiteConfigPayload { siteName?: MultiLangText; siteDescription?: MultiLangText; supportedLocales?: string[]; activeThemeId?: number; enabled?: 0 | 1; }
interface CreateSiteThemePayload { themeId: string; themeName: MultiLangText; primary: SiteTheme['primary']; accent: SiteTheme['accent']; surface: SiteTheme['surface']; text: SiteTheme['text']; border: SiteTheme['border']; enabled?: 0 | 1; }

3.5.12 withdrawal.ts — 提領型別

ts
interface WithdrawalSendCodeResult { id: string; }
interface WithdrawalListParams extends PaginationParams { status?: 'pending' | 'approved' | 'rejected' | 'completed'; startDate?: string; endDate?: string; }
interface WithdrawalRequestPayload { amount: number; cryptoAddressId: number; verifyCode: string; }
interface WithdrawalOrder {
  id: number; userId: number; amount: string; cryptoAddressId: number;
  address: string; network: string;
  status: 'pending' | 'approved' | 'rejected' | 'completed';
  rejectReason: string | null; reviewedBy: string | null;
  reviewedAt: string | null; completedAt: string | null;
  createdAt: string; updatedAt: string;
}
interface AdminWithdrawalReviewPayload { action: 'approve' | 'reject'; rejectReason?: string; }
interface TurnoverDepositStatus { totalDeposits: string; multiplier: number; requiredTurnover: string; completedTurnover: string; remaining: string; completed: boolean; }
interface TurnoverPromoItem { promoId: number; promoTitle: string; rewardAmount: string; requiredTurnover: string; completedTurnover: string; remaining: string; }
interface TurnoverPromoStatus { pendingCount: number; completed: boolean; items: TurnoverPromoItem[]; }
interface TurnoverStatusResult { canWithdraw: boolean; deposit: TurnoverDepositStatus; promo: TurnoverPromoStatus; }

3.6 插件系統(4 個)

3.6.1 pinia.ts — 全域 Store 註冊

項目說明
檔案app/plugins/pinia.ts
類型通用(SSR + CSR)
ts
import { useMainStore } from '@/stores';

declare global {
  function useStore(): ReturnType<typeof useMainStore>;
}

export default defineNuxtPlugin(() => {
  globalThis.useStore = () => useMainStore();
});

說明

  • useStore() 註冊至 globalThis,所有元件和 composable 可直接呼叫
  • 無需 import,Nuxt 自動載入
  • 回傳 useMainStore()(Facade Store)

3.6.2 vueVersion.ts — 佈局切換系統

項目說明
檔案app/plugins/vueVersion.ts
類型通用(SSR + CSR)
ts
import type { AsyncComponentLoader } from 'vue';
import { defineAsyncComponent } from 'vue';
import getConfig, { LAYOUT_ENUM } from '../composables/useConfig';

declare global {
  function vueVersion(layoutMap: Record<string, AsyncComponentLoader>): {
    is: ReturnType<typeof defineAsyncComponent>;
  };
}

export default defineNuxtPlugin(({ provide }) => {
  const hostname = import.meta.client ? window.location.hostname : useRequestURL().hostname;
  provide('hostname', hostname);

  globalThis.vueVersion = (layoutMap) => {
    const config = getConfig(hostname);
    const defaultVersion = LAYOUT_ENUM.a1.toLocaleUpperCase();  // 'A1'
    const currentVersion = config.layout;
    const layout = currentVersion?.toUpperCase() || defaultVersion;
    const loader = layoutMap[layout] ?? layoutMap[defaultVersion];
    if (!loader) throw new Error(`[vueVersion] Layout "${layout}" not found`);
    const is = defineAsyncComponent(loader);
    return { is };
  };
});

說明

  • 根據 domainConfig.layout 決定載入哪個佈局版本的元件
  • vueVersion() 接受 { A1: () => import(...), A2: () => import(...) } 格式
  • 回傳 { is }<component :is /> 使用
  • 同時 provide hostname 供其他 composable 使用

頁面使用範例

vue
<script setup lang="ts">
const { is } = vueVersion({
  A1: () => import("@/components/A1/Home/index.vue"),
  A2: () => import("@/components/A2/Home/index.vue"),
});
</script>
<template>
  <component :is />
</template>

3.6.3 theme.client.ts — 主題初始化

項目說明
檔案app/plugins/theme.client.ts
類型Client-only(.client.ts 後綴)
ts
export default defineNuxtPlugin(() => {
  const { initTheme } = useTheme();
  initTheme();
});

說明

  • Client-only 插件,在瀏覽器端初始化主題
  • 從 cookie(c9-theme)讀取已選主題 ID
  • 呼叫 applyTheme() 設定 CSS 自訂屬性

3.6.4 affiliateTracking.client.ts — 推廣追蹤

項目說明
檔案app/plugins/affiliateTracking.client.ts
類型Client-only
ts
export default defineNuxtPlugin(() => {
  const route = useRoute();
  const refCode = (route.query.refCode || route.query.ref) as string | undefined;
  if (!refCode) return;

  const cookie = useCookie<string | null>('affiliateTrackingV1', {
    path: '/', maxAge: 60 * 60 * 24 * 30, sameSite: 'lax',
  });

  // firstClickWins: 不覆蓋已有的推廣碼
  if (cookie.value) return;
  cookie.value = refCode;

  // fire-and-forget:不等回應,不影響頁面載入
  try { useApi().trackClick({ refCode }); } catch { /* silent */ }
});

說明

  • 從 URL query 讀取 refCoderef 參數
  • 採用 first-click-wins 策略(不覆蓋已有推廣碼)
  • 存入 cookie(30 天有效期)
  • Fire-and-forget 呼叫 trackClick API

3.7 狀態管理(Pinia Stores)

3.7.1 架構總覽

useMainStore()(Facade)
  ├── useAppStore()      ← isReady, isLoading, doms, enums
  ├── useUserStore()     ← userDetail, countryCodes, loginConfig
  ├── useGameStore()     ← gameList, gameTypeConfigs
  └── usePromoStore()    ← promoTags

所有 Store 使用 setup store 語法(Composition API)。 Facade Store 透過 storeToRefs() 取得原始 ComputedRef,避免 Pinia v3 + Vue 3.5 新 reactivity 系統下雙層 ComputedRef 解包問題。

3.7.2 useAppStore — 應用狀態

項目說明
檔案app/stores/appStore.ts
Store ID'app'
State型別說明
isReadyref(false)App 是否就緒(SplashScreen 依此淡出)
isLoadingref(false)全域載入中狀態
domsref({})DOM 參考(保留欄位)
enumsref({})後端枚舉快取(ERROR_CODES 等)
Action參數說明
setIsReadyboolean設定就緒狀態
setIsRoadingboolean設定載入狀態(注意原始碼拼寫為 Roading)
setDomsRecord<string, any>設定 DOM 參考
setEnumsPartial<EnumsResult>設定枚舉資料
Getter型別說明
getIsReadyComputedRef<boolean>
getIsRoadingComputedRef<boolean>
getDomsComputedRef<Record<string, any>>
getEnumsComputedRef<Partial<EnumsResult>>

3.7.3 useUserStore — 用戶狀態

項目說明
檔案app/stores/userStore.ts
Store ID'user'
State型別說明
userDetailref<Partial<UserDetail>>({})用戶詳細資料
countryCodesref<CountryCode[]>([])國碼列表
loginConfigref<LoginConfigResult>({ google: '' })登入設定(OAuth client ID 等)
Action參數說明
setUserDetailPartial<UserDetail>設定用戶資料
setCountryCodesCountryCode[]設定國碼列表
setLoginConfigLoginConfigResult設定登入設定

3.7.4 useGameStore — 遊戲狀態

項目說明
檔案app/stores/gameStore.ts
Store ID'game-data'
State型別說明
gameListRef<GameListResult>遊戲列表(含 mapping、provider、狀態分類)
gameTypeConfigsref<GameTypeConfig[]>([])遊戲分類設定

3.7.5 usePromoStore — 活動狀態

項目說明
檔案app/stores/promoStore.ts
Store ID'promo'
State型別說明
promoTagsref<PromoTag[]>([])活動標籤列表

3.7.6 useMainStore — Facade Store

項目說明
檔案app/stores/index.ts
Store ID'main'

Facade 模式:組合所有 Feature Store 的 actions 和 getters 成單一介面。

ts
export const useMainStore = defineStore('main', () => {
  const app = useAppStore();
  const user = useUserStore();
  const game = useGameStore();
  const promo = usePromoStore();

  // 使用 storeToRefs 取得原始 ComputedRef
  const { getDoms, getIsReady, getIsRoading, getEnums } = storeToRefs(app);
  const { getUserDetail, getCountryCodes, getLoginConfig } = storeToRefs(user);
  const { getGameList, getGameTypeConfigs } = storeToRefs(game);
  const { getPromoTags } = storeToRefs(promo);

  return {
    // App
    setDoms: app.setDoms, setIsReady: app.setIsReady,
    setIsRoading: app.setIsRoading, setEnums: app.setEnums,
    getDoms, getIsReady, getIsRoading, getEnums,
    // User
    setUserDetail: user.setUserDetail, setCountryCodes: user.setCountryCodes,
    setLoginConfig: user.setLoginConfig,
    getUserDetail, getCountryCodes, getLoginConfig,
    // Game
    setGameList: game.setGameList, setGameTypeConfigs: game.setGameTypeConfigs,
    getGameList, getGameTypeConfigs,
    // Promo
    setPromoTags: promo.setPromoTags, getPromoTags,
  };
});

3.8 主題系統

3.8.1 架構概述

ThemePreset(靜態定義)

useTheme().initTheme()(從 cookie 讀取選擇)

applyTheme()(設定 CSS 自訂屬性)

main.css @theme(橋接 CSS 變數 → Tailwind emerald 色票)

元件使用 Tailwind class(自動套用主題色)

3.8.2 CSS 變數橋接機制

main.css 中定義 @theme 區塊,將 Tailwind 的 emerald 色票重新指向 CSS 自訂屬性:

css
@theme {
  --color-emerald-50:  var(--c9-primary-50, #ecfdf5);
  --color-emerald-100: var(--c9-primary-100, #d1fae5);
  --color-emerald-200: var(--c9-primary-200, #a7f3d0);
  --color-emerald-300: var(--c9-primary-300, #6ee7b7);
  --color-emerald-400: var(--c9-primary-400, #34d399);
  --color-emerald-500: var(--c9-primary-500, #10b981);
  --color-emerald-600: var(--c9-primary-600, #059669);
  --color-emerald-700: var(--c9-primary-700, #047857);
  --color-emerald-800: var(--c9-primary-800, #065f46);
  --color-emerald-900: var(--c9-primary-900, #064e3b);
  --color-emerald-950: var(--c9-primary-950, #022c22);
}

:root {
  --c9-glow: var(--c9-primary-glow, 16, 185, 129);
}

效果:當 applyTheme() 設定 --c9-primary-* 後,所有使用 emerald-* Tailwind class 的元件自動更新顏色。

3.8.3 ThemePreset 介面

ts
interface ThemePreset {
  themeId: string;          // 唯一識別碼
  themeName: string;        // i18n key(如 'theme.emerald')
  icon: string;             // Lucide 圖示名稱
  preview: string;          // 預覽色(hex)
  shades: Record<number, string>;  // 50-950 色階(hex)
  glow: string;             // 發光色(R, G, B 格式)
}

3.8.4 A1 主題預設(6 組寶石色系)

themeIdthemeName圖示預覽色glow
emeraldtheme.emeraldi-lucide-leaf#34d39916, 185, 129
ambertheme.amberi-lucide-flame#fbbf24245, 158, 11
skytheme.skyi-lucide-cloud#38bdf814, 165, 233
violettheme.violeti-lucide-sparkles#a78bfa139, 92, 246
rosetheme.rosei-lucide-heart#fb7185244, 63, 94
cyantheme.cyani-lucide-droplets#22d3ee6, 182, 212

emerald 完整色階

色階Hex
50#ecfdf5
100#d1fae5
200#a7f3d0
300#6ee7b7
400#34d399
500#10b981
600#059669
700#047857
800#065f46
900#064e3b
950#022c22

amber 完整色階

色階Hex
50#fffbeb
100#fef3c7
200#fde68a
300#fcd34d
400#fbbf24
500#f59e0b
600#d97706
700#b45309
800#92400e
900#78350f
950#451a03

sky 完整色階

色階Hex
50#f0f9ff
100#e0f2fe
200#bae6fd
300#7dd3fc
400#38bdf8
500#0ea5e9
600#0284c7
700#0369a1
800#075985
900#0c4a6e
950#082f49

violet 完整色階

色階Hex
50#f5f3ff
100#ede9fe
200#ddd6fe
300#c4b5fd
400#a78bfa
500#8b5cf6
600#7c3aed
700#6d28d9
800#5b21b6
900#4c1d95
950#2e1065

rose 完整色階

色階Hex
50#fff1f2
100#ffe4e6
200#fecdd3
300#fda4af
400#fb7185
500#f43f5e
600#e11d48
700#be123c
800#9f1239
900#881337
950#4c0519

cyan 完整色階

色階Hex
50#ecfeff
100#cffafe
200#a5f3fc
300#67e8f9
400#22d3ee
500#06b6d4
600#0891b2
700#0e7490
800#155e75
900#164e63
950#083344

3.8.5 A2 主題預設(6 組金屬色系)

themeIdthemeName圖示預覽色glow
champagnetheme.champagnei-lucide-crown#d4a574212, 165, 116
roseGoldtheme.roseGoldi-lucide-gem#b76e79183, 110, 121
platinumtheme.platinumi-lucide-shield#8c9ead140, 158, 173
onyxtheme.onyxi-lucide-diamond#4a4a4a74, 74, 74
sapphiretheme.sapphirei-lucide-hexagon#2d5f8a45, 95, 138
burgundytheme.burgundyi-lucide-wine#800020128, 0, 32

champagne 完整色階

色階Hex
50#fdf8f0
100#f9eddb
200#f2d8b4
300#e8be85
400#d4a574
500#c08a55
600#a87040
700#8d5a35
800#73482e
900#5f3c29
950#341e14

roseGold 完整色階

色階Hex
50#fdf2f4
100#fce7ea
200#f9d0d8
300#f4adb9
400#e88395
500#b76e79
600#a15564
700#874452
800#713c48
900#613641
950#351a21

platinum 完整色階

色階Hex
50#f5f7f9
100#e8ecf1
200#d5dce4
300#b6c3d0
400#8c9ead
500#6e8495
600#5a6d7e
700#4b5a68
800#414d58
900#39434c
950#252c32

onyx 完整色階

色階Hex
50#f6f6f6
100#e7e7e7
200#d1d1d1
300#b0b0b0
400#888888
500#6d6d6d
600#5d5d5d
700#4a4a4a
800#3d3d3d
900#2d3436
950#1a1a1a

sapphire 完整色階

色階Hex
50#f0f7ff
100#dfecfc
200#c5ddfa
300#9dc6f6
400#6ea7ef
500#4a87e5
600#2d5f8a
700#2b5580
800#284869
900#253e58
950#1a283a

burgundy 完整色階

色階Hex
50#fef2f3
100#fde3e5
200#fdcbd1
300#fba4ad
400#f76e7e
500#ee3f55
600#db1f3d
700#b81530
800#800020
900#6b0a21
950#3e000f

3.8.6 CSS 自訂屬性完整列表

CSS 變數說明設定來源
--c9-primary-50主色 50 色階applyTheme()
--c9-primary-100主色 100 色階applyTheme()
--c9-primary-200主色 200 色階applyTheme()
--c9-primary-300主色 300 色階applyTheme()
--c9-primary-400主色 400 色階applyTheme()
--c9-primary-500主色 500 色階applyTheme()
--c9-primary-600主色 600 色階applyTheme()
--c9-primary-700主色 700 色階applyTheme()
--c9-primary-800主色 800 色階applyTheme()
--c9-primary-900主色 900 色階applyTheme()
--c9-primary-950主色 950 色階applyTheme()
--c9-primary-glow發光色(R, G, B)applyTheme()
--c9-glow發光色別名(fallback):root in main.css

3.8.7 主題切換流程

用戶選擇主題(Theme Modal)

setTheme(themeId)
  ├── currentThemeId.value = themeId
  ├── applyTheme(theme)
  │     ├── document.documentElement.style.setProperty('--c9-primary-{shade}', hex)  × 11
  │     ├── document.documentElement.style.setProperty('--c9-primary-glow', glow)
  │     ├── appConfig.ui.colors.primary = themeId(Nuxt UI 動態色彩)
  │     └── appConfig.ui.colors.success = themeId
  └── themeCookie.value = themeId(存入 cookie,1 年有效期)

3.8.8 後端主題轉換

後端 SiteTheme 格式與前端 ThemePreset 不同,使用 siteThemeToPreset() 轉換:

ts
function siteThemeToPreset(st: SiteTheme): ThemePreset {
  return {
    themeId: st.themeId,
    themeName: `theme.${st.themeId.replace(/^default-/, '')}`,
    icon: 'i-lucide-palette',
    preview: st.primary.base,
    shades: {
      50: '', 100: '', 200: '',
      300: st.primary.light,
      400: st.primary.base,
      500: st.primary.base,
      600: st.primary.dark,
      700: '', 800: '', 900: '', 950: '',
    },
    glow: st.primary.glow,
  };
}

3.9 多語系(i18n)

3.9.1 設定

項目說明
套件@nuxtjs/i18n 10.2.1
策略no_prefix(URL 無語系前綴)
存儲i18n_redirected cookie
預設語系zh-TW
支援語系zh-TW, en-US, zh-CN, th-TH, vi-VN
格式flat key-value JSON
檔案位置i18n/locales/{locale}.json

3.9.2 使用方式

Template

vue
<template>
  <p>{{ $t('game.gameLobby') }}</p>
</template>

Script

ts
const { t } = useI18n();
const label = t('auth.login');

3.9.3 i18n Key Namespace 列表

以下為從 zh-TW.json 提取的所有一級 namespace 分類。

Namespace用途範例 Key
common通用文字common.notify, common.confirm, common.cancel, common.save, common.loading
auth認證相關auth.login, auth.register, auth.logout, auth.sessionExpired, auth.account, auth.password
game遊戲game.gameLobby, game.recentlyPlayed, game.search, game.noGames, game.players
deposit存款deposit.title, deposit.fiat, deposit.credit, deposit.crypto, deposit.verifyRequired
withdrawal提領withdrawal.title, withdrawal.amount, withdrawal.turnover, withdrawal.request
wallet錢包wallet.title, wallet.bankCard, wallet.creditCard, wallet.crypto
bankCard銀行卡bankCard.add, bankCard.bankCode, bankCard.status.pending, bankCard.status.approved
creditCard信用卡creditCard.add, creditCard.cardNumber, creditCard.holderName
cryptoAddress加密錢包cryptoAddress.add, cryptoAddress.network, cryptoAddress.address
transaction交易紀錄transaction.title, transaction.deposit, transaction.withdrawal, transaction.dividend
vipVIPvip.title, vip.level, vip.progress, vip.benefits, vip.rebate
tierVIP 等級tier.bronze, tier.gold, tier.platinum, tier.diamond
promo活動promo.title, promo.claim, promo.claimed, promo.condition.none, promo.condition.firstDeposit
affiliate代理推廣affiliate.title, affiliate.dashboard, affiliate.downline, affiliate.commission
alliance聯盟alliance.title, alliance.plan, alliance.referralCode
inbox站內信inbox.title, inbox.personal, inbox.system, inbox.markAllRead
mission任務mission.title, mission.daily, mission.weekly, mission.monthly, mission.claim
kycKYC 認證kyc.title, kyc.basicInfo, kyc.document, kyc.liveness, kyc.review
setting個人設定setting.title, setting.email, setting.mobile, setting.password, setting.avatar
theme主題名稱theme.emerald, theme.amber, theme.sky, theme.violet, theme.rose, theme.cyan
format格式化format.deadline
nav導航nav.home, nav.game, nav.promo, nav.vip, nav.affiliate, nav.alliance
footer頁尾footer.copyright, footer.terms, footer.privacy
help幫助help.title, help.faq
error錯誤error.notFound, error.serverError
liveSports即時賽事liveSports.title, liveSports.live, liveSports.odds
ranking排行榜ranking.title, ranking.realtime, ranking.daily, ranking.weekly
betRecord投注紀錄betRecord.title, betRecord.gameType, betRecord.status
challenge挑戰challenge.title
tour代理導覽tour.welcome, tour.step1, tour.apply, tour.dismiss
learnMore了解更多learnMore.title

3.9.4 語系不一致處理

登入後,Layout 偵測 store.getUserDetail.locale 與 cookie locale 是否一致:

  • 不一致時彈出 Modal 提示用戶切換
  • 用戶確認後呼叫 i18n.setLocale() + updateLocale() API

3.9.5 HTTP Header 注入

  • useHttp 自動在每個請求的 locales header 中帶入當前語系
  • 後端依此 header 回傳對應語系的錯誤訊息和多語系欄位

3.10 域名設定(Domain Config)

3.10.1 架構

config/domainConfig/
├── index.ts     # DomainConfigEntry 型別 + resolveDomainConfig() + DEFAULT_DOMAIN
├── a1.ts        # a1 站點域名映射
└── a2.ts        # a2 站點域名映射(預留)

3.10.2 DomainConfigEntry 介面

ts
interface DomainConfigEntry {
  baseUrl: string;      // 後端 API base URL
  imgUrl: string;       // R2 圖片公開 URL
  socketUrl?: string;   // WebSocket URL
  siteId: string;       // 白牌站點 ID(用於 API site-name header)
  layout: string;       // 前台模板代碼(a1, a2)
}

3.10.3 目前域名映射

a1.ts

ts
export const entries: Record<string, DomainConfigEntry> = {
  localhost: {
    baseUrl: 'http://localhost:8080',
    imgUrl: 'https://pub-8ad0488587374373b2fd30c725fac6bc.r2.dev',
    siteId: 'a2',
    layout: 'a2',
  },
  'c9-ec.zeabur.app': {
    baseUrl: 'https://c9-be.zeabur.app',
    imgUrl: 'https://pub-8ad0488587374373b2fd30c725fac6bc.r2.dev',
    siteId: 'a1',
    layout: 'a1',
  },
};

注意:localhost 目前對應 siteId: 'a2', layout: 'a2'(開發時使用 A2 佈局)。

a2.ts

ts
export const entries: Record<string, DomainConfigEntry> = {
  // 預留給 A2 站點的域名映射
};

3.10.4 解析函式

ts
const DEFAULT_DOMAIN = 'localhost';

function resolveDomainConfig(hostname: string): DomainConfigEntry {
  return domainConfig[hostname] ?? domainConfig[DEFAULT_DOMAIN]!;
}
  • 精確匹配 hostname
  • 找不到時 fallback 至 localhost 設定

3.10.5 新增站點流程

  1. 建立 config/domainConfig/{siteCode}.ts 檔案
  2. 匯出 entries Record
  3. config/domainConfig/index.ts import 並展開至 domainConfig
  4. 確保後端 site-config 表有對應記錄

3.11 路由守衛與中介層

3.11.1 auth.global.ts — 全域認證守衛

項目說明
檔案app/middleware/auth.global.ts
類型全域中介層(.global.ts 後綴自動載入)
保護路徑/user/*
ts
const PROTECTED_PREFIXES = ['/user'];

export default defineNuxtRouteMiddleware((to) => {
  const needsAuth = PROTECTED_PREFIXES.some((p) => to.path.startsWith(p));
  if (!needsAuth) return;

  const token = useCookie<string | null>('token', { path: '/' });
  if (token.value) return;

  // 記住目標頁面
  const redirectCookie = useCookie<string | null>('redirectTo', { path: '/' });
  redirectCookie.value = to.fullPath;

  // 開啟登入 Modal
  const loginModalOpen = useState('loginModalOpen', () => false);
  loginModalOpen.value = true;

  return navigateTo('/');
});

攔截流程

  1. 檢查目標路徑是否在 PROTECTED_PREFIXES
  2. 檢查 token cookie 是否存在
  3. 無 token → 存目標路徑至 redirectTo cookie → 開啟登入 Modal → 導回首頁
  4. 登入成功後,Login Modal 讀取 redirectTo cookie 並導回原始目標頁

3.12 工具函式(Utils)

3.12.1 utsFormat — 格式化工具

項目說明
檔案app/utils/utsFormat.ts
依賴moment-timezone, useI18n()
方法參數回傳說明
formatNumberval, minDecimals=0, maxDecimals=2string千分位數字格式化
formatAmountvalstring固定 2 位小數金額
formatAmountisFinitevalstring安全金額格式化(非數值回傳
formatDatedateStr, fmt='YYYY/MM/DD'string日期格式化
formatDateTimedateStrstring日期時間(YYYY-MM-DD HH:mm)
formatDateHasdateStrstring有值顯示日期時間,無值顯示
formatDateShortdateStrstring短日期(MM/DD + 截止)
formatTimeisoStrstring時間(上午/下午 HH:mm)

3.12.2 utsVipTier — VIP 等級樣式

項目說明
檔案app/utils/utsVipTier.ts
依賴useI18n()

VipTierStyle 介面

ts
interface VipTierStyle {
  bg: string;         // 背景漸層 class
  badge: string;      // 徽章 class
  bar: string;        // 進度條 class
  text: string;       // 文字色 class
  badgeColor: string; // UBadge color
  icon: string;       // emoji 圖示
  ring: string;       // 邊框 class
}

4 種等級樣式

Tier文字色圖示Badge Color
bronzetext-amber-500noto:3rd-place-medalwarning
goldtext-amber-400noto:1st-place-medalwarning
platinumtext-blue-400noto:gem-stoneinfo
diamondtext-purple-400noto:crownerror

方法

方法說明
getStyle(tier)取得 tier 樣式(fallback 至 bronze)
getLabel(tier)取得 tier 多語系名稱

3.12.3 utsPromo — 活動工具

項目說明
檔案app/utils/utsPromo.ts
依賴useI18n(), useMainStore(), useApi()
方法說明
getTagColor(tag)取得活動標籤 UBadge 顏色(API 優先,fallback 至 FALLBACK_COLOR_MAP)
getTagLabel(tag)取得活動標籤多語系顯示文字
CONDITION_MAP活動條件類型映射(none, first_deposit, deposit_threshold, vip_level)
fetchPromoTags()取得活動標籤並存入 store

3.12.4 utsBankCard — 銀行卡工具

項目說明
檔案app/utils/utsBankCard.ts
常數說明
STATUS_MAP[0]{ label: '待審核', color: 'warning' }
STATUS_MAP[1]{ label: '已通過', color: 'success' }
STATUS_MAP[2]{ label: '已拒絕', color: 'error' }

3.12.5 gameConstants — 遊戲常數

項目說明
檔案app/utils/gameConstants.ts

GAME_TYPE_VALUE_ENUM

KeyValue說明
sports1體育
slot2老虎機
live3真人
lottery4彩票
chess5棋牌
esports8電競
crypto9加密遊戲
fish10捕魚

CHILD_GAME_TYPES

ts
const CHILD_GAME_TYPES = [2, 5, 9, 10] as const;
// slot, chess, crypto, fish — 這些類型含子遊戲列表

PROVIDER_LOGOS

63 個遊戲供應商的 Logo 映射,格式為 { name: string; logo: string }[]

3.12.6 themePresets — 主題預設

見 3.8.4 章節。

3.12.7 themePresetsA2 — A2 主題預設

見 3.8.5 章節。


3.13 CSS 樣式系統

3.13.1 main.css 結構

css
@import "tailwindcss";
@import "@nuxt/ui";

/* Theme Color Bridge — 見 3.8.2 */
@theme { ... }

:root { --c9-glow: var(--c9-primary-glow, 16, 185, 129); }

/* scrollbar-hide utility */
@utility scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
  &::-webkit-scrollbar { display: none; }
}

/* Sidebar 隱藏捲軸 */
[data-slot="root"][data-collapsed] * { scrollbar-width: none; }

/* Modal/Dialog/Dropdown 隱藏捲軸 */
[role="dialog"] * { scrollbar-width: none; }

3.13.2 設計規範

規範說明
暗色主題固定暗色,無 dark: 變體
背景色bg-[#0a1120], bg-[#131f30], bg-slate-800/900
文字層次text-white/80(主要)、text-white/60(次要)、text-white/40(提示)
主題色emerald-* class(透過 CSS 變數動態切換)
發光效果shadow-[0_0_16px_-2px_rgba(var(--c9-glow),0.4)]
玻璃卡片bg-white/5 ring-1 ring-white/10
響應式Mobile-first(sm:, lg:, xl:
圖示Lucide(<Icon name="i-lucide-xxx" />)+ Noto emoji

3.13.3 自訂 CSS Utilities

Utility說明
scrollbar-hide隱藏捲軸但保持可捲動

3.14 測試

3.14.1 單元測試(Vitest)

項目說明
設定檔vitest.config.ts
兩個 Projectunit(node 環境)+ nuxt(@nuxt/test-utils + happy-dom)
指令說明
yarn test跑全部測試
yarn test:unit只跑單元測試
yarn test:nuxt只跑元件測試
yarn test:watchWatch 模式

3.14.2 E2E 測試(Playwright)

項目說明
設定檔playwright.config.ts
瀏覽器Desktop Chromium
並行fullyParallel: true
CI 重試2 次
指令說明
yarn test:e2e跑全部 E2E
yarn test:e2e:uiUI 模式

3.14.3 Vitest 設定檔完整內容

ts
// vitest.config.ts
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { defineVitestProject } from '@nuxt/test-utils/config'

export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['test/unit/*.{test,spec}.ts'],
          environment: 'node',
        },
      },
      await defineVitestProject({
        test: {
          name: 'nuxt',
          include: ['test/nuxt/*.{test,spec}.ts'],
          environment: 'nuxt',
          environmentOptions: {
            nuxt: {
              rootDir: fileURLToPath(new URL('.', import.meta.url)),
              domEnvironment: 'happy-dom',
            },
          },
        },
      }),
    ],
  },
})

兩個 Project 的差異

屬性unitnuxt
環境node(純邏輯測試)nuxt(@nuxt/test-utils,模擬 Nuxt 運行環境)
DOMhappy-dom(輕量 DOM 模擬)
適用場景工具函式、型別、常數Vue 元件渲染、composable 測試
目錄test/unit/test/nuxt/

3.14.4 Playwright 設定檔完整內容

ts
// playwright.config.ts
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'

export default defineConfig<ConfigOptions>({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
    nuxt: {
      rootDir: fileURLToPath(new URL('.', import.meta.url)),
    },
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
})

設定詳細說明

屬性說明
testDir./testsE2E 測試檔案目錄
fullyParalleltrue測試案例完全並行執行
forbidOnlyCI 環境為 trueCI 環境禁止 .only
retriesCI: 2, 本地: 0CI 環境失敗重試 2 次
workersCI: 1, 本地: autoCI 環境單 worker,本地依 CPU
reporter'html'產生 HTML 格式報告
trace'on-first-retry'首次重試時收集 trace
nuxt.rootDir專案根目錄@nuxt/test-utils 整合
projectsChromium僅使用 Desktop Chrome 測試

3.14.5 測試目錄結構

c9-ec/
├── test/
│   ├── unit/                    # Vitest 單元測試 (node 環境)
│   │   └── *.{test,spec}.ts     # 工具函式、常數、邏輯測試
│   └── nuxt/                    # Vitest + @nuxt/test-utils (happy-dom)
│       └── *.{test,spec}.ts     # Vue 元件渲染、composable 測試
└── tests/                       # Playwright E2E 測試
    └── *.spec.ts                # 端對端功能測試

3.15 佈局系統(Layouts)

3.15.1 佈局總覽

佈局檔案風格說明
a1layouts/a1.vue暗色科技風Dashboard 式佈局,側邊欄 + 頂部導航
a2layouts/a2.vue奢華深金風頂部導航列 + 固定右側面板

3.15.2 A1 佈局架構

UDashboardGroup (storage: cookie, key: c9-dashboard)
├── A1LayoutSidebar            # 側邊欄(可收合,含遊戲類型/導航/客服)
└── UDashboardPanel            # 主內容面板
    ├── #header
    │   └── UDashboardNavbar   # 頂部導航列
    │       ├── #title         # Logo(手機: logoSmall / 桌面: logoBig)
    │       └── #right         # 餘額、存款、使用者下拉選單
    └── #body
        ├── <slot />           # 頁面內容
        ├── A1LayoutFooter     # 頁尾
        └── A1LayoutBottomBar  # 行動版底部導航

全域 Modal(掛載在佈局層級)

Modal用途
A1ModalAgentTour代理推廣導覽
A1ModalBuyCrypto購買加密貨幣引導
A1ModalContactSupport聯繫客服
A1LayoutLiveChatLiveChat 腳本注入
A1ModalSetPassword設定密碼(三方登入新玩家)
Locale Mismatch Dialog語系不一致提示

A1 佈局特色

特性說明
側邊欄收合UDashboardGroup 管理 Cookie 持久化
餘額刷新動畫旋轉圖示 + refreshUserData()
使用者選單UDropdownMenu 含 6 組功能分類,支援鍵盤快捷鍵
VIP 徽章導航列內嵌 VIP 等級標籤
站內信手機版通知鈴鐺含未讀數量 badge
路由置頂路由切換後自動捲至頂部(桌面: panel / 手機: window)
CSS 規範bg-[#0a1120] 深色底、emerald 主題色、ring-white/10 邊框

3.15.3 A2 佈局架構

div.a2-layout (CSS 變數注入)
├── A2LayoutNavbar              # 頂部導航列(sticky, 含動態遊戲選單)
├── main (max-w-[1400px])       # 主內容區域
│   └── <slot />                # 頁面內容
├── A2LayoutFooter              # 頁尾
├── A2LayoutBottomBar           # 行動版底部導航
├── A2ModalAgentTour            # 代理導覽
├── Locale Mismatch Dialog      # 語系不一致提示
├── A2ModalBuyCrypto            # 購買加密貨幣
├── A2ModalContactSupport       # 聯繫客服
├── A2LayoutLiveChat            # LiveChat 腳本
└── A2ModalSetPassword          # 設定密碼

A2 佈局特色

特性說明
佈局風格奢華精品風,暗色金調
導航模式水平頂部導航列(取代 A1 的側邊欄)
固定右側面板桌面版登入後顯示固定右側操作面板
CSS 變數體系完全獨立的 --a2-* 變數系統
圓角規範rounded-xl / rounded-2xl(較 A1 更圓潤)
動畫效果行動選單展開使用 Transition 動畫
遊戲選單桌面版 UDropdownMenu 含動態遊戲類型子項
幫助選單可展開式手機選單(7 個幫助子頁面)

3.15.4 A2 CSS 變數系統

A2 佈局使用完全獨立的 CSS 變數體系,定義在 layouts/a2.vue<style> 區塊內。

背景色變數

變數用途
--a2-bg-page#141110頁面背景
--a2-bg-card#1e1a16卡片背景
--a2-bg-elevated#262220提升區塊背景
--a2-bg-navbarrgba(20, 17, 16, 0.95)導航列背景(半透明毛玻璃)
--a2-bg-footer#0f0c08頁尾背景
--a2-bg-hoverrgba(255, 255, 255, 0.04)Hover 狀態
--a2-bg-activergba(255, 255, 255, 0.08)Active 狀態

文字色變數

變數用途
--a2-text-primary#f5f0e8主要文字(暖白)
--a2-text-secondary#c4b8aa次要文字
--a2-text-muted#8a7e72靜音文字
--a2-text-hint#5a5048提示文字
--a2-text-on-dark#f5f0e8深色背景上的文字

邊框變數

變數用途
--a2-border-subtlergba(255, 255, 255, 0.04)極細邊框
--a2-border-defaultrgba(255, 255, 255, 0.08)預設邊框
--a2-border-strongrgba(255, 255, 255, 0.15)強調邊框
--a2-border-lightrgba(255, 255, 255, 0.06)淡色邊框

主色調變數(暗面金色系,可被主題系統覆寫)

變數說明
--a2-primary-50rgba(184, 148, 63, 0.06)極淡
--a2-primary-100rgba(184, 148, 63, 0.12)
--a2-primary-200rgba(184, 148, 63, 0.2)
--a2-primary-300rgba(184, 148, 63, 0.3)中淡
--a2-primary-400var(--c9-primary-400, #c99240)中(可被主題覆寫)
--a2-primary-500var(--c9-primary-500, #b07a30)中強(可被主題覆寫)
--a2-primary-600var(--c9-primary-600, #946328)強(可被主題覆寫)
--a2-primary-700var(--c9-primary-700, #7a5022)深(可被主題覆寫)
--a2-primary-800#d4b86a金色亮面
--a2-primary-900#c4a85a金色中面
--a2-primary-950#b8943f金色暗面

金色系變數

變數用途
--a2-gold#b8943f金色主色
--a2-gold-light#d4b86a金色亮面
--a2-gold-dark#8a6e2e金色暗面

陰影變數

變數
--a2-shadow-sm0 1px 3px rgba(0, 0, 0, 0.3)
--a2-shadow-md0 4px 16px rgba(0, 0, 0, 0.35)
--a2-shadow-lg0 12px 40px rgba(0, 0, 0, 0.4)

其他

變數用途
--a2-radius1rem預設圓角

Nuxt UI 主色覆寫

A2 佈局在 .a2-layout scope 內覆寫 Nuxt UI 的 --ui-color-primary-* 變數:

css
.a2-layout {
  --ui-primary: var(--a2-primary-500);
  --ui-color-primary-50: var(--a2-primary-50);
  --ui-color-primary-100: var(--a2-primary-100);
  /* ... 50~950 全部覆寫 ... */
  --ui-color-primary-950: var(--a2-primary-950);
}

輔助 CSS Class

Class說明
.a2-band-elevated交替帶區塊(覆寫 --a2-bg-page: #1a1612
.a2-full-bleed突破 max-width 容器延伸到視窗邊緣

3.15.5 A2 導航列結構(Navbar)

A2 Navbar 是一個複雜的元件(940 行),包含以下主要區塊:

桌面版(lg+)

<header> (sticky top-0, backdrop-blur)
├── Logo (NuxtLink + NuxtImg)
├── Desktop Navigation (nav)
│   ├── Home link
│   ├── Alliance link
│   ├── Casino Dropdown (UDropdownMenu, 動態遊戲類型)
│   ├── Promo link
│   ├── VIP link
│   ├── Mission link
│   ├── Challenges link
│   └── Help link
├── Search (UContentSearchButton + A2GameSearch)
├── Logged-in: Inbox + Balance + Deposit + User Dropdown
└── Not logged-in: Theme + Locale + Login + Register

手機版(< lg)

├── Hamburger Menu Button
└── Slide-Down Mobile Menu (Transition 動畫)
    ├── Main nav links (6 項)
    ├── Casino sub-items (可展開,動態遊戲類型)
    ├── Help sub-items (可展開,7 個幫助頁)
    ├── Action buttons (購買加密貨幣、聯繫客服)
    ├── Affiliate link (代理才顯示)
    └── Theme + Locale toggles

桌面版固定右側面板(lg+, 登入後顯示)

<aside> (fixed right-3, top-50%, -translate-y-50%)
├── Notifications (system + promo 分開)
├── Balance display + refresh
├── Deposit button
├── User dropdown (avatar + VIP badge)
└── Theme + Locale toggles

3.15.6 佈局選擇機制

佈局由 useConfig() 解析域名時決定:

hostname → domainConfig[hostname].layout → layoutName
app.vue: const layoutName = (layout || 'a1') as 'a1' | 'a2'
→ <NuxtLayout :name="layoutName"> 動態切換佈局

SplashScreen 也依佈局切換

html
<A2LayoutSplashScreen v-if="layoutName === 'a2'" />
<CommonSplashScreen v-else />

3.15.7 A1 vs A2 設計對比

特性A1A2
導航模式側邊欄(UDashboardGroup頂部水平導航列
色調科技深藍(#0a1120奢華深金(#141110
主題色寶石色系(emerald/amber/sky/violet/rose/cyan)金屬色系(champagne/roseGold/platinum/onyx/sapphire/burgundy)
邊框ring-white/10ring-[var(--a2-border-default)]
文字text-white/80text-[var(--a2-text-primary)](暖白 #f5f0e8
按鈕bg-emerald-500 漸層金色漸層(--a2-gold
圓角rounded-full / rounded-xlrounded-xl / rounded-2xl
動畫基本精緻(SVG 描邊環、星點閃爍、金色掃光線)
發光效果rgba(var(--c9-glow), 0.4)rgba(184, 148, 63, 0.3)
SplashScreen簡單淡出奢華動畫(SVG 金環描邊 + 呼吸光暈 + 星點)
桌面面板無額外面板固定右側操作面板
遊戲選單側邊欄內嵌頂部 Dropdown 下拉選單
幫助中心單一連結可展開子選單(7 個子頁面)

3.15.8 A2 SplashScreen 動畫詳解

A2 SplashScreen 包含多層精緻動畫:

動畫CSS Class效果
SVG 描邊環.a2s-ring金色圓環描邊動畫(2.8s 循環)
光點追蹤.a2s-ring-glow短弧線跟隨主環(blur 效果)
Logo 進場.a2s-logo-enter縮放進場 + 呼吸式光暈
背景呼吸.a2s-breathe徑向漸層光暈脈動(5s 循環)
星點閃爍.a2s-sparkle10 個金色星點隨機閃爍(4s 循環)
掃光線.a2s-sweep金色水平掃光線(2s 循環)
文字淡入.a2s-text-fade"Loading" 文字淡入 + 脈動
離場.a2-splash-leave-*1s 淡出 + 模糊效果

3.16 頁面路由詳細文件

3.16.1 頁面總覽(20 頁)

所有頁面均使用 vueVersion() plugin 實現佈局切換(A1/A2 動態載入)。

app/pages/
├── index.vue                    # 首頁
├── alliance.vue                 # 聯盟計劃
├── challenges.vue               # 挑戰 (mock data)
├── mission.vue                  # 任務系統
├── help/index.vue               # 幫助中心
├── game/index.vue               # 遊戲大廳
├── game/play.vue                # 遊戲啟動
├── promo/index.vue              # 活動中心
├── promo/[id].vue               # 活動詳情
├── redirect/[action].vue        # 重導向處理 (OAuth callback)
├── user/affiliate.vue           # 代理推廣
├── user/bet-record.vue          # 投注紀錄
├── user/deposit.vue             # 存款
├── user/inbox.vue               # 站內信
├── user/kyc.vue                 # KYC 認證
├── user/setting.vue             # 個人設定
├── user/transaction.vue         # 交易紀錄
├── user/vip.vue                 # VIP 中心
├── user/wallet.vue              # 錢包管理
└── user/withdrawal.vue          # 提領

3.16.2 頁面 vueVersion 映射表

頁面路由A1 元件A2 元件
/A1/Home/index.vueA2/Home/index.vue
/allianceA1/Alliance/index.vueA2/Alliance/index.vue
/challengesA1/Mission/index.vueA2/Mission/index.vue
/missionA1/Mission/index.vueA2/Mission/index.vue
/helpA1/Help/Center.vueA2/Help/Center.vue
/gameA1/Game/index.vueA2/Game/index.vue
/game/playA1/Game/Play.vueA2/Game/Play.vue
/promoA1/Promo/Center.vueA2/Promo/Center.vue
/promo/[id]A1/Promo/Detail.vueA2/Promo/Detail.vue
/user/affiliateA1/User/Affiliate/index.vueA2/User/Affiliate/index.vue
/user/bet-recordA1/User/BetRecord/index.vueA2/User/BetRecord/index.vue
/user/depositA1/User/Deposit/index.vueA2/User/Deposit/index.vue
/user/inboxA1/User/Inbox/index.vueA2/User/Inbox/index.vue
/user/kycA1/User/Kyc/index.vueA2/User/Kyc/index.vue
/user/settingA1/User/Setting.vueA2/User/Setting.vue
/user/transactionA1/User/Transaction/index.vueA2/User/Transaction/index.vue
/user/vipA1/User/Vip/index.vueA2/User/Vip/index.vue
/user/walletA1/User/Wallet/index.vueA2/User/Wallet/index.vue
/user/withdrawalA1/User/Withdrawal/index.vueA2/User/Withdrawal/index.vue

3.16.3 標準頁面模式(vueVersion 模式)

redirect/[action].vue 外,所有頁面均使用統一的 vueVersion 模式:

vue
<template>
  <div>
    <component :is />
  </div>
</template>
<script setup>
const { is } = vueVersion({
  A1: () => import('@/components/A1/{Module}/{Component}.vue'),
  A2: () => import('@/components/A2/{Module}/{Component}.vue'),
});
</script>
  • vueVersion() 讀取 useConfig().layout 並轉大寫('a1''A1'
  • 使用 defineAsyncComponent() 動態載入對應元件
  • 頁面本身不包含任何業務邏輯,僅作為路由入口

3.16.4 特殊頁面:redirect/[action].vue

此頁面處理 OAuth 回調,不使用 vueVersion 模式:

/redirect/google?code=xxx&state=xxx
  → handlers.google()
    → 檢查 bindMode cookie(setting 頁綁定 vs 登入)
    → 綁定模式:呼叫 bindGoogle API → 導向 /setting
    → 登入模式:呼叫 loginGoogle API → setToken → 導向 redirectAfter 或 /

/redirect/line
  → handlers.line()(預留,尚未實作)

安全機制

機制說明
safePath()防止開放重導向(必須以 / 開頭,不可為 //
str()安全型別轉換(處理陣列/未定義值)
oauth_bind_mode cookie區分綁定模式 vs 登入模式

3.16.5 路由保護範圍

路徑前綴保護說明
/user/*需登入auth.global.ts 守衛
/公開首頁
/game公開遊戲大廳(啟動遊戲需登入)
/game/play公開入口元件內部檢查登入狀態
/promo公開活動中心
/promo/[id]公開活動詳情
/alliance公開聯盟計劃
/mission公開任務系統
/challenges公開挑戰
/help公開幫助中心
/redirect/*公開OAuth 回調

3.16.6 路由滾動行為

ts
// router.options.ts
export default <RouterConfig>{
  scrollBehavior(to, _from, savedPosition) {
    if (savedPosition) return savedPosition;  // 瀏覽器返回/前進時還原位置
    if (to.hash) return { el: to.hash, behavior: 'smooth' };  // hash 錨點
    return { top: 0, left: 0 };  // 預設捲至頂部
  },
};

3.17 鍵盤快捷鍵完整參考

3.17.1 A1 佈局快捷鍵

快捷鍵動作目標路徑
Shift+H首頁/
Shift+G遊戲大廳/game
Shift+P活動中心/promo
Shift+VVIP 中心/user/vip
Shift+M任務/mission
Shift+F聯盟計劃/alliance
Shift+E挑戰/challenges
Shift+B購買加密貨幣開啟 Modal
Shift+C聯繫客服開啟 Modal
Shift+A代理推廣/user/affiliate
Shift+Q幫助中心/help
Shift+S搜尋開啟搜尋
快捷鍵動作
M切換使用者選單
D存款
W提領
Shift+U個人設定
Shift+YKYC 認證
Shift+I站內信
Shift+D存款
Shift+W提領
Shift+K錢包管理
Shift+R交易紀錄
Shift+B投注紀錄
Shift+Meta+Q登出

3.17.2 A2 佈局快捷鍵

A2 佈局包含 A1 的所有快捷鍵,額外新增遊戲類型和幫助子頁面的 Chord 快捷鍵:

遊戲類型 Chord 快捷鍵(G 開頭)

快捷鍵動作目標路徑
G → 0遊戲大廳/game?tab=gameLobby
G → 1真人/game?tab=live
G → 2老虎機/game?tab=slot
G → 3棋牌/game?tab=chess
G → 4體育/game?tab=sports
G → 5彩票/game?tab=lottery
G → 6電競/game?tab=esports
G → 7加密遊戲/game?tab=crypto
G → 8捕魚/game?tab=fish
G → 9遊戲供應商/game?tab=gameProvider

幫助頁面 Chord 快捷鍵(Q 開頭)

快捷鍵動作目標路徑
Q → 1關於我們/help?type=aboutUs
Q → 2公平性/help?type=fair
Q → 3服務條款/help?type=terms
Q → 4隱私政策/help?type=privacy
Q → 5常見問題/help?type=faq
Q → 6負責任博彩/help?type=gambling
Q → 7免責聲明/help?type=disclaimer

3.18 Nuxt 設定檔詳解

3.18.1 nuxt.config.ts 完整設定

ts
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  devServer: { port: 3010 },

  app: {
    head: {
      meta: [
        {
          name: 'viewport',
          content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no',
        },
      ],
    },
  },

  modules: [
    '@nuxt/eslint',
    '@nuxt/hints',
    '@nuxt/image',
    '@nuxt/scripts',
    '@nuxt/test-utils',
    '@nuxt/ui',
    '@nuxtjs/i18n',
    '@nuxt/content',
    '@pinia/nuxt',
  ],

  css: ['~/assets/css/global.scss', '~/assets/css/main.css'],

  i18n: {
    strategy: 'no_prefix',
    defaultLocale: 'zh-TW',
    langDir: 'locales',
    locales: [
      { code: 'zh-TW', iso: 'zh-TW', name: '繁體中文', file: 'zh-TW.json' },
      { code: 'en-US', iso: 'en-US', name: 'English', file: 'en-US.json' },
      { code: 'zh-CN', iso: 'zh-CN', name: '简体中文', file: 'zh-CN.json' },
      { code: 'th-TH', iso: 'th-TH', name: 'ภาษาไทย', file: 'th-TH.json' },
      { code: 'vi-VN', iso: 'vi-VN', name: 'Tiếng Việt', file: 'vi-VN.json' },
    ],
  },
});

3.18.2 設定詳細說明

設定項說明
compatibilityDate'2025-07-15'Nuxt 相容性日期
devtools{ enabled: true }開啟 Vue DevTools
devServer.port3010開發伺服器端口
app.head.meta.viewport...user-scalable=no禁止使用者縮放(搭配 usePreventZoom
css['~/assets/css/global.scss', '~/assets/css/main.css']全域樣式載入順序

3.18.3 app.config.ts

ts
export default defineAppConfig({
  ui: {
    colors: {
      primary: 'emerald',     // Nuxt UI 主色 = emerald
      success: 'emerald',     // 成功色也用 emerald
    },
    modal: {
      slots: {
        overlay: 'z-50',      // Modal overlay z-index
        content: 'z-50',      // Modal content z-index
      },
    },
  },
});

3.18.4 i18n 設定說明

設定項說明
strategy'no_prefix'URL 不帶語系前綴,locale 存 cookie
defaultLocale'zh-TW'預設繁體中文
langDir'locales'語系 JSON 檔案目錄(相對於 i18n/ 資料夾)
locales5 個語系物件每個含 code, iso, name, file

3.19 A2 元件完整列表

3.19.1 A2 元件目錄結構

A2 佈局共有 75 個 Vue 元件,結構與 A1 完全對稱,但設計風格為奢華金色調。

components/A2/
├── Alliance/         # 聯盟計劃 (1 個)
├── Game/             # 遊戲相關 (10 個)
├── Help/             # 幫助中心 (1 個)
├── Home/             # 首頁 (4 個)
├── Layout/           # 佈局元件 (5 個)
├── Mission/          # 任務 (1 個)
├── Modal/            # 彈窗 (15 個)
├── Promo/            # 活動 (2 個)
├── PromoLinkCard.vue # 活動連結卡片 (1 個)
└── User/             # 用戶相關 (35 個)

3.19.2 A2 Layout 元件(5 個)

元件檔案用途A1 對應
A2LayoutNavbarLayout/Navbar.vue頂部導航列(940 行,含動態遊戲選單、固定右側面板)A1LayoutSidebar + a1.vue Navbar
A2LayoutFooterLayout/Footer.vue頁尾A1LayoutFooter
A2LayoutBottomBarLayout/BottomBar.vue行動版底部導航A1LayoutBottomBar
A2LayoutLiveChatLayout/LiveChat.vueLiveChat 腳本注入A1LayoutLiveChat
A2LayoutSplashScreenLayout/SplashScreen.vue奢華啟動動畫(208 行,含 SVG 金環動畫)CommonSplashScreen

3.19.3 A2 Home 元件(4 個)

元件檔案用途
A2HomeHome/index.vue首頁組合(Banner + LiveSports + Promo + Lobby + Rankings + FAQ + Partners)
A2HomeBannerHome/Banner.vue首頁輪播 Banner
A2HomeLiveSportsHome/LiveSports.vue即時體育賽事
A2HomePromoHome/Promo.vue首頁活動推薦

A2 首頁組合結構

A2Home
├── A2HomeBanner         # Banner 輪播
├── A2HomeLiveSports     # 即時賽事
├── A2HomePromo          # 活動推薦
├── A2GameLobby          # 遊戲大廳
├── A2GameRankList       # 排行榜
├── FAQ (UAccordion)     # 常見問題(優先後台設定,fallback i18n)
└── Partners Grid        # 遊戲供應商 Logo 網格

3.19.4 A2 Game 元件(10 個)

元件檔案用途
A2GameGame/index.vue遊戲模組入口
A2GameLobbyGame/Lobby.vue遊戲大廳(含最近遊玩)
A2GamePlayGame/Play.vue遊戲啟動 iframe
A2GameSearchGame/Search.vue遊戲搜尋
A2GameProviderGame/Provider.vue遊戲供應商卡片
A2GameRankListGame/RankList.vue排行榜
A2GameListBarGame/ListBar.vue水平捲動列表
A2GameLoadMoreGame/LoadMore.vue載入更多按鈕
A2GameEmptyGame/Empty.vue空狀態提示

3.19.5 A2 Modal 元件(15 個)

元件檔案用途
A2ModalLoginModal/Login.vue登入
A2ModalRegisterModal/Register.vue註冊
A2ModalEditPasswordModal/EditPassword.vue修改密碼
A2ModalSetPasswordModal/SetPassword.vue設定密碼
A2ModalThemeModal/Theme.vue主題切換
A2ModalLocaleModal/Locale.vue語系切換
A2ModalVerifyUserInfoModal/VerifyUserInfo.vue用戶資訊驗證
A2ModalContactSupportModal/ContactSupport.vue聯繫客服
A2ModalBuyCryptoModal/BuyCrypto.vue購買加密貨幣引導
A2ModalBindGoogleAuthModal/BindGoogleAuth.vueGoogle Authenticator 2FA
A2ModalAddBankCardModal/AddBankCard.vue新增銀行卡
A2ModalAddCreditCardModal/AddCreditCard.vue新增信用卡
A2ModalAddCryptoAddressModal/AddCryptoAddress.vue新增加密錢包
A2ModalBankCardDetailModal/BankCardDetail.vue銀行卡詳情
A2ModalAgentTourModal/AgentTour.vue代理推廣導覽

3.19.6 A2 User 元件(35 個)

子目錄數量元件
(根)1Setting
BetRecord/1index
Deposit/4index, Fiat, Credit, Crypto
Withdrawal/1index
Wallet/4index, Fiat, Credit, Crypto
Transaction/5index, Deposit, Withdrawal, Dividend, Promo
Vip/6index, StatusCard, LevelList, Benefits, MyRebates, RebateTable
Affiliate/7index, Dashboard, Downline, Commission, Settlement, Withdrawal, Alliance
Inbox/1index
Kyc/6index, StatusCard, StepBasicInfo, StepDocUpload, StepLiveness, StepReview

3.19.7 A2 其他元件

元件檔案用途
A2AllianceAlliance/index.vue聯盟計劃
A2HelpCenterHelp/Center.vue幫助中心
A2MissionMission/index.vue任務列表
A2PromoCenterPromo/Center.vue活動中心
A2PromoDetailPromo/Detail.vue活動詳情
A2PromoLinkCardPromoLinkCard.vue活動連結卡片

3.20 業務邏輯流程圖

3.20.1 認證流程

用戶訪問受保護頁面
  → auth.global.ts 攔截
    → 無 token → 儲存 redirectTo cookie → 開啟 Login Modal → 導回 /
    → 有 token → 放行

登入流程:
  Login Modal → 輸入帳密 → POST /auth/login (帶 device fingerprint)
    → 成功 → setToken() cookie (48hr) → refreshUserData()
      → 檢查 redirectTo cookie → 導回原始頁面
      → layout watcher 檢查 hasPassword → 無密碼 → 開啟 SetPassword Modal
      → layout watcher 檢查 locale mismatch → 不一致 → 開啟 Locale Modal
    → 失敗 → ERROR_CODES 查表 → toast 顯示錯誤

Google OAuth 登入:
  Login Modal → 點擊 Google → GET /auth/google/url → window.location = authUrl
    → Google 認證 → redirect 回 /redirect/google?code=xxx&state=xxx
      → 檢查 oauth_bind_mode cookie
        → 綁定模式 → bindGoogle API → 導回 /setting
        → 登入模式 → loginGoogle API → setToken → refreshUserData → 導回 redirectAfter

登出:
  用戶選單 → logout() → 清除 token cookie → 清除 store → 導回首頁

3.20.2 存款流程

/user/deposit 頁面
  → useCash() 初始化
    → usePaymentChannels().fetchChannels() → 取得可用金流通道
    → useExchangeRate().fetchRates() → 取得即時匯率

用戶選擇支付方式 (Fiat / Credit / Crypto)
  → Fiat:填寫金額 → 顯示 TWD 轉換 → POST /deposit → 取得付款連結/ATM 資訊
  → Credit:填寫金額 + 信用卡資訊 → POST /deposit → 跳轉付款頁
  → Crypto:選擇幣種/網路 → 顯示 USDT 匯率 → POST /deposit → 取得收款地址 + QR Code

後端處理:
  → 建立 deposit-order (pending)
  → 路由至對應金流商 (wantong / usdt)
  → 金流商回調 → 更新訂單狀態
  → 成功 → 更新用戶餘額 → 觸發存款任務進度

3.20.3 遊戲啟動流程

遊戲大廳 → 點擊遊戲卡片
  → 未登入 → 開啟 Login Modal
  → 已登入 → 導航至 /game/play?provider=xxx&gameCode=xxx

Play 頁面:
  → POST /game/launch → 取得遊戲 URL + session token
    → 失敗碼 5010 → 遊戲黑名單攔截
    → 成功 → iframe 載入遊戲 URL

遊戲中:
  → 下注 → S2S callback → debit 扣款 → 更新 bet-order
  → 派彩 → S2S callback → credit 加款 → 更新 bet-detail
  → 投注結算後自動連動:VIP 等級重算 + 活動打碼量 + 任務進度

離開遊戲:
  → 關閉 iframe → 模擬結束遊戲 Modal → 回到遊戲大廳

3.21 元件總數統計

3.21.1 完整元件數量

分類A1A2Common合計
Layout55010
Home4408
Game1010020
User4035075
Modal1615031
Promo3306
Alliance1102
Mission1102
Help1102
SplashScreen0022
ConfirmDialog0000
合計81752158

注:A1 多出的元件主要在 Modal(VerifyUserInfo 額外一個變體)和 User(A1 含更多分頁子元件)。

3.21.2 Composable 數量

分類數量
業務邏輯20
API(模組化)12
型別定義13
合計45

3.21.3 完整專案檔案統計

類別數量說明
Pages20檔案路由
Components158A1 (81) + A2 (75) + Common (2)
Composables4520 業務 + 12 API + 13 型別
Plugins42 通用 + 2 client-only
Stores54 feature + 1 facade
Utils7工具函式
Layouts2a1 + a2
Middleware1auth.global.ts
Config3domainConfig/index + a1 + a2
i18n Locales5zh-TW, en-US, zh-CN, th-TH, vi-VN
Theme Presets12A1 (6) + A2 (6)

第 4 章:後台 (c9-ims) 技術規格

4.1 專案總覽

項目說明
專案名稱c9-ims (後台管理系統)
框架Next.js 16.1.6 (React 19.2.3 + TypeScript 5, strict)
UI 元件庫shadcn/ui new-york (Radix UI + Tailwind CSS v4)
狀態管理Zustand v5.0.11 (客戶端 UI + enum + 站點篩選) + TanStack React Query v5.90.21 (伺服器狀態)
多語系next-intl v4.8.3 (zh-TW, en-US, zh-CN, th-TH, vi-VN), localePrefix: "never"
認證NextAuth 5.0.0-beta.30 (JWT 策略 + Credentials Provider)
表格SimpleTable (真實 API 頁) / DataTable (Demo 頁, TanStack React Table v8.21)
表單React Hook Form v7.71 + Zod v4.3.6 (登入頁) / useState + safeParse (Admin 表單)
API 客戶端Axios 1.13.5 (動態 baseURL + JWT 攔截器 + x-site-code 注入 + 401 retry)
圖表Recharts v3.7.0
富文本Tiptap v3.20
圖示Lucide React
開發埠號3011 (next dev --turbopack -p 3011)
套件管理pnpm

4.1.1 關鍵依賴版本

next                16.1.6
react / react-dom   19.2.3
next-auth           5.0.0-beta.30
next-intl           4.8.3
@tanstack/react-query  5.90.21
@tanstack/react-table  8.21.3
zustand             5.0.11
axios               1.13.5
@tiptap/react       3.20.0
recharts            3.7.0
zod                 4.3.6
tailwindcss         v4
react-hook-form     7.71.2
sonner              2.0.7
date-fns            4.1.0

4.2 目錄結構

c9-ims/
├── sites/                              # 白牌站點配置(根目錄外掛)
│   └── a1/
│       ├── config.ts                   # 站點配置 (id, name, features, theme)
│       └── theme.ts                    # 主題色票 (OKLCH light/dark)
├── public/sites/a1/logo.svg
├── src/
│   ├── middleware.ts                   # i18n 路由 + auth 保護(401→/login?expired=1)
│   ├── app/
│   │   ├── globals.css                 # Tailwind v4 + CSS variables (:root + .dark)
│   │   ├── layout.tsx                  # 根 Layout (passthrough)
│   │   ├── api/
│   │   │   ├── auth/[...nextauth]/route.ts  # NextAuth handler
│   │   │   └── v1/                     # Demo API (dashboard, users)
│   │   └── [locale]/
│   │       ├── layout.tsx              # Server: 載入 siteConfig,包裹 Providers
│   │       ├── page.tsx                # redirect → /dashboard
│   │       ├── (admin)/               # 管理群組路由 (Sidebar + Header)
│   │       │   ├── layout.tsx          # AdminLayout: flex h-screen
│   │       │   ├── dashboard/
│   │       │   ├── system/             # 15 個子路由
│   │       │   ├── players/            # 7 個子路由
│   │       │   ├── activity/           # 4 個子路由
│   │       │   ├── mail/               # 2 個子路由
│   │       │   ├── finance/            # 7 個子路由
│   │       │   ├── game/               # 2 個子路由
│   │       │   ├── vip/                # 4 個子路由
│   │       │   ├── reports/            # 7 個子路由
│   │       │   ├── risk-control/       # 3 個子路由
│   │       │   ├── affiliate/          # 8 個子路由
│   │       │   ├── roles/              # 1 個頁面
│   │       │   └── users/              # 3 個子路由
│   │       └── (auth)/               # 認證群組路由 (置中卡片)
│   │           ├── layout.tsx
│   │           └── login/
│   ├── components/
│   │   ├── dataTable/                 # TanStack Table v8 (4 個元件,Demo 頁用)
│   │   ├── layout/                    # 佈局元件 (10 個)
│   │   ├── shared/                    # 共用元件 (11 個)
│   │   └── ui/                        # shadcn/ui 元件 (16 個)
│   ├── config/
│   │   ├── domainConfig/              # 域名設定 (多站點)
│   │   ├── getHostname.ts             # SSR/CSR hostname 取得
│   │   ├── siteRegistry.ts            # 動態 import 站點配置
│   │   ├── types.ts                   # FeatureFlags, ThemeColors, Theme, SiteConfig
│   │   └── useConfig.ts               # SiteConfigContext + hooks
│   ├── hooks/
│   │   ├── api/                       # 依領域拆分的 API hooks (7 個 + barrel)
│   │   ├── useApi.ts                  # Facade: 合併所有領域 hooks
│   │   ├── useApiQuery.ts             # TanStack Query 封裝 (3 個 wrapper)
│   │   ├── useMultiSiteTabs.ts        # 多站點 Tab 共用 hook
│   │   ├── useDomainConfig.ts         # Client hook: resolveDomainConfig
│   │   ├── useInitEnums.ts            # EnumInitializer 元件
│   │   ├── useNotify.ts               # sonner toast 薄封裝
│   │   ├── usePermissions.ts          # RBAC: can(), canRead(), canWrite(), isRoot()
│   │   └── useR2Url.ts               # R2 key → 完整圖片 URL 轉換
│   ├── i18n/
│   │   ├── routing.ts                 # defineRouting: 5 語系, localePrefix: "never"
│   │   ├── request.ts                 # getRequestConfig: unflatten flat JSON → nested
│   │   └── navigation.ts             # 匯出 locale-aware Link, redirect, useRouter, usePathname
│   ├── lib/
│   │   ├── apiClient.ts              # Axios 實例:動態 baseURL + JWT + x-site-code + 401 retry
│   │   ├── auth.ts                    # NextAuth: Credentials provider, JWT callbacks
│   │   ├── useHttp.ts                # httpRequest(): 三層錯誤碼映射 + toast
│   │   ├── locales.ts                # LOCALE_LABELS + 工具函式
│   │   ├── utils.ts                   # cn() = clsx + tailwind-merge
│   │   └── validations/              # Zod v4 schemas (7 個)
│   ├── messages/                      # 5 個語系檔案 (flat dot-notation JSON)
│   ├── stores/                        # Zustand stores (3 個)
│   └── types/                         # TypeScript 型別 (11 個)
├── components.json                    # shadcn/ui: new-york, RSC, lucide, cssVariables
├── next.config.ts                     # withNextIntl plugin
├── tsconfig.json                      # strict, @/* → ./src/*, @sites/* → ./sites/*
└── pnpm-lock.yaml

4.3 白牌站點配置系統

4.3.1 站點配置載入流程

1. [locale]/layout.tsx (Server Component)
   → getHostnameServer() → 讀取 request headers 取得 hostname
2. resolveDomainConfig(hostname)
   → 查表 domainConfig[hostname] → { baseUrl, imgUrl, siteId }
3. loadSiteConfig(siteId)
   → 動態 import sites/{siteId}/config.ts → SiteConfig 物件
4. fetchBackendSiteConfig(baseUrl, siteId)
   → GET /api/site-config?admin=true → 覆寫 siteName / supportedLocales / logo
5. <Providers siteConfig={enrichedConfig}>
   → 注入 SiteConfigContext,供所有子元件使用

4.3.2 DomainConfigEntry 介面

位置:src/config/domainConfig/index.ts

typescript
export interface DomainConfigEntry {
  /** 後端 API base URL,例如 "http://localhost:8080" */
  baseUrl: string;
  /** R2 圖片公開 URL,例如 "https://pub-xxx.r2.dev" */
  imgUrl: string;
  /** 白牌站點 ID,對應 site-config.prefix + 本地 @sites/{siteId}/config */
  siteId: string;
}

域名映射範例src/config/domainConfig/a1.ts):

typescript
export const entries: Record<string, DomainConfigEntry> = {
  localhost: {
    baseUrl: "http://localhost:8080",
    imgUrl: "https://pub-8ad0488587374373b2fd30c725fac6bc.r2.dev",
    siteId: "a1",
  },
  "c9-ims.zeabur.app": {
    baseUrl: "https://c9-be.zeabur.app",
    imgUrl: "https://pub-8ad0488587374373b2fd30c725fac6bc.r2.dev",
    siteId: "a1",
  },
};

解析函式

typescript
export const DEFAULT_DOMAIN = "localhost";

export function resolveDomainConfig(hostname: string): DomainConfigEntry {
  return domainConfig[hostname] ?? domainConfig[DEFAULT_DOMAIN]!;
}

4.3.3 SiteConfig 完整介面

位置:src/config/types.ts

typescript
export interface SiteConfig {
  id: string;                    // 站點唯一識別碼
  name: string;                  // 站點顯示名稱
  description: string;           // 站點描述
  logo: string;                  // Logo 路徑
  favicon: string;               // Favicon 路徑
  defaultLocale: "zh-TW" | "en-US" | "zh-CN" | "th-TH" | "vi-VN";
  supportedLocales: ("zh-TW" | "en-US" | "zh-CN" | "th-TH" | "vi-VN")[];
  features: FeatureFlags;        // 功能開關 (13 個)
  theme: Theme;                  // 主題配置 (light + dark)
}

4.3.4 功能開關 FeatureFlags(13 個)

typescript
export interface FeatureFlags {
  enableAnalytics: boolean;           // Dashboard 分析報表
  enableBilling: boolean;             // 帳單模組
  enableUserManagement: boolean;      // 用戶管理(獨立頁面)
  enableRBAC: boolean;                // 角色權限管理
  enableI18nSwitcher: boolean;        // 多語系切換器
  enableSystemManagement: boolean;    // 系統管理(管理員/群組/紀錄/站點設定)
  enableFinanceManagement: boolean;   // 財務管理
  enableAffiliateManagement: boolean; // 代理中心
  enableVipManagement: boolean;       // VIP 管理
  enableReports: boolean;             // 報表資訊
  enableGameManagement: boolean;      // 遊戲管理
  enableRiskControl: boolean;         // 風控設置
  enablePlayerManagement: boolean;    // 玩家管理
}

4.3.5 主題色票系統(OKLCH 格式)

typescript
export interface ThemeColors {
  background: string;              // 背景色
  foreground: string;              // 前景色
  card: string;                    // 卡片背景
  cardForeground: string;          // 卡片前景
  popover: string;                 // 彈出層背景
  popoverForeground: string;       // 彈出層前景
  primary: string;                 // 主色
  primaryForeground: string;       // 主色前景
  secondary: string;               // 次色
  secondaryForeground: string;     // 次色前景
  muted: string;                   // 靜音色
  mutedForeground: string;         // 靜音前景
  accent: string;                  // 強調色
  accentForeground: string;        // 強調前景
  destructive: string;             // 危險色
  border: string;                  // 邊框色
  input: string;                   // 輸入框邊框
  ring: string;                    // 焦點環
  chart1: string;                  // 圖表色 1
  chart2: string;                  // 圖表色 2
  chart3: string;                  // 圖表色 3
  chart4: string;                  // 圖表色 4
  chart5: string;                  // 圖表色 5
  sidebar: string;                 // Sidebar 背景
  sidebarForeground: string;       // Sidebar 前景
  sidebarPrimary: string;          // Sidebar 主色
  sidebarPrimaryForeground: string; // Sidebar 主色前景
  sidebarAccent: string;           // Sidebar 強調色
  sidebarAccentForeground: string; // Sidebar 強調前景
  sidebarBorder: string;           // Sidebar 邊框
  sidebarRing: string;             // Sidebar 焦點環
}

export interface Theme {
  light: ThemeColors;   // 亮色主題(30+ CSS 變數)
  dark: ThemeColors;    // 暗色主題(30+ CSS 變數)
  radius: string;       // 圓角大小,例如 "0.625rem"
}

4.3.6 站點配置 Context Hooks

位置:src/config/useConfig.ts

typescript
// 取得完整站點配置
export function useConfig(): SiteConfig;

// 快捷 Hook:取得功能開關
export function useFeatureFlags(): FeatureFlags;

// 快捷 Hook:取得主題配置
export function useSiteTheme(): Theme;

4.3.7 站點註冊表

位置:src/config/siteRegistry.ts

typescript
const siteModules: Record<string, () => Promise<{ config: SiteConfig }>> = {
  a1: () => import("@sites/a1/config"),
  // 新增站點在此登記
};

export const SITE_IDS = Object.keys(siteModules);

export async function loadSiteConfig(siteId: string): Promise<SiteConfig>;

4.4 API Hooks 完整文件

所有 API hooks 位於 src/hooks/api/,共 7 個領域 hook。每個 hook 回傳一個方法物件,透過 httpRequest() 發送 HTTP 請求至後端 NestJS API。

4.4.1 useAuthApi — 認證與通用 API

位置:src/hooks/api/useAuthApi.ts

方法名稱HTTP 方法URL參數說明
getEnumsGET/common/enums取得後端所有枚舉(含 ERROR_CODES)
adminLoginPOST/admin/login{ email: string; password: string }管理員登入,回傳 JWT token + admin 資訊
adminSendVerifyCodePOST/admin/send-verify-code{ email: string }發送 Email 驗證碼(管理員註冊用)
adminVerifyEmailPOST/admin/verify-email{ email: string; code: string }驗證 Email 驗證碼
adminRegisterPOST/admin/register{ email, password, name, code, groupId? }管理員註冊(需先驗證 Email)
getAdminProfileGET/admin/profile取得當前管理員個人資料
generateGoogleAuthPOST/admin/google-auth/generate產生 Google Authenticator QR Code + Secret
enableGoogleAuthPOST/admin/google-auth/enable{ code: string }啟用 2FA(需輸入 TOTP 驗證碼)
disableGoogleAuthPOST/admin/google-auth/disable{ code: string }停用 2FA(需輸入 TOTP 驗證碼)

使用範例

typescript
import { useAuthApi } from "@/hooks/api";

const api = useAuthApi();
const res = await api.adminLogin({ email: "admin@c9.com", password: "123456" });
if (res?.code === 200) {
  const { token, admin } = res.result;
}

4.4.2 useAdminApi — 管理員、群組、操作紀錄、R2 儲存

位置:src/hooks/api/useAdminApi.ts

管理員 CRUD

方法名稱HTTP 方法URL參數說明
getAdminsGET/admin/listQueryParams & { keyword?, status?, groupId?, startDate?, endDate? }管理員列表(分頁 + 篩選)
getAdminGET/admin/:idid: number取得單一管理員詳情
createAdminPOST/admin/create{ email, password, name, groupId? }新增管理員
updateAdminPATCH/admin/:idid: number, payload: Record<string, unknown>更新管理員(name, password, groupId, status, allowedSiteCodes)
deleteAdminDELETE/admin/:idid: number刪除管理員

群組 CRUD

方法名稱HTTP 方法URL參數說明
getGroupsGET/admin/groups/listQueryParams & { keyword?, status?, startDate?, endDate? }群組列表
getGroupGET/admin/groups/:idid: number取得單一群組(含權限列表)
createGroupPOST/admin/groups/create{ name, permissions?, description? }新增群組
updateGroupPATCH/admin/groups/:idid: number, payload更新群組
deleteGroupDELETE/admin/groups/:idid: number刪除群組

操作紀錄

方法名稱HTTP 方法URL參數說明
getAdminLogsGET/admin/logs/listQueryParams & Record<string, unknown>管理員操作紀錄列表

R2 儲存操作紀錄

方法名稱HTTP 方法URL參數說明
getR2LogsGET/admin/reports/r2-logsQueryParams & Record<string, unknown>R2 操作日誌(上傳/刪除紀錄)

R2 檔案管理

方法名稱HTTP 方法URL參數說明
r2ListGET/admin/r2/list{ prefix?, continuationToken?, maxKeys?, siteConfigId? }列出 R2 物件(檔案 + 資料夾)
r2UploadPOST/admin/r2/uploadFormData(含 file + key + siteConfigId)上傳檔案至 R2
r2DeletePOST/admin/r2/delete{ keys: string[], siteConfigId? }批次刪除 R2 物件
r2MovePOST/admin/r2/move{ sourceKey, destinationKey, isFolder, siteConfigId? }移動/重命名 R2 物件
r2CreateFolderPOST/admin/r2/create-folder{ name, prefix?, siteConfigId? }建立 R2 資料夾
r2DeleteFolderPOST/admin/r2/delete-folder{ prefix, siteConfigId? }刪除 R2 資料夾(含子項目)

4.4.3 useFinanceApi — 財務管理

位置:src/hooks/api/useFinanceApi.ts

存款相關

方法名稱HTTP 方法URL參數說明
getDepositsGET/depositQueryParams & Record<string, unknown>存款列表
getDepositReviewGET/admin/finance/deposit-reviewQueryParams & { status?, startDate?, endDate? }存款審核列表
reviewDepositPATCH/admin/finance/deposit-review/:idid, { action: "approve"|"reject", rejectReason? }審核存款訂單

提領相關

方法名稱HTTP 方法URL參數說明
getWithdrawalsGET/withdrawal/admin/listQueryParams & Record<string, unknown>提領列表(前台 withdrawal 模組)
reviewWithdrawalPOST/withdrawal/admin/:id/reviewid: string, { action, remark? }審核提領(前台 withdrawal 模組)
getAdminWithdrawalsGET/admin/finance/withdrawalsQueryParams & { status?, userId?, keyword?, startDate?, endDate? }提領列表(admin finance 模組)
adminReviewWithdrawalPOST/admin/finance/withdrawals/:id/reviewid: number, { action, rejectReason? }審核提領
adminUploadWithdrawalProofPOST/admin/finance/withdrawals/:id/upload-proofid: number, file: File上傳提領憑證
adminCompleteWithdrawalPOST/admin/finance/withdrawals/:id/completeid: number標記提領為已完成

前台用戶管理

方法名稱HTTP 方法URL參數說明
getAuthUsersGET/admin/finance/usersQueryParams & { keyword? }前台用戶列表(搜尋帳號/名稱)
getAuthUserGET/admin/finance/users/:idid: number取得前台用戶詳情(含錢包、存提款、投注紀錄)
updateAuthUserPATCH/admin/finance/users/:idid, { name?, email?, mobile? }更新前台用戶資料
updateUserVendorGroupPATCH/admin/users/:userId/vendor-groupuserId, vendorGroupId設定用戶金流群組

餘額調整

方法名稱HTTP 方法URL參數說明
adjustBalancePOST/admin/finance/adjust-balance{ userId, amount, type: "add"|"deduct", reason }手動調整用戶餘額

銀行卡 CRUD

方法名稱HTTP 方法URL參數說明
getBankCardsGET/admin/finance/bank-cardsQueryParams & { userId?, status?, startDate?, endDate?, keyword? }銀行卡列表
createBankCardPOST/admin/finance/bank-cards{ userId, bankCode, bankAccount, branch, holderName }新增銀行卡
updateBankCardPATCH/admin/finance/bank-cards/:idid, data更新銀行卡
reviewBankCardPATCH/admin/finance/bank-cards/:id/reviewid, { status }審核銀行卡
deleteBankCardDELETE/admin/finance/bank-cards/:idid刪除銀行卡

信用卡 CRUD

方法名稱HTTP 方法URL參數說明
getCreditCardsGET/admin/finance/credit-cardsQueryParams & { userId?, status?, startDate?, endDate?, keyword? }信用卡列表
createCreditCardPOST/admin/finance/credit-cards{ userId, cardNumber, holderName, cvv, expiryDate }新增信用卡
updateCreditCardPATCH/admin/finance/credit-cards/:idid, data更新信用卡
reviewCreditCardPATCH/admin/finance/credit-cards/:id/reviewid, { status }審核信用卡
deleteCreditCardDELETE/admin/finance/credit-cards/:idid刪除信用卡

加密地址 CRUD

方法名稱HTTP 方法URL參數說明
getCryptoAddressesGET/admin/finance/crypto-addressesQueryParams & { userId?, status?, startDate?, endDate?, keyword? }加密地址列表
createCryptoAddressPOST/admin/finance/crypto-addresses{ userId, walletName, currency?, network?, address }新增加密地址
updateCryptoAddressPATCH/admin/finance/crypto-addresses/:idid, data更新加密地址
reviewCryptoAddressPATCH/admin/finance/crypto-addresses/:id/reviewid, { status }審核加密地址
deleteCryptoAddressDELETE/admin/finance/crypto-addresses/:idid刪除加密地址

金流群組管理

方法名稱HTTP 方法URL參數說明
getVendorGroupsGET/admin/vendor-groups/list金流群組列表
createVendorGroupPOST/admin/vendor-groups/create{ name: Record<string,string>, enabled? }新增金流群組
updateVendorGroupPATCH/admin/vendor-groups/:idid, { name?, enabled? }更新金流群組
deleteVendorGroupDELETE/admin/vendor-groups/:idid刪除金流群組

金流通道管理

方法名稱HTTP 方法URL參數說明
getVendorChannelsGET/admin/vendor-channels/list{ groupId? }金流通道列表
createVendorChannelPOST/admin/vendor-channels/createRecord<string, unknown>新增金流通道
updateVendorChannelPATCH/admin/vendor-channels/:idid, data更新金流通道
deleteVendorChannelDELETE/admin/vendor-channels/:idid刪除金流通道
getGroupChannelIdsGET/admin/vendor-groups/:groupId/channelsgroupId取得群組關聯的通道 ID 列表
setGroupChannelsPUT/admin/vendor-groups/:groupId/channelsgroupId, channelIds: number[]設定群組關聯的通道

4.4.4 useAffiliateApi — 代理推廣管理

位置:src/hooks/api/useAffiliateApi.ts

代理列表與管理

方法名稱HTTP 方法URL參數說明
getAffiliatesGET/affiliate/admin/agentsQueryParams & Record<string, unknown>代理列表(支援 siteCode 篩選)
createAgentPOST/affiliate/admin/create-agent{ userId, agentCode? }手動綁定用戶為代理
setAgentTierPOST/affiliate/admin/set-agent-tier{ agentId, tierCode }手動設定代理等級

佣金結算

方法名稱HTTP 方法URL參數說明
getSettlementsGET/affiliate/admin/settlementsQueryParams & Record<string, unknown>佣金結算列表
reviewSettlementPOST/affiliate/admin/settlements/:id/reviewid, { action, rejectReason? }審核佣金結算
getSettlementRiskLogsGET/affiliate/admin/settlements/:id/risk-logsid取得結算風控紀錄

代理提款

方法名稱HTTP 方法URL參數說明
getAffiliateWithdrawalsGET/affiliate/admin/withdrawalsQueryParams & Record<string, unknown>代理提款列表
reviewAffiliateWithdrawalPOST/affiliate/admin/withdrawals/:id/reviewid, { action, rejectReason? }審核代理提款
completeAffiliateWithdrawalPOST/affiliate/admin/withdrawals/:id/completeid標記代理提款已完成

綁定管理

方法名稱HTTP 方法URL參數說明
adminBindPOST/affiliate/admin/bind{ memberId, agentId, action, remark? }手動綁定上下線
getBindLogsGET/affiliate/admin/bind-logsQueryParams & Record<string, unknown>綁定紀錄列表

聯盟配置

方法名稱HTTP 方法URL參數說明
getCommissionRatesGET/affiliate/admin/commission-ratesQueryParams佣金費率列表
upsertCommissionRatePOST/affiliate/admin/commission-ratesRecord<string, unknown>新增/更新佣金費率
getAgentTiersGET/affiliate/admin/agent-tiersQueryParams代理等級列表
upsertAgentTierPOST/affiliate/admin/agent-tiersRecord<string, unknown>新增/更新代理等級
deleteAgentTierDELETE/affiliate/admin/agent-tiers/:idid刪除代理等級
getVipMilestonesGET/affiliate/admin/vip-milestonesQueryParamsVIP 里程碑列表
upsertVipMilestonePOST/affiliate/admin/vip-milestonesRecord<string, unknown>新增/更新 VIP 里程碑
deleteVipMilestoneDELETE/affiliate/admin/vip-milestones/:idid刪除 VIP 里程碑

聯盟模板

方法名稱HTTP 方法URL參數說明
previewAllianceTemplateGET/affiliate/admin/preview-template預覽聯盟預設模板資料
loadAllianceTemplatePOST/affiliate/admin/load-templateRecord<string, unknown>帶入聯盟模板(transaction 內寫入)

4.4.5 useVipApi — VIP 等級與返水

位置:src/hooks/api/useVipApi.ts

方法名稱HTTP 方法URL參數說明
getVipLevelsGET/vip/levelsQueryParamsVIP 等級列表(支援 siteCode 篩選)
createVipLevelPOST/vip/levelsRecord<string, unknown>新增 VIP 等級
updateVipLevelPATCH/vip/levels/:idid, payload更新 VIP 等級
deleteVipLevelDELETE/vip/levels/:idid刪除 VIP 等級
getVipRebatesGET/vip/rebatesQueryParams返水規則列表(支援 siteCode 篩選)
createVipRebatePOST/vip/rebatesRecord<string, unknown>新增返水規則
updateVipRebatePATCH/vip/rebates/:idid, payload更新返水規則
deleteVipRebateDELETE/vip/rebates/:idid刪除返水規則
bulkUpsertVipRebatesPOST/vip/rebates/bulk{ items: { level, gameType, rebateRate }[] }批次新增/更新返水規則
previewVipTemplateGET/vip/preview-template預覽 VIP 預設模板資料
loadVipTemplatePOST/vip/load-templateRecord<string, unknown>帶入 VIP 模板

4.4.6 useGameApi — 遊戲管理

位置:src/hooks/api/useGameApi.ts

方法名稱HTTP 方法URL參數說明
getGameProvidersGET/game/admin/providersQueryParams & { gameType?, siteCode? }遊戲供應商列表
createGameProviderPOST/game/admin/providersRecord<string, unknown>(含 siteCode)新增遊戲供應商
updateGameProviderPATCH/game/admin/providers/:idid, payload更新遊戲供應商
deleteGameProviderDELETE/game/admin/providers/:idid刪除遊戲供應商
getGameTypeConfigsGET/game/admin/type-configsQueryParams遊戲分類列表
createGameTypeConfigPOST/game/admin/type-configsRecord<string, unknown>新增遊戲分類
updateGameTypeConfigPATCH/game/admin/type-configs/:idid, payload更新遊戲分類
deleteGameTypeConfigDELETE/game/admin/type-configs/:idid刪除遊戲分類
previewGameTemplateGET/game/admin/preview-template預覽遊戲預設模板
loadGameTemplatePOST/game/admin/load-templatedata, params?: { siteCode? }帶入遊戲模板(支援指定站點)
copyGameSiteDataPOST/game/admin/copy-site-data{ sourceSiteCode, targetSiteCode, type: "providers"|"typeConfigs" }跨站複製遊戲資料

4.4.7 useContentApi — 內容、風控、報表、站點設定

位置:src/hooks/api/useContentApi.ts

站點設定

方法名稱HTTP 方法URL參數說明
getSiteConfigsGET/site-config/admin/list取得所有站點列表(含主題)
createSiteConfigPOST/site-config/admin{ siteCode, prefix, layout?, siteName, siteDescription? }新增站點
updateSiteConfigPATCH/site-config/admin/:idid, payload更新站點設定
deleteSiteConfigDELETE/site-config/admin/:idid刪除站點
uploadDomainAssetPOST/site-config/admin/:id/domain-assetsiteConfigId, FormData上傳域名素材(logo/favicon)
uploadCustomerServiceIconPOST/site-config/admin/:id/customer-service-iconsiteConfigId, FormData上傳客服圖示

活動管理

方法名稱HTTP 方法URL參數說明
getPromosGET/admin/promos/listQueryParams & { tag?, enabled?, startDate?, endDate?, siteCode?, title?, conditionType? }活動列表
getPromoGET/admin/promos/:idid取得單一活動詳情
createPromoPOST/admin/promos/createFormData新增活動(含圖片上傳)
updatePromoPATCH/admin/promos/:idid, FormData更新活動
deletePromoDELETE/admin/promos/:idid刪除活動

活動標籤

方法名稱HTTP 方法URL參數說明
getPromoTagsGET/admin/promo-tags/list{ siteCode? }活動標籤列表
createPromoTagPOST/admin/promo-tags/create{ name, label?, color?, sortOrder?, enabled?, siteCode? }新增標籤
updatePromoTagPATCH/admin/promo-tags/:idid, data更新標籤
deletePromoTagDELETE/admin/promo-tags/:idid刪除標籤

站內信

方法名稱HTTP 方法URL參數說明
getInboxAdminGET/inbox/admin/listQueryParams & { scope? }站內信列表
sendInboxPOST/inbox/admin/sendRecord<string, unknown>發送站內信
updateInboxPATCH/inbox/admin/:idid, payload更新站內信
deleteInboxDELETE/inbox/admin/:idid刪除站內信

風控管理

方法名稱HTTP 方法URL參數說明
getRiskIpRulesGET/admin/risk/ip-rulesQueryParams & { type?, keyword? }IP 黑白名單列表
createRiskIpRulePOST/admin/risk/ip-rules{ ip, type, remark? }新增 IP 規則
updateRiskIpRulePATCH/admin/risk/ip-rules/:idid, { ip?, type?, remark? }更新 IP 規則
deleteRiskIpRuleDELETE/admin/risk/ip-rules/:idid刪除 IP 規則
riskLookupGET/admin/risk/lookupQueryParams & { keyword?, startDate?, endDate? }IP/裝置指紋反查用戶
getLoginFailuresGET/admin/risk/login-failuresQueryParams & { keyword?, startDate?, endDate? }登入失敗紀錄
getRiskGameBlacklistGET/admin/risk/game-blacklistQueryParams & { userId?, gameType? }遊戲黑名單列表
createRiskGameBlacklistPOST/admin/risk/game-blacklist{ userId, gameType?, productId?, remark? }新增遊戲黑名單
deleteRiskGameBlacklistDELETE/admin/risk/game-blacklist/:idid刪除遊戲黑名單

報表 API

方法名稱HTTP 方法URL參數說明
getReportPlayersGET/admin/reports/playersQueryParams & { keyword?, vipLevel?, startDate?, endDate?, bankAccount?, cardNumber?, cryptoAddress?, vipOp?, vipFilterLevel?, online?, siteCode? }玩家報表
getVipPlayersGET/admin/reports/vip-playersQueryParams & { keyword?, vipLevelMin?, vipLevelMax?, tier?, relegationStatus?, vipHold?, minBet?, maxBet?, startDate?, endDate? }VIP 玩家報表
getReportBetRecordsGET/admin/reports/bet-recordsQueryParams & { userId?, keyword?, gameType?, gamePlatform?, status?, startDate?, endDate?, siteCode? }投注紀錄報表
getReportOverviewGET/admin/reports/overview{ startDate?, endDate?, siteCode? }總覽報表(統計 + 每日摘要)
getReportProfitLossGET/admin/reports/profit-loss{ startDate?, endDate?, groupBy?, gameType?, siteCode? }損益報表
getReportGamesGET/admin/reports/games{ startDate?, endDate?, gamePlatform?, gameType?, siteCode? }遊戲報表
getReportPromosGET/admin/reports/promosQueryParams & { startDate?, endDate?, siteCode? }活動報表
getReportPlayerSummaryGET/admin/reports/player-summaryQueryParams & { keyword?, sortBy?, sortOrder?, vipLevel?, startDate?, endDate?, siteCode? }玩家簡表
getReportHistoryGET/admin/reports/historyQueryParams & { type?, startDate?, endDate? }歷史紀錄
exportReportGET/admin/reports/export/:typetype: string, params?匯出報表 CSV

4.4.8 useApi — Facade 合併

位置:src/hooks/useApi.ts

useApi() 是向下相容的 Facade hook,合併所有 7 個領域 hook 的方法為單一扁平物件:

typescript
export function useApi() {
  return {
    ...useAuthApi(),
    ...useAdminApi(),
    ...useFinanceApi(),
    ...useAffiliateApi(),
    ...useVipApi(),
    ...useGameApi(),
    ...useContentApi(),
  };
}

建議:新代碼直接引入特定領域 hook(如 useFinanceApi()),避免合併整包造成不必要的依賴。


4.5 核心 Hooks 完整文件

4.5.1 useApiQuery — TanStack Query 封裝

位置:src/hooks/useApiQuery.ts

提供三個 wrapper,簡化 TanStack Query 與後端 ApiResponse<T> 格式的整合。

useApiQuery<TResult>

通用查詢封裝,自動從 ApiResponse.result 提取資料。

typescript
export function useApiQuery<TResult = unknown>(
  queryKey: QueryKey,
  queryFn: () => Promise<ApiResponse<TResult>>,
  options?: Omit<UseQueryOptions<TResult | null, Error>, "queryKey" | "queryFn">,
): UseQueryResult<TResult | null, Error>;

內部邏輯

  1. 呼叫 queryFn() 取得 ApiResponse<TResult>
  2. 檢查 res?.code === 200
  3. 回傳 res.result ?? null
  4. 非 200 時回傳 null

使用範例

typescript
const { data, isLoading, refetch } = useApiQuery<AdminUser>(
  ["admin", adminId],
  () => api.getAdmin(adminId),
  { enabled: !!adminId },
);
// data: AdminUser | null

useApiListQuery<TItem>

列表查詢封裝,自動正規化兩種後端回傳格式。

typescript
export function useApiListQuery<TItem = any>(
  queryKey: unknown[],
  queryFn: () => Promise<any>,
  options?: { enabled?: boolean },
): {
  data: { items: TItem[]; total: number };
  isLoading: boolean;
  refetch: () => void;
};

內部邏輯

  1. 使用 useApiQuery 取得 raw result
  2. 若 result 為陣列 → items = resulttotal = result.length
  3. 若 result 為物件 → items = result.items ?? []total = result.total ?? result.pagination?.total ?? 0

使用範例

typescript
const { data: listData, isLoading } = useApiListQuery<AdminUser>(
  ["admins", page, filters],
  () => api.getAdmins({ page, pageSize: 20, ...filters }),
);
const { items, total } = listData;

useApiMutation<TData, TVariables>

Mutation 封裝,支援自動 invalidate 指定的 query keys。

typescript
export function useApiMutation<TData = unknown, TVariables = unknown>(
  mutationFn: (variables: TVariables) => Promise<ApiResponse<TData>>,
  options?: Omit<UseMutationOptions<...>, "mutationFn"> & {
    invalidateKeys?: QueryKey[];
  },
): UseMutationResult<ApiResponse<TData>, Error, TVariables>;

內部邏輯

  1. 執行 mutationFn
  2. 成功後自動 queryClient.invalidateQueries() 指定的 keys
  3. 呼叫 options.onSuccess 回調

使用範例

typescript
const { mutateAsync, isPending } = useApiMutation(
  (data: { name: string }) => api.updateVipLevel(id, data),
  {
    invalidateKeys: [["vip-levels"]],
    onSuccess: () => notify.success("已儲存"),
  },
);

4.5.2 useMultiSiteTabs — 多站點 Tab 邏輯

位置:src/hooks/useMultiSiteTabs.ts

封裝多站點 Tab 切換的完整邏輯,取代約 25 行樣板代碼。

typescript
interface UseMultiSiteTabsOptions {
  onSiteChange?: () => void;  // Tab 切換時的回調(通常用於重置分頁)
}

export function useMultiSiteTabs(options?: UseMultiSiteTabsOptions): {
  visibleSites: SiteInfo[];           // 當前可見站點列表
  activeSiteId: number | null;       // 當前啟用的 Tab 站點 ID
  activeSiteCode: string | undefined; // 全站模式下傳入 API 的 siteCode
  handleSiteChange: (siteId: number) => void; // Tab 切換 handler
  isAllSites: boolean;               // 是否為「全部站點」模式
};

內部邏輯

  1. useSiteFilterStore 取得 selectedSiteCodesites
  2. isAllSites = selectedSiteCode === null
  3. visibleSites:全站模式顯示所有站,單站模式僅顯示該站
  4. activeSiteId:自動追蹤當前 Tab,若可見站點變更則自動切到第一個
  5. activeSiteCode:全站模式下回傳當前 Tab 的 siteCode(用於 API 參數),單站模式回傳 undefined(由 header 自動帶)
  6. handleSiteChange:切換 Tab 時同時呼叫 options.onSiteChange

完整使用範例

typescript
const tSite = useTranslations("system.siteConfig");
const { visibleSites, activeSiteId, activeSiteCode, handleSiteChange } =
  useMultiSiteTabs({ onSiteChange: () => setPage(1) });

// API 呼叫帶入 siteCode
const params: Record<string, unknown> = { page, limit: 20 };
if (activeSiteCode) params.siteCode = activeSiteCode;

// 渲染 Tab
{visibleSites.length > 0 && (
  <SiteTabs
    configs={visibleSites}
    activeSiteId={activeSiteId}
    onSiteChange={handleSiteChange}
    defaultLabel={tSite("defaultSite")}
  />
)}

4.5.3 usePermissions — RBAC 權限檢查

位置:src/hooks/usePermissions.ts

從 NextAuth session 取得 permissionsgroupType,提供權限檢查方法。

typescript
export function usePermissions(): {
  can: (permission: string) => boolean;    // 檢查完整權限字串 e.g. "admin:read"
  canRead: (module: string) => boolean;    // 檢查模組讀取權限 → can(`${module}:read`)
  canWrite: (module: string) => boolean;   // 檢查模組寫入權限 → can(`${module}:write`)
  isRoot: () => boolean;                   // 是否為 root 群組(跳過所有權限檢查)
  permissions: string[];                   // 當前權限列表
  groupType: string;                       // 當前群組類型
};

特殊規則

  • groupType === "root" 時,can() 永遠回傳 true
  • 其他群組類型依 permissions 陣列判斷

16 個權限模組admin, admin-group, admin-log, user, deposit, withdrawal, promo, promo-tag, affiliate, vip, game, risk, report, vendor, finance, site-config

4 種群組類型

GroupType說明
root超級管理員,略過所有權限檢查
super_admin高級管理員,擁有全部權限(除 site-config)
general_admin一般管理員,僅讀取權限
custom自定義群組,依 permissions 陣列控制

4.5.4 useNotify — Toast 通知

位置:src/hooks/useNotify.ts

Sonner toast 的薄封裝,提供四種通知類型。

typescript
export function useNotify(): {
  success: (message: string) => void;
  error: (message: string) => void;
  info: (message: string) => void;
  warning: (message: string) => void;
};

使用範例

typescript
const notify = useNotify();
notify.success("儲存成功");
notify.error("操作失敗");

4.5.5 useInitEnums — 枚舉初始化

位置:src/hooks/useInitEnums.ts

匯出 EnumInitializer React 元件(非 hook),在 Providers 掛載時自動 fetch /api/common/enums

行為

  1. 模組級 flag enumsFetched 確保整個 App 生命週期只 fetch 一次
  2. useRef(fetching) 防止 Strict Mode 下重複請求
  3. 取得 res.data.result.ERROR_CODES 後呼叫 enumStore.setErrorCodes()
  4. 後端未就緒時靜默失敗,不影響前端運作
typescript
function EnumInitializer(): null;

4.5.6 useDomainConfig — 域名配置解析

位置:src/hooks/useDomainConfig.ts

Client Component 專用 hook,從瀏覽器 hostname 解析域名配置。

typescript
export function useDomainConfig(): DomainConfigEntry;
// 回傳 { baseUrl, imgUrl, siteId }

內部邏輯

  1. 呼叫 getHostnameClient() 取得瀏覽器 hostname
  2. 呼叫 resolveDomainConfig(hostname) 查表
  3. 使用 useMemo([]) 快取結果(hostname 不會變化)

4.5.7 useR2Url — R2 圖片 URL 轉換

位置:src/hooks/useR2Url.ts

將 R2 key(路徑)轉為完整公開 URL。

typescript
export function useR2Url(): {
  toR2Url: (key: string | null | undefined) => string | undefined;
};

轉換規則

輸入輸出
null / undefinedundefined
已是完整 URL(http://https:// 開頭)原樣回傳(向後相容舊資料)
R2 key(如 a1/logoSmall.png${imgUrl}/${key}

使用範例

typescript
const { toR2Url } = useR2Url();
const logoUrl = toR2Url(siteConfig.logoSmall);
// "https://pub-xxx.r2.dev/a1/logoSmall.png"

4.6 TypeScript 型別定義

所有型別定義位於 src/types/,共 11 個檔案。index.ts 作為 barrel re-export。

4.6.1 api.ts — API 通用型別

typescript
/** c9-be 標準 API 回應格式 */
export interface ApiResponse<T = any> {
  code: number;           // 200 = 成功,其他 = 業務錯誤碼
  message?: string;       // 錯誤訊息(當前語系)
  result?: T;             // 回傳資料
  path?: string;          // 請求路徑(用於 ERROR_CODES 映射)
  timestamp?: number;     // Unix timestamp
}

/** 分頁回應 */
export interface PaginatedResult<T> {
  items: T[];
  pagination: {
    page: number;
    pageSize: number;
    total: number;
    totalPages: number;
  };
}

/** 通用查詢參數 */
export interface QueryParams {
  page?: number;
  pageSize?: number;
  sort?: string;
  order?: "asc" | "desc";
  search?: string;
  siteCode?: string;
}

4.6.2 role.ts — 權限系統型別

typescript
/** 16 個權限模組 */
export type PermModule =
  | 'admin' | 'admin-group' | 'admin-log' | 'user'
  | 'deposit' | 'withdrawal' | 'promo' | 'promo-tag'
  | 'affiliate' | 'vip' | 'game' | 'risk'
  | 'report' | 'vendor' | 'finance' | 'site-config';

/** 權限動作 */
export type PermAction = 'read' | 'write';

/** 群組類型 */
export type GroupType = 'root' | 'super_admin' | 'general_admin' | 'custom';

/** 完整權限模組列表(用於渲染權限矩陣) */
export const PERM_MODULES: PermModule[];

/** 權限動作列表 */
export const PERM_ACTIONS: PermAction[];

4.6.3 admin.ts — 管理員型別

typescript
/** 後台管理員 */
export interface AdminUser {
  id: number;
  email: string;
  name: string;
  groupId: number | null;
  group?: AdminGroup;
  status: number;                    // 1=啟用, 0=停用
  emailVerified: number;
  googleAuthEnabled?: number;        // 1=已啟用 2FA
  allowedSiteCodes: string[] | null; // null=可存取全部站點
  lastLoginIp: string | null;
  lastLoginAt: string | null;
  createdAt: string;
  updatedAt: string;
}

/** 管理員群組 */
export interface AdminGroup {
  id: number;
  type: 'root' | 'super_admin' | 'general_admin' | 'custom';
  name: string;
  permissions: string[] | null;      // e.g. ["admin:read", "admin:write"]
  description: string | null;
  status: number;
  admins?: AdminUser[];
  createdAt: string;
  updatedAt: string;
}

/** 管理員操作紀錄 */
export interface AdminOperationLog {
  id: number;
  adminId: number;
  admin?: { id: number; email: string; name: string };
  module: string;
  action: string;
  targetId: number | null;
  ip: string;
  userAgent: string | null;
  method: string;
  path: string;
  detail: Record<string, unknown> | null;
  summary: string | null;
  siteCode: string;
  createdAt: string;
}

4.6.4 user.ts — 用戶型別(Demo 頁用)

typescript
export interface UserRole {
  id: string;
  name: string;
  description: string;
  isSystem: boolean;
  permissions: string[];
  createdAt: string;
  updatedAt: string;
}

export interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  status: "active" | "inactive" | "suspended";
  role?: UserRole;
  createdAt: string;
  updatedAt: string;
  lastLoginAt?: string;
}

4.6.5 finance.ts — 財務型別

typescript
/** 銀行卡 */
export interface BankCard {
  id: number;
  userId: number;
  siteCode?: string;
  bankCode: string;
  bankAccount: string;
  branch: string;
  holderName: string;
  idCardFront: string;
  idCardBack: string;
  passbookCover: string;
  status: number;           // 0=pending, 1=approved, 2=rejected
  createdAt: string;
  updatedAt: string;
  userAccount?: string | null;
  userEmail?: string | null;
  userName?: string | null;
  userMobile?: string | null;
}

/** 信用卡 */
export interface CreditCard {
  id: number;
  userId: number;
  siteCode?: string;
  cardNumber: string;
  holderName: string;
  cvv: string;
  expiryDate: string;
  status: number;
  createdAt: string;
  updatedAt: string;
  userAccount?: string | null;
  userEmail?: string | null;
  userName?: string | null;
  userMobile?: string | null;
}

/** 加密地址 */
export interface CryptoAddress {
  id: number;
  userId: number;
  siteCode?: string;
  walletName: string;
  currency: string;
  network: string;
  address: string;
  status: number;
  createdAt: string;
  updatedAt: string;
  userAccount?: string | null;
  userEmail?: string | null;
  userName?: string | null;
  userMobile?: string | null;
}

/** 提領訂單 */
export interface WithdrawalOrder {
  id: number;
  userId: number;
  siteCode?: string;
  amount: string;
  cryptoAddressId: number;
  address: string;
  network: string;
  status: string;           // pending | approved | rejected | completed
  rejectReason: string | null;
  reviewedBy: string | null;
  reviewedAt: string | null;
  proofKey: string | null;
  proofOriginalName: string | null;
  completedBy: string | null;
  completedAt: string | null;
  createdAt: string;
  updatedAt: string;
  user?: { account: string; name: string; email: string | null };
}

/** 前台用戶基本資訊(調帳搜尋用) */
export interface AuthUserBasic {
  id: number;
  account: string;
  name: string;
  email: string | null;
  balance: string;
  frozenBalance: string;
  createdAt: string;
}

/** 存款訂單 */
export interface DepositOrder {
  id: number;
  userId: number;
  siteCode?: string;
  channelName: string;
  currency: string;
  subOrder: string;
  orderAmount: number;
  paymentMethod: string;
  status: string;
  usdAmount: string;
  resultUrl: string | null;
  vendorRequest: any;
  callbackData: any;
  createdAt: string;
}

/** 投注紀錄(玩家詳情頁用) */
export interface BetRecord {
  id: number;
  userId: number;
  gameType: string;
  gamePlatform: string;
  gameName: string;
  betAmount: string;
  betEffective: string;
  winLose: string;
  status: string;
  betDatetime: string;
}

/** 登入紀錄 */
export interface LoginLog {
  id: number;
  userId: number;
  device: string;
  ip: string;
  action: string;
  remark: string;
  lastUse: string;
  createdAt: string;
}

/** 前台用戶完整詳情(含關聯資料) */
export interface AuthUserDetail {
  id: number;
  account: string;
  name: string;
  email: string | null;
  mobile: string | null;
  telegram: string | null;
  google: string | null;
  vipLevel: string;
  vipProgress: string | null;
  totalEffectiveBet: string;
  relegationMissCount: number;
  vipHold: number;
  googleAuthEnabled: number;
  tokenVersion: number;
  balance: string;
  frozenBalance: string;
  locale: string;
  avatar: string | null;
  vendorGroupId: number | null;
  agentCode: string | null;
  level1AgentId: number | null;
  level2AgentId: number | null;
  level3AgentId: number | null;
  createdAt: string;
  bankCards: BankCard[];
  creditCards: CreditCard[];
  cryptoAddresses: CryptoAddress[];
  deposits: DepositOrder[];
  withdrawals: WithdrawalOrder[];
  betRecords: BetRecord[];
  loginLogs: LoginLog[];
}

4.6.6 affiliate.ts — 代理型別

typescript
/** 代理商(含餘額) */
export interface AffiliateAgent {
  id: number;
  account: string;
  email: string;
  name: string;
  agentCode: string;
  agentTier: string;           // bronze / silver / gold / platinum
  available: string;           // 可用餘額 (decimal)
  frozen: string;              // 凍結餘額
  totalEarned: string;
  totalWithdrawn: string;
  directMemberCount?: number;
  siteCode: string;
  createdAt: string;
}

/** 佣金結算記錄 */
export interface AffiliateSettlement {
  id: number;
  siteCode?: string;
  agentId: number;
  weekStart: string;
  weekEnd: string;
  activeMemberCount: number;
  totalNetLoss: string;
  level1Commission: string;
  level2Commission: string;
  level3Commission: string;
  totalCommission: string;
  gameTypeBreakdown: Record<string, string> | null;
  periodType: string;          // weekly / daily
  status: string;              // pending / approved / rejected
  riskFlagged: number;
  riskReasons: string | null;
  reviewedBy: string | null;
  reviewedAt: string | null;
  agentAccount?: string;
  agentName?: string;
  createdAt: string;
  updatedAt: string;
}

/** 代理提款記錄 */
export interface AffiliateWithdrawal {
  id: number;
  siteCode?: string;
  agentId: number;
  agentAccount?: string;
  agentName?: string;
  amount: string;
  method: string;
  bankCardId: number | null;
  cryptoAddressId: number | null;
  status: string;              // pending / approved / rejected / completed
  rejectReason: string | null;
  reviewedBy: string | null;
  reviewedAt: string | null;
  completedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

/** 綁定紀錄 */
export interface AffiliateBindLog {
  id: number;
  siteCode?: string;
  memberId: number;
  agentId: number;
  refCode: string;
  action: string;
  ip: string | null;
  device: string | null;
  operatorAccount: string | null;
  remark: string | null;
  createdAt: string;
}

/** 風控紀錄 */
export interface AffiliateRiskLog {
  id: number;
  settlementId: number;
  agentId: number;
  memberId: number | null;
  riskType: string;
  detail: string | null;
  createdAt: string;
}

/** 佣金費率 */
export interface AllianceCommissionRate {
  id: number;
  siteCode?: string;
  agentTier: string;           // bronze / silver / gold / platinum
  agentLevel: number;          // 1-3
  gameType: string | null;
  commissionRate: string;      // decimal
  enabled: number;
  createdAt: string;
  updatedAt: string;
}

/** VIP 里程碑 */
export interface AllianceVipMilestone {
  id: number;
  siteCode?: string;
  vipLevel: number;
  bonusAmount: string;
  description: string | null;
  enabled: number;
  createdAt: string;
  updatedAt: string;
}

/** 代理等級 */
export interface AllianceAgentTier {
  id: number;
  siteCode?: string;
  tierCode: string;
  tierName: string;
  minTotalEarned: string;
  minActiveMembers: number;
  sortOrder: number;
  createdAt: string;
  updatedAt: string;
}

4.6.7 vip.ts — VIP 型別

typescript
export interface VipLevel {
  id: number;
  level: number;
  siteCode: string;
  name: Record<string, string>;   // 多語系名稱
  tier: string;
  minChip: string;                // 最低投注門檻 (decimal)
  relegationChip: string;         // 保級門檻
  sortOrder: number;
  enabled: number;
  createdAt: string;
  updatedAt: string;
}

export interface VipRebate {
  id: number;
  level: number;
  gameType: string;               // sports/slot/live/lottery/chess/esports/crypto/fish
  rebateRate: string;             // 返水率 (decimal)
  siteCode: string;
  createdAt: string;
  updatedAt: string;
}

4.6.8 game.ts — 遊戲型別

typescript
export interface GameProvider {
  id: number;
  gameCode: string;               // e.g. "slot-betsolutions"
  providerCode: string;           // e.g. "betsolutions"
  gameType: number;               // 1=sports, 2=slot, 3=live, etc.
  label: Record<string, string>;  // 多語系標籤
  areaBlock: boolean;
  maintain: boolean;
  enable: boolean;
  sortOrder: number;
  isHot: boolean;
  siteCode: string;
  createdAt: string;
}

export interface GameTypeConfig {
  id: number;
  gameType: number;
  typeKey: string;                // e.g. "slot"
  label: Record<string, string>;
  icon: string;
  sortOrder: number;
  enabled: boolean;
  siteCode: string;
  createdAt: string;
  updatedAt: string;
}

4.6.9 promo.ts — 活動型別

typescript
export interface Promo {
  id: number;
  siteCode: string;
  title: Record<string, string>;          // 多語系標題
  imgPc: Record<string, string> | null;   // PC 端圖片
  imgMobile: Record<string, string> | null; // 手機端圖片
  content: Record<string, string>;        // 多語系內容 (HTML)
  actionHtml: string | null;
  startTime: string;
  endTime: string;
  tag: string;                            // 活動標籤
  enabled: number;
  conditionType: string;                  // 條件類型
  conditionValue: string;
  rewardAmount: string;                   // 獎勵金額 (decimal)
  turnoverMultiplier: string;             // 流水倍數
  maxClaims: number;                      // 最大領取次數
  claimedCount: number;                   // 已領取次數
  createdAt: string;
  updatedAt: string;
}

4.6.10 report.ts — 報表型別

typescript
/** 玩家報表 */
export interface ReportPlayer {
  id: number;
  siteCode?: string;
  account: string;
  name: string;
  vipLevel: number;
  balance: string;
  frozenBalance: string;
  totalEffectiveBet: string;
  totalDeposit: string;
  totalBet: string;
  totalWinLose: string;
  vendorGroupId: number | null;
  agentCode: string | null;
  level1AgentId: number | null;
  level2AgentId: number | null;
  level3AgentId: number | null;
  registrationIp: string | null;
  registrationDevice: string | null;
  lastActivityAt: string | null;
  createdAt: string;
}

/** 投注紀錄(管理端) */
export interface AdminBetRecord {
  id: number;
  siteCode?: string;
  userId: number;
  userAccount?: string;
  userName?: string;
  gameType: string;
  gamePlatform: string;
  gameName: string;
  betAmount: string;
  betEffective: string;
  winLose: string;
  status: string;
  betDatetime: string;
}

/** 總覽報表 */
export interface ReportOverview {
  siteCode?: string;
  totalUsers: number;
  totalDeposit: string;
  totalWithdrawal: string;
  totalBet: string;
  totalWinLose: string;
  dailySummary: {
    date: string;
    siteCode?: string;
    newUsers: number;
    deposit: string;
    bet: string;
    winLose: string;
  }[];
}

/** 損益報表項目 */
export interface ReportProfitLossItem {
  siteCode?: string;
  period: string;
  deposit: string;
  withdrawal: string;
  betWinLose: string;
  netProfit: string;
}

/** 遊戲報表統計 */
export interface ReportGameStat {
  siteCode?: string;
  gameType: string;
  gamePlatform: string;
  betCount: number;
  totalBet: string;
  totalWinLose: string;
  profitRate: string;
}

/** 活動報表統計 */
export interface ReportPromoStat {
  siteCode?: string;
  promoId: number;
  promoTitle: string;
  tag: string;
  claimCount: number;
  totalReward: string;
  enabled: number;
}

/** VIP 玩家 */
export interface VipPlayer {
  id: number;
  siteCode?: string;
  account: string;
  name: string;
  vipLevel: number;
  tier: string;
  balance: string;
  totalEffectiveBet: string;
  relegationMissCount: number;
  vipHold: number;
  createdAt: string;
}

/** 玩家簡表 */
export interface ReportPlayerSummary {
  id: number;
  siteCode?: string;
  account: string;
  name: string;
  vipLevel: number;
  balance: string;
  totalEffectiveBet: string;
  createdAt: string;
}

/** R2 操作紀錄 */
export interface R2OperationLog {
  id: number;
  siteCode?: string;
  adminId: number;
  admin?: { id: number; email: string; name: string };
  action: string;
  module: string;
  objectKey: string;
  originalName: string | null;
  fileSize: number | null;
  mimeType: string | null;
  ip: string;
  userAgent: string | null;
  detail: Record<string, unknown> | null;
  createdAt: string;
}

/** R2 檔案項目 */
export interface R2Item {
  key: string;
  name: string;
  type: 'folder' | 'file';
  size?: number;
  lastModified?: string;
}

/** R2 列表回應 */
export interface R2ListResponse {
  folders: R2Item[];
  files: R2Item[];
  prefix: string;
  isTruncated: boolean;
  nextContinuationToken: string | null;
}

4.6.11 index.ts — 額外型別定義

index.ts 除了 barrel re-export 外,還定義了以下未歸類的型別:

typescript
/** 活動標籤 */
export interface PromoTag {
  id: number;
  siteCode: string;
  name: string;
  label: Record<string, string>;
  color: string;
  sortOrder: number;
  enabled: number;
  createdAt: string;
  updatedAt: string;
}

/** 金流群組 */
export interface VendorGroup {
  id: number;
  siteCode?: string;
  name: Record<string, string>;
  enabled: number;
  channelCount: number;
  createdAt: string;
  updatedAt: string;
}

/** 金流通道 */
export interface VendorChannel {
  id: number;
  siteCode?: string;
  name: Record<string, string>;
  storeCode: string;
  secret1: string;
  secret2: string | null;
  secret3: string | null;
  secret4: string | null;
  currency: string;
  paymentMethods: string[];
  paymentAddress: string | null;
  enabled: number;
  groups?: { id: number; name: Record<string, string> }[];
  createdAt: string;
  updatedAt: string;
}

/** IP 規則 */
export interface RiskIpRule {
  id: number;
  ip: string;
  type: "blacklist" | "whitelist";
  remark: string;
  siteCode: string;
  createdAt: string;
  updatedAt: string;
}

/** 遊戲黑名單 */
export interface RiskGameBlacklist {
  id: number;
  userId: number;
  userAccount?: string;
  gameType: string | null;
  productId: number | null;
  remark: string;
  siteCode: string;
  createdAt: string;
}

/** 風控查詢結果 */
export interface RiskLookupResult {
  userId: number;
  account: string;
  name: string;
  email: string | null;
  mobile: string | null;
  siteCode: string;
  ip: string;
  device: string;
  lastUse: string;
}

4.7 Zustand 狀態管理

4.7.1 uiStore — UI 狀態

位置:src/stores/uiStore.ts

typescript
interface UIState {
  sidebarOpen: boolean;                    // Sidebar 展開/收合狀態
  toggleSidebar: () => void;               // 切換 Sidebar
  setSidebarOpen: (open: boolean) => void;  // 直接設定 Sidebar 狀態
}

// 初始值
{ sidebarOpen: true }

使用場景:Sidebar 元件與 Header 的漢堡選單按鈕共用此 store。


4.7.2 enumStore — 錯誤碼映射

位置:src/stores/enumStore.ts

typescript
interface EnumState {
  /** 錯誤碼映射表:errorCodes[path][code] = message */
  errorCodes: Record<string, Record<string, string>>;
  setErrorCodes: (codes: Record<string, Record<string, string>>) => void;
}

// 初始值
{ errorCodes: {} }

資料來源EnumInitializer 在 App 啟動時 fetch GET /api/common/enums,取得 ERROR_CODES 後填入。

使用場景httpRequest() 在收到非 200 回應時,使用 errorCodes[path][code] 查找可讀錯誤訊息。支援 :id 動態路由匹配(如 /api/admin/users/123 會嘗試匹配 /api/admin/users/:id)。


4.7.3 siteFilterStore — 多站點篩選(sessionStorage 持久化)

位置:src/stores/siteFilterStore.ts

typescript
interface SiteInfo {
  id: number;
  siteCode: string;
  prefix: string;
  siteName: Record<string, string>;
}

interface SiteFilterState {
  selectedSiteCode: string | null;         // null = 全站, string = 特定站點
  sites: SiteInfo[];                       // 可用站點列表
  setSelectedSiteCode: (code: string | null) => void;
  setSites: (sites: SiteInfo[]) => void;
}

// 持久化設定
persist({
  name: "c9-ims-site-filter",    // sessionStorage key
  storage: createJSONStorage(() => sessionStorage),
})

資料來源SiteFilterInitializer 在 App 啟動時 fetch GET /site-config/admin/list,解析出站點列表後填入 sites

使用場景

  1. SiteSelector(Header 下拉選單)讀寫 selectedSiteCode
  2. apiClient request interceptor 讀取 selectedSiteCode 注入 x-site-code header
  3. useMultiSiteTabs 讀取 selectedSiteCodesites 計算可見站點
  4. AdminContentWrapperkey={selectedSiteCode} 強制 remount

4.8 API 客戶端 (apiClient)

位置:src/lib/apiClient.ts

4.8.1 Token 快取機制

typescript
let cachedToken: string | null = null;

export function setToken(token: string | null): void;  // 由 SessionSync 設定
export function clearToken(): void;                     // 401 時清除

SessionSync 元件(在 Providers 中)監聽 NextAuth session 變化,將 session.accessToken 同步到模組級 cachedToken,避免每次 API 呼叫都需要 getSession()

4.8.2 Request Interceptor — 5 項自動注入

typescript
apiClient.interceptors.request.use(async (config) => {
  // 1. Content-Type(非 FormData 時設定 JSON)
  if (!(config.data instanceof FormData)) {
    config.headers["Content-Type"] = "application/json";
  }

  // 2. 動態 baseURL(從 hostname 解析域名配置)
  const hostname = getHostnameClient();
  const domainCfg = resolveDomainConfig(hostname);
  config.baseURL = `${domainCfg.baseUrl}/api`;

  // 3. site-name header(白牌站點路由)
  config.headers["site-name"] = domainCfg.siteId;

  // 4. locales header(從 NEXT_LOCALE cookie 讀取)
  config.headers["locales"] = document.cookie.match(/NEXT_LOCALE=([^;]+)/)?.[1] || "zh-TW";

  // 5. JWT Authorization(從 cachedToken 快取)
  if (cachedToken) {
    config.headers.Authorization = `Bearer ${cachedToken}`;
  }

  // 6. x-site-code header(多站篩選,跳過 site-config/admin/list)
  const isSiteConfigList = config.url?.includes('/site-config/admin/list');
  if (!isSiteConfigList) {
    const { selectedSiteCode } = useSiteFilterStore.getState();
    if (selectedSiteCode) {
      config.headers['x-site-code'] = selectedSiteCode;
    }
  }

  return config;
});

注入項目彙整表

注入項來源Header 名稱說明
baseURLdomainConfig[hostname]動態解析後端 API 位址
site-namedomainConfig[hostname].siteIdsite-name白牌站點識別
localesNEXT_LOCALE cookielocales多語系(決定後端回傳語系)
Authorization模組級 cachedTokenAuthorizationJWT Bearer Token
x-site-codesiteFilterStore.selectedSiteCodex-site-code多站篩選(跳過 getSiteConfigs)

4.8.3 Response Interceptor — 401 重試流程

收到 401 回應
  → 1. 清除 cachedToken
  → 2. 呼叫 getSession() 嘗試刷新 NextAuth session
  → 3. 取得新 token?
      → 是:更新 cachedToken + 重試原請求
      → 否:signOut() + 導向 /login?expired=1

重試限制:每個請求只重試一次(originalRequest._retry = true)。

4.8.4 httpRequest() — 三層錯誤碼映射

位置:src/lib/useHttp.ts

typescript
export interface HttpOptions {
  errorToast?: boolean;                              // 是否顯示 toast(預設 true)
  errorMessage?: string | Record<string, string>;    // 自訂錯誤文案
}

export async function httpRequest<T = any>(
  config: { url, method?, data?, params?, headers? },
  options?: HttpOptions,
): Promise<ApiResponse<T>>;

三層錯誤碼映射邏輯(當 code !== 200 時):

  1. 層 1 — Store 查表enumStore.errorCodes[path][code]
    • 支援 :id 動態路由匹配:/api/admin/users/123/api/admin/users/:id
  2. 層 2 — 呼叫端覆寫
    • errorMessage: string → 全覆蓋
    • errorMessage: Record<code, msg> → 按 code 覆蓋
  3. 層 3 — Toast 顯示toast.error(message) via sonner
    • 可用 errorToast: false 停用

4.9 NextAuth 認證系統

位置:src/lib/auth.ts

4.9.1 設定總覽

項目設定值
ProviderCredentials(email + password)
Session 策略JWT
Secret"c9-ims-auth-secret-key"
Trust Hosttrue
自訂登入頁/login

4.9.2 Credentials Provider 認證流程

1. 使用者提交 email + password
2. NextAuth 呼叫 authorize() 回調
3. 從 request headers 取得 hostname → resolveDomainConfig → { baseUrl, siteId }
4. POST ${baseUrl}/api/admin/login
   Headers: { "Content-Type": "application/json", "site-name": siteId }
   Body: { email, password }
5. 後端回傳:
   成功 → { code: 200, result: { token, admin: { id, name, email, group } } }
   失敗 → { code: !200, message: "..." }
6. 成功 → 回傳 User 物件,NextAuth 產生 JWT
   失敗 → throw CredentialsSignin

4.9.3 JWT Payload 擴展

typescript
interface ExtendedToken {
  id?: string;
  permissions?: string[];
  groupType?: GroupType;
  accessToken?: string;            // 後端發的 JWT token
  allowedSiteCodes?: string[] | null;
}

4.9.4 Session 擴展

typescript
declare module "next-auth" {
  interface User {
    permissions?: string[];
    groupType?: GroupType;
    accessToken?: string;
    allowedSiteCodes?: string[] | null;
  }
  interface Session {
    user: User & {
      id: string;
      permissions: string[];
      groupType: GroupType;
      allowedSiteCodes: string[] | null;
    };
    accessToken: string;
  }
}

4.9.5 JWT Callbacks

jwt callback:登入時將 user 資料寫入 token:

typescript
async jwt({ token, user }) {
  if (user) {
    token.id = user.id;
    token.permissions = user.permissions;
    token.groupType = user.groupType;
    token.accessToken = user.accessToken;
    token.allowedSiteCodes = user.allowedSiteCodes;
  }
  return token;
}

session callback:從 token 還原到 session:

typescript
async session({ session, token }) {
  session.user.id = token.id;
  session.user.permissions = token.permissions ?? [];
  session.user.groupType = token.groupType ?? "custom";
  session.user.allowedSiteCodes = token.allowedSiteCodes ?? null;
  session.accessToken = token.accessToken;
  return session;
}

4.9.6 SessionSync 元件

位置:src/components/layout/providers.tsx(內嵌元件)

typescript
function SessionSync() {
  const { data: session, status } = useSession();
  const wasAuthenticated = useRef(false);

  useEffect(() => {
    if (session?.accessToken) {
      setToken(session.accessToken);      // 同步到 apiClient 快取
      wasAuthenticated.current = true;
    } else {
      clearToken();
      // 曾登入過但 session 消失 → 導向登入頁
      if (wasAuthenticated.current && status === "unauthenticated") {
        window.location.href = "/login?expired=1";
      }
    }
  }, [session?.accessToken, status]);

  return null;
}

4.10 Middleware

位置:src/middleware.ts

4.10.1 職責

同時處理 i18n 路由認證保護

4.10.2 路由匹配規則

typescript
export const config = {
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

排除 api/_next/_vercel/、靜態檔案(含 .)。

4.10.3 處理邏輯

typescript
const PUBLIC_PATHS = ["/login"];

export default async function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;

  // 公開路徑判斷
  const isPublicPath = PUBLIC_PATHS.some(
    (p) => pathname === p || pathname.startsWith(`${p}/`),
  );

  // HTTPS 偵測(含反向代理)
  const isSecure =
    req.headers.get("x-forwarded-proto") === "https" ||
    req.nextUrl.protocol === "https:";

  // JWT Token 驗證
  const token = await getToken({
    req,
    secret: "c9-ims-auth-secret-key",
    secureCookie: isSecure,
  });

  // 未登入 + 非公開頁面 → 導向登入頁
  if (!token && !isPublicPath) {
    const loginUrl = new URL("/login", req.nextUrl.origin);
    loginUrl.searchParams.set("expired", "1");
    return NextResponse.redirect(loginUrl);
  }

  // 已登入 + 在登入頁 → 導向 dashboard
  if (token && isPublicPath) {
    return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
  }

  // 交由 next-intl middleware 處理 i18n
  return intlMiddleware(req);
}

4.10.4 行為彙整

場景行為
未登入 + 非公開頁面302 重導向 /login?expired=1
已登入 + 在登入頁302 重導向 /dashboard
其他情況交由 next-intl middleware 處理 locale

4.11 Provider 堆疊與初始化

4.11.1 Provider 組合

位置:src/components/layout/providers.tsx

<SessionProvider refetchOnWindowFocus={false}>       ← NextAuth Session
  <QueryClientProvider client={queryClient}>          ← TanStack Query
    <SessionSync />                                   ← JWT token → apiClient 快取
    <SiteConfigProvider value={siteConfig}>            ← 白牌站點配置 Context
      <SiteThemeInjector>                             ← OKLCH CSS 變數注入
        <LocaleGuard />                               ← 語系支援檢查
        <EnumInitializer />                           ← ERROR_CODES 初始化
        <SiteFilterInitializer />                     ← 站點列表初始化
        {children}                                    ← 頁面內容
        <Toaster richColors position="top-right" />   ← Sonner Toast
        <ConfirmDialog />                             ← 全域確認對話框
      </SiteThemeInjector>
    </SiteConfigProvider>
  </QueryClientProvider>
</SessionProvider>

4.11.2 QueryClient 預設配置

typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5 分鐘
      retry: 1,                   // 失敗重試 1 次
    },
  },
});

4.11.3 Admin Layout

位置:src/app/[locale]/(admin)/layout.tsx

typescript
export default function AdminLayout({ children }) {
  return (
    <div className="flex h-screen overflow-hidden">
      <Sidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Header />
        <main className="flex-1 overflow-y-auto p-6" data-scroll-region>
          <ScrollToTop />
          <AdminContentWrapper>{children}</AdminContentWrapper>
        </main>
      </div>
    </div>
  );
}

4.11.4 Locale Layout(Server Component)

位置:src/app/[locale]/layout.tsx

職責

  1. 驗證 locale 是否在支援列表中
  2. 解析域名配置(resolveDomainConfig
  3. 載入本地站點配置(loadSiteConfig
  4. 從後端 API 取得站點名稱 + 域名素材覆寫本地配置
  5. 產生 metadata(title + favicon)
  6. 包裹 NextIntlClientProvider + Providers

後端站點配置 API

GET ${baseUrl}/api/site-config?admin=true
Headers: { "site-name": siteId, "locales": "zh-TW" }
Cache: next.revalidate = 300 (5 分鐘)

4.12 完整頁面文件(68 個 page.tsx)

4.12.1 根頁面與認證頁面

/ — 根頁面重導向

  • 路由src/app/[locale]/page.tsx
  • 行為redirect → /dashboard
  • 元件:無
  • API:無
  • 多站點:不適用

/login — 登入頁

  • 路由src/app/[locale]/(auth)/login/page.tsx
  • 用途:管理員登入(email + password)
  • 佈局:(auth) 群組 — 置中卡片佈局
  • 元件:Card, Input, Label, Button, Loader2
  • 表單:React Hook Form + zodResolver(loginSchema)
  • 認證signIn("credentials", { email, password, redirect: false })
  • 特殊行為
    • URL 帶 ?expired=1 時顯示 session 過期 toast
    • 登入成功後 router.push("/dashboard") + router.refresh()
  • 多站點:不適用
  • 權限:公開頁面

4.12.2 儀表板

/dashboard — 儀表板

  • 路由src/app/[locale]/(admin)/dashboard/page.tsx
  • 用途:統計卡片 + 折線圖 + 柱狀圖 + 最近活動
  • 元件:Card, Recharts (LineChart, BarChart), StatCard (內嵌元件)
  • API:目前使用 Demo 靜態資料
  • Feature FlagenableBilling 控制收入卡片,enableAnalytics 控制圖表區域
  • 多站點:待實作
  • 權限:登入即可存取

4.12.3 系統管理(15 個頁面)

/system/admins — 管理員列表

  • 路由src/app/[locale]/(admin)/system/admins/page.tsx
  • 用途:管理員 CRUD 列表
  • 元件:SimpleTable, FilterBar, StatusBadge, Badge
  • APIgetAdmins(), getGroups(), deleteAdmin()
  • 篩選:keyword (email/name), status (select), groupId (select), startDate, endDate
  • 多站點:不需要(全站共用)
  • 權限canRead("admin")

/system/admins/new — 新增管理員

  • 路由src/app/[locale]/(admin)/system/admins/new/page.tsx
  • 用途:三步驟管理員註冊(發送驗證碼 → 驗證 → 註冊)
  • APIadminSendVerifyCode(), adminVerifyEmail(), adminRegister(), getGroups()
  • 表單:useState + Zod safeParse
  • 多站點:不需要
  • 權限canWrite("admin")

/system/admins/[id] — 編輯管理員

  • 路由src/app/[locale]/(admin)/system/admins/[id]/page.tsx
  • 用途:編輯管理員(name, password, groupId, status, allowedSiteCodes)
  • APIgetAdmin(), updateAdmin(), getGroups(), getSiteConfigs()
  • 多站點:不需要(但可設定管理員的 allowedSiteCodes)
  • 權限canWrite("admin")

/system/groups — 群組列表

  • 路由src/app/[locale]/(admin)/system/groups/page.tsx
  • 用途:管理群組 CRUD 列表
  • APIgetGroups(), deleteGroup()
  • 多站點:不需要(全站共用)
  • 權限canRead("admin-group")

/system/groups/new — 新增群組

  • 路由src/app/[locale]/(admin)/system/groups/new/page.tsx
  • 用途:建立新群組 + 權限矩陣設定
  • APIcreateGroup()
  • 表單:useState + Zod safeParse (createGroupSchema)
  • 權限canWrite("admin-group")

/system/groups/[id] — 編輯群組

  • 路由src/app/[locale]/(admin)/system/groups/[id]/page.tsx
  • 用途:編輯群組名稱、描述、狀態、權限矩陣
  • APIgetGroup(), updateGroup()
  • 權限canWrite("admin-group")

/system/logs — 操作紀錄

  • 路由src/app/[locale]/(admin)/system/logs/page.tsx
  • 用途:管理員操作紀錄查詢
  • APIgetAdminLogs()
  • 多站點:不需要(全站共用)
  • 權限canRead("admin-log")

/system/site-config — 站點基本設定

  • 路由src/app/[locale]/(admin)/system/site-config/page.tsx
  • 用途:站點 CRUD + 站點設定子頁面(OAuth, 遊戲商, 服務商, 代理導覽)
  • APIgetSiteConfigs(), createSiteConfig(), updateSiteConfig(), deleteSiteConfig()
  • 子元件:CreateSiteDialog, OAuthSettings, GameProviderSettings, ServiceProviderSettings, AgentTourSettings, DomainSettings
  • 多站點:本身就是站點管理頁面
  • 權限canRead("site-config")

/system/site-oauth — 三方登入設定

  • 路由src/app/[locale]/(admin)/system/site-oauth/page.tsx
  • 用途:OAuth 提供者設定(Google, Telegram 等)
  • 模式:模式 B(設定頁),支援「同預設站點」前端狀態拷貝
  • 多站點:已完成
  • 權限canRead("site-config")

/system/site-domains — 域名管理

  • 路由src/app/[locale]/(admin)/system/site-domains/page.tsx
  • 用途:域名設定(hostname, browserTitle, logo, favicon, supportedLocales)
  • 模式:模式 B(設定頁),「同預設站點」直接 API 寫入
  • APIgetSiteConfigs(), updateSiteConfig(), uploadDomainAsset()
  • 多站點:已完成
  • 權限canRead("site-config")

/system/site-game-providers — 遊戲商配置

  • 路由src/app/[locale]/(admin)/system/site-game-providers/page.tsx
  • 模式:模式 B(設定頁),支援「同預設站點」前端狀態拷貝
  • 多站點:已完成
  • 權限canRead("site-config")

/system/site-service-providers — 服務商配置

  • 路由src/app/[locale]/(admin)/system/site-service-providers/page.tsx
  • 模式:模式 B(設定頁),支援「同預設站點」前端狀態拷貝
  • 多站點:已完成
  • 權限canRead("site-config")

/system/site-customer-service — 客服配置

  • 路由src/app/[locale]/(admin)/system/site-customer-service/page.tsx
  • 用途:8 種客服管道設定(line, telegram, wechat, facebook, instagram, twitter, discord, custom)+ LiveChat 嵌入腳本
  • 模式:模式 B(設定頁),支援「同預設站點」前端狀態拷貝(editMap + liveChatMap)
  • APIgetSiteConfigs(), updateSiteConfig(), uploadCustomerServiceIcon()
  • 多站點:已完成
  • 權限canRead("site-config")

/system/cloud-storage — 雲端儲存

  • 路由src/app/[locale]/(admin)/system/cloud-storage/page.tsx
  • 用途:R2 檔案管理(瀏覽、上傳、刪除、移動、建立資料夾)
  • APIr2List(), r2Upload(), r2Delete(), r2Move(), r2CreateFolder(), r2DeleteFolder()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("admin")

/system/cloud-storage-logs — 雲端儲存日誌

  • 路由src/app/[locale]/(admin)/system/cloud-storage-logs/page.tsx
  • 用途:R2 操作紀錄(上傳/刪除),含 OS/瀏覽器 UA 解析 + Detail Dialog + mimeType
  • APIgetR2Logs()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("admin")

4.12.4 前台佈局配置(3 個頁面)

/system/layout-bottom-bar — 底部導航列

  • 路由src/app/[locale]/(admin)/system/layout-bottom-bar/page.tsx
  • 用途:前台行動版底部 Tab 項目配置
  • 子元件:BottomBarSettings, bottomBarTemplates
  • 模式:模式 B(設定頁)
  • 多站點:已完成
  • 權限canRead("site-config")
  • 路由src/app/[locale]/(admin)/system/layout-footer/page.tsx
  • 用途:前台頁尾連結、版權資訊配置
  • 子元件:FooterSettings, footerTemplates
  • 模式:模式 B(設定頁)
  • 多站點:已完成
  • 權限canRead("site-config")

/system/layout-learn-more — 了解更多

  • 路由src/app/[locale]/(admin)/system/layout-learn-more/page.tsx
  • 用途:前台「了解更多」FAQ 管理
  • 子元件:LearnMoreSettings, TemplateVariablesEditor, learnMoreTemplates
  • 模式:模式 B(設定頁)
  • 多站點:已完成
  • 權限canRead("site-config")

4.12.5 玩家管理(7 個頁面)

/players/all — 所有玩家

  • 路由src/app/[locale]/(admin)/players/all/page.tsx
  • 用途:前台用戶列表(含餘額、VIP、代理資訊)
  • APIgetReportPlayers(), getVipLevels()
  • 元件:SimpleTable, FilterBar, SiteTabs, PlayerDetailDialog, VendorGroupDialog
  • 篩選:keyword, vipLevel (動態 select), bankAccount, cardNumber, cryptoAddress, online, startDate, endDate
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("user")

/players/all/[id] — 玩家詳情

  • 路由src/app/[locale]/(admin)/players/all/[id]/page.tsx
  • 用途:玩家完整資料(個人資訊、錢包、存提款紀錄、投注紀錄、登入日誌)
  • APIgetAuthUser()
  • 子元件:EditPlayerInfoDialog, AddBankCardDialog, EditBankCardDialog, AddCreditCardDialog, EditCreditCardDialog, AddCryptoAddressDialog, EditCryptoAddressDialog
  • 多站點:不需要(詳情頁)
  • 權限canRead("user")

/players/new-registrations — 新註冊玩家

  • 路由src/app/[locale]/(admin)/players/new-registrations/page.tsx
  • 用途:近期新註冊用戶列表
  • APIgetReportPlayers(), getVipLevels()
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("user")

/players/online — 在線玩家

  • 路由src/app/[locale]/(admin)/players/online/page.tsx
  • 用途:目前在線的玩家列表
  • APIgetReportPlayers({ online: true })
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("user")

/players/login-failures — 登入失敗紀錄

  • 路由src/app/[locale]/(admin)/players/login-failures/page.tsx
  • 用途:登入失敗紀錄查詢
  • APIgetLoginFailures()
  • 篩選:keyword, startDate, endDate
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("user")

/players/tags — 玩家標籤

  • 路由src/app/[locale]/(admin)/players/tags/page.tsx
  • 用途:玩家標籤管理
  • 多站點:待實作
  • 權限canRead("user")

/players/game-reregistration — 遊戲重新註冊

  • 路由src/app/[locale]/(admin)/players/game-reregistration/page.tsx
  • 用途:遊戲帳號重新註冊管理
  • 多站點:待實作
  • 權限canRead("user")

4.12.6 活動管理(4 個頁面)

/activity/promos — 活動列表

  • 路由src/app/[locale]/(admin)/activity/promos/page.tsx
  • 用途:優惠活動 CRUD 列表
  • APIgetPromos(), deletePromo()
  • 篩選:title, tag (select), conditionType (select), enabled (select), startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("promo")

/activity/promos/new — 新增活動

  • 路由src/app/[locale]/(admin)/activity/promos/new/page.tsx
  • 用途:建立新活動
  • APIcreatePromo(), getPromoTags()
  • 元件:PromoForm (多語系標題/內容/圖片)
  • 多站點:不需要(由列表頁帶入 siteCode)
  • 權限canWrite("promo")

/activity/promos/[id] — 編輯活動

  • 路由src/app/[locale]/(admin)/activity/promos/[id]/page.tsx
  • 用途:編輯活動詳情
  • APIgetPromo(), updatePromo(), getPromoTags()
  • 子元件:PromoForm, PromoPreview
  • 多站點:不需要(詳情頁)
  • 權限canWrite("promo")

/activity/tags — 活動標籤

  • 路由src/app/[locale]/(admin)/activity/tags/page.tsx
  • 用途:活動標籤 CRUD
  • APIgetPromoTags(), createPromoTag(), updatePromoTag(), deletePromoTag()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("promo-tag")

4.12.7 站內信(2 個頁面)

/mail/inbox — 站內信管理

  • 路由src/app/[locale]/(admin)/mail/inbox/page.tsx
  • 用途:站內信列表 + 發送/編輯/刪除
  • APIgetInboxAdmin(), sendInbox(), updateInbox(), deleteInbox()
  • 多站點:已完成(SiteTabs)
  • 權限:無特定限制

/mail/settings — 郵件設定

  • 路由src/app/[locale]/(admin)/mail/settings/page.tsx
  • 用途:郵件模板設定
  • 多站點:已完成(模式 B 設定頁)
  • 權限:無特定限制

4.12.8 財務管理(7 個頁面)

/finance/adjust-balance — 調整餘額

  • 路由src/app/[locale]/(admin)/finance/adjust-balance/page.tsx
  • 用途:搜尋用戶 → 選擇 → 加減餘額
  • APIgetAuthUsers(), adjustBalance()
  • 表單:useState + Zod safeParse (adjustBalanceSchema)
  • 多站點:已完成(SiteTabs,手動展開模式)
  • 權限canWrite("finance")

/finance/deposit-settings — 入金設定

  • 路由src/app/[locale]/(admin)/finance/deposit-settings/page.tsx
  • 用途:金流群組 + 金流通道管理
  • APIgetVendorGroups(), createVendorGroup(), getVendorChannels(), createVendorChannel(), setGroupChannels()
  • 子元件:VendorGroupSection, VendorChannelSection, GroupDialog, ChannelDialog
  • 多站點:已完成(SiteTabs)
  • 權限canRead("vendor")

/finance/deposit-review — 存款審核

  • 路由src/app/[locale]/(admin)/finance/deposit-review/page.tsx
  • 用途:存款訂單審核列表
  • APIgetDepositReview(), reviewDeposit()
  • 篩選:orderId, userId, keyword, paymentMethod (select), status (select), startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("deposit")

/finance/withdrawals — 提領審核

  • 路由src/app/[locale]/(admin)/finance/withdrawals/page.tsx
  • 用途:提領訂單三階段審核(待審核 → 已核准 → 已完成)
  • APIgetAdminWithdrawals(), adminReviewWithdrawal(), adminUploadWithdrawalProof(), adminCompleteWithdrawal()
  • 篩選:orderId, status (select), userId, keyword, network, startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("withdrawal")

/finance/bank-cards — 銀行卡列表

  • 路由src/app/[locale]/(admin)/finance/bank-cards/page.tsx
  • APIgetBankCards(), reviewBankCard()
  • 篩選:keyword, userId, status (select), bankCode, holderName, startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("finance")

/finance/credit-cards — 信用卡列表

  • 路由src/app/[locale]/(admin)/finance/credit-cards/page.tsx
  • APIgetCreditCards(), reviewCreditCard()
  • 篩選:keyword, userId, status (select), holderName, startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("finance")

/finance/crypto-addresses — 加密地址列表

  • 路由src/app/[locale]/(admin)/finance/crypto-addresses/page.tsx
  • APIgetCryptoAddresses(), reviewCryptoAddress()
  • 篩選:keyword, userId, status (select), network, currency, startDate, endDate
  • 多站點:已完成(SiteTabs)
  • 權限canRead("finance")

4.12.9 遊戲管理(2 個頁面)

/game/providers — 遊戲供應商

  • 路由src/app/[locale]/(admin)/game/providers/page.tsx
  • 用途:遊戲供應商 CRUD + 模板帶入 + 同預設站點 + 跨站複製
  • APIgetGameProviders(), createGameProvider(), updateGameProvider(), deleteGameProvider(), previewGameTemplate(), loadGameTemplate(), copyGameSiteData(), getSiteConfigs()
  • 元件:SimpleTable, SiteTabs, TemplatePreviewDialog, Dialog
  • 特殊功能:inline Switch 開關(isHot, maintain, enable, areaBlock)
  • 多站點:已完成(模式 A + 同預設站點 + 帶入模板)
  • 權限canRead("game")

/game/type-configs — 遊戲分類設定

  • 路由src/app/[locale]/(admin)/game/type-configs/page.tsx
  • 用途:遊戲分類 CRUD + 模板帶入 + 同預設站點 + 跨站複製
  • APIgetGameTypeConfigs(), createGameTypeConfig(), updateGameTypeConfig(), deleteGameTypeConfig(), previewGameTemplate(), loadGameTemplate(), copyGameSiteData()
  • 多站點:已完成(同 providers 模式)
  • 權限canRead("game")

4.12.10 VIP 管理(4 個頁面)

/vip/levels — VIP 等級

  • 路由src/app/[locale]/(admin)/vip/levels/page.tsx
  • 用途:VIP 等級 CRUD + 模板帶入 + 同預設站點
  • APIgetVipLevels(), createVipLevel(), updateVipLevel(), deleteVipLevel(), previewVipTemplate(), loadVipTemplate()
  • 多站點:已完成(模式 A + 同預設站點 + 帶入模板)
  • 權限canRead("vip")

/vip/rebates — VIP 返水規則

  • 路由src/app/[locale]/(admin)/vip/rebates/page.tsx
  • 用途:8 遊戲類型 Tabs,每 Tab 顯示各等級返水率 Input
  • 特殊模式:inline editing + bulk save
    • 修改追蹤:RateMap (Record<string, string>) 比對 original vs edited
    • 髒資料指示:amber 高亮行 + Tab 上的 amber dot
    • 單一「全部儲存」按鈕 → POST /vip/rebates/bulk
  • APIgetVipRebates(), bulkUpsertVipRebates(), getVipLevels()
  • 多站點:已完成(模式 A + 同預設站點)
  • 權限canRead("vip")

/vip/players — VIP 玩家

  • 路由src/app/[locale]/(admin)/vip/players/page.tsx
  • 用途:VIP 玩家管理列表
  • APIgetVipPlayers()
  • 篩選:keyword, vipLevelMin, vipLevelMax, tier, relegationStatus, vipHold, minBet, maxBet, startDate, endDate
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("vip")

/vip/milestones — VIP 里程碑

  • 路由src/app/[locale]/(admin)/vip/milestones/page.tsx
  • 用途:VIP 里程碑配置(下線 VIP 達標獎勵)
  • APIgetVipMilestones() (來自 useAffiliateApi)
  • 多站點:已完成(SiteTabs)
  • 權限canRead("vip")

4.12.11 報表(7 個頁面)

所有報表頁面均使用 useMultiSiteTabs hook + ExportButton CSV 匯出。

/reports/overview — 總覽報表

  • 路由src/app/[locale]/(admin)/reports/overview/page.tsx
  • 用途:統計卡片(用戶數、存款、提領、投注、盈虧)+ 每日摘要表格
  • APIgetReportOverview()
  • 篩選:startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/players — 玩家報表

  • 路由src/app/[locale]/(admin)/reports/players/page.tsx
  • APIgetReportPlayers()
  • 篩選:keyword, vipLevel (select 0-15), startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/player-summary — 玩家簡表

  • 路由src/app/[locale]/(admin)/reports/player-summary/page.tsx
  • APIgetReportPlayerSummary()
  • 篩選:keyword, vipLevel (select), sortBy, sortOrder, startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/bet-records — 投注紀錄

  • 路由src/app/[locale]/(admin)/reports/bet-records/page.tsx
  • APIgetReportBetRecords()
  • 篩選:keyword, gameType (select), gamePlatform (select), status (select), startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/games — 遊戲報表

  • 路由src/app/[locale]/(admin)/reports/games/page.tsx
  • APIgetReportGames()
  • 篩選:gameType (select), gamePlatform (select), startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/profit-loss — 損益報表

  • 路由src/app/[locale]/(admin)/reports/profit-loss/page.tsx
  • APIgetReportProfitLoss()
  • 篩選:groupBy (日/週/月 select), gameType (select), startDate, endDate
  • 多站點:已完成
  • 權限canRead("report")

/reports/promos — 活動報表

  • 路由src/app/[locale]/(admin)/reports/promos/page.tsx
  • APIgetReportPromos()
  • 篩選:startDate, endDate
  • 多站點:待加入
  • 權限canRead("report")

4.12.12 風控管理(3 個頁面)

/risk-control/ip-rules — IP 黑白名單

  • 路由src/app/[locale]/(admin)/risk-control/ip-rules/page.tsx
  • 用途:IP 黑白名單 CRUD
  • APIgetRiskIpRules(), createRiskIpRule(), updateRiskIpRule(), deleteRiskIpRule()
  • 篩選:type (黑/白名單 select), keyword (IP), startDate, endDate
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("risk")

/risk-control/ip-check — IP/FP 檢查

  • 路由src/app/[locale]/(admin)/risk-control/ip-check/page.tsx
  • 用途:IP/裝置指紋/帳號/姓名/Email/手機反查用戶
  • APIriskLookup()
  • 篩選:keyword (統一搜尋欄)
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("risk")

/risk-control/game-blacklist — 遊戲黑名單

  • 路由src/app/[locale]/(admin)/risk-control/game-blacklist/page.tsx
  • 用途:遊戲黑名單 CRUD(支援全封鎖/類型封鎖/特定遊戲封鎖)
  • APIgetRiskGameBlacklist(), createRiskGameBlacklist(), deleteRiskGameBlacklist()
  • 篩選:userId, userAccount, gameType (select), startDate, endDate
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("risk")

4.12.13 代理中心(8 個頁面)

/affiliate/agents — 代理列表

  • 路由src/app/[locale]/(admin)/affiliate/agents/page.tsx
  • APIgetAffiliates(), createAgent(), setAgentTier()
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("affiliate")

/affiliate/commission-rates — 佣金費率

  • 路由src/app/[locale]/(admin)/affiliate/commission-rates/page.tsx
  • 用途:按代理等級 x 遊戲類型配置佣金費率
  • APIgetCommissionRates(), upsertCommissionRate()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("affiliate")

/affiliate/settlements — 結算紀錄

  • 路由src/app/[locale]/(admin)/affiliate/settlements/page.tsx
  • APIgetSettlements(), reviewSettlement(), getSettlementRiskLogs()
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("affiliate")

/affiliate/aff-withdrawals — 代理提領

  • 路由src/app/[locale]/(admin)/affiliate/aff-withdrawals/page.tsx
  • APIgetAffiliateWithdrawals(), reviewAffiliateWithdrawal(), completeAffiliateWithdrawal()
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("affiliate")

/affiliate/bind-logs — 綁定紀錄

  • 路由src/app/[locale]/(admin)/affiliate/bind-logs/page.tsx
  • APIgetBindLogs(), adminBind()
  • 多站點:已完成(useMultiSiteTabs)
  • 權限canRead("affiliate")

/affiliate/agent-tiers — 代理層級

  • 路由src/app/[locale]/(admin)/affiliate/agent-tiers/page.tsx
  • APIgetAgentTiers(), upsertAgentTier(), deleteAgentTier()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("affiliate")

/affiliate/vip-milestones — VIP 里程碑(聯盟)

  • 路由src/app/[locale]/(admin)/affiliate/vip-milestones/page.tsx
  • APIgetVipMilestones(), upsertVipMilestone(), deleteVipMilestone()
  • 多站點:已完成(SiteTabs)
  • 權限canRead("affiliate")

/affiliate/agent-tour — 代理導覽設定

  • 路由src/app/[locale]/(admin)/affiliate/agent-tour/page.tsx
  • 用途:代理推廣頁面的導覽內容設定
  • 模式:模式 B(設定頁)
  • 多站點:已完成
  • 權限canRead("affiliate")

4.12.14 其他頁面(4 個)

/roles — 角色管理

  • 路由src/app/[locale]/(admin)/roles/page.tsx
  • 用途:重導向至 /system/groups
  • Feature FlagenableRBAC
  • 多站點:不適用

/users — 用戶管理列表

  • 路由src/app/[locale]/(admin)/users/page.tsx
  • Feature FlagenableUserManagement
  • 多站點:不需要

/users/new — 新增用戶

  • 路由src/app/[locale]/(admin)/users/new/page.tsx
  • 多站點:不需要

/users/[id] — 編輯用戶

  • 路由src/app/[locale]/(admin)/users/[id]/page.tsx
  • 多站點:不需要

4.13 表單驗證 Schema(Zod v4)

檔案位置src/lib/validations/套件版本:Zod v4.3.6 Import 方式import { z } from "zod/v4"(注意 v4 使用 sub-import 路徑)

4.13.1 概覽

後台使用 Zod v4 進行所有表單驗證。共有 7 個驗證 Schema 檔案,涵蓋認證、管理員、群組、財務、活動、用戶、角色等業務領域。

檔案Schema 數量衍生型別數量用途
auth.ts11登入表單驗證
admin.ts44管理員三步驟建立 + 編輯驗證
group.ts22群組建立 + 編輯驗證
finance.ts11餘額調整驗證
promo.ts11活動建立/編輯驗證
user.ts22用戶建立 + 編輯驗證
role.ts2 + 1 子 Schema2角色建立 + 編輯驗證

4.13.2 使用模式

後台有兩種表單驗證模式:

模式 A:React Hook Form + zodResolver(推薦,用於登入頁等正式表單)

tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema, type LoginInput } from "@/lib/validations/auth";

const form = useForm<LoginInput>({
  resolver: zodResolver(loginSchema),
  defaultValues: { email: "", password: "" },
});

// 在 JSX 中使用
<input {...form.register("email")} />
{form.formState.errors.email && <p>{form.formState.errors.email.message}</p>}

模式 B:useState + safeParse(用於大多數 Admin 表單)

tsx
import { adminRegisterSchema } from "@/lib/validations/admin";

const [form, setForm] = useState({ email: "", password: "", name: "", code: "" });
const [errors, setErrors] = useState<Record<string, string>>({});

const handleSubmit = () => {
  const result = adminRegisterSchema.safeParse(form);
  if (!result.success) {
    const fieldErrors: Record<string, string> = {};
    for (const issue of result.error.issues) {
      fieldErrors[issue.path[0] as string] = issue.message;
    }
    setErrors(fieldErrors);
    return;
  }
  // result.data 為已驗證的資料
  await api.adminRegister(result.data);
};

4.13.3 Auth Schema — auth.ts

檔案src/lib/validations/auth.ts

typescript
import { z } from "zod/v4";

export const loginSchema = z.object({
  email: z.string().email("請輸入有效的 Email"),
  password: z.string().min(6, "密碼至少 6 個字元"),
});

export type LoginInput = z.infer<typeof loginSchema>;

用途:登入頁面(/login)表單驗證,搭配 React Hook Form + zodResolver 使用。

欄位型別驗證規則錯誤訊息
emailstringemail() 格式檢查"請輸入有效的 Email"
passwordstringmin(6) 最少 6 字元"密碼至少 6 個字元"

4.13.4 Admin Schema — admin.ts

檔案src/lib/validations/admin.ts

後台管理員建立採用三步驟流程(送驗證碼 → 驗證 → 註冊),每步驟各有獨立 Schema:

adminSendCodeSchema(步驟 1:送驗證碼)

typescript
export const adminSendCodeSchema = z.object({
  email: z.string().email("請輸入有效的 Email"),
});
export type AdminSendCodeInput = z.infer<typeof adminSendCodeSchema>;
欄位型別驗證規則說明
emailstringemail()管理員 Email,用於發送驗證碼

adminVerifyCodeSchema(步驟 2:驗證碼確認)

typescript
export const adminVerifyCodeSchema = z.object({
  email: z.string().email("請輸入有效的 Email"),
  code: z.string().min(1, "請輸入驗證碼"),
});
export type AdminVerifyCodeInput = z.infer<typeof adminVerifyCodeSchema>;
欄位型別驗證規則說明
emailstringemail()同步驟 1 的 Email
codestringmin(1)Email 驗證碼

adminRegisterSchema(步驟 3:註冊)

typescript
export const adminRegisterSchema = z.object({
  email: z.string().email("請輸入有效的 Email"),
  password: z.string().min(6, "密碼至少 6 個字元"),
  name: z.string().min(2, "名稱至少 2 個字元"),
  code: z.string().min(1, "請輸入驗證碼"),
  groupId: z.string().optional(),
});
export type AdminRegisterInput = z.infer<typeof adminRegisterSchema>;
欄位型別驗證規則說明
emailstringemail()管理員 Email
passwordstringmin(6)登入密碼
namestringmin(2)管理員顯示名稱
codestringmin(1)Email 驗證碼
groupIdstring?optional()所屬群組 ID(可選)

updateAdminSchema(編輯管理員)

typescript
export const updateAdminSchema = z.object({
  name: z.string().min(2, "名稱至少 2 個字元"),
  password: z.union([z.string().min(6, "密碼至少 6 個字元"), z.literal("")]),
  groupId: z.string().optional(),
  status: z.string(),
  allowedSiteCodes: z.array(z.string()).optional(),
});
export type UpdateAdminInput = z.infer<typeof updateAdminSchema>;
欄位型別驗證規則說明
namestringmin(2)管理員顯示名稱
passwordstring | ""union([min(6), literal("")])密碼(空字串表示不修改)
groupIdstring?optional()所屬群組 ID
statusstring必填狀態值("0" 或 "1")
allowedSiteCodesstring[]?optional()可存取的站點代碼列表(多站點 RBAC)

特殊設計password 欄位使用 z.union() 允許空字串,這是因為編輯管理員時密碼為選填欄位 — 若留空表示不修改密碼,若填寫則需至少 6 字元。

4.13.5 Group Schema — group.ts

檔案src/lib/validations/group.ts

createGroupSchema(建立群組)

typescript
export const createGroupSchema = z.object({
  name: z.string().min(1, "請輸入群組名稱"),
  description: z.string().optional(),
  permissions: z.array(z.string()).min(1, "請至少選擇一個權限"),
});
export type CreateGroupInput = z.infer<typeof createGroupSchema>;
欄位型別驗證規則說明
namestringmin(1)群組名稱
descriptionstring?optional()群組描述
permissionsstring[]min(1)權限列表(格式:"module:action"

updateGroupSchema(編輯群組)

typescript
export const updateGroupSchema = z.object({
  name: z.string().min(1, "請輸入群組名稱"),
  description: z.string().optional(),
  status: z.string(),
  permissions: z.array(z.string()).min(1, "請至少選擇一個權限"),
});
export type UpdateGroupInput = z.infer<typeof updateGroupSchema>;

與建立 Schema 相比,多了 status 欄位用於啟用/停用群組。

4.13.6 Finance Schema — finance.ts

檔案src/lib/validations/finance.ts

typescript
export const adjustBalanceSchema = z.object({
  type: z.enum(["add", "deduct"]),
  amount: z.string().min(1, "請輸入金額"),
  reason: z.string().min(1, "請輸入原因"),
});
export type AdjustBalanceInput = z.infer<typeof adjustBalanceSchema>;
欄位型別驗證規則說明
type"add" | "deduct"enum操作類型:加值 / 扣款
amountstringmin(1)調整金額(字串型別,前端轉 number)
reasonstringmin(1)調整原因(必填,用於審計追蹤)

設計說明amount 使用 string 而非 number 是因為 HTML Input 元素的 value 屬性始終為字串,在提交時由 API 層轉換為 number。

4.13.7 Promo Schema — promo.ts

檔案src/lib/validations/promo.ts

typescript
export const promoSchema = z.object({
  tag: z.string().min(1, "請輸入標籤"),
  startTime: z.string().min(1, "請選擇開始時間"),
  endTime: z.string().min(1, "請選擇結束時間"),
  conditionType: z.string().min(1, "請選擇條件類型"),
  conditionValue: z.string(),
  rewardAmount: z.string().min(1, "請輸入獎勵金額"),
  turnoverMultiplier: z.string(),
  maxClaims: z.string(),
});
export type PromoInput = z.infer<typeof promoSchema>;
欄位型別驗證規則說明
tagstringmin(1)活動標籤
startTimestringmin(1)活動開始時間
endTimestringmin(1)活動結束時間
conditionTypestringmin(1)條件類型(存款/投注等)
conditionValuestring可為空條件數值
rewardAmountstringmin(1)獎勵金額(USD)
turnoverMultiplierstring可為空打碼倍率
maxClaimsstring可為空最大領取次數

4.13.8 User Schema — user.ts

檔案src/lib/validations/user.ts

createUserSchema(建立用戶)

typescript
export const createUserSchema = z.object({
  email: z.string().email("請輸入有效的 Email"),
  name: z.string().min(2, "名稱至少 2 個字元").max(50, "名稱最多 50 個字元"),
  roleId: z.string().min(1, "請選擇角色"),
  status: z.enum(["active", "inactive"]),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
欄位型別驗證規則說明
emailstringemail()用戶 Email
namestringmin(2).max(50)用戶名稱(2-50 字元)
roleIdstringmin(1)指定角色 ID
status"active" | "inactive"enum用戶狀態

updateUserSchema(編輯用戶)

typescript
export const updateUserSchema = createUserSchema.partial();
export type UpdateUserInput = z.infer<typeof updateUserSchema>;

使用 partial() 將所有欄位轉為選填,允許部分更新。

4.13.9 Role Schema — role.ts

檔案src/lib/validations/role.ts

typescript
const permissionSchema = z.object({
  resource: z.enum(["dashboard", "users", "roles", "billing", "analytics"]),
  actions: z.array(z.enum(["read", "create", "update", "delete"])).min(1, "至少選擇一個操作"),
});

export const createRoleSchema = z.object({
  name: z.string().min(2, "角色名稱至少 2 個字元").max(30, "角色名稱最多 30 個字元"),
  description: z.string().max(200, "描述最多 200 個字元").optional(),
  permissions: z.array(permissionSchema).min(1, "至少設定一條權限"),
});

export const updateRoleSchema = createRoleSchema.partial();

子 Schema:permissionSchema(權限項目)

欄位型別驗證規則說明
resourceenum5 種資源dashboard, users, roles, billing, analytics
actionsenum[]至少 1 個read, create, update, delete

createRoleSchema

欄位型別驗證規則說明
namestringmin(2).max(30)角色名稱
descriptionstring?max(200).optional()角色描述
permissionspermissionSchema[]min(1)權限配置陣列

4.13.10 型別推導慣例

所有 Schema 都使用 z.infer<typeof schema> 產生對應的 TypeScript 型別:

typescript
export type LoginInput = z.infer<typeof loginSchema>;
export type AdminRegisterInput = z.infer<typeof adminRegisterSchema>;
export type CreateGroupInput = z.infer<typeof createGroupSchema>;

這確保了 Schema 與型別的單一來源(Single Source of Truth),Schema 修改時型別自動同步。


4.14 共用元件完整文件

4.14.1 SimpleTable — 輕量資料表格

檔案src/components/shared/simpleTable.tsx用途:所有真實 API 頁面的標準資料表格元件,支援伺服器端分頁。

泛型介面

typescript
export interface SimpleColumn<T> {
  /** 欄位唯一識別 key */
  key: string;
  /** 欄位標題(支援字串或 React 節點) */
  header: string | React.ReactNode;
  /** 儲存格渲染函式,接收資料項目和行索引 */
  cell: (item: T, index: number) => React.ReactNode;
  /** 儲存格 CSS className */
  className?: string;
  /** 標題 CSS className */
  headerClassName?: string;
}

interface SimpleTableProps<T> {
  /** 資料陣列 */
  data: T[];
  /** 欄位定義 */
  columns: SimpleColumn<T>[];
  /** 載入狀態(顯示 Loader2 spinner) */
  loading?: boolean;
  /** 空資料提示文字 */
  emptyText?: string;
  /** 分頁配置 */
  pagination?: {
    page: number;
    totalPages: number;
    total?: number;
    onPageChange: (page: number) => void;
  };
  /** 行 key 函式(用於 React key) */
  rowKey?: (item: T) => string | number;
  /** 行點擊處理器 */
  onRowClick?: (item: T) => void;
  /** 行條件式 CSS className */
  rowClassName?: (item: T) => string;
}

渲染邏輯

  1. 載入中loading = true):顯示置中的 Loader2 旋轉動畫
  2. 空資料data.length === 0):顯示 emptyText 提示
  3. 正常渲染
    • 使用 shadcn/ui Table 元件群組(Table, TableHeader, TableBody, TableRow, TableHead, TableCell
    • 外層包裹 rounded-lg border
    • 支援 onRowClick 點擊行,自動加上 cursor-pointer
    • 支援 rowClassName 動態行樣式(例如 VIP 返水頁面的 amber 高亮)
  4. 分頁:底部渲染 Pagination 元件

使用範例

tsx
const columns: SimpleColumn<AdminUser>[] = [
  { key: "id", header: "ID", cell: (admin) => admin.id },
  { key: "email", header: t("email"), cell: (admin) => admin.email },
  {
    key: "status",
    header: t("status"),
    cell: (admin) => (
      <StatusBadge
        status={admin.status}
        label={admin.status === 1 ? t("statusActive") : t("statusInactive")}
        colorMap={{ 1: "emerald", 0: "rose" }}
      />
    ),
  },
  {
    key: "actions",
    header: tc("actions"),
    cell: (admin) => (
      <div className="flex items-center gap-1">
        <Button variant="ghost" size="icon-sm" asChild>
          <Link href={`/system/admins/${admin.id}`}>
            <Pencil className="h-4 w-4" />
          </Link>
        </Button>
      </div>
    ),
  },
];

<SimpleTable<AdminUser>
  data={admins}
  columns={columns}
  loading={loading}
  emptyText={tc("noData")}
  rowKey={(item) => item.id}
  pagination={{ page, totalPages, total, onPageChange: setPage }}
/>

4.14.2 SiteTabs — 多站點 Tab 切換

檔案src/components/shared/SiteTabs.tsx用途:所有多站點頁面的 Tab 切換元件,支援「同預設站點」複製功能。

介面定義

typescript
export interface SiteTabsConfig {
  id: number;
  siteCode: string;
  prefix: string;
  siteName: Record<string, string>;
}

interface SiteTabsProps {
  /** 站點配置列表 */
  configs: SiteTabsConfig[];
  /** 當前啟用的站點 ID */
  activeSiteId: number | null;
  /** 站點切換回呼 */
  onSiteChange: (siteId: number) => void;
  /** 預設站點標籤文字 */
  defaultLabel?: string;
  /** 複製按鈕文字(提供時才顯示複製按鈕) */
  copyLabel?: string;
  /** 點擊複製按鈕的回呼 */
  onCopyFromDefault?: (targetSiteId: number) => void;
}

關鍵特性

  1. Tab 顯示格式:站點名稱 + (siteCode / prefix) + 預設站標記 Badge
  2. 預設站判斷configs[0] 為預設站(第一筆始終為預設站點)
  3. 複製按鈕:只在非預設站 Tab 啟用時顯示,需同時提供 copyLabelonCopyFromDefault
  4. 使用 shadcn/ui TabsTabs + TabsList + TabsTrigger,支援 flex-wrap 自動換行
  5. 空列表保護configs.length === 0 時回傳 null

複製按鈕渲染條件

tsx
{copyLabel && onCopyFromDefault && activeSiteId != null && activeSiteId !== configs[0]?.id && (
  <Button variant="outline" size="sm" onClick={() => onCopyFromDefault(activeSiteId)}>
    <Copy className="h-4 w-4 mr-1" />
    {copyLabel}
  </Button>
)}

4.14.3 FilterBar — 通用篩選列

檔案src/components/shared/filterBar.tsx用途:所有列表頁面的篩選欄位元件,支援三種欄位類型。

欄位型別定義

typescript
interface FilterFieldBase {
  key: string;        // 對應 values Record 的 key
  label: string;      // 欄位標籤
  placeholder?: string;
  className?: string;
}

interface TextFilterField extends FilterFieldBase { type: "text"; }
interface DateFilterField extends FilterFieldBase { type: "date"; }
interface SelectFilterField extends FilterFieldBase {
  type: "select";
  options: { label: string; value: string }[];
}

export type FilterField = TextFilterField | DateFilterField | SelectFilterField;

元件 Props

typescript
export interface FilterBarProps {
  fields: FilterField[];                          // 欄位定義陣列
  values: Record<string, string>;                 // 受控值
  onChange: (values: Record<string, string>) => void;  // 值變更回呼
  onSearch?: () => void;                          // 搜尋按鈕回呼
  onReset?: () => void;                           // 重置後的額外回呼
  searchText?: string;                            // 搜尋按鈕文字
  resetText?: string;                             // 重置按鈕文字
  className?: string;                             // 外層 CSS
}

關鍵行為

  1. Enter 鍵搜尋:text 和 date 欄位支援 Enter 鍵觸發 onSearch
  2. 重置邏輯:清空所有欄位為空字串,然後呼叫 onReset
  3. Select 特殊處理:空值使用 "__all__" 佔位符(因 shadcn Select 不接受空字串 value)
  4. 按鈕對齊:搜尋/重置按鈕上方有隱藏的 <span className="h-4"> 與 Label 高度對齊

使用範例

tsx
<FilterBar
  fields={[
    { key: "keyword", type: "text", label: t("email") + " / " + t("name"), placeholder: tc("search") },
    { key: "status", type: "select", label: t("status"), options: [
      { label: tc("all"), value: "" },
      { label: t("statusActive"), value: "1" },
      { label: t("statusInactive"), value: "0" },
    ]},
    { key: "startDate", type: "date", label: tc("startDate") },
    { key: "endDate", type: "date", label: tc("endDate") },
  ]}
  values={filters}
  onChange={setFilters}
  onSearch={() => setPage(1)}
  searchText={tc("search")}
  resetText={tc("reset")}
/>

4.14.4 StatusBadge — 狀態標籤

檔案src/components/shared/statusBadge.tsx用途:顯示各種狀態值的彩色 Badge 標籤。

預設色彩映射

typescript
const COLOR_MAP: Record<string, string> = {
  amber:   "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-400",
  emerald: "border-emerald-200 bg-emerald-50 text-emerald-700 dark:...",
  rose:    "border-rose-200 bg-rose-50 text-rose-700 dark:...",
  sky:     "border-sky-200 bg-sky-50 text-sky-700 dark:...",
  violet:  "border-violet-200 bg-violet-50 text-violet-700 dark:...",
};

const DEFAULT_STATUS_COLORS: Record<string | number, string> = {
  0: "amber",           // 待處理
  1: "emerald",         // 啟用/通過
  2: "rose",            // 拒絕/停用
  pending: "amber",
  approved: "sky",
  rejected: "rose",
  completed: "emerald",
  active: "emerald",
  inactive: "rose",
};

Props 介面

typescript
interface StatusBadgeProps {
  status: string | number;                         // 狀態值
  label: string;                                   // 顯示文字
  colorMap?: Record<string | number, string>;      // 自訂色彩映射
  className?: string;                              // 額外 CSS
}

色彩查找流程

  1. 使用傳入的 colorMap(若有)或 DEFAULT_STATUS_COLORS
  2. 從 map 中查找 status 對應的色彩名稱(如 "emerald"
  3. COLOR_MAP 中取得對應的 Tailwind CSS classes
  4. 若找不到,預設使用 amber

常見使用場景

場景status 值colorMap
管理員狀態0 / 1{ 1: "emerald", 0: "rose" }
存款訂單狀態0 / 1 / 2預設(amber/emerald/rose)
提款狀態"pending" / "approved" / "completed"預設
Google Auth0 / 1{ 1: "emerald", 0: "rose" }

4.14.5 ConfirmDialog — 確認對話框

檔案src/components/shared/confirmDialog.tsx用途:命令式(imperative)確認對話框,透過 Zustand Store 驅動。

架構設計

此元件採用 Zustand Store + Promise 的命令式模式,而非傳統的宣告式(controlled state)模式:

useConfirm() → store.show(options) → 回傳 Promise<boolean>
  → ConfirmDialog 渲染 AlertDialog
    → 使用者點選確認 → resolve(true)
    → 使用者點選取消 → resolve(false)

Store 定義

typescript
interface ConfirmOptions {
  title: string;
  description?: string;
  confirmText?: string;
  cancelText?: string;
  variant?: "default" | "destructive";
}

interface ConfirmState {
  open: boolean;
  options: ConfirmOptions;
  resolve: ((value: boolean) => void) | null;
  show: (options: ConfirmOptions) => Promise<boolean>;
  handleConfirm: () => void;
  handleCancel: () => void;
}

使用方式

tsx
const confirm = useConfirm();

const handleDelete = async (admin: AdminUser) => {
  const ok = await confirm({
    title: t("deleteConfirm"),
    variant: "destructive",
  });
  if (!ok) return;
  await api.deleteAdmin(admin.id);
};

全域掛載

ConfirmDialog 只需在 Providers 中掛載一次,所有頁面共用同一個 Dialog 實例:

tsx
// providers.tsx
<ConfirmDialog />

4.14.6 Pagination — 分頁元件

檔案src/components/shared/pagination.tsx用途SimpleTable 的分頁導航元件。

Props 介面

typescript
interface PaginationProps {
  page: number;              // 當前頁碼
  totalPages: number;        // 總頁數
  total?: number;            // 總筆數(可選,顯示在左側)
  onPageChange: (page: number) => void;  // 頁碼變更回呼
}

渲染邏輯

  • totalPages <= 1 時不渲染(單頁不需分頁)
  • 左側顯示 {total} total(若提供 total)
  • 右側顯示 Previous / page / totalPages / Next 按鈕
  • Previous 在第 1 頁停用,Next 在最後一頁停用

4.14.7 HtmlEditor — 富文本編輯器

檔案src/components/shared/htmlEditor.tsx用途:活動內容等需要 HTML 格式的編輯欄位。 底層:Tiptap v3.20

Props 介面

typescript
interface HtmlEditorProps {
  value: string;                    // HTML 內容
  onChange: (html: string) => void; // 內容變更回呼
  placeholder?: string;            // 空白提示
}

已啟用的 Tiptap 擴展

擴展功能
StarterKit基礎格式(粗體、斜體、標題 H1-H3、列表、引用等)
Underline底線
Link超連結(openOnClick: false
TextAlign文字對齊(左/中/右)
TextStyle文字樣式容器
Color文字顏色
Image圖片插入(inline: false

工具列按鈕

共 17 個工具列按鈕,分為 6 組(以分隔線區隔):

  1. 文字格式:Bold / Italic / Underline / Strikethrough
  2. 標題:H1 / H2 / H3
  3. 列表:Bullet List / Ordered List
  4. 對齊:Left / Center / Right
  5. 插入:Link / Image / Text Color
  6. 歷史:Undo / Redo

特殊設計

  • immediatelyRender: false:避免 SSR hydration mismatch
  • 外部 value 與內部 editor.getHTML() 不同步時才更新(避免游標跳動)
  • emitUpdate: false:外部同步時不觸發 onUpdate(避免無限迴圈)

4.14.8 ExportButton — CSV 匯出按鈕

檔案src/components/shared/exportButton.tsx用途:報表頁面的 CSV 匯出功能。

Props 介面

typescript
interface ExportButtonProps {
  reportType: string;                    // 報表類型(如 "players", "bet-records")
  params?: Record<string, unknown>;      // 篩選參數
}

匯出流程

  1. 點擊按鈕,設定 exporting = true(顯示 Loader2 spinner)
  2. 呼叫 api.exportReport(reportType, params)
  3. 從回應中取得 { csv, filename }
  4. 建立 Blob(加 BOM \uFEFF 確保 Excel 正確開啟 UTF-8)
  5. 建立臨時 <a> 下載連結,觸發瀏覽器下載
  6. 釋放 Object URL

4.14.9 LoadingSpinner — 載入動畫

檔案src/components/shared/loadingSpinner.tsx用途:全頁或區塊載入指示器。

Props 介面

typescript
interface LoadingSpinnerProps {
  className?: string;
  size?: "sm" | "md" | "lg";  // 預設 "md"
}

尺寸對照

sizeCSS classes
smh-4 w-4
mdh-8 w-8
lgh-12 w-12

4.14.10 AccessDenied — 無權限頁面

檔案src/components/shared/accessDenied.tsx用途:當使用者無權限存取某功能時顯示的提示頁面。

顯示 ShieldX 圖示 + t("errors.forbidden") 錯誤訊息,置中排列。

4.14.11 TemplatePreviewDialog — 模板預覽對話框

檔案src/components/shared/templatePreviewDialog.tsx用途:遊戲管理、VIP 管理的「帶入模板」功能,允許使用者預覽模板資料並在確認前編輯。

型別定義

typescript
export interface TemplateColumn {
  key: string;           // 欄位 key
  header: string;        // 欄位標題
  editable?: boolean;    // 是否可編輯
  type?: "text" | "number";
  className?: string;
  render?: (value: unknown, row: Record<string, unknown>) => React.ReactNode;
}

export interface TemplateSection {
  key: string;           // 分區 key(用於 Tab)
  label: string;         // 分區標籤
  columns: TemplateColumn[];
  data: Record<string, unknown>[];
}

Props 介面

typescript
interface Props {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description?: string;
  sections: TemplateSection[];           // 多分區資料(每分區一個 Tab)
  loading?: boolean;
  confirmLoading?: boolean;
  confirmText?: string;
  cancelText?: string;
  onConfirm: (data: Record<string, Record<string, unknown>[]>) => void;
}

關鍵特性

  1. 多分區 Tab:每個 TemplateSection 渲染為一個 Tab,Tab 上顯示筆數 Badge
  2. 可編輯欄位editable: true 的欄位渲染為 Input,支援即時編輯
  3. 資料同步:開啟 Dialog 時將 sections.data 拷貝到內部 editData state(使用 syncedRef 防止重複同步)
  4. 確認提交onConfirm(editData) 回傳所有(含已編輯的)資料
  5. 最大寬度max-w-4xl,最大高度 85vh,內容區域可捲動

使用場景

頁面sections
遊戲供應商 — 帶入模板providers, typeConfigs
VIP 等級 — 帶入模板levels
VIP 返水 — 帶入模板rebates
代理佣金費率 — 帶入模板commission-rates, agent-tiers, vip-milestones

4.15 佈局元件完整文件

4.15.1 Providers — Context Provider 堆疊

檔案src/components/layout/providers.tsx用途:管理所有全域 Context Provider 的堆疊順序。

Provider 堆疊順序

SessionProvider (NextAuth, refetchOnWindowFocus: false)
  └── QueryClientProvider (TanStack Query, staleTime: 5min, retry: 1)
        └── SessionSync (同步 JWT token 到 apiClient)
        └── SiteConfigProvider (白牌站點配置 Context)
              └── SiteThemeInjector (OKLCH CSS 變數注入)
                    ├── LocaleGuard (語系保護)
                    ├── EnumInitializer (錯誤碼列表)
                    ├── SiteFilterInitializer (站點列表)
                    ├── {children} (頁面內容)
                    ├── Toaster (Toast 通知)
                    └── ConfirmDialog (全域確認框)

SessionSync 子元件

typescript
function SessionSync() {
  const { data: session, status } = useSession();
  const wasAuthenticated = useRef(false);

  useEffect(() => {
    if (session?.accessToken) {
      setToken(session.accessToken as string);  // 同步到 apiClient 模組級快取
      wasAuthenticated.current = true;
    } else {
      clearToken();
      // 曾登入但 session 消失 → 導向登入頁
      if (wasAuthenticated.current && status === "unauthenticated") {
        window.location.href = "/login?expired=1";
      }
    }
  }, [session?.accessToken, status]);

  return null;
}

設計重點

  • wasAuthenticated.current 避免首次載入(未登入狀態)時誤跳轉
  • 使用 window.location.href 而非 router.push,確保完全重新載入清除所有狀態
  • setToken / clearToken 操作 apiClient.ts 中的模組級 cachedToken 變數

QueryClient 配置

typescript
const [queryClient] = useState(
  () => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 60 * 1000,  // 5 分鐘內不重新請求
        retry: 1,                   // 失敗最多重試 1 次
      },
    },
  }),
);

4.15.2 Sidebar — 側邊欄導航

檔案src/components/layout/sidebar.tsx用途:後台主要導航欄,含 14 個群組、50+ 個導航項目。

子元件結構

子元件用途
SidebarItem單一導航連結(icon + label,active 時高亮)
SidebarGroup可折疊群組(icon + label + ChevronDown + children)

導航群組顯示邏輯

每個群組同時檢查兩個條件:

  1. Feature Flag 檢查:useFeatureFlags() 取得站點功能開關
  2. RBAC 權限 檢查:usePermissions() 取得使用者權限
tsx
// 範例:Finance 群組
{flags.enableFinanceManagement && (canRead("finance") || canRead("vendor") || canRead("withdrawal")) && (
  <SidebarGroup icon={CreditCard} label={t("nav.finance")} defaultOpen={isFinanceOpen}>
    {canRead("finance") && <SidebarItem ... />}
    {canRead("vendor") && <SidebarItem ... />}
    {canRead("withdrawal") && <SidebarItem ... />}
  </SidebarGroup>
)}

活躍路由判斷

使用 usePathname() 取得當前路由,判斷 pathname.startsWith(href) 決定是否高亮。群組的 defaultOpen 也是根據子項目是否有 active 來決定。

4.15.3 Header — 頂部導航列

檔案src/components/layout/header.tsx用途:頁面頂部導航列,包含站點選擇、語系切換、個人資料。

包含功能

  1. SiteSelector:多站點切換下拉選單(見 4.15.4)
  2. 語系切換DropdownMenu 列出 LOCALE_LABELS,使用 router.replace(pathname, { locale }) 切換
  3. 個人資料:顯示 session user 資訊 + 登出按鈕
  4. Google Auth:Google Authenticator 2FA 設定按鈕(開啟 GoogleAuthDialog
  5. Admin Profile 查詢:使用 useApiQuery(["admin-profile"], ...) 取得 Google Auth 狀態

4.15.4 SiteSelector — 站點切換選擇器

檔案src/components/layout/SiteSelector.tsx用途:Header 中的站點切換下拉選單。

渲染條件

  • sites.length <= 1:單站點模式,不渲染
  • 路由匹配 HIDE_SITE_SELECTOR_ROUTES:不渲染

隱藏路由列表

typescript
const HIDE_SITE_SELECTOR_ROUTES = [
  "/system/admins",
  "/system/groups",
  "/system/logs",
];

這些頁面為全站共用設定,不區分站點。

切換行為

typescript
const handleSelect = useCallback((code: string | null) => {
  if (code === selectedSiteCode) return;
  setSelectedSiteCode(code);                // 更新 siteFilterStore
  queryClient.removeQueries();              // 清除 TanStack Query cache
}, [selectedSiteCode, setSelectedSiteCode, queryClient]);

重要:切換站點時呼叫 queryClient.removeQueries() 清除所有快取,確保不會顯示前一站點的資料。配合 AdminContentWrapperkey={selectedSiteCode} 強制 remount,所有子頁面會重新拉取資料。

4.15.5 AdminContentWrapper — 管理頁面包裹器

檔案src/components/layout/AdminContentWrapper.tsx用途:透過 React key 強制 remount,實現站點切換時自動重新載入資料。

typescript
export function AdminContentWrapper({ children }: { children: React.ReactNode }) {
  const selectedSiteCode = useSiteFilterStore((s) => s.selectedSiteCode);
  return <div key={selectedSiteCode ?? "__all__"}>{children}</div>;
}

原理:當 selectedSiteCode 改變時,key 值改變,React 會卸載舊的子元件樹並建立新的,等同於頁面完全重新載入。所有 useStateuseApiQuery 等 hooks 都會重新初始化。

4.15.6 SiteFilterInitializer — 站點列表初始化

檔案src/components/layout/SiteFilterInitializer.tsx用途:啟動時拉取後端站點列表,填入 siteFilterStore。

執行流程

  1. 監聽 useSession()status
  2. status === "authenticated" 時執行
  3. 呼叫 api.getSiteConfigs()(GET /site-config/admin/list
  4. 根據管理員的 allowedSiteCodes 過濾可見站點
  5. 將結果映射為 { id, siteCode, prefix, siteName } 填入 store
typescript
const allowedSiteCodes = session?.user?.allowedSiteCodes;
const filtered = allowedSiteCodes
  ? list.filter((s: any) => allowedSiteCodes.includes(s.siteCode))
  : list;

多租戶 RBAC:管理員若設定了 allowedSiteCodes(如 ["C9", "D1"]),只能看到被授權的站點。若未設定(null),可看到所有站點。

4.15.7 SiteThemeInjector — 主題注入器

檔案src/components/layout/themeInjector.tsx用途:將站點主題的 OKLCH 色彩值注入為 CSS 變數。

CSS 變數映射(30+ 個)

CSS VariableThemeColors 屬性用途
--backgroundbackground頁面背景
--foregroundforeground文字顏色
--cardcardCard 背景
--primaryprimary主要色
--secondarysecondary次要色
--mutedmuted柔和色
--accentaccent強調色
--destructivedestructive危險操作色
--borderborder邊框色
--inputinput輸入框邊框色
--ringringFocus ring 色
--chart-1 ~ --chart-5chart1 ~ chart5Recharts 圖表色
--sidebarsidebar側邊欄背景
--sidebar-foregroundsidebarForeground側邊欄文字
--sidebar-primarysidebarPrimary側邊欄主色
--sidebar-accentsidebarAccent側邊欄強調色
--sidebar-bordersidebarBorder側邊欄邊框
--sidebar-ringsidebarRing側邊欄 focus ring

注入方式:使用 <div style={cssVars as React.CSSProperties} className="contents">className="contents" 確保不產生額外的 DOM 層級。

4.15.8 LocaleGuard — 語系保護

檔案src/components/layout/localeGuard.tsx用途:確保當前語系在站點支援的語系列表中。

typescript
useEffect(() => {
  const supported = config.supportedLocales;
  if (supported.length > 0 && !supported.includes(locale as any)) {
    router.replace(pathname, { locale: supported[0] });
  }
}, [locale, config.supportedLocales, router, pathname]);

若當前語系不在站點的 supportedLocales 列表中,自動切換到第一個支援的語系。

4.15.9 ScrollToTop — 路由滾動重置

檔案src/components/layout/scrollToTop.tsx用途:路由切換後自動將捲軸滾回頂部。

三種偵測機制

  1. MutationObserver:偵測 [data-scroll-region] 元素的 children 變化
  2. history.pushState 攔截:Monkey-patch history.pushState 攔截 SPA 導航
  3. popstate 事件:監聽瀏覽器返回/前進按鈕

三種機制互補,確保所有路由變更場景都能觸發滾動重置。

4.15.10 GoogleAuthDialog — 2FA 設定對話框

檔案src/components/layout/google-auth-dialog.tsx用途:Google Authenticator TOTP 二步驟驗證的啟用/停用對話框。

狀態機

狀態說明UI
idle初始狀態顯示「啟用」或「停用」選項
qr取得 QR Code顯示 QR Code 圖片 + Secret 文字
verify驗證碼輸入6 位數字輸入框 + 確認按鈕

流程

  • 啟用:呼叫 api.generateGoogleAuth() 取得 QR Code → 使用者掃描 → 輸入 TOTP 驗證碼 → api.enableGoogleAuth(code) → 完成
  • 停用:輸入當前 TOTP 驗證碼 → api.disableGoogleAuth(code) → 完成

4.16 多語系(i18n)系統完整文件

4.16.1 技術架構

項目說明
套件next-intl v4.8.3
策略localePrefix: "never"(語系存 cookie,URL 不顯示語系前綴)
支援語系zh-TW(預設)、en-US、zh-CN、th-TH、vi-VN
訊息格式Flat dot-notation JSON(執行時 unflatten)
檔案位置src/messages/{locale}.json

4.16.2 檔案結構

src/i18n/
├── routing.ts      # defineRouting: 語系列表 + localePrefix
├── request.ts      # getRequestConfig: 載入語系檔 + unflatten
└── navigation.ts   # createNavigation: locale-aware Link, redirect, useRouter, usePathname

routing.ts — 路由定義

typescript
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["zh-TW", "en-US", "zh-CN", "th-TH", "vi-VN"],
  defaultLocale: "zh-TW",
  localePrefix: "never",
});

localePrefix: "never" 表示 URL 中不會出現語系前綴(如 /en-US/dashboard),改用 cookie 儲存當前語系。

request.ts — 請求配置

typescript
function unflatten(flat: Record<string, string>): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(flat)) {
    const parts = key.split(".");
    let cur: Record<string, unknown> = result;
    for (let i = 0; i < parts.length - 1; i++) {
      const k = parts[i]!;
      if (!(k in cur)) cur[k] = {};
      cur = cur[k] as Record<string, unknown>;
    }
    cur[parts[parts.length - 1]!] = value;
  }
  return result;
}

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
  if (!locale || !routing.locales.includes(locale)) {
    locale = routing.defaultLocale;
  }
  const flat = (await import(`@/messages/${locale}.json`)).default;
  return { locale, messages: unflatten(flat) };
});

unflatten 函式:將 "system.admins.title": "管理員管理" 轉換為巢狀物件 { system: { admins: { title: "管理員管理" } } },供 next-intl 使用。

typescript
export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

重要規則:所有頁面必須使用 @/i18n/navigation 匯出的 LinkuseRouterusePathname,不可使用 next/navigation 的版本(否則會丟失 locale 資訊)。

4.16.3 訊息檔案格式

語系檔案使用扁平 dot-notation 格式,而非巢狀 JSON:

json
{
  "common.save": "儲存",
  "common.cancel": "取消",
  "common.search": "搜尋",
  "common.reset": "重置",
  "common.all": "全部",
  "common.noData": "暫無資料",
  "common.actions": "操作",
  "common.allSites": "全部站點",
  "system.admins.title": "管理員管理",
  "system.admins.email": "Email",
  "system.admins.name": "名稱",
  "nav.dashboard": "儀表板",
  "nav.system": "系統管理"
}

4.16.4 使用方式

Namespace 模式

tsx
const t = useTranslations("system.admins");
t("title");      // → "管理員管理"
t("email");      // → "Email"

const tc = useTranslations("common");
tc("save");      // → "儲存"
tc("noData");    // → "暫無資料"

常用 Namespace

Namespace涵蓋常用 key
common通用按鈕/標籤save, cancel, search, reset, all, noData, actions, confirm, startDate, endDate, index, allSites, exportCsv, exporting, exportSuccess, exportError
nav側邊欄導航dashboard, system, players, finance, game, vip, reports, riskControl, affiliate
system.admins管理員管理title, email, name, group, status, statusActive, statusInactive, addAdmin, deleteConfirm, deleteSuccess, allowedSites, allSitesAccess, googleAuth, googleAuthEnabled, googleAuthDisabled, lastLoginIp, lastLoginAt, noGroup
system.groups群組管理title, name, description, permissions, addGroup
system.siteConfig站點設定defaultSite, copyFromDefault, title
errors錯誤提示forbidden, notFound

4.16.5 ICU 格式注意事項

next-intl 使用 ICU MessageFormat,大括號 {} 會被解析為變數佔位符:

json
// 錯誤 — 會觸發 IntlError: FORMATTING_ERROR
"format": "格式為 {JSON}"

// 正確 — 使用單引號轉義大括號
"format": "格式為 '{'JSON'}'"

4.16.6 多語系工具函式

檔案src/lib/locales.ts

typescript
// 語系顯示名稱對照表
export const LOCALE_LABELS: Record<string, string> = {
  "zh-TW": "繁體中文",
  "en-US": "English",
  "zh-CN": "简体中文",
  "th-TH": "ภาษาไทย",
  "vi-VN": "Tiếng Việt",
};

// 語系代碼轉 FormData field name (e.g. "zh-TW" → "ZhTW")
export function localeToFieldSuffix(locale: string): string {
  return locale.replace("-", "");
}

// 建立空多語系 Record
export function emptyLocaleRecord(locales: string[]): Record<string, string> {
  return Object.fromEntries(locales.map((l) => [l, ""]));
}

4.16.7 語系切換流程

  1. Header DropdownMenu 列出所有語系(使用 LOCALE_LABELS
  2. 使用者點選目標語系
  3. 呼叫 router.replace(pathname, { locale: nextLocale })
  4. next-intl middleware 處理 cookie 設定 + 重新載入頁面
  5. getRequestConfig 載入對應語系檔案
  6. LocaleGuard 驗證語系是否在站點 supportedLocales

4.17 多站點開發完整模式

4.17.1 模式 A:列表頁(使用 useMultiSiteTabs

適用於大多數 CRUD 列表頁面(47+ 個頁面已採用此模式)。

完整範例

tsx
"use client";

import { useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { useApi } from "@/hooks/useApi";
import { useApiListQuery } from "@/hooks/useApiQuery";
import { useMultiSiteTabs } from "@/hooks/useMultiSiteTabs";
import { SiteTabs } from "@/components/shared/SiteTabs";
import { SimpleTable, type SimpleColumn } from "@/components/shared/simpleTable";
import { FilterBar } from "@/components/shared/filterBar";
import type { SomeEntity } from "@/types";

export default function SomePage() {
  const t = useTranslations("module.some");
  const tc = useTranslations("common");
  const tSite = useTranslations("system.siteConfig");
  const api = useApi();

  const [page, setPage] = useState(1);
  const pageSize = 20;
  const [filters, setFilters] = useState<Record<string, string>>({
    keyword: "", status: "", startDate: "", endDate: "",
  });

  // 一行取得多站點狀態
  const { visibleSites, activeSiteId, activeSiteCode, handleSiteChange } =
    useMultiSiteTabs({ onSiteChange: () => setPage(1) });

  // API 參數建構
  const buildParams = useCallback(() => {
    const params: Record<string, unknown> = { page, pageSize };
    if (activeSiteCode) params.siteCode = activeSiteCode;
    if (filters.keyword) params.keyword = filters.keyword;
    if (filters.status) params.status = Number(filters.status);
    if (filters.startDate) params.startDate = filters.startDate;
    if (filters.endDate) params.endDate = filters.endDate;
    return params;
  }, [page, pageSize, activeSiteCode, filters]);

  // 資料查詢
  const { data: listData, isLoading } = useApiListQuery<SomeEntity>(
    ["some-list", page, activeSiteCode, filters],
    () => api.getSomeList(buildParams()),
  );
  const items = listData.items;
  const total = listData.total;
  const totalPages = Math.ceil(total / pageSize);

  // 欄位定義
  const columns: SimpleColumn<SomeEntity>[] = [
    { key: "id", header: "ID", cell: (item) => item.id },
    // ... 其他欄位
  ];

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">{t("title")}</h1>

      {/* 多站點 Tab */}
      {visibleSites.length > 0 && (
        <SiteTabs
          configs={visibleSites}
          activeSiteId={activeSiteId}
          onSiteChange={handleSiteChange}
          defaultLabel={tSite("defaultSite")}
        />
      )}

      {/* 篩選列 */}
      <FilterBar
        fields={[
          { key: "keyword", type: "text", label: t("keyword"), placeholder: tc("search") },
          { key: "startDate", type: "date", label: tc("startDate") },
          { key: "endDate", type: "date", label: tc("endDate") },
        ]}
        values={filters}
        onChange={setFilters}
        onSearch={() => setPage(1)}
        searchText={tc("search")}
        resetText={tc("reset")}
      />

      {/* 資料表格 */}
      <SimpleTable<SomeEntity>
        data={items}
        columns={columns}
        loading={isLoading}
        emptyText={tc("noData")}
        rowKey={(item) => item.id}
        pagination={{ page, totalPages, total, onPageChange: setPage }}
      />
    </div>
  );
}

4.17.2 模式 B:設定頁

適用於站點配置頁面(OAuth、遊戲商配置、服務商配置、客服配置、域名設置、佈局配置等)。

核心差異

比較項目模式 A(列表頁)模式 B(設定頁)
Tab 對應篩選 siteCode 參數對應不同設定記錄的 id
資料來源列表 API + siteCode 篩選各站一筆設定記錄
複製功能後端 API(copy-site-data)前端狀態拷貝(deep clone)
儲存方式獨立 CRUD APIPUT/PATCH 各站設定

典型結構

tsx
// 1. 取得所有站點的設定記錄
const { data: siteConfigData } = useApiQuery(["configs"], () => api.getSiteConfigs());
const configs = useMemo(() => (Array.isArray(siteConfigData) ? siteConfigData : []), [siteConfigData]);

// 2. 每站一個 form state
const [formMap, setFormMap] = useState<Record<number, FormType>>({});

// 3. SiteTabs 搭配「同預設站點」
<SiteTabs
  configs={visibleConfigs}
  activeSiteId={activeSiteId}
  onSiteChange={setActiveSiteId}
  defaultLabel={tSite("defaultSite")}
  copyLabel={tSite("copyFromDefault")}
  onCopyFromDefault={handleCopyFromDefault}
/>

// 4. 複製邏輯(前端深拷貝)
const handleCopyFromDefault = (targetCfgId: number) => {
  const firstCfg = configs[0];
  if (!firstCfg || firstCfg.id === targetCfgId) return;
  const firstForm = formMap[firstCfg.id];
  if (!firstForm) return;
  setFormMap((prev) => ({
    ...prev,
    [targetCfgId]: JSON.parse(JSON.stringify(firstForm)),
  }));
  notify.success(t("copiedFromDefault"));
};

4.17.3 siteCode 傳遞完整流程

用戶操作 Header SiteSelector
  → 更新 siteFilterStore.selectedSiteCode
  → queryClient.removeQueries() 清除快取
  → AdminContentWrapper key 改變 → 子元件 remount
    → 頁面 useMultiSiteTabs() 重新初始化
      → visibleSites / activeSiteCode 更新
    → API 呼叫:
      - 全站模式 (selectedSiteCode = null):
        → apiClient 不帶 x-site-code header
        → 手動傳 siteCode query param(來自 activeSiteCode)
      - 單站模式 (selectedSiteCode = "C9"):
        → apiClient 自動帶 x-site-code: C9 header
        → 不需額外傳 siteCode param
    → 後端 @AdminSiteCode() 裝飾器:
      → 優先讀 x-site-code header
      → 其次讀 query param
      → null = 全站

4.18 程式碼規範與慣例

4.18.1 React 元件規範

規範說明
元件定義函式元件(不使用 React.FC,不使用 class 元件)
Client 宣告所有互動元件頂部加 "use client"
Props 型別使用 TypeScript interface(非 type
Default Export只有 page.tsx 使用 export default,其他元件使用具名 export
子元件定義小型子元件可定義在同檔案(如 StatCarddashboard/page.tsx 內)

4.18.2 命名規範

類型規範範例
React 元件PascalCaseSimpleTable, FilterBar, StatusBadge
元件檔案camelCase.tsxsimpleTable.tsx, filterBar.tsx
佈局元件檔案PascalCase.tsxAdminContentWrapper.tsx, SiteSelector.tsx
頁面檔案page.tsxNext.js 慣例
Hooksuse{Feature}useApi, useNotify, usePermissions
API Hooksuse{Domain}ApiuseAdminApi, useFinanceApi
Store 檔案{feature}Store.tsuiStore.ts, enumStore.ts
Validation{domain}.tsadmin.ts, group.ts
Type 檔案單數名詞admin.ts, affiliate.ts
i18n keysdot-notation"system.admins.title"

4.18.3 Import 規範

tsx
// 正確 — 使用 @ alias
import { useApi } from "@/hooks/useApi";
import { Button } from "@/components/ui/button";
import { Link, useRouter } from "@/i18n/navigation";
import { z } from "zod/v4";

// 錯誤 — 禁止使用
import { useRouter } from "next/navigation";     // 會丟失 locale
import Link from "next/link";                     // 會丟失 locale
import { z } from "zod";                          // v4 需用 sub-import
import { something } from "../../hooks/useApi";   // 禁止相對路徑

4.18.4 狀態管理決策樹

需要管理什麼狀態?
├── 伺服器資料(API 回應)
│   └── TanStack React Query(useApiQuery / useApiListQuery)
├── 全域 UI 狀態
│   └── Zustand(uiStore / siteFilterStore / enumStore)
├── 頁面內部狀態
│   └── useState / useReducer
└── 表單狀態
    ├── 複雜表單(多步驟、動態欄位)
    │   └── React Hook Form + Zod
    └── 簡單表單
        └── useState + safeParse

4.18.5 API 呼叫規範

規範說明
新增 API 方法在對應的 hooks/api/use{Domain}Api.ts 中新增
列表查詢使用 useApiListQuery(自動正規化分頁格式)
單筆查詢使用 useApiQuery(自動提取 result
變更操作使用 useApiMutation(支援自動 invalidate)
錯誤處理不需手動處理,httpRequest 自動三層映射 + toast
成功回呼檢查 res?.code === 200 後執行後續邏輯
Query Key包含所有影響結果的變數:["key", page, siteCode, filters]

4.18.6 多站點開發必備檢查清單

新增頁面時,請確認以下項目:

  1. 是否需要多站點支援?(全站共用設定不需要)
  2. 使用 useMultiSiteTabs hook 取得站點狀態
  3. API 呼叫的 Query Key 包含 activeSiteCode
  4. API 參數包含 siteCode(全站模式時)
  5. 渲染 SiteTabs 元件
  6. 是否需要「同預設站點」功能?
  7. 後端 Entity 是否有 siteCode 欄位?
  8. 後端 Controller 是否使用 @AdminSiteCode() 裝飾器?

4.18.7 i18n 開發規範

  1. 新增翻譯時必須同步更新全部 5 個語系檔案
  2. 使用 useTranslations("namespace") 取得翻譯函式
  3. 通用翻譯用 useTranslations("common")
  4. 避免硬寫文字在元件中
  5. 大括號需轉義:'{'xxx'}'
  6. 錯誤訊息不硬寫,使用 enumStore.errorCodes 查表

第 5 章:後端 (c9-be) 技術規格

5.1 完整 Entity 文件(49 張資料表)

本章節詳細記錄 c9-be 後端專案中所有 49 個 TypeORM Entity 的完整欄位定義,包含資料型別、是否可為空、預設值、註解、索引、唯一約束與關聯關係。

5.1.1 用戶認證模組

AuthUser(auth-user

前台用戶主表,包含帳號、密碼、餘額、VIP、代理、OAuth 等完整欄位。每站帳號獨立(透過 siteCode 區分)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
accountvarchar(50)NO-帳號
passwordvarchar(255)YESnull密碼 (bcrypt),OAuth 用戶可為空
namevarchar(50)NO-暱稱
emailvarchar(100)YESnull電子信箱
emailVerifiedtinyint(1)NO0信箱已驗證 (1=已驗證, 0=未驗證)
mobilevarchar(30)YESnull手機號碼
mobileVerifiedtinyint(1)NO0手機已驗證
avatarvarchar(500)YESnull頭像 URL
balancedecimal(18,6)NO0.000000餘額 (USD)
frozenBalancedecimal(18,6)NO0.000000凍結餘額 (USD)
totalDepositdecimal(18,6)NO0.000000累計存款 (USD)
totalWithdrawaldecimal(18,6)NO0.000000累計提款 (USD)
totalEffectiveBetdecimal(18,6)NO0.000000累計有效投注 (USD)
totalWinLosedecimal(18,6)NO0.000000累計輸贏 (USD)
vipLevelintNO1VIP 等級
vipHoldtinyint(1)NO0VIP 鎖定(0=未鎖, 1=已鎖)
monthlyBetdecimal(18,6)NO0.000000當月累計投注 (USD)
isAgenttinyint(1)NO0是否為代理
agentCodevarchar(30)YESnull代理推廣碼
agentLevelintNO0代理層級 (0=非代理, 1/2/3=層級)
parentAgentIdintYESnull上線代理 ID
googlevarchar(100)YESnullGoogle OAuth sub
telegramvarchar(100)YESnullTelegram ID
vendorGroupIdintYESnull所屬金流群組 ID
tokenVersionintNO0Token 版本號(遞增可強制登出)
googleAuthSecretvarchar(32)YESnullGoogle Authenticator Secret
googleAuthEnabledtinyint(1)NO0Google Auth 啟用狀態
statustinyint(1)NO1帳號狀態 (1=啟用, 0=停用)
lastLoginIpvarchar(45)YESnull最後登入 IP
lastLoginAtdatetimeYESnull最後登入時間
lastActivityAtdatetimeYESnull最後活動時間(節流 60s 更新)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間 (CreateDateColumn)
updatedAtdatetimeNOauto更新時間 (UpdateDateColumn)

唯一約束(Unique Constraints)

  • (siteCode, account) — 同站帳號唯一
  • (siteCode, email) — 同站 Email 唯一
  • (siteCode, mobile) — 同站手機唯一
  • (siteCode, telegram) — 同站 Telegram 唯一
  • (siteCode, google) — 同站 Google 唯一
  • (siteCode, agentCode) — 同站代理碼唯一

索引(Indexes)

  • siteCode — 站點篩選用

關聯(Relations)

  • OneToMany → AuthUserLoginLog(一對多:登入紀錄)
  • ManyToOne → VendorGroup(多對一:金流群組,vendorGroupId FK,onDelete: SET NULL

AuthUserLoginLog(auth-user-login-log

用戶登入/登出紀錄表,記錄每次登入動作、IP、裝置資訊。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintYESnull用戶 ID (FK → auth-user)
actionvarchar(20)NO-動作 (LOGIN / LOGOUT / LOGIN_FAIL / DEL / UNCAPTURED)
ipvarchar(45)YESnull登入 IP
devicevarchar(500)YESnull裝置指紋或 User-Agent
accountvarchar(100)YESnull登入帳號(登入失敗時記錄輸入值)
lastUsedatetimeYESnull最後使用時間
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引

  • (userId, lastUse) — 複合索引
  • ip — IP 反查用

關聯

  • ManyToOne → AuthUseruserId FK,onDelete: CASCADE

5.1.2 遊戲模組

GameProvider(game-provider

遊戲供應商表,記錄每個站點可用的遊戲及其配置。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
gameCodevarchar(50)NO-遊戲碼(如 slot-betsolutions
providerCodevarchar(30)NO-供應商代碼 (betsolutions / rsg)
gameTypeintNO-遊戲類型 (1=SPORTS, 2=SLOT, 3=LIVE, 4=LOTTERY, 5=CHESS, 8=ESPORTS, 9=CRYPTO, 10=FISH)
productIdintYESnull產品 ID(供應商端的遊戲 ID)
labeljsonYESnull遊戲名稱 (多語系 JSON)
iconvarchar(255)YESnull圖示路徑
sortOrderintNO0排序權重
enabledtinyint(1)NO1啟用狀態
metajsonYESnull額外設定(遊戲商特定參數)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, gameCode) — 同站遊戲碼唯一

索引siteCode


GameTypeConfig(game-type-config

遊戲分類配置表,定義每個站點的遊戲分類及其顯示設定。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
gameTypeintNO-遊戲類型編號
typeKeyvarchar(30)NO-類型代碼(如 sports, slot
labeljsonYESnull分類名稱 (多語系 JSON)
iconvarchar(255)YESnull圖示路徑
sortOrderintNO0排序權重
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, gameType) — 同站遊戲類型唯一

索引siteCode


GameTransaction(game-transaction

遊戲交易紀錄表,記錄每一筆遊戲內的轉帳(下注/派彩)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
providervarchar(30)NO-供應商代碼
transactionIdvarchar(100)NO-交易 ID(供應商端唯一)
typevarchar(20)NO-交易類型 (bet / win / refund / rollback)
amountdecimal(18,6)NO0交易金額 (USD)
balanceAfterdecimal(18,6)NO0交易後餘額 (USD)
roundIdvarchar(100)YESnull遊戲回合 ID
gameIdvarchar(50)YESnull遊戲 ID
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束transactionId — 交易 ID 全域唯一

索引userId, siteCode


GamePlayLog(game-play-log

遊戲遊玩紀錄表,UPSERT 方式記錄用戶最近遊玩的遊戲。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
gameCodevarchar(50)NO-遊戲碼
productIdintYESnull產品 ID
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto首次遊玩時間
updatedAtdatetimeNOauto最後遊玩時間

唯一約束(siteCode, userId, gameCode, productId) — 同站用戶+遊戲唯一

索引siteCode


5.1.3 投注紀錄模組

BetOrder(bet-order

注單主表,記錄每一筆投注的完整資訊。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
gameTypevarchar(20)NO-遊戲類型 (sports/slot/live/lottery/chess/esports/crypto/fish)
gamePlatformvarchar(30)NO-遊戲平台代碼
gameNumbervarchar(100)NO-遊戲單號(供應商端唯一)
betAmountdecimal(18,6)NO0投注金額 (USD)
betEffectivedecimal(18,6)NO0有效投注金額 (USD),依 TURNOVER_WEIGHT 計算
winLosedecimal(18,6)NO0輸贏金額 (USD)
statusvarchar(20)NO'valid'注單狀態 (valid / invalid / cancelled)
oddsdecimal(10,2)YESnull賠率
gameNamevarchar(100)YESnull遊戲名稱
betDatetimedatetimeYESnull下注時間
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束gameNumber — 遊戲單號全域唯一

索引userId, siteCode

關聯OneToMany → BetDetail(一對多:注單明細)


BetDetail(bet-detail

注單明細表,記錄每個回合的細項。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
orderIdintNO-注單 ID (FK → bet-order)
roundNovarchar(100)YESnull回合編號
betAmountdecimal(18,6)NO0投注金額
winLosedecimal(18,6)NO0輸贏金額
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

關聯ManyToOne → BetOrderorderId FK,onDelete: CASCADE


5.1.4 VIP 模組

VipLevel(vip-level

VIP 等級定義表,每站獨立配置,等級數量可自由擴充。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
levelintNO-VIP 等級編號
namejsonNO-等級名稱 (多語系,如 {"zh-TW":"青銅 I","en-US":"Bronze I"})
tiervarchar(20)NO-階級 (bronze / gold / platinum / diamond)
minChipdecimal(18,6)NO0升級所需最低累計籌碼 (USD)
relegationChipdecimal(18,6)NO0保級所需籌碼 (USD)
sortOrderintNO0排序權重
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, level) — 同站 VIP 等級唯一

索引siteCode


VipRebate(vip-rebate

VIP 返水規則表,定義各等級各遊戲類型的返水比例。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
levelintNO-VIP 等級
gameTypevarchar(20)NO-遊戲類型 (sports/slot/live/lottery/chess/esports/crypto/fish)
rebateRatedecimal(5,2)NO0返水比例 (%)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, level, gameType) — 同站等級+遊戲類型唯一

索引level, siteCode


VipRebateLog(vip-rebate-log

VIP 反水發放紀錄表,記錄每日反水結算結果。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
settleDatevarchar(10)NO-結算日期 (YYYY-MM-DD)
vipLevelintNO-結算時 VIP 等級
gameTypevarchar(20)NO-遊戲類型
dailyEffectivedecimal(18,6)NO0當日有效投注 (USD)
rebateRatedecimal(5,2)NO0返水比例 (%)
rebateAmountdecimal(18,6)NO0返水金額 (USD)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, settleDate, siteCode


5.1.5 後台管理模組

AdminUser(admin-user

後台管理員帳號表,全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
emailvarchar(100)NO-管理員 Email(唯一)
emailVerifiedtinyint(1)NO0信箱已驗證 (1=已驗證, 0=未驗證)
passwordvarchar(255)NO-密碼 (bcrypt)
namevarchar(50)NO-管理員名稱
groupIdintYESnull所屬群組 ID (FK → admin-group)
statustinyint(1)NO1啟用狀態 (1=啟用, 0=停用)
lastLoginIpvarchar(45)YESnull最後登入 IP
lastLoginAtdatetimeYESnull最後登入時間
tokenVersionintNO0Token 版本號
googleAuthSecretvarchar(32)YESnullGoogle Authenticator Secret
googleAuthEnabledtinyint(1)NO0Google Auth 啟用狀態
allowedSiteCodesjsonYESnull可管理的站點代碼列表,null=全站點
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束email — Email 全域唯一

索引groupId

關聯ManyToOne → AdminGroupgroupId FK,onDelete: SET NULL


AdminGroup(admin-group

管理員群組表,定義群組類型與權限。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
typevarchar(20)NO-群組類型 (root / super_admin / general_admin / custom)
namevarchar(50)NO-群組名稱
permissionsjsonYESnull權限列表 (JSON 陣列,如 ["admin:read","admin:write"])
descriptionvarchar(255)YESnull群組說明
statustinyint(1)NO1啟用狀態
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

關聯OneToMany → AdminUser(一對多:管理員列表)


AdminOperationLog(admin-operation-log

管理員操作紀錄表。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
adminIdintNO-管理員 ID (FK → admin-user)
modulevarchar(50)NO-模組名稱
actionvarchar(50)NO-操作動作 (create / update / delete / review)
targetIdvarchar(50)YESnull操作對象 ID
ipvarchar(45)YESnull操作 IP
userAgentvarchar(500)YESnull瀏覽器 User-Agent
methodvarchar(10)YESnullHTTP 方法
pathvarchar(255)YESnullAPI 路徑
detailjsonYESnull操作詳情
summaryvarchar(500)YESnull操作摘要
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引adminId

關聯ManyToOne → AdminUseradminId FK)


5.1.6 風控模組

RiskIpRule(risk-ip-rule

IP 黑白名單規則表,每站獨立配置。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
ipvarchar(45)NO-IP 位址
typevarchar(20)NO-規則類型 (blacklist / whitelist)
remarkvarchar(255)YESnull備註
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引siteCode


RiskGameBlacklist(risk-game-blacklist

遊戲黑名單表,封鎖特定用戶的遊戲存取。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
gameTypevarchar(20)YESnull遊戲類型(null=封鎖所有遊戲)
productIdintYESnull產品 ID(null=封鎖該類型所有遊戲)
remarkvarchar(255)YESnull備註
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

封鎖規則

  • gameType=null, productId=null → 封鎖用戶所有遊戲
  • gameType='slot', productId=null → 封鎖用戶該類型所有遊戲
  • gameType='slot', productId=123 → 封鎖用戶特定遊戲

索引userId, siteCode


5.1.7 代理推廣模組

AffiliateCommission(affiliate-commission

代理佣金表,記錄每筆投注產生的佣金。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
memberIdintNO-下線會員 ID
betOrderIdintYESnull關聯注單 ID
agentLevelintNO1代理層級 (1/2/3)
gameTypevarchar(20)NO-遊戲類型
netLossdecimal(18,6)NO0下線淨輸 (USD)
commissionRatedecimal(5,2)NO0佣金比例 (%)
commissionAmountdecimal(18,6)NO0佣金金額 (USD)
weekStartdateYESnull結算週起始日
weekEnddateYESnull結算週結束日
settlementIdintYESnull結算單 ID (FK)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引agentId, memberId, siteCode


AffiliateSettlement(affiliate-settlement

代理佣金結算表,記錄每週結算結果。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
weekStartdateNO-結算週起始日(週一)
weekEnddateNO-結算週結束日(週日)
activeMemberCountintNO0活躍下線數
totalBetAmountdecimal(18,6)NO0下線總投注 (USD)
totalNetLossdecimal(18,6)NO0下線總淨輸 (USD)
level1Commissiondecimal(18,6)NO0一級佣金 (USD)
level2Commissiondecimal(18,6)NO0二級佣金 (USD)
level3Commissiondecimal(18,6)NO0三級佣金 (USD)
totalCommissiondecimal(18,6)NO0總佣金 (USD)
platformFeedecimal(18,6)NO0平台費 (USD)
finalAmountdecimal(18,6)NO0實際發放金額 (USD)
statusvarchar(20)NO'pending'狀態 (pending / approved / rejected / paid)
riskFlaggedtinyint(1)NO0是否被風控標記
riskReasonsjsonYESnull風控標記原因
reviewedByvarchar(100)YESnull審核人
reviewedAtdatetimeYESnull審核時間
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, agentId, weekStart) — 同站代理+週起始唯一

索引agentId, siteCode


AffiliateBalance(affiliate-balance

代理餘額表,記錄每個代理的可用餘額與累計收益。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID(唯一)
availabledecimal(18,6)NO0可用餘額 (USD)
frozendecimal(18,6)NO0凍結金額 (USD)
totalEarneddecimal(18,6)NO0累計收益 (USD)
totalWithdrawndecimal(18,6)NO0累計已提款 (USD)
agentTiervarchar(20)YES'bronze'代理等級 (bronze/silver/gold/platinum)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束agentId — 代理 ID 全域唯一


AffiliateWithdrawal(affiliate-withdrawal

代理提款申請表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
amountdecimal(18,6)NO-提款金額 (USD)
methodvarchar(20)NO-提款方式 (crypto / bank)
bankCardIdintYESnull銀行卡 ID
cryptoAddressIdintYESnull加密錢包 ID
statusvarchar(20)NO'pending'狀態 (pending / approved / rejected / completed)
rejectReasonvarchar(255)YESnull拒絕原因
reviewedByvarchar(100)YESnull審核人
reviewedAtdatetimeYESnull審核時間
completedAtdatetimeYESnull完成時間
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引agentId, siteCode


AffiliateClick(affiliate-click

代理推廣連結點擊追蹤表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
refCodevarchar(30)NO-推廣碼
ipvarchar(45)YESnull點擊 IP
userAgentvarchar(500)YESnull瀏覽器 User-Agent
referrervarchar(500)YESnull來源頁面 URL
convertedtinyint(1)NO0是否已轉化 (0=未轉, 1=已轉)
convertedUserIdintYESnull轉化後的用戶 ID
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引agentId, refCode, siteCode


AffiliateBindLog(affiliate-bind-log

代理綁定紀錄表,記錄上下線綁定/解綁動作。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
memberIdintNO-會員 ID
agentIdintNO-代理 ID
refCodevarchar(30)YESnull推廣碼
actionvarchar(20)NO-動作 (bind / unbind / rebind)
ipvarchar(45)YESnull操作 IP
devicevarchar(500)YESnull裝置資訊
operatorAccountvarchar(100)YESnull操作者帳號(管理員手動綁定時記錄)
remarkvarchar(255)YESnull備註
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引memberId, agentId, siteCode


AffiliateRiskLog(affiliate-risk-log

結算風控紀錄表,記錄結算時偵測到的異常。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
settlementIdintNO-結算單 ID
agentIdintNO-代理 ID
memberIdintYESnull會員 ID
riskTypevarchar(50)NO-風控類型
detailjsonYESnull風控詳情
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引settlementId, siteCode


5.1.8 聯盟系統模組

AllianceCommissionRate(alliance-commission-rate

聯盟佣金費率表,定義各代理等級各遊戲類型的佣金比例。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentTiervarchar(20)NO-代理等級 (bronze/silver/gold/platinum)
agentLevelintNO-代理層級 (1/2/3)
gameTypevarchar(20)NO-遊戲類型
commissionRatedecimal(5,2)NO0佣金比例 (%)
enabledtinyint(1)NO1啟用狀態
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(agentTier, agentLevel, gameType) — 等級+層級+遊戲類型唯一


AllianceAgentTier(alliance-agent-tier

代理等級定義表。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
tierCodevarchar(20)NO-等級代碼(唯一,如 bronze/silver/gold/platinum)
tierNamejsonYESnull等級名稱 (多語系)
minTotalEarneddecimal(18,6)NO0最低累計收益門檻 (USD)
minActiveMembersintNO0最低活躍下線數
sortOrderintNO0排序權重
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束tierCode — 等級代碼唯一


AllianceVipMilestone(alliance-vip-milestone

VIP 里程碑定義表,下線 VIP 等級達標時的獎勵設定。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
vipLevelintNO-VIP 等級門檻(唯一)
bonusAmountdecimal(18,6)NO0獎勵金額 (USD)
descriptionjsonYESnull說明 (多語系)
enabledtinyint(1)NO1啟用狀態
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束vipLevel — VIP 等級唯一


AllianceVipMilestoneLog(alliance-vip-milestone-log

VIP 里程碑發放紀錄表。含 siteCode。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
memberIdintNO-會員 ID
vipLevelintNO-VIP 等級
bonusAmountdecimal(18,6)NO0獎勵金額 (USD)
milestoneIdintYESnull里程碑定義 ID
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, agentId, memberId, vipLevel) — 同站代理+會員+VIP 唯一


AllianceReferralCode(alliance-referral-code

聯盟推廣碼表,每個代理最多 10 個推廣碼。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
agentIdintNO-代理 ID
codevarchar(30)NO-推廣碼
labelvarchar(50)YESnull渠道標籤
enabledtinyint(1)NO1啟用狀態
convertCountintNO0轉化數
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, code) — 同站推廣碼唯一


5.1.9 金流模組

DepositOrder(deposit-order

存款訂單表,記錄每一筆存款的完整資訊。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
channelIdintYESnull金流通道 ID
subOrdervarchar(30)NO-商家訂單編號(唯一)
orderAmountdecimal(18,6)NO0訂單金額 (USD)
vendorAmountdecimal(18,6)NO0金流商收款金額(當地幣別)
paymentMethodvarchar(20)NO-支付方式 (fiat / credit / crypto)
statusvarchar(20)NO'pending'訂單狀態 (pending / created / paid / failed / cancelled)
exchangeRatedecimal(18,10)YESnull匯率
resultUrltextYESnull金流商回傳的支付頁面 URL
callbackDatajsonYESnull金流商回調原始資料
proofImagevarchar(500)YESnull付款證明圖片 URL
expectedCodevarchar(20)YESnullATM 預期收款銀行代碼
expectedAccountvarchar(30)YESnullATM 預期收款帳號
userCardLastValuevarchar(10)YESnull信用卡末五碼
payerNamevarchar(50)YESnull付款人姓名
payerMobilevarchar(30)YESnull付款人手機
payerEmailvarchar(100)YESnull付款人 Email
reviewedByvarchar(100)YESnull審核人
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束subOrder — 商家訂單編號全域唯一

索引userId, siteCode


VendorGroup(vendor-group

金流群組表,用於將用戶分配至不同的金流通道組合。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
namejsonNO-群組名稱 (多語系)
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引siteCode


VendorChannel(vendor-channel

金流通道表,儲存各金流商的連線設定。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
namejsonNO-通道名稱 (多語系)
storeCodevarchar(50)NO-商店代碼
secret1varchar(255)NO-金鑰 1
secret2varchar(255)YESnull金鑰 2
secret3varchar(255)YESnull金鑰 3
secret4varchar(255)YESnull金鑰 4
currencyvarchar(10)NO-幣別 (TWD/USD/CNY/THB/VND)
paymentMethodssimple-arrayNO-支持的支付方式 (fiat,credit,crypto)
paymentAddressvarchar(255)YESnullUSDT 收款地址
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引siteCode


VendorGroupChannel(vendor-group-channel

金流群組與通道的多對多關聯表。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
groupIdintNO-群組 ID (FK → vendor-group)
channelIdintNO-通道 ID (FK → vendor-channel)
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(groupId, channelId) — 群組+通道唯一

關聯

  • ManyToOne → VendorGroupgroupId FK,onDelete: CASCADE
  • ManyToOne → VendorChannelchannelId FK,onDelete: CASCADE

5.1.10 錢包模組

BankCard(bank-card

銀行卡表,儲存用戶的銀行帳戶資訊。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
bankCodevarchar(10)NO-銀行代碼
bankAccountvarchar(30)NO-銀行帳號
branchvarchar(50)NO-分行名稱
holderNamevarchar(50)NO-持卡人姓名
idCardFrontvarchar(500)YESnull身分證正面 (R2 path)
idCardBackvarchar(500)YESnull身分證反面 (R2 path)
passbookCovervarchar(500)YESnull存摺封面 (R2 path)
statustinyint(1)NO0審核狀態 (0=待審核, 1=通過, 2=拒絕)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, siteCode


CreditCard(credit-card

信用卡表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
cardNumbervarchar(20)NO-卡號
holderNamevarchar(50)NO-持卡人姓名
cvvvarchar(5)NO-CVV
expiryDatevarchar(10)NO-有效期 (MM/YY)
statustinyint(1)NO0審核狀態 (0=待審核, 1=通過, 2=拒絕)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, siteCode


CryptoAddress(crypto-address

加密貨幣錢包地址表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
walletNamevarchar(100)NO-錢包名稱
currencyvarchar(10)NO'USDT'幣種
networkvarchar(20)NO'TRC-20'網路協議
addressvarchar(255)NO-錢包地址
statustinyint(1)NO0審核狀態 (0=待審核, 1=通過, 2=拒絕)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, siteCode


5.1.11 活動模組

Promo(promo

活動促銷表,記錄所有優惠活動。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
titlejsonNO-活動標題 (多語系)
contentjsonNO-活動內容 (多語系 HTML)
actionHtmltextYESnull渲染連結/按鈕 HTML
imgPcjsonYESnullPC 版橫幅圖片 (多語系 URL)
imgMobilejsonYESnull手機版橫幅圖片 (多語系 URL)
startTimedatetimeNO-活動開始時間
endTimedatetimeNO-活動結束時間
tagvarchar(30)NO-活動標籤
enabledtinyint(1)NO1啟用狀態
conditionTypevarchar(30)NO'none'領取條件類型 (none/deposit_threshold/vip_level/first_deposit)
conditionValuevarchar(50)YES'0'條件門檻值
rewardAmountdecimal(18,6)NO0獎勵金額 (USD)
turnoverMultiplierdecimal(10,2)NO0打碼量倍數
maxClaimsintNO0最大領取總數 (0=無限)
claimedCountintNO0已領取數
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引siteCode


PromoClaim(promo-claim

活動領取紀錄表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
promoIdintNO-活動 ID
userIdintNO-用戶 ID
rewardAmountdecimal(18,6)NO0獎勵金額 (USD)
requiredTurnoverdecimal(18,6)NO0所需打碼量 (USD)
completedTurnoverdecimal(18,6)NO0已完成打碼量 (USD)
turnoverCompletedtinyint(1)NO0打碼量是否已完成
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, promoId, userId) — 同站活動+用戶唯一(每人每活動限領一次)


PromoTag(promo-tag

活動標籤表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
namevarchar(30)NO-標籤代碼
labeljsonYESnull標籤名稱 (多語系)
colorvarchar(20)YESnull顏色色碼
sortOrderintNO0排序權重
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, name) — 同站標籤名唯一


5.1.12 站內信模組

Notification(notification

站內信表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintYESnull目標用戶 ID(null=全站廣播)
titlejsonNO-通知標題 (多語系)
contentjsonNO-通知內容 (多語系 HTML)
categoryvarchar(20)NO-分類 (system / promo)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, siteCode


NotificationRead(notification-read

通知已讀紀錄表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
notificationIdintNO-通知 ID
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, userId, notificationId) — 同站用戶+通知唯一


5.1.13 站點配置模組

SiteConfig(site-config

站點設定表,儲存每個白牌站點的完整配置。此表的 siteCode 欄位本身就是站點定義(非多站篩選用途)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
siteCodevarchar(30)NO-站點代碼(唯一)
prefixvarchar(30)NO-白牌前綴(唯一,對應 R2 路徑)
layoutvarchar(30)YES'a1'前台模板代碼
siteNamejsonNO-站點名稱 (多語系)
siteDescriptionjsonYESnull站點介紹 (多語系)
supportedLocalesjsonYESnull支援語系列表
activeThemeIdintYESnull當前啟用主題 ID
mascotsjsonYESnull吉祥物列表 (R2 URLs)
bottomBarConfigjsonYESnull前台底部導航列配置
footerConfigjsonYESnull前台頁尾配置
learnMoreConfigjsonYESnull了解更多 FAQ 配置 (多語系 title+content 陣列)
customerServiceConfigjsonYESnull客服管道配置 (8 種管道 + LiveChat)
domainsjsonYESnull域名設置 (hostname, protocol 等)
oauthProvidersjsonYESnull三方登入配置 (Google/Telegram)
gameProvidersjsonYESnull遊戲商配置
serviceProvidersjsonYESnull服務商配置 (SMS/Email)
templateVariablesjsonYESnull模板變數
notificationConfigjsonYESnull通知設定
depositMethodsjsonYESnull存款方式設定
enabledtinyint(1)NO1啟用狀態
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束siteCode — 站點代碼唯一、prefix — 前綴唯一

關聯OneToMany → SiteTheme(一對多:主題列表)


SiteTheme(site-theme

站點主題表,定義各站點的色彩方案。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
themeIdvarchar(50)NO-主題識別碼(唯一)
themeNamejsonNO-主題名稱 (多語系)
primaryjsonNO-主色系 (base/dark/light/glow)
accentjsonNO-強調色 (gold/info/violet/cyan/error)
surfacejsonNO-表面色 (page/navbar/card/modal/sidebar)
textjsonNO-文字色 (primary/secondary/muted/hint)
borderjsonNO-邊框色 (subtle/default/strong)
enabledtinyint(1)NO1啟用狀態
siteConfigIdintNO-站點配置 ID (FK → site-config)
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束themeId — 主題識別碼唯一

關聯ManyToOne → SiteConfigsiteConfigId FK,onDelete: CASCADE


5.1.14 提領模組

WithdrawalOrder(withdrawal-order

提領訂單表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
amountdecimal(18,6)NO-提領金額 (USD)
cryptoAddressIdintYESnull加密錢包 ID
addressvarchar(255)YESnull提領地址
networkvarchar(20)YESnull網路協議
statusvarchar(20)NO'pending'狀態 (pending / approved / rejected / completed)
rejectReasonvarchar(255)YESnull拒絕原因
reviewedByvarchar(100)YESnull審核人
proofKeyvarchar(500)YESnull代付證明 R2 key
proofOriginalNamevarchar(255)YESnull代付證明原始檔名
completedByvarchar(100)YESnull完成出款操作者
completedAtdatetimeYESnull完成時間
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引userId, siteCode


5.1.15 排行榜模組

RankList(rank-list

排行榜表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
gameNamevarchar(100)NO-遊戲名稱
betAmountdecimal(18,6)NO0投注金額 (USD)
multiplierdecimal(10,2)NO0倍率
payoutdecimal(18,6)NO0派彩金額 (USD)
isAnonymoustinyint(1)NO0是否匿名
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引siteCode


5.1.16 任務模組

Mission(mission

任務定義表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
categoryvarchar(20)NO-任務類別 (deposit / bet)
periodTypevarchar(10)NO-週期類型 (daily / weekly / monthly)
tierintNO-任務階層(同類同週期的不同門檻)
thresholddecimal(18,6)NO0門檻值 (USD)
rewardAmountdecimal(18,6)NO0獎勵金額 (USD)
vipRequiredintNO0VIP 等級要求(0=無要求)
turnoverMultiplierdecimal(10,2)NO0打碼量倍數
enabledtinyint(1)NO1啟用狀態
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, category, periodType, tier) — 同站類別+週期+層級唯一


MissionProgress(mission-progress

任務進度表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
userIdintNO-用戶 ID
periodTypevarchar(10)NO-週期類型
periodKeyvarchar(20)NO-週期標識 (如 2026-03-02, 2026-W10)
depositTotaldecimal(18,6)NO0累計存款 (USD)
betTotaldecimal(18,6)NO0累計投注 (USD)
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, userId, periodType, periodKey) — 同站用戶+週期唯一


MissionClaim(mission-claim

任務領取紀錄表。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
missionIdintNO-任務 ID
userIdintNO-用戶 ID
periodKeyvarchar(20)NO-週期標識
rewardAmountdecimal(18,6)NO0獎勵金額 (USD)
requiredTurnoverdecimal(18,6)NO0所需打碼量 (USD)
completedTurnoverdecimal(18,6)NO0已完成打碼量 (USD)
turnoverCompletedtinyint(1)NO0打碼量是否已完成
siteCodevarchar(30)NO'C9'所屬站點代碼
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

唯一約束(siteCode, missionId, userId, periodKey) — 同站任務+用戶+週期唯一


5.1.17 R2 儲存模組

R2OperationLog(r2-operation-log

R2 雲端儲存操作紀錄表。全站共用(不含 siteCode)。

欄位型別NullableDefaultComment / 說明
idint (PrimaryGeneratedColumn)NOauto主鍵
adminIdintNO-管理員 ID (FK → admin-user)
actionvarchar(30)NO-操作動作 (upload / delete / move / create-folder / delete-folder)
modulevarchar(30)NO-模組名稱
objectKeyvarchar(500)NO-物件 Key
originalNamevarchar(255)YESnull原始檔名
fileSizeintYESnull檔案大小 (bytes)
mimeTypevarchar(100)YESnullMIME 類型
ipvarchar(45)YESnull操作 IP
userAgentvarchar(500)YESnull瀏覽器 User-Agent
detailjsonYESnull操作詳情
createdAtdatetimeNOauto建立時間
updatedAtdatetimeNOauto更新時間

索引adminId

關聯ManyToOne → AdminUseradminId FK)


5.1.18 Entity 統計總表

分類含 siteCode不含 siteCode合計
用戶認證202
遊戲404
投注紀錄202
VIP303
後台管理033
風控202
代理推廣707
聯盟系統235
金流314
錢包303
活動303
站內信202
站點配置022
提領101
排行榜101
任務303
R2 儲存011
合計371249

5.2 完整 API 端點文件(205+ 個端點)

本章節詳細記錄所有 API 端點的 HTTP 方法、路由、Guards、Decorators、請求參數/Body、回應格式。所有路由均有 /api 全域前綴。

5.2.1 認證模組(Auth)— 17 個端點

Controller: AuthController路由前綴: /api/authSwagger Tag: Auth

#MethodRouteGuardsPermission說明
1POST/auth/register--用戶註冊
2POST/auth/login--用戶登入
3POST/auth/login/google--Google OAuth 登入
4POST/auth/login/telegram--Telegram 登入
5GET/auth/user-detailJwtAuthGuard-取得用戶詳細資料
6GET/auth/balanceJwtAuthGuard-取得用戶餘額
7PATCH/auth/profileJwtAuthGuard-更新個人資料
8POST/auth/change-passwordJwtAuthGuard-變更密碼
9POST/auth/set-passwordJwtAuthGuard-首次設定密碼(OAuth 用戶)
10POST/auth/send-email-verifyJwtAuthGuard-發送 Email 驗證碼
11POST/auth/verify-emailJwtAuthGuard-驗證 Email
12POST/auth/send-mobile-verifyJwtAuthGuard-發送手機驗證碼
13POST/auth/verify-mobileJwtAuthGuard-驗證手機號碼
14POST/auth/upload-avatarJwtAuthGuard-上傳頭像 (multipart)
15POST/auth/bind/googleJwtAuthGuard-綁定 Google 帳號
16POST/auth/unbind/googleJwtAuthGuard-解綁 Google 帳號
17POST/auth/bind/telegramJwtAuthGuard-綁定 Telegram 帳號

端點詳細說明

POST /auth/register

  • Body: RegisterDto{ account: string, password: string, name: string, refCode?: string, device?: string }
  • 回應: { accessToken, user: { id, account, name, ... } }
  • 錯誤碼: 2001 帳號已存在、2002 推廣碼不存在
  • 業務邏輯: 建立用戶 → 自動分配金流群組(依語系) → 若有 refCode 則綁定上線代理 → 記錄登入紀錄

POST /auth/login

  • Body: LoginDto{ account: string, password: string, device?: string }
  • 回應: { accessToken, user: { id, account, name, balance, vipLevel, ... } }
  • 錯誤碼: 2001 帳號或密碼錯誤、2002 帳號已停用、2003 需要 2FA 驗證
  • 業務邏輯: 驗證帳密 → 檢查 status → 若啟用 2FA 則回傳需驗證標記 → 簽發 JWT → 記錄登入紀錄(IP + device)

POST /auth/login/google

  • Body: { idToken: string, device?: string }
  • 業務邏輯: 驗證 Google ID Token → 查找/建立用戶 → 自動綁定 google sub → 簽發 JWT

POST /auth/login/telegram

  • Body: LoginTelegramDto{ id, first_name, last_name?, username?, photo_url?, auth_date, hash, device? }
  • 業務邏輯: HMAC-SHA256 驗證 Telegram hash → 查找/建立用戶 → 簽發 JWT

5.2.2 遊戲模組(Game)— 17 個端點

Controller: GameController路由前綴: /api/gameSwagger Tag: Game

前台端點(5 個)

#MethodRouteGuards說明
1GET/game/providersOptionalJwtAuthGuard遊戲供應商列表(支援 gameType 篩選)
2POST/game/launchJwtAuthGuard啟動遊戲(回傳遊戲 URL)
3POST/game/simulateJwtAuthGuard模擬遊戲結果(開發測試用)
4POST/game/demo-試玩模式(不需登入)
5GET/game/recentJwtAuthGuard最近遊玩的遊戲列表

Admin 端點(11 個)

#MethodRouteGuardsPermission說明
6GET/game/admin/providersAdminJwtAuthGuard-遊戲供應商列表 (Admin)
7POST/game/admin/providersAdminJwtAuthGuard-新增遊戲供應商
8PATCH/game/admin/providers/:idAdminJwtAuthGuard-更新遊戲供應商
9DELETE/game/admin/providers/:idAdminJwtAuthGuard-刪除遊戲供應商
10GET/game/admin/type-configsAdminJwtAuthGuard-遊戲分類列表
11POST/game/admin/type-configsAdminJwtAuthGuard-新增遊戲分類
12PATCH/game/admin/type-configs/:idAdminJwtAuthGuard-更新遊戲分類
13DELETE/game/admin/type-configs/:idAdminJwtAuthGuard-刪除遊戲分類
14GET/game/admin/preview-templateAdminJwtAuthGuard-預覽模板
15POST/game/admin/load-templateAdminJwtAuthGuard-帶入模板(按 @AdminSiteCode 寫入)
16POST/game/admin/copy-site-dataAdminJwtAuthGuard-跨站複製

S2S 回調端點(2 個)

#MethodRouteGuards說明
17POST/game/betsolutions/callback-BetSolutions S2S 回調(@ApiExcludeEndpoint)
18POST/game/rsg/callback-RSG S2S 回調(DES 加解密,@ApiExcludeEndpoint)

POST /game/launch 業務邏輯:

  1. 驗證用戶 JWT
  2. 檢查 AdminRiskService.isUserBlockedFromGame() 遊戲黑名單(錯誤碼 5010)
  3. 取得對應 provider 的 launch URL
  4. UPSERT game-play-log 記錄遊玩紀錄
  5. 回傳遊戲 URL

POST /game/admin/copy-site-data 業務邏輯:

  • Body: { sourceSiteCode: string, targetSiteCode: string, type: 'providers' | 'typeConfigs' }
  • 在 transaction 內先刪除目標站點的資料,再從來源站點複製

5.2.3 錢包模組(Wallet)— 9 個端點

銀行卡(BankCard)— 3 個端點

Controller: BankCardController路由前綴: /api/wallet/bank-cardGuards: JwtAuthGuard(Controller 級)

#MethodRoute說明
1POST/wallet/bank-card/add新增銀行卡(multipart: idCardFront/idCardBack/passbookCover)
2GET/wallet/bank-card/list取得銀行卡列表
3DELETE/wallet/bank-card/:id刪除銀行卡

POST /wallet/bank-card/add

  • Body: AddBankCardDto + 3 個圖片檔案(各最大 5MB)
  • 圖片上傳至 R2,路徑記錄在 entity
  • 錯誤碼: 2001 請上傳身分證正面、2002 請上傳身分證反面、2003 請上傳銀行存摺封面、2004 此銀行卡已存在

信用卡(CreditCard)— 3 個端點

Controller: CreditCardController路由前綴: /api/wallet/credit-cardGuards: JwtAuthGuard(Controller 級)

#MethodRoute說明
1POST/wallet/credit-card/add新增信用卡
2GET/wallet/credit-card/list取得信用卡列表
3DELETE/wallet/credit-card/:id刪除信用卡

加密錢包(CryptoAddress)— 3 個端點

Controller: CryptoAddressController路由前綴: /api/wallet/crypto-addressGuards: JwtAuthGuard(Controller 級)

#MethodRoute說明
1POST/wallet/crypto-address/add新增加密錢包地址
2GET/wallet/crypto-address/list取得加密錢包列表
3DELETE/wallet/crypto-address/:id刪除加密錢包地址

5.2.4 金流模組(Vendor)— 6 個端點

Controller: VendorController路由前綴: /api/vendor

#MethodRouteGuards說明
1GET/vendor/channelsJwtAuthGuard取得可用金流通道列表(依語系/群組篩選)

萬通金流(Wantong)— 2 個端點

Controller: WantongController路由前綴: /api/vendor/wantong

#MethodRouteGuards說明
2POST/vendor/wantong/add-atmJwtAuthGuardATM 入金
3POST/vendor/wantong/add-cardJwtAuthGuard信用卡入金
4POST/vendor/wantong/callback-萬通回調(@ApiExcludeEndpoint)

USDT(加密貨幣)— 1 個端點

Controller: UsdtController路由前綴: /api/vendor/usdt

#MethodRouteGuards說明
5POST/vendor/usdt/callback-USDT 回調(@ApiExcludeEndpoint)

5.2.5 存款模組(Deposit)— 4 個端點

Controller: DepositController路由前綴: /api/deposit

#MethodRouteGuards說明
1GET/deposit/exchange-rate-取得台幣即時匯率
2GET/deposit/crypto-rate-取得加密貨幣匯率
3GET/deposit/channelsJwtAuthGuard取得可用存款通道
4POST/depositJwtAuthGuard建立存款訂單
5POST/deposit/confirmJwtAuthGuard確認存款(上傳付款證明)

POST /deposit

  • Body: DepositDto{ channelId, paymentMethod, subOrder, orderAmount, expectedCode?, expectedAccount?, userCardLastValue?, productDes?, msg?, payerName?, payerMobile?, payerEmail? }
  • 業務邏輯: 建立訂單 → 路由至對應金流商(萬通/USDT) → 回傳支付頁面 URL
  • 存款確認後觸發: MissionService.updateDepositProgress() 更新任務存款進度

5.2.6 VIP 模組(VIP)— 13 個端點

Controller: VipController路由前綴: /api/vip

前台端點(3 個)

#MethodRouteGuards說明
1GET/vip/levels-VIP 等級列表(含 siteCode 篩選)
2GET/vip/rebates-VIP 返水規則列表
3GET/vip/user-statusJwtAuthGuard用戶 VIP 狀態

Admin 端點(10 個)

#MethodRouteGuardsPermission說明
4GET/vip/admin/levelsAdminJwtAuthGuard, PermissionsGuardvip:readVIP 等級列表 (Admin)
5POST/vip/admin/levelsAdminJwtAuthGuard, PermissionsGuardvip:write新增 VIP 等級
6PATCH/vip/admin/levels/:idAdminJwtAuthGuard, PermissionsGuardvip:write更新 VIP 等級
7DELETE/vip/admin/levels/:idAdminJwtAuthGuard, PermissionsGuardvip:write刪除 VIP 等級
8GET/vip/admin/rebatesAdminJwtAuthGuard, PermissionsGuardvip:read返水規則列表 (Admin)
9POST/vip/admin/rebates/bulkAdminJwtAuthGuard, PermissionsGuardvip:write批次新增/更新返水規則 (Bulk Upsert)
10GET/vip/admin/preview-templateAdminJwtAuthGuard-預覽模板
11POST/vip/admin/load-templateAdminJwtAuthGuard-帶入模板
12POST/vip/admin/copy-site-dataAdminJwtAuthGuard-跨站複製
13POST/vip/admin/trigger-settlementAdminJwtAuthGuard-手動觸發反水結算

5.2.7 活動模組(Promo)— 7 個端點

Controller: PromoController路由前綴: /api/promo

#MethodRouteGuards說明
1GET/promo/listOptionalJwtAuthGuard活動列表(前台,activeOnly)
2GET/promo/:idOptionalJwtAuthGuard活動詳情
3GET/promo/claimsJwtAuthGuard我的活動領取紀錄
4GET/promo/tags-活動標籤列表
5POST/promo/:id/claimJwtAuthGuard領取活動獎勵

POST /promo/:id/claim 業務邏輯:

  1. 驗證用戶是否滿足領取條件(conditionType 檢查)
  2. 檢查是否已領取(unique constraint: siteCode + promoId + userId)
  3. 檢查 maxClaims 上限
  4. 發放獎勵金額至用戶餘額
  5. 設定打碼量要求(rewardAmount * turnoverMultiplier)

5.2.8 排行榜模組(Ranking)— 1 個端點

Controller: RankingController路由前綴: /api/ranking

#MethodRouteGuards說明
1GET/ranking-排行榜列表

5.2.9 投注紀錄模組(BetRecord)— 2 個端點

Controller: BetRecordController路由前綴: /api/bet-record

#MethodRouteGuards說明
1GET/bet-record/ordersJwtAuthGuard注單列表(支援 gameType/status/日期篩選)
2GET/bet-record/orders/:idJwtAuthGuard注單詳情(含明細)

5.2.10 代理推廣模組(Affiliate)— 39 個端點

Controller: AffiliateController路由前綴: /api/affiliate

前台代理端點(21 個)

#MethodRouteGuards說明
1POST/affiliate/track-click-追蹤推廣連結點擊
2POST/affiliate/applyJwtAuthGuard申請成為代理
3GET/affiliate/tourJwtAuthGuard代理導覽資訊
4GET/affiliate/dashboardJwtAuthGuard代理儀表板
5GET/affiliate/downlineJwtAuthGuard下線列表
6GET/affiliate/commissionsJwtAuthGuard佣金紀錄
7GET/affiliate/settlementsJwtAuthGuard結算紀錄
8GET/affiliate/balanceJwtAuthGuard代理餘額
9POST/affiliate/withdrawJwtAuthGuard代理提款申請
10GET/affiliate/withdrawalsJwtAuthGuard代理提款紀錄
11GET/affiliate/alliance-info-聯盟資訊(公開)
12GET/affiliate/tier-info-代理等級資訊(公開)
13GET/affiliate/vip-milestones-VIP 里程碑(公開)
14GET/affiliate/referral-codesJwtAuthGuard我的推廣碼列表
15POST/affiliate/referral-codesJwtAuthGuard新增推廣碼(最多 10 個)
16PATCH/affiliate/referral-codes/:idJwtAuthGuard更新推廣碼
17DELETE/affiliate/referral-codes/:idJwtAuthGuard刪除推廣碼

Admin 代理管理端點(17 個)

#MethodRouteGuardsPermission說明
18GET/affiliate/admin/agentsAdminJwtAuthGuard, PermissionsGuardaffiliate:read代理列表
19POST/affiliate/admin/create-agentAdminJwtAuthGuard, PermissionsGuardaffiliate:write手動綁定用戶為代理
20GET/affiliate/admin/settlementsAdminJwtAuthGuard, PermissionsGuardaffiliate:read佣金結算列表
21POST/affiliate/admin/settlements/:id/reviewAdminJwtAuthGuard, PermissionsGuardaffiliate:write結算審核 (approve/reject)
22GET/affiliate/admin/settlements/:id/risk-logsAdminJwtAuthGuard, PermissionsGuardaffiliate:read結算風控紀錄
23GET/affiliate/admin/withdrawalsAdminJwtAuthGuard, PermissionsGuardaffiliate:read代理提款列表
24POST/affiliate/admin/withdrawals/:id/reviewAdminJwtAuthGuard, PermissionsGuardaffiliate:write提款審核
25POST/affiliate/admin/withdrawals/:id/completeAdminJwtAuthGuard, PermissionsGuardaffiliate:write提款完成
26POST/affiliate/admin/bindAdminJwtAuthGuard, PermissionsGuardaffiliate:write手動綁定上下線
27GET/affiliate/admin/bind-logsAdminJwtAuthGuard, PermissionsGuardaffiliate:read綁定紀錄
28GET/affiliate/admin/commission-ratesAdminJwtAuthGuard-佣金費率列表
29POST/affiliate/admin/commission-ratesAdminJwtAuthGuard-新增/更新佣金費率
30DELETE/affiliate/admin/commission-rates/:idAdminJwtAuthGuard-刪除佣金費率
31GET/affiliate/admin/vip-milestonesAdminJwtAuthGuard-VIP 里程碑列表
32POST/affiliate/admin/vip-milestonesAdminJwtAuthGuard-新增/更新 VIP 里程碑
33DELETE/affiliate/admin/vip-milestones/:idAdminJwtAuthGuard-刪除 VIP 里程碑
34GET/affiliate/admin/agent-tiersAdminJwtAuthGuard-代理等級列表
35POST/affiliate/admin/agent-tiersAdminJwtAuthGuard-新增/更新代理等級
36DELETE/affiliate/admin/agent-tiers/:idAdminJwtAuthGuard-刪除代理等級
37GET/affiliate/admin/preview-templateAdminJwtAuthGuard-預覽模板
38POST/affiliate/admin/load-templateAdminJwtAuthGuard-帶入模板
39POST/affiliate/admin/set-agent-tierAdminJwtAuthGuard-手動調整代理等級
40POST/affiliate/admin/trigger-settlementAdminJwtAuthGuard-手動觸發週結
41POST/affiliate/admin/trigger-daily-settlementAdminJwtAuthGuard-手動觸發日結

5.2.11 站內信模組(Inbox)— 7 個端點

Controller: InboxController路由前綴: /api/inbox

#MethodRouteGuardsPermission說明
1GET/inbox/listJwtAuthGuard-用戶收件匣列表
2GET/inbox/unread-countJwtAuthGuard-未讀通知數量
3POST/inbox/:id/readJwtAuthGuard-標記通知為已讀
4POST/inbox/read-allJwtAuthGuard-全部標記為已讀
5POST/inbox/admin/sendAdminJwtAuthGuard, PermissionsGuard-管理員發送通知
6PATCH/inbox/admin/:idAdminJwtAuthGuard, PermissionsGuard-更新通知
7GET/inbox/admin/listAdminJwtAuthGuard, PermissionsGuard-管理員通知列表
8DELETE/inbox/admin/:idAdminJwtAuthGuard, PermissionsGuard-刪除通知

5.2.12 站點設定模組(SiteConfig)— 12 個端點

Controller: SiteConfigController路由前綴: /api/site-config

#MethodRouteGuards說明
1GET/site-config-取得當前站點公開配置
2GET/site-config/admin/listAdminJwtAuthGuard站點列表(含主題)
3POST/site-config/adminAdminJwtAuthGuard新增站點
4PATCH/site-config/admin/:idAdminJwtAuthGuard更新站點設定
5DELETE/site-config/admin/:idAdminJwtAuthGuard刪除站點(cascade 刪主題)
6GET/site-config/admin/:siteConfigId/themesAdminJwtAuthGuard主題列表
7POST/site-config/admin/:siteConfigId/themesAdminJwtAuthGuard新增主題
8PATCH/site-config/admin/themes/:idAdminJwtAuthGuard更新主題
9DELETE/site-config/admin/themes/:idAdminJwtAuthGuard刪除主題
10POST/site-config/admin/:id/domain-assetAdminJwtAuthGuard上傳域名素材
11POST/site-config/admin/:id/customer-service-iconAdminJwtAuthGuard上傳客服圖示
12PATCH/site-config/admin/:siteConfigId/mascotsAdminJwtAuthGuard更新吉祥物
13GET/site-config/admin/:siteCode/customer-serviceAdminJwtAuthGuard取得客服設定

5.2.13 提領模組(Withdrawal)— 7 個端點

Controller: WithdrawalController路由前綴: /api/withdrawal

#MethodRouteGuardsPermission說明
1POST/withdrawal/send-codeJwtAuthGuard-發送提領驗證碼
2POST/withdrawal/requestJwtAuthGuard-申請提領
3GET/withdrawal/listJwtAuthGuard-我的提領紀錄
4GET/withdrawal/turnover-statusJwtAuthGuard-打碼量狀態

POST /withdrawal/request

  • Body: RequestWithdrawalDto{ amount: number, cryptoAddressId: number, verifyCode: string }
  • 業務邏輯: 驗證信箱驗證碼 → 檢查餘額充足 → 凍結餘額 → 建立提領訂單

5.2.14 任務模組(Mission)— 3 個端點

Controller: MissionController路由前綴: /api/mission

#MethodRouteGuards說明
1GET/mission/listJwtAuthGuard任務列表(含進度)
2GET/mission/claimsJwtAuthGuard任務領取紀錄
3POST/mission/:id/claimJwtAuthGuard領取任務獎勵

5.2.15 其他模組

公用端點(Common)— 1 個端點

#MethodRouteGuards說明
1GET/common/enums-取得列舉資料 + ERROR_CODES(依語系)

即時體育(LiveSports)— 1 個端點

#MethodRouteGuards說明
1GET/live-sports-即時賽事列表(Redis 快取,30min 更新)

健康檢查(App)— 1 個端點

#MethodRouteGuards說明
1GET/-Health check

5.2.16 後台管理模組(Admin)— 90 個端點

Controller: AdminController路由前綴: /api/adminSwagger Tag: Admin

認證管理(4 個)

#MethodRouteGuardsPermission說明
1POST/admin/login--管理員登入
2POST/admin/register--管理員註冊
3POST/admin/send-verify-code--發送驗證碼
4POST/admin/verify-email--驗證 Email

個人資料 + 2FA(4 個)

#MethodRouteGuards說明
5GET/admin/profileAdminJwtAuthGuard取得管理員個人資料
6PATCH/admin/profileAdminJwtAuthGuard更新管理員個人資料
7POST/admin/google-auth/setupAdminJwtAuthGuard設定 Google Authenticator(回傳 QR Code)
8POST/admin/google-auth/verifyAdminJwtAuthGuard驗證 TOTP 啟用 2FA
9POST/admin/google-auth/disableAdminJwtAuthGuard停用 2FA

權限(1 個)

#MethodRouteGuards說明
10GET/admin/permissions/allAdminJwtAuthGuard取得所有權限列表

管理員 CRUD(5 個)

#MethodRouteGuardsPermission說明
11GET/admin/listAdminJwtAuthGuard, PermissionsGuardadmin:read管理員列表
12GET/admin/:idAdminJwtAuthGuard, PermissionsGuardadmin:read取得管理員詳情
13POST/admin/createAdminJwtAuthGuard, PermissionsGuardadmin:write新增管理員
14PATCH/admin/:idAdminJwtAuthGuard, PermissionsGuardadmin:write更新管理員
15DELETE/admin/:idAdminJwtAuthGuard, PermissionsGuardadmin:write刪除管理員

群組 CRUD(5 個)

#MethodRouteGuardsPermission說明
16GET/admin/groups/listAdminJwtAuthGuard, PermissionsGuardadmin-group:read群組列表
17GET/admin/groups/:idAdminJwtAuthGuard, PermissionsGuardadmin-group:read群組詳情
18POST/admin/groups/createAdminJwtAuthGuard, PermissionsGuardadmin-group:write新增群組
19PATCH/admin/groups/:idAdminJwtAuthGuard, PermissionsGuardadmin-group:write更新群組
20DELETE/admin/groups/:idAdminJwtAuthGuard, PermissionsGuardadmin-group:write刪除群組

操作紀錄(1 個)

#MethodRouteGuardsPermission說明
21GET/admin/logs/listAdminJwtAuthGuard, PermissionsGuardadmin-log:read操作紀錄列表

活動管理(8 個)

#MethodRouteGuardsPermission說明
22GET/admin/promos/listAdminJwtAuthGuard, PermissionsGuardpromo:read活動列表
23GET/admin/promos/:idAdminJwtAuthGuard, PermissionsGuardpromo:read活動詳情
24POST/admin/promos/createAdminJwtAuthGuard, PermissionsGuardpromo:write新增活動 (multipart)
25PATCH/admin/promos/:idAdminJwtAuthGuard, PermissionsGuardpromo:write更新活動 (multipart)
26DELETE/admin/promos/:idAdminJwtAuthGuard, PermissionsGuardpromo:write刪除活動
27GET/admin/promo-tags/listAdminJwtAuthGuard, PermissionsGuardpromo-tag:read標籤列表
28POST/admin/promo-tags/createAdminJwtAuthGuard, PermissionsGuardpromo-tag:write新增標籤
29PATCH/admin/promo-tags/:idAdminJwtAuthGuard, PermissionsGuardpromo-tag:write更新標籤
30DELETE/admin/promo-tags/:idAdminJwtAuthGuard, PermissionsGuardpromo-tag:write刪除標籤

財務管理(26 個)

#MethodRouteGuardsPermission說明
31GET/admin/finance/deposit-reviewAdminJwtAuthGuard, PermissionsGuard-存款審核列表
32PATCH/admin/finance/deposit-review/:idAdminJwtAuthGuard, PermissionsGuard-審核存款 (approve/reject)
33GET/admin/finance/usersAdminJwtAuthGuard, PermissionsGuarduser:read前台用戶列表
34GET/admin/finance/users/:idAdminJwtAuthGuard, PermissionsGuarduser:read用戶詳情
35PATCH/admin/finance/users/:idAdminJwtAuthGuard, PermissionsGuarduser:write更新用戶資料
36PATCH/admin/users/:userId/vendor-groupAdminJwtAuthGuard, PermissionsGuarduser:write更新用戶金流群組
37POST/admin/finance/adjust-balanceAdminJwtAuthGuard, PermissionsGuardfinance:write人工調節餘額
38-42-/admin/finance/bank-cards/*AdminJwtAuthGuard, PermissionsGuardfinance:read/write銀行卡 CRUD + 審核 (5 個)
43-47-/admin/finance/credit-cards/*AdminJwtAuthGuard, PermissionsGuardfinance:read/write信用卡 CRUD + 審核 (5 個)
48-52-/admin/finance/crypto-addresses/*AdminJwtAuthGuard, PermissionsGuardfinance:read/write加密錢包 CRUD + 審核 (5 個)
53-56-/admin/finance/withdrawals/*AdminJwtAuthGuard, PermissionsGuardwithdrawal:read/write提領列表、審核、上傳憑證、完成 (4 個)

金流商管理(9 個)

#MethodRouteGuardsPermission說明
57GET/admin/vendor-groups/listAdminJwtAuthGuard, PermissionsGuardvendor:read金流群組列表
58POST/admin/vendor-groups/createAdminJwtAuthGuard, PermissionsGuardvendor:write新增金流群組
59PATCH/admin/vendor-groups/:idAdminJwtAuthGuard, PermissionsGuardvendor:write更新金流群組
60DELETE/admin/vendor-groups/:idAdminJwtAuthGuard, PermissionsGuardvendor:write刪除金流群組
61GET/admin/vendor-groups/:id/channelsAdminJwtAuthGuard, PermissionsGuardvendor:read群組通道列表
62PUT/admin/vendor-groups/:id/channelsAdminJwtAuthGuard, PermissionsGuardvendor:write設定群組通道
63GET/admin/vendor-channels/listAdminJwtAuthGuard, PermissionsGuardvendor:read金流通道列表
64POST/admin/vendor-channels/createAdminJwtAuthGuard, PermissionsGuardvendor:write新增金流通道
65PATCH/admin/vendor-channels/:idAdminJwtAuthGuard, PermissionsGuardvendor:write更新金流通道
66DELETE/admin/vendor-channels/:idAdminJwtAuthGuard, PermissionsGuardvendor:write刪除金流通道

報表(10 個)

#MethodRouteGuardsPermission說明
67GET/admin/reports/playersAdminJwtAuthGuard, PermissionsGuardreport:read玩家報表(25+ 篩選參數)
68GET/admin/reports/vip-playersAdminJwtAuthGuard, PermissionsGuardreport:readVIP 玩家報表
69GET/admin/reports/bet-recordsAdminJwtAuthGuard, PermissionsGuardreport:read投注紀錄報表
70GET/admin/reports/overviewAdminJwtAuthGuard, PermissionsGuardreport:read總體報表
71GET/admin/reports/profit-lossAdminJwtAuthGuard, PermissionsGuardreport:read損益報表
72GET/admin/reports/gamesAdminJwtAuthGuard, PermissionsGuardreport:read遊戲報表
73GET/admin/reports/promosAdminJwtAuthGuard, PermissionsGuardreport:read優惠報表
74GET/admin/reports/player-summaryAdminJwtAuthGuard, PermissionsGuardreport:read玩家簡表
75GET/admin/reports/r2-logsAdminJwtAuthGuard, PermissionsGuardreport:readR2 操作紀錄
76GET/admin/reports/export/:typeAdminJwtAuthGuard, PermissionsGuardreport:read匯出報表 (CSV)

風控管理(8 個)

#MethodRouteGuardsPermission說明
77GET/admin/risk/ip-rulesAdminJwtAuthGuard, PermissionsGuardrisk:readIP 規則列表
78POST/admin/risk/ip-rulesAdminJwtAuthGuard, PermissionsGuardrisk:write新增 IP 規則
79PATCH/admin/risk/ip-rules/:idAdminJwtAuthGuard, PermissionsGuardrisk:write更新 IP 規則
80DELETE/admin/risk/ip-rules/:idAdminJwtAuthGuard, PermissionsGuardrisk:write刪除 IP 規則
81GET/admin/risk/login-failuresAdminJwtAuthGuard, PermissionsGuardrisk:read登入失敗列表
82GET/admin/risk/lookupAdminJwtAuthGuard, PermissionsGuardrisk:readIP/FP 檢查
83GET/admin/risk/game-blacklistAdminJwtAuthGuard, PermissionsGuardrisk:read遊戲黑名單列表
84POST/admin/risk/game-blacklistAdminJwtAuthGuard, PermissionsGuardrisk:write新增遊戲黑名單
85DELETE/admin/risk/game-blacklist/:idAdminJwtAuthGuard, PermissionsGuardrisk:write刪除遊戲黑名單

R2 儲存管理(6 個)

#MethodRouteGuardsPermission說明
86GET/admin/r2/listAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 檔案列表
87POST/admin/r2/uploadAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 上傳檔案 (multipart, 最大 50MB)
88POST/admin/r2/deleteAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 批次刪除
89POST/admin/r2/moveAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 移動檔案/資料夾
90POST/admin/r2/create-folderAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 建立資料夾
91POST/admin/r2/delete-folderAdminJwtAuthGuard, PermissionsGuardstorage:manageR2 刪除資料夾 (遞迴)

5.3 完整 DTO 文件(53 個 DTO)

本章節記錄所有 Data Transfer Object 的完整欄位定義,包含驗證規則(class-validator)與 Swagger 描述。

5.3.1 認證模組 DTO(7 個)

RegisterDto

檔案: src/modules/auth/dto/register.dto.ts

欄位型別驗證規則必填說明
accountstring@IsString()YES帳號
passwordstring@IsString(), @MinLength(6)YES密碼(至少 6 碼)
namestring@IsString()YES暱稱
refCodestring@IsOptional(), @IsString()NO推廣碼
devicestring@IsOptional(), @IsString()NO裝置指紋(FingerprintJS)

LoginDto

檔案: src/modules/auth/dto/login.dto.ts

欄位型別驗證規則必填說明
accountstring@IsString()YES帳號
passwordstring@IsString(), @MinLength(6)YES密碼
devicestring@IsOptional(), @IsString()NO裝置指紋

LoginTelegramDto

檔案: src/modules/auth/dto/login-telegram.dto.ts

欄位型別驗證規則必填說明
idnumber@IsInt()YESTelegram User ID
first_namestring@IsString()YES名字
last_namestring@IsOptional(), @IsString()NO姓氏
usernamestring@IsOptional(), @IsString()NOTelegram Username
photo_urlstring@IsOptional(), @IsString()NO頭像 URL
auth_datenumber@IsInt()YES認證時間戳
hashstring@IsString()YESHMAC-SHA256 驗證 Hash
devicestring@IsOptional(), @IsString()NO裝置指紋

SetPasswordDto

檔案: src/modules/auth/dto/set-password.dto.ts

欄位型別驗證規則必填說明
passwordstring@IsString(), @MinLength(6)YES新密碼

SendEmailVerifyDto

檔案: src/modules/auth/dto/send-email-verify.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YES目標 Email

SendMobileVerifyDto

檔案: src/modules/auth/dto/send-mobile-verify.dto.ts

欄位型別驗證規則必填說明
mobilestring@IsString()YES手機號碼
countryCodestring@IsOptional(), @IsString()NO國碼

PhoneValidateDto

檔案: src/modules/auth/dto/phone-validate.dto.ts

欄位型別驗證規則必填說明
mobilestring@IsString()YES手機號碼
codestring@IsString()YES驗證碼

5.3.2 後台管理模組 DTO(10 個)

AdminLoginDto

檔案: src/modules/admin/dto/admin-login.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YES管理員 Email
passwordstring@IsString(), @MinLength(6)YES密碼

RegisterAdminDto

檔案: src/modules/admin/dto/register-admin.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YESEmail
passwordstring@IsString(), @MinLength(6)YES密碼
namestring@IsString()YES名稱
verifyCodestring@IsString()YES驗證碼

CreateAdminDto

檔案: src/modules/admin/dto/create-admin.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YESEmail
passwordstring@IsString(), @MinLength(6)YES密碼
namestring@IsString()YES名稱
groupIdnumber@IsOptional(), @IsInt()NO群組 ID
allowedSiteCodesstring[]@IsOptional(), @IsArray(), @IsString({ each: true })NO可管理站點代碼

UpdateAdminDto

檔案: src/modules/admin/dto/update-admin.dto.ts

  • 繼承 PartialType(CreateAdminDto),所有欄位均為選填

CreateAdminGroupDto

檔案: src/modules/admin/dto/create-admin-group.dto.ts

欄位型別驗證規則必填說明
namestring@IsString()YES群組名稱
typestring@IsString(), @IsIn([...])YES群組類型
permissionsstring[]@IsOptional(), @IsArray()NO權限列表
descriptionstring@IsOptional(), @IsString()NO說明

UpdateAdminGroupDto

  • 繼承 PartialType(CreateAdminGroupDto)

QueryAdminLogDto

檔案: src/modules/admin/dto/query-admin-log.dto.ts

欄位型別驗證規則必填說明
pagenumber@IsOptional(), @IsInt()NO頁碼
pageSizenumber@IsOptional(), @IsInt()NO每頁筆數
adminIdnumber@IsOptional(), @IsInt()NO管理員 ID 篩選
modulestring@IsOptional(), @IsString()NO模組篩選
actionstring@IsOptional(), @IsString()NO動作篩選
startDatestring@IsOptional(), @IsString()NO起始日期
endDatestring@IsOptional(), @IsString()NO結束日期

SendVerifyCodeDto

檔案: src/modules/admin/dto/send-verify-code.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YES目標 Email

VerifyEmailDto

檔案: src/modules/admin/dto/verify-email.dto.ts

欄位型別驗證規則必填說明
emailstring@IsEmail()YESEmail
codestring@IsString()YES驗證碼

GoogleAuthCodeDto

檔案: src/modules/admin/dto/google-auth-code.dto.ts

欄位型別驗證規則必填說明
codestring@IsString()YES6 位 TOTP 驗證碼

5.3.3 存款模組 DTO(1 個)

DepositDto

檔案: src/modules/deposit/dto/deposit.dto.ts

欄位型別驗證規則必填說明
channelIdnumber@IsInt()YES金流通道 ID
paymentMethodstring@IsString(), @IsIn(['fiat','credit','crypto'])YES支付方式
subOrderstring@IsString(), @MinLength(8), @MaxLength(24)YES商家訂單編號
orderAmountnumber@IsNumber({ maxDecimalPlaces: 6 }), @Min(0.000001)YES訂單金額 (USDT)
expectedCodestring@IsOptional(), @IsString()NOATM 銀行代碼
expectedAccountstring@IsOptional(), @IsString()NOATM 帳號
userCardLastValuestring@IsOptional(), @IsString()NO信用卡末五碼
productDesstring@IsOptional(), @IsString()NO商品描述
msgstring@IsOptional(), @IsString()NO備註
payerNamestring@IsOptional(), @IsString()NO付款人姓名
payerMobilestring@IsOptional(), @IsString()NO付款人手機
payerEmailstring@IsOptional(), @IsString()NO付款人 Email

5.3.4 VIP 模組 DTO(4 個)

CreateVipLevelDto

檔案: src/modules/vip/dto/create-vip-level.dto.ts

欄位型別驗證規則必填說明
levelnumber@IsInt(), @Min(1)YESVIP 等級編號
nameRecord<string, string>@Transform(JSON.parse), @IsObject()YES等級名稱(多語系)
tierstring@IsString(), @IsIn(['bronze','gold','platinum','diamond'])YES階級
minChipnumber@IsNumber(), @Min(0)YES升級所需最低籌碼
relegationChipnumber@IsNumber(), @Min(0)YES保級所需籌碼
sortOrdernumber@IsOptional(), @IsInt()NO排序權重
enablednumber@IsOptional(), @IsInt()NO啟用狀態

UpdateVipLevelDto

  • PartialType(CreateVipLevelDto) — 所有欄位選填

CreateVipRebateDto

檔案: src/modules/vip/dto/create-vip-rebate.dto.ts

欄位型別驗證規則必填說明
levelnumber@IsInt()YESVIP 等級
gameTypestring@IsString()YES遊戲類型
rebateRatenumber@IsNumber()YES返水比例 (%)

UpdateVipRebateDto

  • PartialType(CreateVipRebateDto) — 所有欄位選填

5.3.5 活動模組 DTO(2 個)

CreatePromoDto

檔案: src/modules/promo/dto/create-promo.dto.ts

欄位型別驗證規則必填說明
titleRecord<string, string>@Transform(JSON.parse), @IsObject()YES活動標題(多語系)
contentRecord<string, string>@Transform(JSON.parse), @IsObject()YES活動內容(多語系 HTML)
actionHtmlstring@IsOptional(), @IsString()NO渲染連結/按鈕 HTML
startTimestring@IsDateString()YES開始時間 (ISO 8601)
endTimestring@IsDateString()YES結束時間 (ISO 8601)
tagstring@IsString(), @MaxLength(30)YES活動標籤
enablednumber@IsOptional(), @Transform(Number), @IsInt()NO啟用狀態
conditionTypestring@IsString(), @IsIn([...])YES領取條件類型
conditionValuestring@IsOptional(), @IsString()NO條件門檻值
rewardAmountnumber@Transform(Number), @IsNumber()YES獎勵金額 (USD)
maxClaimsnumber@IsOptional(), @Transform(Number), @IsInt()NO最大領取數
turnoverMultipliernumber@IsOptional(), @Transform(Number), @IsNumber()NO打碼量倍數

UpdatePromoDto

  • PartialType(CreatePromoDto) — 所有欄位選填

5.3.6 代理推廣模組 DTO(12 個)

TrackClickDto

欄位型別必填說明
refCodestringYES推廣碼
referrerstringNO來源頁面

ApplyAgentDto

欄位型別必填說明
agentCodestringNO自訂推廣碼(3-20 碼,不填自動產生)

CreateAgentDto

欄位型別必填說明
userIdnumberYES用戶 ID
agentCodestringNO代理碼

QueryCommissionsDto / QuerySettlementsDto / QueryDownlineDto

  • 均為分頁+篩選 DTO,包含 page, pageSize, startDate, endDate 等篩選欄位

RequestWithdrawalDto(代理提款)

欄位型別必填說明
amountnumberYES提款金額
methodstringYES提款方式 (crypto/bank)
bankCardIdnumberNO銀行卡 ID
cryptoAddressIdnumberNO加密錢包 ID

ReviewSettlementDto

欄位型別必填說明
actionstringYES動作 (approve/reject)
rejectReasonstringNO拒絕原因

ReviewWithdrawalDto

欄位型別必填說明
actionstringYES動作 (approve/reject)
rejectReasonstringNO拒絕原因

AdminBindDto

欄位型別必填說明
memberIdnumberYES會員 ID
agentIdnumberYES代理 ID
remarkstringNO備註

CreateReferralCodeDto

欄位型別必填說明
codestringYES推廣碼(3-30 英數字,@Matches(/^[a-zA-Z0-9]+$/))
labelstringNO渠道標籤(最大 50 碼)

SetAgentTierDto

欄位型別必填說明
agentIdnumberYES代理 ID
tierCodestringYES等級代碼

UpsertCommissionRateDto / UpsertVipMilestoneDto / UpsertAgentTierDto

  • 批次新增/更新用 DTO,用於「帶入模板」功能

5.3.7 錢包模組 DTO(3 個)

AddBankCardDto

欄位型別必填說明
bankCodestringYES銀行代碼
bankAccountstringYES銀行帳號
branchstringYES分行名稱
holderNamestringYES持卡人姓名

AddCreditCardDto

欄位型別必填說明
cardNumberstringYES卡號
holderNamestringYES持卡人姓名
cvvstringYESCVV
expiryDatestringYES有效期 (MM/YY)

AddCryptoAddressDto

欄位型別必填說明
walletNamestringYES錢包名稱
currencystringNO幣種(預設 USDT)
networkstringNO網路(預設 TRC-20)
addressstringYES錢包地址

5.3.8 金流模組 DTO(4 個)

AddAtmDto(萬通 ATM)

欄位型別必填說明
subOrderstringYES訂單編號
orderAmountnumberYES金額
expectedCodestringYES銀行代碼
expectedAccountstringYES帳號

AddCardDto(萬通信用卡)

欄位型別必填說明
subOrderstringYES訂單編號
orderAmountnumberYES金額
userCardLastValuestringYES卡號末五碼

WantongCallbackDto / UsdtCallbackDto

  • S2S 回調用 DTO,接收金流商回調參數

5.3.9 站點設定模組 DTO(4 個)

CreateSiteConfigDto

欄位型別必填說明
siteCodestringYES站點代碼(唯一,最大 30 碼)
prefixstringYES白牌前綴(唯一)
layoutstringNO前台模板代碼(預設 a1)
siteNameRecord<string, string>YES站點名稱(多語系)
siteDescriptionRecord<string, string>NO站點介紹(多語系)
supportedLocalesstring[]NO支援語系

UpdateSiteConfigDto

  • PartialType(CreateSiteConfigDto) — 所有欄位選填

CreateSiteThemeDto

欄位型別必填說明
themeIdstringYES主題識別碼(最大 50 碼)
themeNameRecord<string, string>YES主題名稱(多語系)
primaryRecord<string, string>YES主色系 (base/dark/light/glow)
accentRecord<string, string>YES強調色
surfaceRecord<string, any>YES表面色
textRecord<string, string>YES文字色
borderRecord<string, string>YES邊框色
enablednumberNO啟用狀態

UpdateSiteThemeDto

  • PartialType(CreateSiteThemeDto) — 所有欄位選填

5.3.10 站內信模組 DTO(1 個)

AdminSendNotificationDto

欄位型別必填說明
userIdnumberNO目標用戶 ID(不傳=全站廣播)
titleRecord<string, string>YES通知標題(多語系)
contentRecord<string, string>YES通知內容(多語系 HTML)
categorystringYES分類 (system / promo)

5.3.11 提領模組 DTO(2 個)

RequestWithdrawalDto(前台提領)

欄位型別必填說明
amountnumberYES提領金額 (USD)
cryptoAddressIdnumberYES加密錢包 ID
verifyCodestringYES郵箱驗證碼(6 碼)

ReviewWithdrawalDto(Admin 審核)

欄位型別必填說明
actionstringYES動作 (approve / reject)
rejectReasonstringNO拒絕原因

5.3.12 R2 模組 DTO(1 個)

QueryR2LogDto

欄位型別必填說明
pagenumberNO頁碼
pageSizenumberNO每頁筆數
actionstringNO操作類型篩選
adminIdnumberNO管理員 ID 篩選
keywordstringNO關鍵字搜尋
startDatestringNO起始日期
endDatestringNO結束日期

5.4 完整 Service 文件

本章節記錄各模組 Service 的核心方法、函式簽名、業務邏輯與資料庫查詢模式。

5.4.1 認證服務(AuthService)

檔案: src/modules/auth/auth.service.ts注入依賴: userRep, loginLogRep, config, jwtSv, i18n, cache, affiliateSv, siteConfigSv, vendorGroupRep, vendorGroupChannelRep

核心方法

方法簽名說明
register()(dto: RegisterDto, ip?, device?, siteName?) → Promise<{ accessToken, user }>用戶註冊。建立用戶 → 自動分配金流群組 → 綁定推廣碼上線 → 記錄登入紀錄 → 簽發 JWT
login()(dto: LoginDto, ip?, device?, siteName?) → Promise<{ accessToken, user }>用戶登入。驗證帳密 → 檢查 status → 2FA 檢查 → 更新 lastLoginIp/At → 記錄紀錄 → 簽發 JWT
loginGoogle()(idToken, device?, ip?, siteName?) → Promise<{ accessToken, user }>Google OAuth 登入。驗證 ID Token → 查找/建立用戶 → 自動綁定
loginTelegram()(dto, ip?, device?, siteName?) → Promise<{ accessToken, user }>Telegram 登入。HMAC 驗證 → 查找/建立用戶
getUserDetail()(userId: number) → Promise<AuthUser>取得用戶完整資料
updateProfile()(userId, body) → Promise<void>更新個人資料(name, email, mobile)
changePassword()(userId, oldPw, newPw) → Promise<void>變更密碼(驗證舊密碼 → bcrypt 加密新密碼 → 遞增 tokenVersion)
setPassword()(userId, password) → Promise<void>首次設定密碼(OAuth 用戶無密碼時使用)
sendEmailVerify()(userId, email) → Promise<void>發送 Email 驗證碼(Resend API)
verifyEmail()(userId, code) → Promise<void>驗證 Email(比對驗證碼 → 更新 emailVerified)
uploadAvatar()(userId, file, siteName) → Promise<{ url }>上傳頭像至 R2(sharp 壓縮至 200x200 webp)

業務邏輯重點

金流群組自動分配:

語系 → 幣別對應:zh-TW→TWD, en-US→USD, zh-CN→CNY, th-TH→THB, vi-VN→VND
查詢 vendor-group-channel → vendor-channel.currency 匹配幣別
分配第一個匹配的 vendor-group

Token Version 機制:

  • DB 中 tokenVersion 欄位,JWT payload 包含此值
  • 變更密碼 / 管理員停用帳號時遞增 tokenVersion
  • JWT Strategy 驗證時比對,不一致則拒絕(強制登出效果)
  • Cache TTL 60s,避免每次都查 DB

5.4.2 管理員服務(AdminService)

檔案: src/modules/admin/admin.service.ts注入依賴: adminUserRep, adminGroupRep, operationLogRep, config, jwtSv, i18n, cache, userRep, depositOrderRep, bankCardSv, creditCardSv, cryptoAddressSv, withdrawalSv, vendorSv

核心方法

方法簽名說明
login()(dto: AdminLoginDto) → { accessToken, admin }管理員登入(支援 2FA)
register()(dto) → { accessToken, admin }管理員註冊(驗證碼驗證)
ensureDefaultAdmin()() → Promise<void>OnModuleInit:啟動時確保預設管理員存在
listAdmins()(query) → { items, pagination }管理員列表(含群組資訊)
createAdmin()(dto, operatorId, req) → AdminUser新增管理員 + 記錄操作紀錄
updateAdmin()(id, dto, operatorId, req) → AdminUser更新管理員 + 記錄操作紀錄
deleteAdmin()(id, operatorId, req) → void刪除管理員 + 記錄操作紀錄
listGroups()() → AdminGroup[]群組列表
createGroup()(dto, operatorId, req) → AdminGroup新增群組
updateGroup()(id, dto, operatorId, req) → AdminGroup更新群組
deleteGroup()(id, operatorId, req) → void刪除群組(檢查是否有管理員)
queryLogs()(query) → { items, pagination }查詢操作紀錄
listDepositReview()(query) → { items, pagination }存款審核列表
reviewDeposit()(id, action, reason?, reviewer?) → void審核存款
listAuthUsers()(query) → { items, pagination }前台用戶列表
getUserDetail()(id, siteCode?) → AuthUser用戶詳情
adjustBalance()(body, operatorId, req) → void人工調節餘額 + 記錄操作紀錄
logOperation()(data) → void記錄操作紀錄

操作紀錄記錄模式

所有寫入操作(create/update/delete/review)統一透過 logOperation() 記錄至 admin-operation-log 表:

typescript
await this.logOperation({
  adminId: operatorId,
  module: 'admin',
  action: 'create',
  targetId: String(newAdmin.id),
  ip: req?.ip,
  userAgent: req?.headers?.['user-agent'],
  method: req?.method,
  path: req?.originalUrl,
  detail: { email: dto.email, name: dto.name },
  summary: `建立管理員: ${dto.email}`,
});

5.4.3 管理員報表服務(AdminReportService)

檔案: src/modules/admin/admin-report.service.ts

核心方法

方法簽名說明
listPlayers()(query) → { items, pagination }玩家報表(25+ 篩選參數:關鍵字、VIP 等級/比較、餘額比較、投注比較、代理篩選、日期範圍)
listVipPlayers()(query) → { items, pagination }VIP 玩家報表(等級範圍、tier、保級狀態、投注範圍)
listBetRecords()(query) → { items, pagination }投注紀錄報表(含 JOIN 用戶帳號/名稱)
getOverview()(query) → { summary, dailySummary[] }總體報表(統計卡片 + 每日摘要)
getProfitLoss()(query) → { items }損益報表(依 day/week/month 分組)
listGameStats()(query) → { items }遊戲報表(依遊戲類型/平台分組)
listPromoStats()(query) → { items, pagination }優惠報表(活動領取統計)
listPlayerSummary()(query) → { items, pagination }玩家簡表(精簡用戶列表,支援排序)
exportReport()(type, query) → string匯出 CSV(支援 9 種報表類型)

查詢模式

報表查詢使用 TypeORM QueryBuilder,結合多個工具函式:

typescript
const qb = this.userRep.createQueryBuilder('u');
// 分頁
const { skip, take } = parsePagination(query);
qb.skip(skip).take(take);
// 日期範圍
applyDateRange(qb, 'u.createdAt', query.startDate, query.endDate);
// siteCode 篩選
if (query.siteCode) qb.andWhere('u.siteCode = :siteCode', { siteCode: query.siteCode });
// 數值比較篩選
if (query.balanceOp && query.balanceVal) {
  const op = { gt: '>', gte: '>=', eq: '=', lte: '<=', lt: '<' }[query.balanceOp];
  qb.andWhere(`u.balance ${op} :balanceVal`, { balanceVal: query.balanceVal });
}

5.4.4 管理員風控服務(AdminRiskService)

檔案: src/modules/admin/admin-risk.service.ts

核心方法

方法簽名說明
listIpRules()(query) → { items, pagination }IP 規則列表(含 type/keyword/siteCode 篩選)
createIpRule()(ip, type, remark, siteCode) → RiskIpRule新增 IP 規則
updateIpRule()(id, ip, type, remark) → RiskIpRule更新 IP 規則
deleteIpRule()(id) → void刪除 IP 規則
listLoginFailures()(query) → { items, pagination }登入失敗紀錄(篩選 action='LOGIN_FAIL')
lookupByIpOrDevice()(query) → { items, pagination }IP/FP 反查用戶
listGameBlacklist()(query) → { items, pagination }遊戲黑名單列表
createGameBlacklist()(userId, gameType?, productId?, remark, siteCode) → RiskGameBlacklist新增遊戲黑名單
deleteGameBlacklist()(id) → void刪除遊戲黑名單
isUserBlockedFromGame()(userId, gameType?, productId?, siteCode?) → boolean核心方法:檢查用戶是否被封鎖遊戲(被 GameService 呼叫)

isUserBlockedFromGame() 封鎖檢查邏輯:

  1. 查詢 risk-game-blacklist
  2. 條件:userId 匹配 AND(gameType IS NULL OR gameType 匹配)AND(productId IS NULL OR productId 匹配)
  3. 若有匹配記錄則返回 true(封鎖)

5.4.5 遊戲服務(GameService)

檔案: src/modules/game/game.service.ts

核心方法

方法簽名說明
getProviders()(gameType?, siteCode?) → GameProvider[]遊戲供應商列表(Cache-Aside,1hr TTL)
launchGame()(userId, gameCode, productId?) → { url }啟動遊戲(檢查黑名單 → 取得 URL → 記錄遊玩)
simulateGame()(userId, gameCode, ...) → result模擬遊戲結果(開發測試用)
demoGame()(gameCode, productId?) → { url }試玩模式
getRecentGames()(userId) → GamePlayLog[]最近遊玩列表
recordGamePlay()(userId, gameCode, productId?) → voidUPSERT game-play-log
afterBetSettlement()(userId, betOrder) → void投注結算後連鎖觸發
adminListProviders()(siteCode?) → GameProvider[]Admin 供應商列表
adminCreateProvider()(dto, siteCode?) → GameProviderAdmin 新增供應商
copyGameSiteData()(source, target, type) → void跨站複製(transaction 內先刪後插)
loadTemplate()(siteCode?) → void帶入模板

投注結算後連鎖觸發

typescript
async afterBetSettlement(userId: number, betOrder: BetOrder) {
  // 1. VIP 等級重算(只升不降)
  await this.vipSv.recalculateUserVip(userId);
  // 2. 活動打碼量累計
  await this.promoSv.updatePromoTurnover(userId, betOrder.betEffective);
  // 3. 任務投注進度
  await this.missionSv.updateBetProgress(userId, betOrder.betEffective);
}

5.4.6 VIP 服務(VipService)

檔案: src/modules/vip/vip.service.ts

核心方法

方法簽名說明
getLevels()(siteCode?) → VipLevel[]VIP 等級列表(Cache-Aside,1hr TTL)
getRebates()(siteCode?) → VipRebate[]返水規則列表(Cache-Aside,1hr TTL)
getUserVipStatus()(userId, siteCode?) → { level, tier, ... }用戶 VIP 狀態
recalculateUserVip()(userId) → void重算 VIP 等級(只升不降)
dailyRebateSettlement()() → voidCron: 每日反水結算
monthlyRelegationCheck()() → voidCron: 月度保級檢查
bulkUpsertRebates()(rebates[], siteCode?) → void批次新增/更新返水規則
copySiteData()(source, target, type) → void跨站複製
loadTemplate()(siteCode?) → void帶入模板
triggerSettlement()(siteCode?) → void手動觸發反水結算

VIP 等級重算邏輯

1. 查詢用戶 totalEffectiveBet
2. 查詢該站所有 VIP 等級(按 minChip DESC 排序)
3. 找到第一個 minChip <= totalEffectiveBet 的等級
4. 若新等級 > 當前等級:更新 vipLevel
5. 若新等級 <= 當前等級:不變(只升不降)
6. VIP 升級後觸發里程碑獎勵檢查

5.4.7 代理結算服務(AffiliateSettlementService)

檔案: src/modules/affiliate/affiliate-settlement.service.ts

核心方法

方法簽名說明
weeklySettlement()() → voidCron: 每週一 03:00 佣金週結
dailySettlement()() → voidCron: 每日 03:30 佣金日結
triggerSettlement()() → void手動觸發週結
triggerDailySettlement()() → void手動觸發日結

週結算邏輯

1. 計算上週日期範圍(週一 ~ 週日)
2. 查詢所有代理(isAgent=1)
3. 對每個代理:
   a. 查詢下線的投注紀錄(按遊戲類型分組)
   b. 計算淨輸金額 = |winLose|(取絕對值,僅計入用戶輸的部分)
   c. 根據代理等級 × 遊戲類型查詢佣金費率
   d. 計算佣金 = netLoss × commissionRate
   e. 建立 affiliate-commission 紀錄
   f. 建立/更新 affiliate-settlement 紀錄
4. 執行風控檢測(AffiliateRiskService)
5. 若風控通過:自動核准 → 發放至代理餘額
6. 若風控標記:設為 pending → 等待人工審核

5.4.8 代理風控服務(AffiliateRiskService)

檔案: src/modules/affiliate/affiliate-risk.service.ts

風控檢測項目

檢測項目說明
異常 IP 登入下線與代理使用相同 IP 登入
異常裝置下線與代理使用相同裝置指紋
低活躍度下線投注金額過低但佣金過高
快速註冊短時間內大量下線註冊

5.4.9 聯盟服務(AllianceService)

檔案: src/modules/affiliate/alliance.service.ts

核心方法

方法簽名說明
getCommissionRates()() → AllianceCommissionRate[]佣金費率列表
upsertCommissionRate()(dto) → void新增/更新佣金費率
getAgentTiers()() → AllianceAgentTier[]代理等級列表
upsertAgentTier()(dto) → void新增/更新代理等級
getVipMilestones()() → AllianceVipMilestone[]VIP 里程碑列表
upsertVipMilestone()(dto) → void新增/更新 VIP 里程碑
getReferralCodes()(agentId) → AllianceReferralCode[]推廣碼列表
createReferralCode()(agentId, dto) → AllianceReferralCode新增推廣碼(最多 10 個)
loadTemplate()(siteCode?) → void帶入模板(atomic transaction)
previewTemplate()() → { commissionRates, agentTiers, vipMilestones }預覽模板

5.4.10 站點設定服務(SiteConfigService)

檔案: src/modules/site-config/site-config.service.ts

核心方法

方法簽名說明
getPublicConfig()(siteName?) → SiteConfig取得公開站點配置(含主題、客服、learnMore)
listConfigs()() → SiteConfig[]站點列表(含主題 eager load)
createConfig()(dto) → SiteConfig新增站點
updateConfig()(id, dto) → SiteConfig更新站點(支援巢狀 JSON 更新)
deleteConfig()(id) → void刪除站點(cascade 刪除主題)
listThemes()(siteConfigId) → SiteTheme[]主題列表
createTheme()(siteConfigId, dto) → SiteTheme新增主題
updateTheme()(id, dto) → SiteTheme更新主題
deleteTheme()(id) → void刪除主題(若為啟用中則自動清除 activeThemeId)
uploadDomainAsset()(id, type, file) → { url }上傳域名素材(logoSmall/logoBig/favicon)
uploadCustomerServiceIcon()(id, file) → { url }上傳客服管道圖示

5.4.11 共用服務(CommonService)

檔案: src/modules/common/common.service.ts

OnModuleInit 行為

啟動時遞迴掃描 src/i18n/zh-TW/*.json,找出所有數字 key(如 "2001": "帳號已存在")建構錯誤碼映射表。

getEnums() 回傳結構

typescript
{
  ERROR_CODES: {
    '/api/auth/register': { '2001': '帳號已存在', '2002': '推廣碼不存在' },
    '/api/auth/login': { '2001': '帳號或密碼錯誤', '2002': '帳號已停用' },
    // ... 32+ API 路徑的錯誤碼
  },
  GAME_TYPES: { 1: 'sports', 2: 'slot', ... },
  BET_ORDER_STATUS: ['valid', 'invalid', 'cancelled'],
}

5.4.12 R2 服務(R2Service)

檔案: src/modules/r2/r2.service.ts裝飾器: @Global() — 全域可用

核心方法

方法簽名說明
upload()(buffer, key, contentType, siteName?) → { key, url, size }上傳檔案至 R2
uploadToPath()(buffer, path, contentType, siteConfigId?) → { key, url, size }依站點配置上傳
deleteObjects()(keys[], siteConfigId?) → { deleted }批次刪除
listWithFolders()(prefix, token?, maxKeys?, siteConfigId?) → { files, folders, nextToken }列表(含資料夾)
moveObject()(source, dest, siteConfigId?) → void移動檔案
moveFolder()(source, dest, siteConfigId?) → void移動資料夾
createFolder()(prefix, name, siteConfigId?) → { key }建立資料夾
deleteFolder()(prefix, siteConfigId?) → { deleted }遞迴刪除資料夾
logOperation()(data) → void記錄操作紀錄至 r2-operation-log
queryLogs()(query) → { items, pagination }查詢操作紀錄

5.4.13 工具函式

truncateUsd(value: number): number

檔案: src/utils/decimal.ts

USD 金額無條件捨去至 6 位小數:

typescript
Math.floor(value * 1e6) / 1e6

parsePagination(query: any): { skip: number, take: number }

檔案: src/utils/pagination.ts

正規化分頁參數:

typescript
const page = Math.max(1, Number(query.page) || 1);
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize) || 20));
return { skip: (page - 1) * pageSize, take: pageSize };

applyDateRange(qb, column, startDate?, endDate?)

檔案: src/utils/query-helpers.ts

為 QueryBuilder 加入日期區間篩選:

typescript
if (startDate) qb.andWhere(`${column} >= :startDate`, { startDate: `${startDate} 00:00:00` });
if (endDate) qb.andWhere(`${column} <= :endDate`, { endDate: `${endDate} 23:59:59` });

resolveText(json: Record<string, string> | string): string

檔案: src/utils/i18n.ts

解析多語系 JSON 欄位,優先順序:當前語系 → zh-TW fallback → 第一個值 → 空字串。

toNum(value: any): number

檔案: src/utils/helper.ts

安全數字轉換,非數字回傳 0。


5.5 自動排程 (Cron Jobs)

系統共有 5 個自動排程任務,使用 @nestjs/schedule@Cron() 裝飾器宣告。所有排程邏輯分散在 VipServiceAffiliateSettlementServiceLiveSportsService 三個 Service 中。


5.5.1 每日反水結算 (Daily Rebate Settlement)

項目說明
Cron 表達式0 5 0 * * *
執行時間每日 00:05
所在 ServiceVipService (src/modules/vip/vip.service.ts)
方法名稱handleDailyRebateCron()settleDailyRebate()
影響資料表auth-user (balance)、vip-rebate-log (新增紀錄)
多站點按用戶的 siteCode 自動匹配對應站點的反水規則

完整流程

handleDailyRebateCron()

  ├─ 1. buildDateRange(targetDate?)
  │     - 預設為昨日 (Date.now() - 86400000)
  │     - 產出 dayStart = "YYYY-MM-DD 00:00:00"
  │     - 產出 dayEnd = "YYYY-MM-DD 23:59:59"
  │     - 產出 settleDate = "YYYY-MM-DD"

  ├─ 2. fetchUserBetsByDay(dayStart, dayEnd)
  │     - 查詢 bet-order 表
  │     - 條件: betDatetime BETWEEN dayStart AND dayEnd, status = 'valid'
  │     - GROUP BY userId, gameType
  │     - SELECT: userId, gameType, SUM(betEffective) AS dailyEffective
  │     - 回傳 Map<userId, { gameType, dailyEffective }[]>

  ├─ 3. buildRebateMap()
  │     - 查詢所有 vip-rebate 記錄
  │     - 建立 Map<"siteCode-level-gameType", rebateRate>
  │     - 例如: "C9-3-slot" → 0.56 (%)

  ├─ 4. 批次載入用戶
  │     - userRep.find({ where: { id: In(userIds) } })
  │     - 取得 vipLevel + siteCode

  ├─ 5. 逐用戶計算反水 calculateUserRebate()
  │     - 遍歷用戶的每筆 gameType 投注
  │     - bet-order.gameType 為數字 ("2"),需透過 GAME_TYPE_LABELS 轉換為標籤 ("slot")
  │     - 查找 rebateMap["siteCode-level-gameTypeLabel"]
  │     - 反水金額 = truncateUsd(dailyEffective * rate / 100)
  │     - 產生 VipRebateLog 紀錄

  ├─ 6. 發放反水
  │     - UPDATE auth-user SET balance = balance + rebateAmount WHERE id = userId
  │     - INSERT INTO vip-rebate-log (紀錄每筆反水明細)

  └─ 7. 回傳統計
       - usersProcessed: 處理的用戶數
       - totalRebate: 總發放金額 (USD)

反水金額計算公式

反水金額 = Math.floor(dailyEffective × (rebateRate / 100) × 1e6) / 1e6
  • dailyEffective:用戶昨日該遊戲類型的有效投注總額
  • rebateRate:該站點 VIP 等級對應遊戲類型的反水比率 (%)
  • 結果使用 truncateUsd() 無條件捨去至小數 6 位

反水規則對照表 (預設模板)

VIP 等級sportsslotlivelotterychessesportscryptofish
1 (青銅 I)0.20%0.50%0.50%0.50%0.50%0.30%0.50%0.50%
2 (青銅 II)0.25%0.55%0.53%0.53%0.53%0.33%0.53%0.53%
3 (青銅 III)0.30%0.60%0.56%0.56%0.56%0.36%0.56%0.56%
...........................
15 (鑽石 III)0.90%1.50%1.00%1.10%1.10%0.80%1.10%1.10%

VipRebateLog 紀錄欄位

欄位範例值說明
userId42用戶 ID
settleDate"2026-03-01"結算日期 (昨日)
vipLevel5結算時的 VIP 等級
gameType"slot"遊戲類型標籤
dailyEffective"1500.000000"昨日有效投注
rebateRate"0.70"反水比率 (%)
rebateAmount"10.500000"反水金額
siteCode"C9"所屬站點

5.5.2 月度保級檢查 (Monthly Relegation Check)

項目說明
Cron 表達式0 0 1 1 * *
執行時間每月 1 號 01:00
所在 ServiceVipService (src/modules/vip/vip.service.ts)
方法名稱handleMonthlyRelegationCron()checkMonthlyRelegation()
影響資料表auth-user (vipLevel, relegationMissCount)
多站點查詢所有站點的用戶,各站等級配置獨立

完整流程

handleMonthlyRelegationCron()

  ├─ 1. 載入所有啟用的 VIP 等級配置
  │     - levelRep.find({ where: { enabled: 1 }, order: { level: 'ASC' } })
  │     - 建立 Map<level, VipLevel>

  ├─ 2. 計算上月時間範圍
  │     - lastMonthStart = 上月 1 號 00:00:00
  │     - lastMonthEnd = 上月最後一天 23:59:59

  ├─ 3. 查詢 VIP 2+ 用戶
  │     - WHERE CAST(vipLevel AS UNSIGNED) >= 2
  │     - (VIP 1 級無保級需求)

  ├─ 4. 逐用戶檢查保級
  │     │
  │     ├─ VIP 5+ 且 vipHold = 1 → 跳過 (保級鎖定)
  │     │
  │     ├─ 查詢上月有效投注
  │     │   - SUM(betEffective) WHERE betDatetime BETWEEN lastMonthStart AND lastMonthEnd
  │     │
  │     ├─ monthlyEffective >= relegationChip → 達標
  │     │   - 若 relegationMissCount > 0 → 重置為 0
  │     │
  │     └─ monthlyEffective < relegationChip → 未達標
  │         │
  │         ├─ newMissCount < 2 → 第 1 月警告
  │         │   - UPDATE relegationMissCount = newMissCount
  │         │   - warned++
  │         │
  │         └─ newMissCount >= 2 → 連續 2 月未達,降級
  │             - newLevel = MAX(currentLevel - 1, 1)
  │             - UPDATE vipLevel = newLevel, relegationMissCount = 0
  │             - demoted++

  └─ 5. 回傳統計
       - checked: 檢查的用戶數
       - warned: 第 1 月未達標 (警告) 的用戶數
       - demoted: 降級的用戶數

保級規則

規則說明
VIP 1不參與保級檢查
VIP 2-4上月有效投注 < relegationChip 則 missCount+1;連續 2 月降 1 級
VIP 5+支援保級鎖定 (vipHold=1);鎖定時跳過保級檢查
降級下限最低降至 VIP 1 (Math.max(currentLevel - 1, 1))
達標後重置 relegationMissCount 為 0

保級門檻 (預設模板)

VIP 等級保級門檻 (relegationChip, USD)
1 (青銅 I)0 (無需保級)
2 (青銅 II)200
3 (青銅 III)500
4 (青銅 IV)2,000
5 (青銅 V)5,000
6 (青銅 VI)20,000
7 (黃金 I)58,000
8 (黃金 II)150,000
9 (黃金 III)250,000
10 (白金 I)500,000
11 (白金 II)1,500,000
12 (白金 III)2,500,000
13 (鑽石 I)5,000,000
14 (鑽石 II)12,500,000
15 (鑽石 III)25,000,000

5.5.3 代理佣金週結 (Weekly Commission Settlement)

項目說明
Cron 表達式0 0 3 * * 1
執行時間每週一 03:00
所在 ServiceAffiliateSettlementService (src/modules/affiliate/affiliate-settlement.service.ts)
方法名稱handleWeeklySettlementCron()settleWeek()settleRange()
影響資料表affiliate-settlementaffiliate-commissionaffiliate-balanceaffiliate-risk-log
多站點按代理的 siteCode 自動區分

完整流程

handleWeeklySettlementCron()

  ├─ 1. getLastWeekRange(refDate)
  │     - 計算上週一 ~ 上週日的日期範圍
  │     - weekStart = "YYYY-MM-DD" (上週一)
  │     - weekEnd = "YYYY-MM-DD" (上週日)

  ├─ 2. settleRange(weekStartStr, weekEndStr, 'weekly')
  │     │
  │     ├─ 冪等檢查
  │     │   - 查詢 settlement 表是否已有該 weekStart + periodType
  │     │   - 已存在 → 跳過 ("already settled, skipping")
  │     │
  │     ├─ 載入佣金比例 rateMap
  │     │   - allianceSv.buildRateMap()
  │     │   - Map<"agentTier-agentLevel-gameType", rate>
  │     │   - 例如: "gold-1-slot" → 38.00 (%)
  │     │
  │     ├─ 查詢期間所有 valid 注單
  │     │   - betDatetime BETWEEN weekStart AND weekEnd
  │     │   - status = 'valid'
  │     │   - SELECT: id, userId, winLose, gameType, status, invalidReason
  │     │
  │     ├─ 查詢會員的代理綁定
  │     │   - 載入 level1AgentId, level2AgentId, level3AgentId
  │     │   - 建立 memberMap<userId, AuthUser>
  │     │
  │     ├─ Pre-load 代理的 agentTier
  │     │   - 從 affiliate-balance 表取得各代理的 agentTier
  │     │   - agentTierMap<agentId, tierCode>
  │     │
  │     ├─ 三層佣金計算
  │     │   - 遍歷所有注單
  │     │   - netLoss = MAX(0, -winLose)  (玩家虧損 = 代理收益)
  │     │   - 排除自投注 (agentId === order.userId)
  │     │   - 排除 invalidReason 不為空的注單
  │     │   │
  │     │   ├─ Level 1 (直屬代理)
  │     │   │   rate = getRate(agentId, 1, gameTypeLabel)
  │     │   │   amount = truncateUsd(netLoss × rate / 100)
  │     │   │
  │     │   ├─ Level 2 (上級代理)
  │     │   │   rate = getRate(agentId, 2, gameTypeLabel)
  │     │   │   amount = truncateUsd(netLoss × rate / 100)
  │     │   │
  │     │   └─ Level 3 (頂級代理)
  │     │       rate = getRate(agentId, 3, gameTypeLabel)
  │     │       amount = truncateUsd(netLoss × rate / 100)
  │     │
  │     ├─ 建立結算紀錄 (每個代理一筆)
  │     │   - affiliateSettlement: {
  │     │       agentId, weekStart, weekEnd,
  │     │       activeMemberCount,
  │     │       totalNetLoss,
  │     │       level1Commission, level2Commission, level3Commission,
  │     │       totalCommission,
  │     │       gameTypeBreakdown (JSON),
  │     │       periodType: 'weekly',
  │     │       status: 'pending'
  │     │   }
  │     │
  │     ├─ 寫入佣金明細 (每筆注單一筆)
  │     │   - affiliateCommission: {
  │     │       agentId, memberId, betOrderId,
  │     │       agentLevel, gameType,
  │     │       netLoss, commissionRate, commissionAmount,
  │     │       weekStart, weekEnd, settlementId
  │     │   }
  │     │
  │     ├─ 風控檢測
  │     │   - riskSv.checkSettlementRisk(settlement, memberIds)
  │     │   - 檢查異常下線行為
  │     │   - flagged → flaggedCount++
  │     │
  │     └─ 回傳統計
  │         agentsProcessed, totalCommission, flaggedCount

  └─ 注意: status = 'pending',需後台管理員手動審核 (approve/reject)
       - approve → 佣金入帳到 affiliate-balance.available
       - reject → 不入帳

佣金比例查找邏輯 getRate()

typescript
// 優先順序:
// 1. 精確匹配: agentTier-agentLevel-gameTypeLabel (如 "gold-1-slot")
// 2. 通用匹配: agentTier-agentLevel-* (如 "gold-1-*")
// 3. 無匹配: 0
const getRate = (agentId, agentLevel, gameTypeLabel) => {
  const agentTier = agentTierMap.get(agentId) ?? 'bronze';
  return rateMap.get(`${agentTier}-${agentLevel}-${gameTypeLabel}`)
      ?? rateMap.get(`${agentTier}-${agentLevel}-*`)
      ?? 0;
};

佣金審核三階段流程

pending (排程自動建立)
  ↓ approve
approved (管理員審核通過 → 佣金入帳到 affiliate-balance)
  
pending
  ↓ reject
rejected (管理員駁回 → 不入帳)

5.5.4 代理佣金日結 (Daily Commission Settlement)

項目說明
Cron 表達式0 30 3 * * *
執行時間每日 03:30
所在 ServiceAffiliateSettlementService (src/modules/affiliate/affiliate-settlement.service.ts)
方法名稱handleDailySettlementCron()settleDay()settleRange()
影響資料表affiliate-settlementaffiliate-commissionaffiliate-balanceaffiliate-risk-log
多站點同週結

流程

與週結共用 settleRange() 核心邏輯,差異如下:

差異點週結日結
periodType'weekly''daily'
日期範圍上週一 ~ 上週日昨日一天
冪等 keyweekStart + 'weekly'dayStr + 'daily'
執行時間每週一 03:00每日 03:30

日結時間計算

typescript
async settleDay(targetDate?: Date) {
  const ref = targetDate || new Date();
  const yesterday = new Date(ref.getTime() - 86400000);
  const dayStr = this.formatDate(yesterday);  // "YYYY-MM-DD"
  return this.settleRange(dayStr, dayStr, 'daily');
  // startStr === endStr 表示只結算昨日一天
}

5.5.5 即時賽事快取更新 (Live Sports Cache Refresh)

項目說明
Cron 表達式0 */30 * * * *
執行時間每 30 分鐘
所在 ServiceLiveSportsService (src/modules/live-sports/live-sports.service.ts)
方法名稱refreshCache()
影響資料表無 (僅寫入 Redis 快取)
外部 APIAPI-Football (v3.football.api-sports.io)

完整流程

refreshCache()

  ├─ 0. 檢查 apiKey
  │     - 若 LIVE_SPORTS_API_KEY 未設定 → 跳過

  ├─ 1. fetchFixtures()
  │     - 同時呼叫兩個 API:
  │       (a) GET /fixtures?live=all        → 進行中的比賽
  │       (b) GET /fixtures?date=YYYY-MM-DD&status=NS  → 今日尚未開始的比賽
  │     - 使用 Promise.allSettled 避免單一 API 失敗影響整體
  │     - 合併去重 (by fixture.id)
  │     - 排序: 進行中比賽優先 → 按開賽時間排序
  │     - 進行中狀態: 1H, 2H, HT, ET, BT, P

  ├─ 2. fetchOddsForFixtures(fixtureIds)
  │     - 配額感知: remainingQuota < 10 → 跳過
  │     - 最多取前 5 場比賽的賠率 (節省配額)
  │     - GET /odds?fixture={id}&bookmaker=1&bet=1
  │     - 解析主場/和局/客場賠率

  ├─ 3. buildCachedItems(fixtures, oddsMap)
  │     - 取前 20 場比賽
  │     - 建立語言無關的 CachedItem[]
  │     - 包含: fixtureId, kickoffAt, status, league, home, away, odds

  ├─ 4. 寫入 Redis 快取
  │     - cache.set('live_sports:banner', items, 35min)
  │     - TTL 35 分鐘 (略長於 Cron 間隔,確保不會快取空窗)

  └─ 5. 錯誤處理
       - 失敗時不清除既有快取 → 繼續提供過時資料
       - console.error 記錄錯誤

讀取快取 (getBannerItems)

getBannerItems(lang)

  ├─ 1. 查詢 Redis 快取 → 命中則 applyI18n() 翻譯後回傳

  └─ 2. Cache miss (冷啟動)
       - 若有 apiKey → 同步執行 refreshCache()
       - 再次查詢快取 → applyI18n() 回傳

i18n 翻譯

  • 比賽狀態使用 liveSports.status.{statusShort} i18n key 翻譯
  • 運動類型使用 liveSports.sport i18n key
  • 若翻譯 key 不存在 → fallback 為原始英文 statusLong

API 配額管理

項目說明
配額追蹤從回應 header x-ratelimit-requests-remaining 讀取
低配額保護remainingQuota < 10 → 跳過賠率查詢
賠率限制每次最多查 5 場比賽的賠率
初始值remainingQuota = 100

5.5.6 排程總覽表

#排程名稱Cron時間Service方法
1每日反水結算0 5 0 * * *每日 00:05VipServicehandleDailyRebateCron()
2月度保級檢查0 0 1 1 * *每月 1 號 01:00VipServicehandleMonthlyRelegationCron()
3代理佣金週結0 0 3 * * 1每週一 03:00AffiliateSettlementServicehandleWeeklySettlementCron()
4代理佣金日結0 30 3 * * *每日 03:30AffiliateSettlementServicehandleDailySettlementCron()
5即時賽事快取0 */30 * * * *每 30 分鐘LiveSportsServicerefreshCache()

5.5.7 排程執行時序圖 (每日)

00:00 ─────────────────────────────────────────────────────
00:05  ■ VIP 每日反水結算 (結算昨日投注反水)
00:30  ■ 即時賽事快取更新
01:00  ■ VIP 月度保級檢查 (僅每月 1 號)
01:30
02:00
02:30
03:00  ■ 代理佣金週結 (僅每週一)
03:30  ■ 代理佣金日結 (結算昨日佣金)
04:00
...
23:30  ■ 即時賽事快取更新
24:00 ─────────────────────────────────────────────────────

5.5.8 手動觸發機制

後台管理員可透過 Admin API 手動觸發結算:

端點說明權限
POST /affiliate/admin/trigger-settlement手動觸發週結affiliate:write
POST /affiliate/admin/trigger-daily-settlement手動觸發日結affiliate:write

手動觸發呼叫的是相同的 settleWeek() / settleDay() 方法,享有相同的冪等保護 (已結算則跳過)。

VIP 反水和保級檢查目前無手動觸發端點,僅由 Cron 自動執行。


5.6 種子資料腳本 (Seed Scripts)

所有種子腳本位於 scripts/ 目錄,使用 TypeORM DataSource 直連資料庫執行 raw SQL INSERT。主入口為 seed-all.ts,其餘為獨立功能腳本。共 23 個腳本檔案。


5.6.1 主入口腳本 — seed-all.ts

項目說明
執行方式npx ts-node scripts/seed-all.ts
功能全資料表假資料生成(37+ 表)
站點數量5 個站點 (C9, B1, B2, B3, B4)
用戶數量每站 30 個 (6 代理 + 24 會員)
語系支援5 語系 (zh-TW, en-US, zh-CN, vi-VN, th-TH)
密碼規則密碼 = 帳號 (bcrypt hash)

執行流程 (4 階段)

Phase 0: 清空所有資料表
  - SET FOREIGN_KEY_CHECKS = 0
  - DELETE FROM + ALTER TABLE AUTO_INCREMENT = 1 (41 張表)
  - SET FOREIGN_KEY_CHECKS = 1

Phase 1: 全域資料 (無 siteCode)
  - game-provider (3 筆: slot-betsolutions, crypto-betsolutions, slot-rsg)
  - vip-level (15 筆: VIP 1-15,含 5 語系名稱)
  - alliance-agent-tier (4 筆: bronze, silver, gold, platinum)
  - alliance-commission-rate (108 筆: 4 tiers × 3 levels × 9 gameTypes)
  - alliance-vip-milestone (5 筆: VIP 3/5/7/10/13 獎勵)

Phase 2: 站點設定與主題
  - site-config (5 筆: 各站含 siteName/siteDescription/learnMoreConfig)
  - site-theme (5 筆: 各站預設主題色)

Phase 3: 每站資料 (× 5 站)
  - auth-user (30 筆/站)
  - auth-user-login-log (~100 筆/站)
  - vendor-group (3 筆/站)
  - vendor-channel (3 筆/站)
  - vendor-group-channel (4 筆/站)
  - bank-card (20 筆/站)
  - credit-card (20 筆/站)
  - crypto-address (20 筆/站)
  - deposit-order (~60 筆/站)
  - withdrawal-order (15 筆/站)
  - bet-order (~100 筆/站)
  - bet-detail (~400 筆/站)
  - game-transaction (50 筆/站)
  - game-play-log (~25 筆/站)
  - vip-rebate (120 筆/站: 15 levels × 8 gameTypes)
  - vip-rebate-log (30 筆/站)
  - rank-list (30 筆/站)
  - promo-tag (2 筆/站)
  - promo (2 筆/站)
  - promo-claim (~10 筆/站)
  - notification (15 筆/站)
  - notification-read (~15 筆/站)
  - mission (30 筆/站: 2 categories × 3 periods × 5 tiers)
  - mission-progress (~50 筆/站)
  - mission-claim (~15 筆/站)
  - risk-game-blacklist (3 筆/站)
  - affiliate-balance (6 筆/站)
  - alliance-referral-code (12 筆/站)
  - affiliate-click (30 筆/站)
  - affiliate-settlement (~18 筆/站)
  - affiliate-commission (~50 筆/站)
  - affiliate-withdrawal (~10 筆/站)
  - affiliate-bind-log (12 筆/站)

站點定義

siteCodeprefix站點名稱主題色主題名稱
C9c9C9 娛樂城#10b981 (翡翠綠)Emerald
B1b1寶盈娛樂城#3b82f6 (皇家藍)Royal Blue
B2b2星際娛樂城#8b5cf6 (星際紫)Star Purple
B3b3皇冠娛樂城#f59e0b (皇冠金)Crown Gold
B4b4鳳凰娛樂城#ef4444 (鳳凰紅)Phoenix Red

用戶生成規則

項目規則
帳號格式{siteCode}{firstName}{序號} (如 c9james01)
特殊帳號第 1 站第 1 個用戶為 otis01 (管理測試帳號)
VIP 分佈60% VIP 1-3, 25% VIP 4-6, 10% VIP 7-9, 5% VIP 10-15
餘額範圍0 ~ 50,000 USD
代理碼格式{siteCode}-AG{序號} (如 C9-AG01)
語系分配隨機從 5 語系中選取

代理層級結構

每站 6 個代理:
  Agent 1 (頂層) ←── Agent 4, Agent 5 (二層)
  Agent 2 (頂層) ←── Agent 6 (二層)
  Agent 3 (頂層)

每站 24 個會員:
  Member N → level1AgentId = Agent[(N-1) % 6 + 1]
           → level2AgentId = Agent 的上線 (若有)
           → level3AgentId = null (最多 3 層)

多語系 JSON 輔助函數

typescript
/** 5-locale JSON helper */
function L(zhTW: string, enUS: string, zhCN: string, viVN: string, thTH: string): string {
  return JSON.stringify({
    'zh-TW': zhTW, 'en-US': enUS, 'zh-CN': zhCN, 'vi-VN': viVN, 'th-TH': thTH
  });
}

所有多語系欄位 (siteName, siteDescription, themeName, promoTitle, notifTitle 等) 均使用此函數生成完整 5 語系 JSON。


5.6.2 獨立種子腳本

seed-site-config.ts — 站點設定種子

項目說明
執行方式npx ts-node scripts/seed-site-config.ts
功能清空並重建 site-config + site-theme
清空TRUNCATE site-theme → TRUNCATE site-config (FK 順序)
資料含 bottomBarConfig (行動版底部導航列)、footerConfig (頁尾)、learnMoreConfig (了解更多)
特點完整的前台佈局預設配置 (JSON 格式)

seed-vendor.ts — 金流資料種子

項目說明
執行方式npx ts-node scripts/seed-vendor.ts
功能清空並重建 vendor-group + vendor-channel + vendor-group-channel
清空順序vendor-group-channel → vendor-channel → vendor-group (FK 順序)
資料3 個群組 (預設/VIP/測試) + 3 個通道 (萬通/USDT/測試)

seed-deposit.ts — 存款訂單種子

項目說明
執行方式npx ts-node scripts/seed-deposit.ts
功能生成隨機存款訂單
狀態分佈paid (70%), pending (10%), created (10%), failed (10%)
幣種TWD (萬通) / USDT (加密)
匯率TWD → USD 使用 30.5 ~ 32.5 隨機匯率

seed-deposit-order.ts — 存款訂單種子 (獨立版本)

項目說明
執行方式npx ts-node scripts/seed-deposit-order.ts
功能與 seed-deposit.ts 類似,獨立版本
差異可能有不同的資料分佈或欄位

seed-vip.ts — VIP 等級/反水種子

項目說明
執行方式npx ts-node scripts/seed-vip.ts
功能清空並重建 vip-level + vip-rebate
等級數量15 級 (青銅 I-VI, 黃金 I-III, 白金 I-III, 鑽石 I-III)
反水規則15 等級 × 8 遊戲類型 = 120 筆
使用 UPSERTON DUPLICATE KEY UPDATE 避免重複

seed-bet-record.ts — 投注紀錄種子

項目說明
執行方式npx ts-node scripts/seed-bet-record.ts
功能生成 bet-order + bet-detail 隨機投注資料
遊戲平台betsolutions (slot/crypto) + rsg (slot)
狀態分佈valid (90%), invalid (7%), cancelled (3%)
注單格式{siteCode}_{platformCode}_{timestamp}_{seq}
有效投注betEffective = betAmount × TURNOVER_WEIGHT[gameType]
打碼權重CRYPTO/FISH = 0.5, 其他 = 1.0

seed-ranking.ts — 排行榜種子

項目說明
執行方式npx ts-node scripts/seed-ranking.ts
功能生成 rank-list 排行榜資料
資料含 gameName, betAmount, multiplier, payout
匿名40% 的記錄設為匿名 (isAnonymous = 1)

seed-ranking-users.ts — 排行榜用戶種子

項目說明
執行方式npx ts-node scripts/seed-ranking-users.ts
功能補充排行榜用戶關聯資料

seed-inbox.ts — 站內信種子

項目說明
執行方式npx ts-node scripts/seed-inbox.ts
功能生成 notification + notification-read
通知類型system (系統) / promo (活動)
範圍全域通知 (userId=NULL) + 個人通知
多語系標題和內容均為 5 語系 JSON
通知標題範例系統維護通知、新活動上線、VIP 等級提升、存款成功等 (10 種)

seed-mission.ts — 任務系統種子

項目說明
執行方式npx ts-node scripts/seed-mission.ts
功能生成 mission 任務定義
任務類別deposit (存款) / bet (投注)
週期類型daily / weekly / monthly
層級5 層 (tier 1-5),門檻遞增
使用 UPSERT重複執行不報錯

任務定義範例:

類別週期層級門檻獎勵VIP 需求打碼倍率
depositdaily1$10$0.3003x
depositdaily2$50$0.8003x
depositdaily3$100$1.5005x
depositdaily4$200$3.00VIP 35x
depositdaily5$300$6.00VIP 58x
betdaily1$20$0.3003x
...weekly/monthly...............

seed-withdrawal.ts — 提領訂單種子

項目說明
執行方式npx ts-node scripts/seed-withdrawal.ts
功能生成 withdrawal-order 提領訂單
狀態分佈completed (40%), approved (30%), pending (15%), rejected (15%)
金額範圍$50 ~ $5,000
網路TRC-20, ERC-20, BEP-20

seed-learn-more.ts — 了解更多 FAQ 種子

項目說明
執行方式npx ts-node scripts/seed-learn-more.ts
功能將預設 FAQ 寫入 site-config.learnMoreConfig
條件僅更新 learnMoreConfig 為 NULL 的記錄 (不覆蓋已有資料)
FAQ 數量5 組問答
多語系問題和答案均為 3-5 語系 JSON
佔位符使用 {siteName} 佔位符,前端渲染時替換為實際站點名稱

FAQ 預設內容:

#問題說明
1{siteName} 是什麼?平台介紹
2{siteName} 是否拿到許可?合法性說明
3如何開始使用 {siteName}?註冊流程
4{siteName} 提供哪些遊戲?遊戲類型介紹
5如何進行存款和提款?金流說明

seed-layout-defaults.ts — 前台佈局預設配置

項目說明
執行方式npx ts-node scripts/seed-layout-defaults.ts
功能寫入 site-config 的佈局 JSON 欄位
配置bottomBarConfig (底部導航列)、footerConfig (頁尾)
條件更新所有 site-config 記錄

seed-agent-promo.ts — 代理活動種子

項目說明
執行方式npx ts-node scripts/seed-agent-promo.ts
功能生成代理推廣相關的活動資料
關聯promo 表 + promo-claim 表

seed-merchants.ts — 商戶資料種子

項目說明
執行方式npx ts-node scripts/seed-merchants.ts
功能生成商戶相關資料

5.6.3 工具腳本

assign-all-channels.ts — 分配所有金流通道

項目說明
執行方式npx ts-node scripts/assign-all-channels.ts
功能將所有 vendor-channel 分配到所有 vendor-group
邏輯遍歷 group × channel 組合,INSERT IGNORE 到 vendor-group-channel
用途開發環境快速讓所有群組都能使用所有通道

clear-deposit.ts — 清除存款資料

項目說明
執行方式npx ts-node scripts/clear-deposit.ts
功能清空 deposit-order 表
用途開發環境重置存款資料

5.6.4 圖片/素材生成腳本

generate-promo-images.ts — 活動橫幅圖片生成

項目說明
執行方式npx ts-node scripts/generate-promo-images.ts
功能使用 sharp 套件生成活動橫幅圖片
輸出上傳至 Cloudflare R2 儲存
格式16:9 橫幅圖片 (Banner)
用途為活動頁面提供預設橫幅圖

generate-mascot-avatars.ts — 吉祥物頭像生成

項目說明
執行方式npx ts-node scripts/generate-mascot-avatars.ts
功能使用 sharp 套件生成吉祥物頭像圖片
輸出上傳至 Cloudflare R2 儲存
用途為各站點提供吉祥物圖片素材

generate-mascots.mjs — 吉祥物生成 (ESM)

項目說明
執行方式node scripts/generate-mascots.mjs
功能ESM 格式的吉祥物生成腳本
差異使用 .mjs 副檔名,採用 ES Module 語法

5.6.5 SQL 清理腳本

cleanup-promo.sql — 清理活動資料

項目說明
執行方式直接在 MySQL 客戶端執行
功能清理無效或過期的活動資料
格式純 SQL 檔案 (非 TypeScript)

5.6.6 腳本完整清單

#腳本檔案類型用途影響的資料表
1seed-all.ts主入口全資料表假資料 (5 站 × 30 用戶)37+ 張表
2seed-site-config.ts獨立種子站點設定 + 主題 + 佈局site-config, site-theme
3seed-vendor.ts獨立種子金流群組/通道vendor-group, vendor-channel, vendor-group-channel
4seed-deposit.ts獨立種子存款訂單deposit-order
5seed-deposit-order.ts獨立種子存款訂單 (獨立版)deposit-order
6seed-vip.ts獨立種子VIP 等級 + 反水vip-level, vip-rebate
7seed-bet-record.ts獨立種子投注紀錄bet-order, bet-detail
8seed-ranking.ts獨立種子排行榜rank-list
9seed-ranking-users.ts獨立種子排行榜用戶rank-list
10seed-inbox.ts獨立種子站內信notification, notification-read
11seed-mission.ts獨立種子任務定義mission
12seed-withdrawal.ts獨立種子提領訂單withdrawal-order
13seed-learn-more.ts獨立種子了解更多 FAQsite-config (learnMoreConfig)
14seed-layout-defaults.ts獨立種子前台佈局配置site-config (bottomBarConfig, footerConfig)
15seed-agent-promo.ts獨立種子代理活動promo, promo-claim
16seed-merchants.ts獨立種子商戶資料-
17assign-all-channels.ts工具金流通道全分配vendor-group-channel
18clear-deposit.ts工具清除存款資料deposit-order
19generate-promo-images.ts素材生成活動橫幅圖片 → R2(R2 儲存)
20generate-mascot-avatars.ts素材生成吉祥物頭像 → R2(R2 儲存)
21generate-mascots.mjs素材生成吉祥物圖片 (ESM)(R2 儲存)
22cleanup-promo.sqlSQL 清理清理活動資料promo, promo-claim

5.6.7 腳本共用模式

DataSource 初始化

所有 TypeScript 種子腳本使用相同的 DataSource 初始化模式:

typescript
import 'dotenv/config';  // 或 dotenv.config({ path: '.env.local' })
import { DataSource } from 'typeorm';

const ds = new DataSource({
  type: 'mysql',
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  charset: 'utf8mb4',
});

async function main() {
  await ds.initialize();
  const qr = ds.createQueryRunner();
  // ... 執行種子邏輯
  await ds.destroy();
}
main().catch(console.error);

清空表格順序

由於 FK 約束,清空表格需遵循正確順序 (先刪子表再刪父表):

typescript
await qr.query('SET FOREIGN_KEY_CHECKS = 0');
// 從最深的子表開始刪除
await qr.query('DELETE FROM `notification-read`');
await qr.query('DELETE FROM `notification`');
// ...
await qr.query('SET FOREIGN_KEY_CHECKS = 1');

效能考量

  • 使用 raw SQL INSERT 而非 TypeORM Repository.save() (效能更佳)
  • 批次 INSERT (多行 VALUES) 減少 SQL 執行次數
  • bcrypt hash 是效能瓶頸 (每個用戶約 200ms)
  • AUTO_INCREMENT 重置確保 ID 可預測

隨機資料生成工具函數

函數用途範例
rand(min, max)隨機浮點數rand(0, 50000)23456.789
randInt(min, max)隨機整數randInt(1, 15)7
pick(arr)隨機選取陣列元素pick(['a', 'b', 'c'])'b'
fmt(date)格式化為 MySQL datetime"2026-01-15 14:30:22"
fmtDate(date)格式化為日期"2026-01-15"
randomPastDate(days)隨機過去 N 天內的日期-
randomYesterday()隨機昨日時間點-
esc(str)SQL 字串轉義 (單引號)"it's""it\\'s"
L(zhTW, enUS, zhCN, viVN, thTH)5 語系 JSON見上方

第 6 章:跨專案整合規範

6.1 統一回應格式

三個專案使用統一的 API 回應格式,確保前後端溝通一致:

6.1.1 成功回應

json
{
  "code": 200,
  "message": "ok",
  "result": { /* 實際資料 */ },
  "timestamp": 1709369200000,
  "path": "/api/auth/user-detail"
}

6.1.2 業務錯誤回應

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

HTTP Status 維持 200(僅 401 例外)。

6.1.3 未授權回應

json
{
  "code": 401,
  "message": "Unauthorized",
  "data": null,
  "timestamp": 1709369200000,
  "path": "/api/auth/user-detail"
}

HTTP Status 為 401。

6.1.4 各專案判斷方式

專案成功判斷錯誤處理
c9-ecres?.code === 200 && res.resultuseHttp 自動查 ERROR_CODES + toast
c9-imsres?.code === 200httpRequest() 三層映射 + sonner toast
c9-beController return data → Interceptor 包裹Service throw HttpException

6.2 錯誤碼系統

6.2.1 完整流程

後端 (c9-be)
  → Service 拋出 HttpException({ code: 2001, message: i18n翻譯 })
  → AllExceptionsFilter 正規化為統一格式
  → GET /common/enums 回傳 ERROR_CODES map

前台 (c9-ec)
  → app.vue onMounted → getEnumsCsr() → store.setEnums(ERROR_CODES)
  → useHttp 攔截 code !== 200 → ERROR_CODES[path][code] 查表
  → useToast() 顯示錯誤

後台 (c9-ims)
  → EnumInitializer mount → GET /common/enums → enumStore.setErrorCodes()
  → httpRequest() 攔截 code !== 200 → errorCodes[path][code] 查表
  → sonner toast.error() 顯示錯誤

6.2.2 絕不硬寫原則

所有三個專案嚴格遵守「絕不硬寫錯誤文字」原則:

  • 後端:錯誤文字一律透過 this.i18n.t('authError.register.2001') 取得
  • 前台:錯誤文字由 store.getEnums.ERROR_CODES[path][code] 查表取得
  • 後台:錯誤文字由 enumStore.errorCodes[path][code] 查表取得
  • 新增錯誤碼:只需在後端 i18n/{locale}/xxxError.json 新增 key,前端自動生效

6.3 多語系對齊

6.3.1 統一語系代碼

語系代碼語言三專案一致
zh-TW繁體中文是(預設語系)
en-US英文
zh-CN簡體中文
th-TH泰文
vi-VN越南文

6.3.2 各專案 i18n 策略

專案框架策略語系來源
c9-ec@nuxtjs/i18nno_prefix (cookie: i18n_redirected)locales header
c9-imsnext-intlno_prefix (cookie: NEXT_LOCALE)locales header
c9-benestjs-i18nHeaderResolver讀取 locales header

6.3.3 語系傳遞流程

用戶選擇語系
  → c9-ec: 寫入 i18n_redirected cookie → useHttp 注入 locales header
  → c9-ims: 寫入 NEXT_LOCALE cookie → apiClient 注入 locales header
  → c9-be: HeaderResolver(['locales']) 解析 → i18n.t() 回傳對應語系

6.3.4 DB 多語系欄位

json
{
  "zh-TW": "VIP 1 — 青銅會員",
  "en-US": "VIP 1 — Bronze Member",
  "zh-CN": "VIP 1 — 青铜会员",
  "th-TH": "VIP 1 — สมาชิกบรอนซ์",
  "vi-VN": "VIP 1 — Hội viên Đồng"
}

後端 resolveText(json) 解析順序:當前語系 → zh-TW fallback → first value → ''


6.4 認證體系

6.4.1 三種認證角色

角色認證方式JWT Strategy過期時間
前台用戶JWT Bearer Token'jwt'7 天
後台管理員Admin JWT'admin-jwt'7 天
後台 IMSNextAuth 5 (JWT)Credentials Providersession

6.4.2 Token 傳遞流程

前台 (c9-ec → c9-be)

登入成功 → 後端回傳 JWT → 存入 token cookie
→ useHttp 讀取 cookie → Authorization: Bearer {token}
→ 後端 JwtAuthGuard 驗證

後台 (c9-ims → c9-be)

登入成功 → 後端回傳 JWT → NextAuth session 儲存 accessToken
→ SessionSync 同步到 apiClient cachedToken
→ apiClient interceptor 注入 Authorization header
→ 後端 AdminJwtAuthGuard 驗證

6.4.3 401 處理

專案行為
c9-ec清除 token cookie → 儲存 redirectTo → 導回首頁 + toast
c9-ims清除 cachedToken → 嘗試 getSession 重試 → 失敗則 signOut + 導向 /login?expired=1
c9-be回傳 HTTP 401 { code: 401, message: "Unauthorized" }

6.4.4 tokenVersion 機制

後端在 DB 中維護 tokenVersion 遞增欄位:

  • 每次登入 tokenVersion +1
  • JWT payload 包含 tokenVersion
  • Strategy 驗證時比對 cache/DB 中的版本
  • 管理員可透過修改 tokenVersion 強制登出用戶

6.5 多站點 (Multi-Tenant) 架構

6.5.1 siteCode 傳遞機制

場景HeaderDecorator說明
前台 APIsite-name: a1@SiteName()白牌站點路由
後台 API (全站)x-site-code@AdminSiteCode() → null回傳所有站點資料
後台 API (單站)x-site-code: C9@AdminSiteCode() → "C9"篩選特定站點
Entity 插入-SiteCodeSubscriber自動填入 SITE_CODE env

6.5.2 各專案站點辨識

專案辨識方式設定位置
c9-echostname → domainConfig → siteIdconfig/domainConfig/a1.ts
c9-imshostname → domainConfig → siteId + NEXT_PUBLIC_SITE_IDconfig/domainConfig/a1.ts
c9-beSITE_CODE 環境變數 + site-name header.env.local

6.5.3 後台多站點操作流程

Header SiteSelector 切換
  → siteFilterStore.selectedSiteCode 更新
  → apiClient interceptor 注入 x-site-code header(或不帶)
  → AdminContentWrapper key 變更 → 子頁面 remount
  → SiteTabs 顯示可見站點 → 切換 activeSiteCode
  → API 呼叫帶入 siteCode query param(全站模式)
  → 後端 @AdminSiteCode() 讀取並篩選

6.6 資料精度規範

6.6.1 統一精度標準

用途DB 型別精度TS 型別
金額decimal(18,6)小數 6 位string
匯率decimal(18,10)小數 10 位string
百分比decimal(5,2)小數 2 位string
倍率decimal(10,2)小數 2 位string

6.6.2 截斷規則

  • 後端truncateUsd(value) = Math.floor(value * 1e6) / 1e6(無條件捨去)
  • 前端:顯示時格式化為 6 位小數
  • 運算:TypeORM decimal 讀取為 string → Number() 轉換 → 運算 → truncateUsd() 截斷

6.6.3 幣別

  • 系統內部所有金額一律使用 USD
  • 入金時由後端透過台灣銀行即時匯率(tw-exchange)轉換
  • 金流群組依語系自動分配:zh-TW→TWD, en-US→USD, zh-CN→CNY, th-TH→THB, vi-VN→VND

6.7 API 呼叫流程

6.7.1 前台 (c9-ec → c9-be)

元件 → useApi() [Facade]
  → use{Module}Api() [領域 composable]
    → useHttp<T>() / useHttpAsync<T>() [HTTP 原語]
      → fetch() [原生 API]
        ├── baseUrl = domainConfig[hostname].baseUrl
        ├── site-name = domainConfig[hostname].siteId
        ├── locales = i18n_redirected cookie
        ├── Authorization = token cookie
        └── 401 → 清 token + toast + redirect

6.7.2 後台 (c9-ims → c9-be)

元件 → useApi() [Facade]
  → use{Domain}Api() [領域 hook]
    → httpRequest() [三層錯誤碼映射 + toast]
      → apiClient [Axios 攔截器]
        ├── 動態 baseURL (域名解析 domainConfig)
        ├── site-name header (白牌路由)
        ├── locales header (NEXT_LOCALE cookie)
        ├── x-site-code header (siteFilterStore 自動注入)
        ├── Authorization: Bearer JWT (SessionSync 快取)
        └── 401 Retry (清 token → 重取 session → retry → 失敗導向登入)

6.8 白牌站點新增流程

6.8.1 後端 (c9-be)

  1. 在後台 IMS 或 Swagger 呼叫 POST /site-config/admin 建立新站點
  2. 設定 siteCode, prefix, siteName, supportedLocales
  3. 建立站點主題 POST /site-config/admin/:siteConfigId/themes
  4. 執行 seed-all.ts 為新站點產生基礎資料(VIP 等級、遊戲商等)

6.8.2 前台 (c9-ec)

  1. config/domainConfig/ 新增站點檔案(如 a2.ts
  2. 設定 hostname → { baseUrl, imgUrl, siteId, layout } 映射
  3. config/domainConfig/index.ts import 新站點

6.8.3 後台 (c9-ims)

  1. sites/ 新增站點目錄(如 sites/a2/
  2. 建立 config.ts(features, theme)+ theme.ts(OKLCH 色票)
  3. config/domainConfig/ 新增 hostname 映射
  4. config/siteRegistry.ts 新增站點 ID 白名單

第 7 章:部署與維運

7.1 部署架構

7.1.1 開發環境 Port 分配

服務Port說明
c9-ec3010Nuxt dev server
c9-ims3011Next.js Turbopack dev server
c9-be8080NestJS API server
MySQL3306資料庫
Redis6379快取
Swagger UI8080/api/docsAPI 文件

7.1.2 生產環境 (Zeabur)

服務域名說明
c9-ecc9-ec.zeabur.app前台
c9-bec9-be.zeabur.app後端 API
MySQLZeabur MySQL雲端資料庫
RedisZeabur Redis雲端快取
R2Cloudflare R2圖片/檔案儲存

7.2 一鍵啟動

7.2.1 同時啟動三個專案

bash
# 根目錄
cd c9 && yarn dev

concurrently 同時啟動,彩色標籤區分:

  • [ec] 藍色 — 前台
  • [ims] 綠色 — 後台
  • [be] 黃色 — 後端

7.2.2 單獨啟動

bash
yarn dev:ec    # 只啟動前台 (http://localhost:3010)
yarn dev:ims   # 只啟動後台 (http://localhost:3011)
yarn dev:be    # 只啟動後端 (http://localhost:8080/api)

7.3 建置與部署

7.3.1 各專案建置指令

專案建置啟動
c9-ecyarn buildyarn preview
c9-imsyarn build:a1yarn start:a1
c9-beyarn buildyarn start:prod

7.3.2 資料庫初始化

bash
# 1. 確保 MySQL 已啟動且資料庫存在
# 2. 設定 .env.local 環境變數
# 3. 啟動後端(TypeORM synchronize=dev 自動建表)
cd c9-be && yarn dev

# 4. 產生假資料(可選)
npx ts-node scripts/seed-all.ts

7.4 一鍵 Commit / Push

7.4.1 統一 Commit

bash
yarn commit    # 互動式選擇 type + 輸入訊息,依序 commit 有變更的子專案

流程:

  1. 掃描三個子專案是否有變更
  2. 互動式選擇 commit type (feat/fix/refactor/style/perf/docs/test/chore)
  3. 選填 scope + 輸入 commit message
  4. 依序 git add . && git commit 每個有變更的子專案

7.4.2 統一 Push

bash
yarn push       # commit + push(有變更才處理)
yarn push:all   # 只推送未 push 的 commit
yarn push:ec    # 只推送 c9-ec
yarn push:ims   # 只推送 c9-ims
yarn push:be    # 只推送 c9-be

7.5 GitHub 倉庫

專案倉庫分支
c9-ecgit@github.com:zxc38380166/c9-ec.gitmaster
c9-imsgit@github.com:zxc38380166/c9-ims.gitmaster
c9-begit@github.com:zxc38380166/c9-be.gitmaster

7.6 測試策略

7.6.1 各專案測試

專案單元測試元件測試E2E 測試型別檢查
c9-ecVitest (node)Vitest + @nuxt/test-utils (happy-dom)Playwright (Chromium)-
c9-ims---yarn typecheck (tsc --noEmit)
c9-beJest-supertest-

7.6.2 測試指令

c9-ec:

bash
yarn test          # 跑全部 (Vitest)
yarn test:unit     # 只跑單元測試
yarn test:nuxt     # 只跑元件測試
yarn test:e2e      # E2E (Playwright)

c9-ims:

bash
yarn typecheck     # TypeScript 型別檢查
yarn lint          # ESLint 檢查

c9-be:

bash
yarn test          # 單元測試 (Jest)
yarn test:e2e      # E2E 測試
yarn test:cov      # 測試覆蓋率

7.7 程式碼品質工具

工具c9-ecc9-imsc9-be
ESLint@nuxt/eslinteslint-config-next@nestjs/eslint-config
Prettier-prettierprettier
TypeScript5.65 (strict)5.7 (strict)
bash
# 各專案
yarn lint       # 程式碼檢查
yarn format     # 格式化 (Prettier)

7.8 工具依賴

工具用途安裝方式
concurrently同時啟動多個專案已在根目錄 devDependencies
git-cz互動式 git commitnpm i -g git-cz
ghGitHub CLI (建立/刪除倉庫)brew install gh
pnpmc9-ims 套件管理npm i -g pnpm

7.9 參考文件索引

文件位置用途
CLAUDE.md根目錄 + 各子專案AI 助手參考指南
PROJECT_API.mdc9-ec/public/ + c9-be/205+ 個 API 端點完整參考
PROJECT_API_DOC.mdc9-be/12,000+ 行人類可讀 API 文件
PROJECT_SPEC.mdc9-ec/public/ + c9-be/全端專案規格書
PROJECT_SPEC_RD.mddocs/RD 技術規格書(本文件)
Swagger UIhttp://localhost:8080/api/docs互動式 API 文件

附錄

附錄 A:完整環境變數參考

A.1 c9-be 環境變數(.env.local

變數名型別預設值說明
DB_HOSTstringlocalhostMySQL 主機位址
DB_PORTnumber3306MySQL 端口
DB_USERNAMEstringrootMySQL 使用者
DB_PASSWORDstringMySQL 密碼
DB_DATABASEstringc9_dbMySQL 資料庫名稱
DB_SYNCHRONIZEbooleantrueTypeORM 自動同步(僅開發環境)
JWT_SECRETstring前台用戶 JWT 簽名密鑰
JWT_ADMIN_SECRETstring後台管理員 JWT 簽名密鑰
JWT_EXPIRE_DAYSnumber7JWT 過期天數
SITE_CODEstringC9預設站點代碼
REDIS_HOSTstringlocalhostRedis 主機位址
REDIS_PORTnumber6379Redis 端口
REDIS_PASSWORDstringRedis 密碼(可選)
R2_ACCOUNT_IDstringCloudflare R2 帳號 ID
R2_ACCESS_KEY_IDstringR2 存取金鑰 ID
R2_SECRET_ACCESS_KEYstringR2 存取金鑰密碼
R2_BUCKET_NAMEstringc9-assetsR2 Bucket 名稱
R2_PUBLIC_URLstringR2 公開存取 URL
RESEND_API_KEYstringResend Email API Key
RESEND_FROM_EMAILstring寄件者 Email
TWILIO_ACCOUNT_SIDstringTwilio 帳號 SID
TWILIO_AUTH_TOKENstringTwilio 認證 Token
TWILIO_PHONE_NUMBERstringTwilio 發送手機號碼
GOOGLE_CLIENT_IDstringGoogle OAuth Client ID
GOOGLE_CLIENT_SECRETstringGoogle OAuth Client Secret
WANTONG_MERCHANT_IDstring萬通金流商戶 ID
WANTONG_API_KEYstring萬通金流 API Key
WANTONG_CALLBACK_URLstring萬通金流回調 URL
USDT_MERCHANT_IDstringUSDT 支付商戶 ID
USDT_API_KEYstringUSDT 支付 API Key
USDT_CALLBACK_URLstringUSDT 支付回調 URL
API_FOOTBALL_KEYstringAPI-Football 密鑰
BETSOLUTIONS_MERCHANT_IDstringBetSolutions 商戶 ID
BETSOLUTIONS_PRIVATE_KEYstringBetSolutions 私鑰
RSG_OPERATOR_TOKENstringRSG 運營商 Token
RSG_SECRET_KEYstringRSG DES 加解密 Key
TELEGRAM_BOT_TOKENstringTelegram Bot Token

A.2 c9-ims 環境變數(.env.local

變數名型別預設值說明
NEXTAUTH_URLstringhttp://localhost:3011NextAuth 回調 URL
NEXTAUTH_SECRETstringc9-ims-auth-secret-keyNextAuth 加密密鑰
NEXT_PUBLIC_SITE_IDstringa1當前站點 ID(對應 domainConfig)
NEXT_PUBLIC_API_URLstringhttp://localhost:8080後端 API URL(SSR fallback)
NEXT_PUBLIC_R2_URLstringR2 公開存取 URL

A.3 c9-ec 環境變數

c9-ec 不使用 .env 檔案,所有配置透過程式碼中的 domainConfig 靜態定義:

typescript
// config/domainConfig/a1.ts
export const entries: Record<string, DomainConfigEntry> = {
  'localhost': {
    baseUrl: 'http://localhost:8080',
    imgUrl: 'https://pub-xxx.r2.dev',
    siteId: 'a1',
    layout: 'a1',
  },
  'c9-ec.zeabur.app': {
    baseUrl: 'https://c9-be.zeabur.app',
    imgUrl: 'https://pub-xxx.r2.dev',
    siteId: 'a1',
    layout: 'a1',
  },
};

附錄 B:錯誤碼參考

B.1 認證模組錯誤碼(authError)

錯誤碼路徑說明
2001/api/auth/register帳號已存在
2002/api/auth/registerEmail 已被使用
2003/api/auth/register推薦碼無效
2004/api/auth/register推薦碼已過期
2010/api/auth/login帳號或密碼錯誤
2011/api/auth/login帳號已被停用
2012/api/auth/login需要 2FA 驗證碼
2013/api/auth/login2FA 驗證碼錯誤
2020/api/auth/send-otpOTP 發送過於頻繁
2021/api/auth/verify-otpOTP 驗證碼錯誤
2022/api/auth/verify-otpOTP 已過期
2030/api/auth/change-password原密碼錯誤
2040/api/auth/bind-googleGoogle 帳號已綁定其他用戶
2041/api/auth/bind-telegramTelegram 帳號已綁定其他用戶

B.2 金流模組錯誤碼(financeError)

錯誤碼路徑說明
3001/api/deposit存款金額低於最低限額
3002/api/deposit存款金額超過最高限額
3003/api/deposit金流通道暫時關閉
3004/api/deposit無可用金流群組
3010/api/withdrawal/request提款金額低於最低限額
3011/api/withdrawal/request提款金額超過餘額
3012/api/withdrawal/request尚有未完成的提款訂單
3013/api/withdrawal/request打碼量未達標
3014/api/withdrawal/requestOTP 驗證失敗
3020/api/wallet/bank-card銀行卡數量已達上限
3021/api/wallet/credit-card信用卡數量已達上限
3022/api/wallet/crypto-address加密地址數量已達上限

B.3 遊戲模組錯誤碼(gameError)

錯誤碼路徑說明
5001/api/game/launch遊戲供應商維護中
5002/api/game/launch遊戲不存在
5003/api/game/launch餘額不足
5010/api/game/launch用戶在遊戲黑名單中
5011/api/game/launch該地區不支援此遊戲

B.4 VIP 模組錯誤碼(vipError)

錯誤碼路徑說明
6001/api/vip/rebate-claim今日反水已領取
6002/api/vip/rebate-claim無可領取的反水

B.5 代理模組錯誤碼(affiliateError)

錯誤碼路徑說明
7001/api/affiliate/apply已經是代理
7002/api/affiliate/apply不符合代理申請條件
7010/api/affiliate/withdrawal代理餘額不足
7011/api/affiliate/withdrawal尚有未完成的代理提款
7020/api/affiliate/referral-code推廣碼數量已達上限(10 個)

B.6 管理員模組錯誤碼(adminError)

錯誤碼路徑說明
8001/api/admin/login帳號或密碼錯誤
8002/api/admin/login需要 2FA 驗證碼
8003/api/admin/login2FA 驗證碼錯誤
8010/api/admin/registerEmail 已存在
8020/api/admin/google-auth/verifyGoogle Auth 驗證碼錯誤

附錄 C:資料庫關聯圖

C.1 用戶域

auth-user (1)
  ├── (1:N) bank-card
  ├── (1:N) credit-card
  ├── (1:N) crypto-address
  ├── (1:N) deposit-order
  ├── (1:N) withdrawal-order
  ├── (1:N) bet-order
  ├── (1:N) bet-detail
  ├── (1:N) game-transaction
  ├── (1:N) game-play-log
  ├── (1:N) promo-claim
  ├── (1:N) notification
  ├── (1:N) notification-read
  ├── (1:N) mission-progress
  ├── (1:N) mission-claim
  ├── (1:N) vip-rebate-log
  ├── (1:N) auth-user-login-log
  ├── (1:N) affiliate-commission
  ├── (1:N) affiliate-settlement
  ├── (1:N) affiliate-withdrawal
  ├── (1:N) affiliate-click
  ├── (1:N) affiliate-bind-log
  ├── (1:N) alliance-referral-code
  └── (1:N) risk-game-blacklist

C.2 站點域

site-config (1)
  ├── (1:N) site-theme
  ├── (1:N) game-provider           ← siteCode 關聯
  ├── (1:N) game-type-config        ← siteCode 關聯
  ├── (1:N) vip-level               ← siteCode 關聯
  ├── (1:N) vip-rebate              ← siteCode 關聯
  ├── (1:N) risk-ip-rule            ← siteCode 關聯
  ├── (1:N) alliance-commission-rate ← siteCode 關聯
  ├── (1:N) alliance-agent-tier      ← siteCode 關聯
  ├── (1:N) alliance-vip-milestone   ← siteCode 關聯
  └── (1:N) mission                  ← siteCode 關聯

C.3 遊戲域

game-provider (1)
  ├── (1:N) game-type-config     ← providerCode 關聯
  └── (1:N) game-play-log        ← providerCode 關聯

bet-order (1)
  └── (1:N) bet-detail

game-transaction (N:1) auth-user
  ├── type: 'debit' | 'credit' | 'rollback'
  └── providerCode + roundId 唯一

C.4 金流域

vendor-group (1)
  └── (N:M) vendor-channel      ← 透過 vendor-group-channel 中間表

deposit-order
  ├── (N:1) auth-user
  └── vendorType: 'fiat' | 'credit' | 'crypto'

withdrawal-order
  ├── (N:1) auth-user
  └── status: 'pending' → 'approved' → 'completed' | 'rejected'

C.5 代理域

affiliate-commission
  ├── (N:1) auth-user (agentId)
  └── (N:1) auth-user (fromUserId)

affiliate-settlement
  ├── (N:1) auth-user (agentId)
  ├── (1:N) affiliate-risk-log
  └── status: 'pending' → 'approved' | 'rejected'

affiliate-withdrawal
  ├── (N:1) auth-user (agentId)
  └── status: 'pending' → 'approved' → 'completed' | 'rejected'

alliance-referral-code
  ├── (N:1) auth-user (userId)
  └── allianceCode ←→ masterRefCode 雙重查找

C.6 VIP 域

vip-level (per-site)
  └── (1:N) vip-rebate          ← level + gameType 唯一

vip-rebate-log
  ├── (N:1) auth-user
  └── type: 日反水 / 保級獎勵 / VIP 里程碑

附錄 D:API 端點快速索引

D.1 前台 API(按使用場景)

用戶註冊/登入

方法端點Guard說明
POST/api/auth/register-帳密註冊
POST/api/auth/login-帳密登入
POST/api/auth/login-google-Google OAuth 登入
POST/api/auth/login-telegram-Telegram 登入
GET/api/auth/user-detailJWT取得用戶資料
POST/api/auth/change-passwordJWT修改密碼
POST/api/auth/send-otpJWT發送 Email OTP
POST/api/auth/verify-otpJWT驗證 OTP
POST/api/auth/bind-googleJWT綁定 Google
POST/api/auth/unbind-googleJWT解綁 Google
POST/api/auth/bind-telegramJWT綁定 Telegram
POST/api/auth/unbind-telegramJWT解綁 Telegram
POST/api/auth/setup-2faJWT設定 2FA
POST/api/auth/verify-2faJWT驗證 2FA
POST/api/auth/disable-2faJWT停用 2FA
GET/api/auth/avatarsJWT取得頭像列表
PATCH/api/auth/avatarJWT更新頭像

遊戲操作

方法端點Guard說明
GET/api/game/providersOptional遊戲供應商列表
GET/api/game/type-configsOptional遊戲分類列表
POST/api/game/launchJWT啟動遊戲
POST/api/game/demo-試玩遊戲
GET/api/game/recentJWT最近遊玩
GET/api/game/favoritesJWT收藏遊戲
POST/api/game/favoriteJWT加入收藏
DELETE/api/game/favorite/:idJWT移除收藏

存款/提款

方法端點Guard說明
POST/api/depositJWT提交存款
GET/api/deposit/ordersJWT存款紀錄
GET/api/deposit/exchange-rate-即時匯率
GET/api/deposit/crypto-rate-加密貨幣匯率
POST/api/withdrawal/requestJWT提交提款
GET/api/withdrawal/listJWT提款紀錄
GET/api/withdrawal/turnover-statusJWT打碼量狀態

錢包管理

方法端點Guard說明
GET/api/wallet/bank-cardsJWT銀行卡列表
POST/api/wallet/bank-cardJWT新增銀行卡
DELETE/api/wallet/bank-card/:idJWT刪除銀行卡
GET/api/wallet/credit-cardsJWT信用卡列表
POST/api/wallet/credit-cardJWT新增信用卡
DELETE/api/wallet/credit-card/:idJWT刪除信用卡
GET/api/wallet/crypto-addressesJWT加密地址列表
POST/api/wallet/crypto-addressJWT新增加密地址
DELETE/api/wallet/crypto-address/:idJWT刪除加密地址

VIP 系統

方法端點Guard說明
GET/api/vip/statusJWTVIP 狀態
GET/api/vip/levelsOptionalVIP 等級列表
GET/api/vip/rebatesOptional反水率列表
GET/api/vip/rebate-historyJWT反水紀錄

代理系統

方法端點Guard說明
POST/api/affiliate/applyJWT申請成為代理
GET/api/affiliate/dashboardJWT代理儀表板
GET/api/affiliate/downlinesJWT下線列表
GET/api/affiliate/commissionsJWT佣金紀錄
GET/api/affiliate/settlementsJWT結算紀錄
GET/api/affiliate/balanceJWT代理餘額
POST/api/affiliate/withdrawalJWT代理提款
GET/api/affiliate/withdrawalsJWT代理提款紀錄

站內信

方法端點Guard說明
GET/api/inbox/listJWT站內信列表
GET/api/inbox/:idJWT站內信詳情
POST/api/inbox/readJWT標記已讀
POST/api/inbox/read-allJWT全部已讀
DELETE/api/inbox/:idJWT刪除站內信
GET/api/inbox/unread-countJWT未讀數量

活動/任務

方法端點Guard說明
GET/api/promo/listOptional活動列表
GET/api/promo/:idOptional活動詳情
POST/api/promo/claimJWT領取活動
GET/api/promo/claimsJWT領取紀錄
GET/api/promo/tags-活動標籤
GET/api/mission/listJWT任務列表
POST/api/mission/claimJWT領取任務獎勵

D.2 後台 Admin API(按模組)

認證

方法端點Guard說明
POST/api/admin/login-管理員登入
POST/api/admin/registerAdminJWT建立管理員
GET/api/admin/profileAdminJWT取得個人資料
PATCH/api/admin/profileAdminJWT更新個人資料
POST/api/admin/google-auth/generateAdminJWT產生 2FA QR Code
POST/api/admin/google-auth/verifyAdminJWT驗證 2FA
POST/api/admin/google-auth/disableAdminJWT停用 2FA

管理員 CRUD

方法端點GuardPermission說明
GET/api/admin/listAdminJWTadmin:read管理員列表
POST/api/admin/createAdminJWTadmin:write建立管理員
GET/api/admin/:idAdminJWTadmin:read管理員詳情
PATCH/api/admin/:idAdminJWTadmin:write更新管理員
DELETE/api/admin/:idAdminJWTadmin:write刪除管理員

群組 CRUD

方法端點GuardPermission說明
GET/api/admin/groups/listAdminJWTadmin-group:read群組列表
POST/api/admin/groups/createAdminJWTadmin-group:write建立群組
GET/api/admin/groups/:idAdminJWTadmin-group:read群組詳情
PATCH/api/admin/groups/:idAdminJWTadmin-group:write更新群組
DELETE/api/admin/groups/:idAdminJWTadmin-group:write刪除群組
GET/api/admin/permissions/allAdminJWT-權限清單

報表

方法端點GuardPermission說明
GET/api/admin/reports/playersAdminJWTreport:read玩家報表
GET/api/admin/reports/vip-playersAdminJWTreport:readVIP 玩家
GET/api/admin/reports/bet-recordsAdminJWTreport:read投注紀錄
GET/api/admin/reports/overviewAdminJWTreport:read總覽報表
GET/api/admin/reports/profit-lossAdminJWTreport:read損益報表
GET/api/admin/reports/gamesAdminJWTreport:read遊戲報表
GET/api/admin/reports/promosAdminJWTreport:read活動報表
GET/api/admin/reports/player-summaryAdminJWTreport:read玩家簡表
GET/api/admin/reports/r2-logsAdminJWTreport:readR2 操作紀錄
GET/api/admin/reports/exportAdminJWTreport:readCSV 匯出

附錄 E:常見開發問題排除

E.1 環境建置問題

MySQL 連線失敗

Error: ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'localhost'

解法:檢查 .env.local 中的 DB_PASSWORD 是否正確,並確認 MySQL 服務已啟動。

Redis 連線失敗

Error: connect ECONNREFUSED 127.0.0.1:6379

解法:確認 Redis 服務已啟動。macOS 可使用 brew services start redis

pnpm 版本不相容

ERR_PNPM_UNSUPPORTED_ENGINE  Unsupported environment

解法npm install -g pnpm@latest 更新至最新版本。

E.2 前台 (c9-ec) 問題

SSR 時 useHttp 報錯

Error: [nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

解法useHttp 必須在 Vue setup function 或 Nuxt composable 中呼叫。不能在 callback、setTimeout 或 Promise.then 中直接呼叫。

主題切換不生效

解法:確認 main.css 中的 @theme 區塊正確引用了 var(--c9-primary-*) CSS 變數。

i18n 語系切換後頁面空白

解法:檢查 nuxt.config.tsi18n.defaultLocale 設定,以及 cookie 名稱是否為 i18n_redirected

E.3 後台 (c9-ims) 問題

next-intl ICU 格式錯誤

IntlError: FORMATTING_ERROR: Expected "," but found "}"

解法:翻譯檔中的大括號需用單引號轉義:'{'變數名'}'

TanStack Query 資料不刷新

解法:確認 queryKey 包含所有依賴值(page, siteCode, filters)。站點切換時 SiteSelector 已自動清除 Query cache。

API 呼叫返回 401

解法

  1. 確認 SessionSync 元件有正確同步 token 到 apiClient
  2. 檢查後端 JWT_ADMIN_SECRET 與前端登入時使用的是否一致
  3. 確認 JWT 未過期(預設 7 天)

權限頁面顯示 AccessDenied

解法:確認管理員的群組 (admin-group) 中包含對應頁面的權限模組。root 群組類型自動擁有所有權限。

E.4 後端 (c9-be) 問題

TypeORM synchronize 欄位衝突

QueryFailedError: ER_CANT_DROP_FIELD_OR_KEY

解法:開發環境 DB_SYNCHRONIZE=true 會自動同步 Entity 變更到資料庫。如果欄位衝突,可嘗試刪除並重建資料庫,然後重新 seed。

S2S Callback 簽名驗證失敗

解法

  1. BetSolutions:確認 BETSOLUTIONS_PRIVATE_KEY 與遊戲商提供的一致
  2. RSG:確認 RSG_SECRET_KEY 用於 DES 加解密,且 key 長度正確

Cron Job 未執行

解法:確認 @nestjs/scheduleapp.module.ts 中有 import ScheduleModule.forRoot()。查看日誌中是否有 [Scheduler] 標記。

R2 上傳失敗

解法:確認環境變數 R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKET_NAME 都已正確設定。

E.5 跨專案整合問題

前端收到 code !== 200 但沒有顯示錯誤訊息

解法

  1. 確認後端 GET /common/enums 能正常回傳 ERROR_CODES
  2. 確認前端啟動時有呼叫 getEnumsCsr()EnumInitializer 已掛載
  3. 檢查 path 匹配:前端使用的 URL 路徑需與後端 path 欄位一致

多站點資料未隔離

解法

  1. 確認後端 Entity 有 siteCode 欄位且有 @Index()
  2. 確認 Service 中的 QueryBuilder 有 .andWhere('alias.siteCode = :siteCode')
  3. 確認 Controller 使用 @AdminSiteCode() 裝飾器
  4. 確認前台 API 有傳送 site-name header

新增站點後前台/後台看不到

解法

  1. 後端:確認 site-config 表中已新增站點記錄
  2. 前台:確認 domainConfig 中有新站點的域名映射
  3. 後台:確認 siteRegistry.ts 中有新站點 ID

附錄 F:術語表

術語說明
白牌 (White Label)一套系統服務多個品牌站點,每站獨立域名、主題、設定
站點代碼 (siteCode)用於區分站點的唯一代碼(如 C9, B1, B2)
遊戲商 (Provider)提供遊戲內容的第三方供應商(如 BetSolutions, RSG)
金流商 (Vendor)提供支付通道的第三方服務商(如萬通金流)
打碼量 (Turnover)用戶需要達到的投注額度才能提款
反水 (Rebate)VIP 會員根據投注額獲得的現金回饋
保級 (Relegation)VIP 等級降級檢查,每月 1 日執行
S2S 回調 (Server-to-Server Callback)遊戲商伺服器直接呼叫後端 API 的機制
轉帳錢包 (Transfer Wallet)遊戲內下注/派彩透過 S2S 回調更新餘額的模式
推廣碼 (Referral Code)代理用於推廣新用戶的唯一識別碼
聯盟碼 (Alliance Code)推廣碼的別名,支援雙重查找
帶入模板 (Load Template)從預設資料匯入配置到指定站點的功能
同預設站點 (Copy From Default)將預設站點的配置複製到目標站點
Feature Flag控制功能模組顯示/隱藏的開關
OKLCH後台使用的色彩空間標準,比 HSL 更具感知均勻性
ComposableVue 3 的可組合函式,用於封裝可重用邏輯
HookReact 中的可重用邏輯封裝函式
GuardNestJS 的路由守衛,用於認證和授權
DecoratorNestJS 的裝飾器,用於標記路由元資料
DTOData Transfer Object,定義 API 請求/回應的資料結構
EntityTypeORM 的資料庫實體,對應一張資料表
InterceptorNestJS 的攔截器,用於統一包裝回應格式
SubscriberTypeORM 的事件訂閱器,用於 Entity 生命週期鉤子
QueryBuilderTypeORM 的查詢建構器,用於複雜 SQL 查詢
Facade Pattern外觀模式,useApi() 合併所有 domain hook 的統一入口
ICU Format國際化訊息格式標準,next-intl 使用此格式
sessionStorage瀏覽器分頁級儲存,siteFilterStore 使用此機制持久化


附錄 G:完整 Middleware 與 Guard 參考

本附錄詳細說明 C9 平台三個子專案中所有中介層(Middleware)與路由守衛(Guard)的實作細節,包含認證流程、權限檢查、以及請求攔截邏輯。

G.1 前台 (c9-ec) — Nuxt 全域路由守衛

前台僅有一個全域路由守衛,負責保護需要登入才能存取的頁面。

G.1.1 auth.global.ts — 認證攔截中介層

項目說明
檔案路徑c9-ec/app/middleware/auth.global.ts
類型Nuxt 全域中介層(.global.ts 後綴自動載入)
觸發時機每次路由切換時自動執行

保護路徑定義

typescript
const PROTECTED_PREFIXES = ['/user'];

所有以 /user 開頭的路徑(如 /user/deposit/user/vip/user/wallet 等共 10 個頁面)均需登入才能存取。

攔截流程

路由切換觸發
  → 檢查目標路徑是否以 PROTECTED_PREFIXES 開頭
    → 否:直接放行(公開頁面如首頁、遊戲大廳、活動中心)
    → 是:檢查 token cookie
      → 有 token:放行
      → 無 token:
        1. 將目標路徑存入 redirectTo cookie
        2. 設定 loginModalOpen = true(開啟登入 Modal)
        3. navigateTo('/') 導回首頁

完整原始碼

typescript
const PROTECTED_PREFIXES = ['/user'];

export default defineNuxtRouteMiddleware((to) => {
  const needsAuth = PROTECTED_PREFIXES.some((p) => to.path.startsWith(p));
  if (!needsAuth) return;

  const token = useCookie<string | null>('token', { path: '/' });
  if (token.value) return;

  // 記住目標頁面,登入後跳回
  const redirectCookie = useCookie<string | null>('redirectTo', { path: '/' });
  redirectCookie.value = to.fullPath;

  // 打開登入 Modal
  const loginModalOpen = useState('loginModalOpen', () => false);
  loginModalOpen.value = true;

  return navigateTo('/');
});

設計特點

特點說明
非強制導向登入頁使用 Modal 彈出登入表單,而非跳轉至獨立登入頁
路由記憶透過 redirectTo cookie 記住目標頁面,登入後自動跳回
SSR 安全使用 useCookie 而非 localStorage,伺服器端渲染時也能讀取
擴充方式新增保護路徑只需修改 PROTECTED_PREFIXES 陣列

前台認證 Token 管理

項目說明
Token 來源後端 /api/auth/login 回傳 JWT
儲存方式Cookie(token),路徑 /,HttpOnly 視後端設定
過期時間7 天(後端 JWT 設定)
失效處理useHttp 收到 401 時自動清除 token → 導回首頁 + toast 提示

G.2 後台 (c9-ims) — Next.js 中介層

後台使用 Next.js Edge Middleware,同時處理 i18n 路由與認證保護。

G.2.1 middleware.ts — 統一中介層

項目說明
檔案路徑c9-ims/src/middleware.ts
執行環境Edge Runtime(每次請求都會執行)
Matcher`["/((?!api

公開路徑白名單

typescript
const PUBLIC_PATHS = ["/login"];

認證流程

請求進入
  → 解析 pathname
  → 判斷是否為公開路徑
  → 偵測 HTTPS(含反向代理 x-forwarded-proto 檢查)
  → getToken() 讀取 NextAuth JWT
    → 未登入 + 非公開路徑:
        redirect → /login?expired=1(帶 expired 參數顯示 toast)
    → 已登入 + 在登入頁:
        redirect → /dashboard
    → 其他情況:
        交由 next-intl middleware 處理 i18n 路由

完整原始碼

typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

const intlMiddleware = createMiddleware(routing);
const PUBLIC_PATHS = ["/login"];

export default async function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;
  const isPublicPath = PUBLIC_PATHS.some(
    (p) => pathname === p || pathname.startsWith(`${p}/`),
  );

  // 偵測 HTTPS(反向代理後可能內部是 HTTP)
  const isSecure =
    req.headers.get("x-forwarded-proto") === "https" ||
    req.nextUrl.protocol === "https:";

  const token = await getToken({
    req,
    secret: "c9-ims-auth-secret-key",
    secureCookie: isSecure,
  });

  if (!token && !isPublicPath) {
    const loginUrl = new URL("/login", req.nextUrl.origin);
    loginUrl.searchParams.set("expired", "1");
    return NextResponse.redirect(loginUrl);
  }

  if (token && isPublicPath) {
    return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
  }

  return intlMiddleware(req);
}

export const config = {
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

i18n 路由設定src/i18n/routing.ts):

typescript
export const routing = defineRouting({
  locales: ["zh-TW", "en-US", "zh-CN", "th-TH", "vi-VN"],
  defaultLocale: "zh-TW",
  localePrefix: "never",  // locale 存 cookie,URL 不帶前綴
});

HTTPS 偵測邏輯

條件說明
x-forwarded-proto === "https"反向代理(如 Nginx、Cloudflare)設定的 header
req.nextUrl.protocol === "https:"直接 HTTPS 連線
secureCookie決定是否讀取 __Secure- 前綴的 cookie

G.3 後台 (c9-ims) — NextAuth 認證配置

G.3.1 auth.ts — NextAuth 5 配置

項目說明
檔案路徑c9-ims/src/lib/auth.ts
NextAuth 版本5.0.0-beta.30
策略JWT
ProviderCredentials only

登入流程

前端 signIn("credentials", { email, password })
  → NextAuth authorize()
    → 解析 hostname → resolveDomainConfig → 取得 baseUrl + siteId
    → POST ${baseUrl}/api/admin/login
      headers: { "Content-Type": "application/json", "site-name": siteId }
      body: { email, password }
    → 回應 code === 200:
        回傳 User { id, name, email, accessToken, permissions, groupType, allowedSiteCodes }
    → 回應 code !== 200 或連線失敗:
        throw CredentialsSignin

JWT Callback — Token 擴展

typescript
async jwt({ token, user }) {
  if (user) {
    token.id = user.id;
    token.permissions = user.permissions;
    token.groupType = user.groupType;
    token.accessToken = user.accessToken;
    token.allowedSiteCodes = user.allowedSiteCodes;
  }
  return token;
}

Session Callback — 回傳擴展

typescript
async session({ session, token }) {
  session.user.id = token.id;
  session.user.permissions = token.permissions ?? [];
  session.user.groupType = token.groupType ?? "custom";
  session.user.allowedSiteCodes = token.allowedSiteCodes ?? null;
  session.accessToken = token.accessToken;
  return session;
}

型別擴展(Module Augmentation):

擴展介面新增欄位說明
Userpermissions?: string[]權限陣列
UsergroupType?: GroupType群組類型
UseraccessToken?: string後端 JWT Token
UserallowedSiteCodes?: string[] | null允許存取的站點
SessionaccessToken: string傳遞至客戶端的 JWT

G.4 後端 (c9-be) — Guards(路由守衛)

後端共有 4 個 Guard,分為前台認證與後台認證兩組。

G.4.1 JwtAuthGuard — 前台用戶認證

項目說明
檔案路徑c9-be/src/modules/auth/guards/jwt-auth.guard.ts
StrategyAuthGuard('jwt')
用途保護前台用戶端點(如個人資料、存款、提領等)
typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

對應 Strategyauth/strategies/jwt.strategy.ts):

設定項
Strategy name'jwt'(Passport 預設)
SecretJWT_SECRET env → fallback 'c9-secret'
JWT 來源ExtractJwt.fromAuthHeaderAsBearerToken()
Payload{ sub: userId, account, tokenVersion }

驗證流程

請求帶入 Authorization: Bearer <token>
  → Passport 解析 JWT payload
  → JwtStrategy.validate(payload)
    → 查 cache: cache:auth:tv:{userId}
      → cache miss → 查 DB auth-user
    → 比對 tokenVersion
      → 不一致 → throw UnauthorizedException
    → 節流更新 lastActivityAt(60s 間隔,記錄在線狀態)
    → 回傳 { id, account }(附加至 req.user)

G.4.2 OptionalJwtAuthGuard — 前台可選認證

項目說明
檔案路徑c9-be/src/modules/auth/guards/optional-jwt-auth.guard.ts
StrategyAuthGuard('jwt')
用途某些端點可選認證(如遊戲列表:登入用戶顯示最近遊玩)
typescript
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(_err: any, user: any) {
    return user || null;  // 未認證不報錯,回傳 null
  }
}

與 JwtAuthGuard 的差異

比較項JwtAuthGuardOptionalJwtAuthGuard
未帶 Token拋出 401 Unauthorized回傳 req.user = null
Token 無效拋出 401 Unauthorized回傳 req.user = null
Token 有效設定 req.user設定 req.user
使用場景必須登入的端點登入/未登入皆可的端點

G.4.3 AdminJwtAuthGuard — 後台管理員認證

項目說明
檔案路徑c9-be/src/modules/admin/guards/admin-jwt-auth.guard.ts
StrategyAuthGuard('admin-jwt')
用途保護所有後台管理端點
typescript
@Injectable()
export class AdminJwtAuthGuard extends AuthGuard('admin-jwt') {}

對應 Strategyadmin/strategies/admin-jwt.strategy.ts):

設定項
Strategy name'admin-jwt'(自定名稱與前台區分)
SecretADMIN_JWT_SECRET → fallback JWT_SECRET → fallback 'c9-secret'
Payload{ sub: adminId, email, tokenVersion, role: 'admin', groupId?, groupType? }

驗證流程

請求帶入 Authorization: Bearer <admin-token>
  → Passport 解析 JWT payload
  → AdminJwtStrategy.validate(payload)
    → 確認 role === 'admin'
    → 查 cache: cache:admin:tv:{adminId}
      → cache miss → 查 DB admin-user(確認 status=1)
    → 比對 tokenVersion
    → 回傳 { id, email, role: 'admin', groupId, groupType }

G.4.4 PermissionsGuard — RBAC 權限檢查

項目說明
檔案路徑c9-be/src/modules/admin/guards/permissions.guard.ts
依賴Reflector、AdminGroup Repository、Cache Manager
用途搭配 @RequirePermissions() 裝飾器檢查管理員權限

完整權限檢查流程

@RequirePermissions('deposit:write')
  → PermissionsGuard.canActivate()
    → 從 Reflector 讀取 required permissions metadata
      → 無設定:直接通過
    → 從 request 取得 user
      → 無 user:拋出 ForbiddenException
    → 檢查 groupType
      → root:直接通過(ROOT 擁有所有權限)
    → 檢查 groupId
      → 無 groupId:拋出 ForbiddenException('無權限:未分配群組')
    → 查詢群組權限
      → 先查 cache: cache:admin-group:perms:{groupId}(60s TTL)
      → cache miss → 查 DB admin-group.permissions
      → 群組已停用:拋出 ForbiddenException
    → 比對所有所需權限
      → 全部通過:return true
      → 缺少任一:拋出 ForbiddenException('無權限執行此操作')

權限系統常數admin/constants/permissions.ts):

常數說明
PERM_MODULES16 個模組:admin, admin-group, admin-log, user, deposit, withdrawal, promo, promo-tag, affiliate, vip, game, risk, report, vendor, finance, site-config
PERM_ACTIONS2 種操作:read, write
ALL_PERMISSIONS32 個權限 key(16 模組 x 2 操作)— ROOT 專用
SUPER_ADMIN_PERMISSIONS30 個(排除 site-config:read, site-config:write)
GENERAL_ADMIN_PERMISSIONS15 個(僅 read,排除 site-config)
GROUP_TYPESroot, super_admin, general_admin, custom

G.5 後端 (c9-be) — 自定義 Decorators

G.5.1 @AdminSiteCode() — 後台站點篩選

typescript
export const AdminSiteCode = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext): string | null => {
    const req = ctx.switchToHttp().getRequest();
    return req.headers['x-site-code'] || req.query?.siteCode || null;
  },
);
優先順序來源回傳值
1x-site-code request header特定站點代碼 (string)
2siteCode query parameter特定站點代碼 (string)
3皆無null(代表全站)

G.5.2 @SiteName() — 前台站點名稱

typescript
export const SiteName = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext): string => {
    const req = ctx.switchToHttp().getRequest();
    return req.headers['site-name'] || process.env.SITE_CODE || 'default';
  },
);
優先順序來源說明
1site-name request header前台 HTTP Client 注入
2SITE_CODE 環境變數後端預設站點
3'default'最終 fallback

G.5.3 SiteCodeSubscriber — TypeORM 自動填入 siteCode

typescript
@EventSubscriber()
export class SiteCodeSubscriber implements EntitySubscriberInterface {
  beforeInsert(event: InsertEvent<any>) {
    const hasSiteCode = event.metadata.columns.some(
      (c) => c.propertyName === 'siteCode'
    );
    if (hasSiteCode && event.entity && !event.entity.siteCode) {
      event.entity.siteCode = process.env.SITE_CODE || 'C9';
    }
  }
}
觸發時機Entity 條件行為
beforeInsertsiteCode 欄位且值為空自動填入 SITE_CODE env 或 'C9'
beforeInsert已有 siteCode不覆寫(保留手動設定值)

G.6 後端 (c9-be) — 全域 Filter 與 Interceptor

G.6.1 AllExceptionsFilter — 統一錯誤回應

項目說明
檔案路徑c9-be/src/utils/http-exception.filter.ts
裝飾器@Catch() — 捕獲所有例外
註冊方式app.useGlobalFilters(new AllExceptionsFilter())

回應格式化規則

原始 HTTP Status最終 HTTP Status說明
401 Unauthorized401唯一保持原始 status 的情況
其他所有狀態200業務錯誤統一用 200 + code 區分

回應 body 結構

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

G.6.2 SuccessResponseInterceptor — 統一成功回應

項目說明
檔案路徑c9-be/src/utils/success-response.interceptor.ts
註冊方式app.useGlobalInterceptors(new SuccessResponseInterceptor())

Bypass 條件(不包裝的回應):

條件場景
已含 { code, message, data }Service 手動建構的回應
已含 { StatusCode, Data }遊戲商 S2S callback 回傳格式

標準包裝格式

json
{
  "code": 200,
  "message": "ok",
  "result": { /* 原始回傳資料 */ },
  "timestamp": 1709370000000,
  "path": "/api/vip/levels"
}

G.6.3 Bootstrap Pipeline 完整順序

NestFactory.create(AppModule)
  → app.use(cookieParser())                          // Cookie 解析
  → app.use(compression())                           // Gzip 壓縮
  → app.setGlobalPrefix('api')                       // 路由前綴 /api
  → app.useGlobalPipes(ValidationPipe)               // DTO 驗證(whitelist + transform)
  → app.enableCors({ origin: true, credentials })    // CORS 全開
  → app.useGlobalFilters(AllExceptionsFilter)        // 統一錯誤格式
  → app.useGlobalInterceptors(SuccessResponseInterceptor) // 統一成功格式
  → SwaggerModule.setup('api/docs', ...)             // Swagger UI
  → app.listen(PORT, '0.0.0.0')                      // 啟動監聽

附錄 H:完整 Seed 腳本參考

本附錄詳細說明 C9 平台後端的假資料生成腳本,包含執行方式、資料結構、以及每個腳本的用途。

H.1 Seed 腳本總覽

腳本用途執行指令
seed-all.ts主入口 — 多站點完整假資料(5 站 x 30 用戶)npx ts-node scripts/seed-all.ts
seed-site-config.ts站點設定 + 主題種子資料(單站 C9)npx ts-node scripts/seed-site-config.ts
seed-vendor.ts金流群組/通道種子資料npx ts-node scripts/seed-vendor.ts
seed-vip.tsVIP 等級/反水種子資料npx ts-node scripts/seed-vip.ts
seed-deposit.ts存款訂單種子資料npx ts-node scripts/seed-deposit.ts
seed-deposit-order.ts存款訂單(獨立版本)npx ts-node scripts/seed-deposit-order.ts
seed-bet-record.ts投注紀錄種子資料npx ts-node scripts/seed-bet-record.ts
seed-ranking.ts排行榜種子資料npx ts-node scripts/seed-ranking.ts
seed-ranking-users.ts排行榜用戶種子資料npx ts-node scripts/seed-ranking-users.ts
seed-inbox.ts站內信種子資料npx ts-node scripts/seed-inbox.ts
seed-mission.ts任務系統種子資料npx ts-node scripts/seed-mission.ts
seed-withdrawal.ts提領訂單種子資料npx ts-node scripts/seed-withdrawal.ts
seed-learn-more.ts了解更多 FAQ 種子資料npx ts-node scripts/seed-learn-more.ts
seed-layout-defaults.ts前台佈局預設配置npx ts-node scripts/seed-layout-defaults.ts
seed-agent-promo.ts代理活動種子資料npx ts-node scripts/seed-agent-promo.ts
seed-merchants.ts商戶種子資料npx ts-node scripts/seed-merchants.ts
assign-all-channels.ts分配所有金流通道至群組npx ts-node scripts/assign-all-channels.ts
clear-deposit.ts清除存款資料(工具腳本)npx ts-node scripts/clear-deposit.ts
generate-promo-images.ts活動橫幅圖片生成 + R2 上傳npx ts-node scripts/generate-promo-images.ts
generate-mascot-avatars.ts吉祥物頭像生成 + R2 上傳npx ts-node scripts/generate-mascot-avatars.ts
generate-mascots.mjs吉祥物生成(ESM 版本)node scripts/generate-mascots.mjs
cleanup-promo.sql清理活動資料 SQL手動執行

H.2 seed-all.ts — 主入口腳本詳解

H.2.1 執行環境

bash
npx ts-node scripts/seed-all.ts

環境變數載入順序

  1. .env(基礎設定)
  2. .env.local(覆寫,含資料庫密碼等敏感資訊)

資料庫連線:直接使用 TypeORM DataSource,不經過 NestJS Module。

H.2.2 站點定義(5 個站點)

站點代碼前綴名稱主題色說明
C9c9C9 娛樂城#10b981 (翡翠綠)主站
B1b1寶盈娛樂城#3b82f6 (皇家藍)白牌站 1
B2b2星際娛樂城#8b5cf6 (星際紫)白牌站 2
B3b3皇冠娛樂城#f59e0b (皇冠金)白牌站 3
B4b4鳳凰娛樂城#ef4444 (鳳凰紅)白牌站 4

所有站點名稱均包含 5 語系翻譯(zh-TW, en-US, zh-CN, vi-VN, th-TH)。

H.2.3 執行階段(4 個 Phase)

Phase 0: 清空所有資料表

清空 41 張資料表(依 FK 順序),使用 SET FOREIGN_KEY_CHECKS = 0 繞過外鍵限制,每張表 DELETE + ALTER TABLE AUTO_INCREMENT = 1 重置 ID。

Phase 1: 全域資料(不含 siteCode)

資料表筆數說明
game-provider3slot-betsolutions, crypto-betsolutions, slot-rsg
vip-level15青銅 I-VI、黃金 I-III、白金 I-III、鑽石 I-III
alliance-agent-tier4bronze, silver, gold, platinum
alliance-commission-rate1084 等級 x 3 層級 x (1 通用 + 8 遊戲類型)
alliance-vip-milestone5VIP 3/5/7/10/13 達標獎勵

Phase 2: 站點設定與主題

資料表筆數說明
site-config5每站一筆,含 learnMoreConfig JSON
site-theme5每站一組主題,含 primary/accent/surface/text/border

Phase 3: 每站資料(每站獨立生成,以下為每站的資料量)

資料表每站筆數說明
auth-user306 代理 + 24 會員,密碼 = 帳號
auth-user-login-log~90-150每人 2-5 筆登入紀錄
vendor-group3預設/VIP/測試
vendor-channel3萬通支付/USDT 支付/測試通道
vendor-group-channel4群組-通道關聯
bank-card20含各種審核狀態
credit-card20含各種審核狀態
crypto-address20TRC-20/ERC-20/BEP-20
deposit-order~60-120代理 3-5 筆、會員 1-3 筆
withdrawal-order15含各種狀態
bet-order~60-150每人 2-5 筆注單
bet-detail~300-750每注單 1-10 筆明細
game-transaction50bet/win/cancel/jackpot
game-play-log~20-30最近遊玩紀錄(UPSERT)
vip-rebate12015 等級 x 8 遊戲類型
vip-rebate-log30反水發放紀錄
rank-list30排行榜紀錄
promo-tag2代理/熱門
promo2代理招募/週末加碼
promo-claim~8-10活動領取紀錄
notification158 全站通知 + 7 個人通知
notification-read~15-20已讀紀錄
mission302 類別 x 3 週期 x 5 層級
mission-progress~40-60任務進度
mission-claim~10-15任務領取
risk-game-blacklist3風控封鎖紀錄
affiliate-balance6每個代理一筆
alliance-referral-code12每代理 2 個推廣碼
affiliate-click30點擊追蹤紀錄
affiliate-settlement18每代理 3 週結算
affiliate-commission~36-90每結算 2-5 筆佣金
affiliate-withdrawal~5-10代理提款
affiliate-bind-log~10-15綁定紀錄

H.2.4 預設測試帳號

帳號密碼角色站點說明
otis01otis01代理 + 會員C9第一位用戶(ID=1)
c9james02c9james02代理C9自動生成帳號
c9oliver03 ~ c9owen30同帳號會員C9每站 30 人
b1james01 ~ b1owen30同帳號各角色B1B1 站用戶
b2james01 ~ b2owen30同帳號各角色B2B2 站用戶
b3james01 ~ b3owen30同帳號各角色B3B3 站用戶
b4james01 ~ b4owen30同帳號各角色B4B4 站用戶

重要:所有測試帳號的密碼等於帳號名稱(經 bcrypt hash 後儲存)。

後台管理員帳號由 AdminModule.onModuleInit() 自動建立(非 seed 腳本),預設為:

  • Email: ADMIN_DEFAULT_EMAIL 環境變數(預設 root
  • Password: ADMIN_DEFAULT_PASSWORD 環境變數(預設 root

H.2.5 VIP 等級預設資料

等級名稱Tier累積投注門檻 (USD)保級投注 (USD)
1青銅 Ibronze00
2青銅 IIbronze3,600200
3青銅 IIIbronze12,000500
4青銅 IVbronze60,0002,000
5青銅 Vbronze180,0005,000
6青銅 VIbronze360,00020,000
7黃金 Igold660,00058,000
8黃金 IIgold2,400,000150,000
9黃金 IIIgold5,000,000250,000
10白金 Iplatinum10,000,000500,000
11白金 IIplatinum30,000,0001,500,000
12白金 IIIplatinum60,000,0002,500,000
13鑽石 Idiamond100,000,0005,000,000
14鑽石 IIdiamond500,000,00012,500,000
15鑽石 IIIdiamond960,000,00025,000,000

H.2.6 反水率預設表(15 等級 x 8 遊戲類型,單位 %)

等級sportsslotlivelotterychessesportscryptofish
10.200.500.500.500.500.300.500.50
70.500.800.700.700.700.500.700.70
100.651.000.800.800.800.600.800.80
150.901.501.001.101.100.801.101.10

H.3 seed-site-config.ts — 站點設定種子

獨立於 seed-all.ts 的單站版本,僅建立 C9 站點的完整設定,包含:

設定項說明
bottomBarConfig行動版底部導航列(5 個項目:存款、活動中心、遊戲、體育、VIP)
footerConfig頁尾區塊(6 組連結:娛樂遊戲、體育競技、優惠活動、支付方式、客戶服務、關於我們)
learnMoreConfig了解更多 FAQ(5 個問答:平台介紹、許可證、安全性、貨幣支援、遊戲類型)
customerServiceConfig客服管道設定(7 個管道 + LiveChat 設定)
site-theme翡翠綠主題(primary/accent/surface/text/border 完整色號)

H.4 其他輔助腳本

H.4.1 圖片生成腳本

腳本輸出說明
generate-promo-images.tsR2 上傳使用 Sharp 生成活動橫幅圖片
generate-mascot-avatars.tsR2 上傳讀取 scripts/mascots/*.png 並上傳至 R2
generate-mascots.mjs本地檔案ESM 版吉祥物生成(10 個 PNG:dragon, fox, knight, mermaid, owl, phoenix, tiger, angel, wizard, wolf)

H.4.2 工具腳本

腳本用途
assign-all-channels.ts將所有金流通道分配到所有金流群組
clear-deposit.ts清除存款訂單資料(開發除錯用)
cleanup-promo.sqlSQL 腳本清理活動相關資料

附錄 I:Nuxt 與 Next.js 配置完整參考

本附錄詳細說明前台 (c9-ec) 與後台 (c9-ims) 的框架配置檔案。

I.1 前台 (c9-ec) — nuxt.config.ts

typescript
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  devServer: { port: 3010 },

  app: {
    head: {
      meta: [
        {
          name: 'viewport',
          content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'
        },
      ],
    },
  },

  modules: [
    '@nuxt/eslint',
    '@nuxt/hints',
    '@nuxt/image',
    '@nuxt/scripts',
    '@nuxt/test-utils',
    '@nuxt/ui',
    '@nuxtjs/i18n',
    '@nuxt/content',
    '@pinia/nuxt',
  ],

  css: ['~/assets/css/global.scss', '~/assets/css/main.css'],

  i18n: {
    strategy: 'no_prefix',
    defaultLocale: 'zh-TW',
    langDir: 'locales',
    locales: [
      { code: 'zh-TW', iso: 'zh-TW', name: '繁體中文', file: 'zh-TW.json' },
      { code: 'en-US', iso: 'en-US', name: 'English', file: 'en-US.json' },
      { code: 'zh-CN', iso: 'zh-CN', name: '简体中文', file: 'zh-CN.json' },
      { code: 'th-TH', iso: 'th-TH', name: 'ภาษาไทย', file: 'th-TH.json' },
      { code: 'vi-VN', iso: 'vi-VN', name: 'Tiếng Việt', file: 'vi-VN.json' },
    ],
  },
});

I.1.1 配置項說明

配置項說明
compatibilityDate'2025-07-15'Nuxt 4 向後相容日期
devtools.enabledtrue啟用 Vue DevTools
devServer.port3010開發伺服器 port
app.head.metaviewport禁止用戶縮放(行動裝置最佳化)

I.1.2 Nuxt 模組說明(9 個)

模組版本用途
@nuxt/eslint-ESLint 整合
@nuxt/hints-效能最佳化提示
@nuxt/image2.0.0圖片最佳化(WebP、響應式)
@nuxt/scripts0.13.2第三方腳本管理
@nuxt/test-utils3.23.0測試工具(Vitest + Playwright 整合)
@nuxt/uiv4.4.0UI 元件庫(基於 Tailwind CSS v4)
@nuxtjs/i18n10.2.1多語系支援
@nuxt/contentv3內容管理(Markdown → Vue)
@pinia/nuxt0.11.3Pinia 狀態管理整合

I.1.3 CSS 載入順序

  1. ~/assets/css/global.scss — 全域 SCSS(目前為空,保留擴充)
  2. ~/assets/css/main.css — Tailwind v4 + Nuxt UI + 主題色變數橋接 + 滾動條隱藏

I.1.4 i18n 策略

設定說明
strategy'no_prefix'URL 不顯示語系前綴
defaultLocale'zh-TW'預設繁體中文
langDir'locales'語系檔案目錄(i18n/locales/
儲存方式i18n_redirected cookie跨頁面持久化

I.2 後台 (c9-ims) — next.config.ts

typescript
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

const nextConfig: NextConfig = {
  reactStrictMode: false,
};

export default withNextIntl(nextConfig);

I.2.1 配置項說明

配置項說明
reactStrictModefalse停用 React Strict Mode(避免 double render 影響 API 呼叫)
withNextIntl plugin./src/i18n/request.tsnext-intl 國際化 plugin

I.2.2 next-intl 配置鏈

next.config.ts → withNextIntl("./src/i18n/request.ts")
  → src/i18n/request.ts → getRequestConfig()
    → unflatten flat JSON → nested object
  → src/i18n/routing.ts → defineRouting()
    → locales: ["zh-TW", "en-US", "zh-CN", "th-TH", "vi-VN"]
    → defaultLocale: "zh-TW"
    → localePrefix: "never"
  → src/i18n/navigation.ts
    → 匯出 locale-aware Link, redirect, useRouter, usePathname

I.2.3 站點配置(sites/a1/config.ts

typescript
export const config: SiteConfig = {
  id: "a1",
  name: "Admin A1",
  description: "A1 後台管理系統",
  logo: "/sites/a1/logo.svg",
  favicon: "/sites/a1/favicon.ico",
  defaultLocale: "zh-TW",
  supportedLocales: ["zh-TW", "en-US", "zh-CN", "th-TH", "vi-VN"],
  features: { /* 13 個功能開關 */ },
  theme: { light: { /* 30 OKLCH 色值 */ }, dark: { /* 30 OKLCH 色值 */ }, radius: "0.625rem" },
};

I.2.4 功能開關完整列表

Feature Flag預設值控制功能
enableAnalyticsfalse分析報表模組
enableBillingfalse帳單模組
enableUserManagementfalse用戶管理獨立頁面
enableRBACfalse角色權限管理獨立頁面
enableI18nSwitchertrueHeader 語系切換器
enableSystemManagementtrue系統管理(管理員/群組/紀錄/站點設定)
enableFinanceManagementtrue財務管理(餘額/銀行卡/提領/入金設定)
enableAffiliateManagementtrue代理管理(含聯盟系統)
enableVipManagementtrueVIP 管理(等級/返水/里程碑)
enableReportstrue報表資訊(7 個子頁面)
enableGameManagementtrue遊戲管理(遊戲商/類型設定)
enableRiskControltrue風控設置(IP 規則/檢查/遊戲黑名單)
enablePlayerManagementtrue玩家管理(全部/新註冊/線上/登入失敗)

I.3 後台 (c9-ims) — NextAuth 完整配置

I.3.1 設定摘要

設定項
Secret"c9-ims-auth-secret-key"
Trust Hosttrue
Session Strategyjwt
ProviderCredentials(email + password)
Custom Sign-in Page/login

I.3.2 Credentials Provider 流程

authorize(credentials, request)
  → 從 request headers 取得 host
  → resolveDomainConfig(host) → { baseUrl, siteId }
  → POST ${baseUrl}/api/admin/login
      headers: { "Content-Type": "application/json", "site-name": siteId }
      body: { email, password }
  → 成功 (code=200):
      return {
        id: admin.id,
        name: admin.name,
        email: admin.email,
        accessToken: token,
        permissions: admin.group?.permissions ?? [],
        groupType: admin.group?.type ?? "custom",
        allowedSiteCodes: admin.allowedSiteCodes ?? null,
      }
  → 失敗:
      throw new CredentialsSignin(message)

I.3.3 JWT Token 生命週期

signIn → authorize() → 回傳 User
  → jwt callback: 將 User 欄位寫入 JWT token
  → session callback: 將 JWT token 欄位暴露至 Session
    → 客戶端透過 useSession() 取得
      → apiClient 透過 SessionSync 同步 accessToken

I.4 後端 (c9-be) — TypeORM 配置

I.4.1 config/typeorm.config.ts

設定項說明
type'mysql'MySQL 資料庫
hostDB_HOST env資料庫主機
portDB_PORT env資料庫 port
charset'utf8mb4'支援 emoji 等 4-byte 字元
timezone'+08:00'統一 UTC+8 時區
autoLoadEntitiestrue自動載入所有 forFeature() 註冊的 Entity
synchronizeisDev only僅開發環境自動同步 schema
loggingfalse不輸出 SQL 日誌
subscribers[SiteCodeSubscriber]自動填入 siteCode

附錄 J:測試架構與指令參考

本附錄詳細說明 C9 平台的測試架構、配置檔案、以及測試指令。

J.1 前台 (c9-ec) — 測試架構

前台使用三層測試策略:Vitest 單元測試、Nuxt 元件測試、Playwright E2E 測試。

J.1.1 Vitest 配置(vitest.config.ts

typescript
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { defineVitestProject } from '@nuxt/test-utils/config'

export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['test/unit/*.{test,spec}.ts'],
          environment: 'node',
        },
      },
      await defineVitestProject({
        test: {
          name: 'nuxt',
          include: ['test/nuxt/*.{test,spec}.ts'],
          environment: 'nuxt',
          environmentOptions: {
            nuxt: {
              rootDir: fileURLToPath(new URL('.', import.meta.url)),
              domEnvironment: 'happy-dom',
            },
          },
        },
      }),
    ],
  },
})

兩個 Test Project

Project環境測試目錄用途
unitnodetest/unit/純邏輯單元測試(工具函式、API 邏輯)
nuxtnuxt (happy-dom)test/nuxt/Vue 元件測試(含 Nuxt 自動匯入、composable)

Nuxt 元件測試環境特點

特點說明
DOM 環境happy-dom(比 jsdom 更快)
自動匯入Nuxt composables(useRouteuseState 等)自動可用
模組模擬@nuxt/test-utils 自動 mock Nuxt modules
rootDir指向專案根目錄,確保路徑正確解析

J.1.2 Playwright 配置(playwright.config.ts

typescript
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'

export default defineConfig<ConfigOptions>({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
    nuxt: {
      rootDir: fileURLToPath(new URL('.', import.meta.url)),
    },
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
})

配置說明

設定說明
testDir'./tests'E2E 測試目錄
fullyParalleltrue完全平行執行
forbidOnlyCI 環境為 true禁止 .only() 進入 CI
retriesCI=2, 本地=0CI 環境重試 2 次
workersCI=1, 本地=自動CI 環境單工作者
reporter'html'生成 HTML 報告
trace'on-first-retry'首次重試時收集追蹤
projectsChromium only僅 Desktop Chrome

J.1.3 前台測試指令

指令說明
yarn test執行全部 Vitest 測試(unit + nuxt)
yarn test:watchWatch 模式(檔案變更自動重跑)
yarn test:unit僅執行單元測試
yarn test:nuxt僅執行 Nuxt 元件測試
yarn test:e2e執行 Playwright E2E 測試
yarn test:e2e:ui開啟 Playwright UI 模式(互動式偵錯)

J.1.4 測試檔案命名規範

類型檔案路徑命名規範
單元測試test/unit/*.spec.ts{feature}.spec.ts
元件測試test/nuxt/*.spec.ts{Component}.spec.ts
E2E 測試tests/*.spec.ts{flow}.spec.ts

J.2 後端 (c9-be) — 測試架構

後端使用 Jest 作為測試框架,搭配 NestJS 的 @nestjs/testing 模組。

J.2.1 Jest 配置

E2E 測試配置test/jest-e2e.json):

json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  }
}
設定說明
testEnvironment'node'Node.js 環境
testRegex.e2e-spec.ts$E2E 測試檔案匹配
transformts-jestTypeScript 轉譯

J.2.2 E2E 測試範例

typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

NestJS 測試模式

步驟說明
1. Test.createTestingModule建立測試模組(可覆寫 Provider)
2. .compile()編譯模組
3. createNestApplication()建立 NestJS 應用實例
4. app.init()初始化(觸發 lifecycle hooks)
5. supertest發送 HTTP 請求驗證

J.2.3 後端測試指令

指令說明
yarn test執行全部單元測試
yarn test:watchWatch 模式
yarn test:cov執行測試 + 覆蓋率報告
yarn test:e2e執行 E2E 測試
yarn test:debugDebug 模式測試

J.2.4 單元測試慣例

慣例說明
檔案位置與源碼同目錄:*.spec.ts
命名格式{service-name}.spec.ts
Mock 方式Jest mock + NestJS overrideProvider()
DB 操作Mock Repository(不連接真實 DB)

J.3 測試策略總覽

層級前台 (c9-ec)後端 (c9-be)
單元測試Vitest (node env)Jest
元件/整合測試Vitest + @nuxt/test-utils (happy-dom)@nestjs/testing + supertest
E2E 測試Playwright (Chromium)supertest
覆蓋率yarn test --coverageyarn test:cov
CI 整合Playwright retries=2, workers=1Jest 標準配置

附錄 K:CSS 變數與主題系統完整參考

本附錄詳細說明 C9 平台的主題系統實作,包含前台 CSS 變數橋接、後台 OKLCH 色彩系統、以及動態主題切換機制。

K.1 前台 (c9-ec) — CSS 變數橋接系統

前台使用「CSS 變數橋接」模式:將 Tailwind CSS 的 emerald 色系透過 CSS custom properties 覆寫,使得運行時可動態切換主題色。

K.1.1 main.css — 核心樣式

css
@import "tailwindcss";
@import "@nuxt/ui";

/* Theme Color Bridge */
@theme {
  --color-emerald-50:  var(--c9-primary-50, #ecfdf5);
  --color-emerald-100: var(--c9-primary-100, #d1fae5);
  --color-emerald-200: var(--c9-primary-200, #a7f3d0);
  --color-emerald-300: var(--c9-primary-300, #6ee7b7);
  --color-emerald-400: var(--c9-primary-400, #34d399);
  --color-emerald-500: var(--c9-primary-500, #10b981);
  --color-emerald-600: var(--c9-primary-600, #059669);
  --color-emerald-700: var(--c9-primary-700, #047857);
  --color-emerald-800: var(--c9-primary-800, #065f46);
  --color-emerald-900: var(--c9-primary-900, #064e3b);
  --color-emerald-950: var(--c9-primary-950, #022c22);
}

:root {
  --c9-glow: var(--c9-primary-glow, 16, 185, 129);
}

橋接原理

Tailwind emerald-500 → --color-emerald-500
  → 讀取 CSS variable: var(--c9-primary-500, #10b981)
    → 若 --c9-primary-500 已設定(由 useTheme.applyTheme 注入)→ 使用自定色
    → 若未設定 → 使用 fallback #10b981(原始 emerald 色)

K.1.2 CSS 變數完整列表

CSS Variable預設值 (Emerald)用途
--c9-primary-50#ecfdf5最淺底色
--c9-primary-100#d1fae5淺底色
--c9-primary-200#a7f3d0淺色
--c9-primary-300#6ee7b7中淺色(light)
--c9-primary-400#34d399中色(base)
--c9-primary-500#10b981主色
--c9-primary-600#059669深色(dark)
--c9-primary-700#047857深色
--c9-primary-800#065f46最深色
--c9-primary-900#064e3b極深色
--c9-primary-950#022c22最極深色
--c9-primary-glow16, 185, 129發光效果(RGB 值)

K.1.3 附加工具樣式

樣式選擇器用途
scrollbar-hide@utility隱藏滾動條
Sidebar scrollbar[data-slot="root"][data-collapsed]收合 sidebar 隱藏滾動條
Dialog scrollbar[role="dialog"]Modal/Drawer/Dropdown 隱藏滾動條

K.2 前台 (c9-ec) — 主題預設與切換

K.2.1 6 組內建主題預設(utils/themePresets.ts

主題 ID名稱預覽色圖示Glow 值
emerald翡翠綠#34d399i-lucide-leaf16, 185, 129
amber琥珀金#fbbf24i-lucide-flame245, 158, 11
sky天空藍#38bdf8i-lucide-cloud14, 165, 233
violet薰衣紫#a78bfai-lucide-sparkles139, 92, 246
rose玫瑰紅#fb7185i-lucide-heart244, 63, 94
cyan青色#22d3eei-lucide-droplets6, 182, 212

每組主題包含完整的 11 階 shades(50-950)+ glow RGB 值。

K.2.2 useTheme Composable — 主題管理

核心方法

方法說明
initTheme()啟動時從 cookie 讀取主題 ID,套用至 DOM
setTheme(themeId)切換主題:更新 CSS 變數 + 寫入 cookie
applyTheme(theme)底層實作:遍歷 shades → document.documentElement.style.setProperty()
fetchSiteConfig()從後端取得站點配置,合併伺服器端主題至本地 presets

主題切換流程

使用者點擊「主題切換」
  → setTheme('amber')
    → 找到 amber ThemePreset
    → applyTheme(amberTheme)
      → root.style.setProperty('--c9-primary-50', '#fffbeb')
      → root.style.setProperty('--c9-primary-100', '#fef3c7')
      → ... (全部 11 階)
      → root.style.setProperty('--c9-primary-glow', '245, 158, 11')
      → appConfig.ui.colors.primary = 'amber'  // Nuxt UI 色盤切換
    → 寫入 c9-theme cookie(1 年過期)
    → (若已登入)嘗試同步至後端

伺服器端主題合併

fetchSiteConfig()
  → GET /site-config/public
  → 回傳 activeTheme + availableThemes
  → 若 activeTheme.themeId 不在本地 presets 中
    → siteThemeToPreset(activeTheme) 轉換格式
    → 加入 presets 陣列

K.3 後台 (c9-ims) — OKLCH 色彩系統

後台使用 OKLCH 色彩空間(Lightness / Chroma / Hue),這是 CSS Color Level 4 標準的一部分,提供更均勻的感知色彩分佈。

K.3.1 globals.css — CSS 變數定義

Light Mode(:root)— 30 個變數

變數OKLCH 值用途
--backgroundoklch(1 0 0)頁面背景(純白)
--foregroundoklch(0.145 0 0)主要文字(深灰)
--cardoklch(1 0 0)卡片背景
--card-foregroundoklch(0.145 0 0)卡片文字
--primaryoklch(0.205 0 0)主要按鈕/連結
--primary-foregroundoklch(0.985 0 0)主要按鈕文字
--secondaryoklch(0.97 0 0)次要元素背景
--mutedoklch(0.97 0 0)弱化元素背景
--muted-foregroundoklch(0.556 0 0)弱化文字
--accentoklch(0.97 0 0)強調元素
--destructiveoklch(0.577 0.245 27.325)危險操作(紅色)
--borderoklch(0.922 0 0)邊框
--inputoklch(0.922 0 0)輸入框邊框
--ringoklch(0.708 0 0)Focus ring
--chart-1 ~ --chart-5各色圖表色系(5 色)
--sidebaroklch(0.985 0 0)Sidebar 背景
--sidebar-primaryoklch(0.205 0 0)Sidebar 主色
--sidebar-borderoklch(0.922 0 0)Sidebar 邊框

Dark Mode(.dark)— 30 個變數

變數OKLCH 值與 Light 差異
--backgroundoklch(0.145 0 0)深色背景
--foregroundoklch(0.985 0 0)白色文字
--cardoklch(0.205 0 0)深灰卡片
--primaryoklch(0.922 0 0)淺色主色(反轉)
--secondaryoklch(0.269 0 0)深灰次要
--muted-foregroundoklch(0.708 0 0)較亮的弱化文字
--destructiveoklch(0.704 0.191 22.216)較亮的紅色
--borderoklch(1 0 0 / 10%)半透明白邊
--inputoklch(1 0 0 / 15%)半透明白輸入框
--sidebaroklch(0.205 0 0)深色 sidebar
--sidebar-primaryoklch(0.488 0.243 264.376)藍紫色主色

K.3.2 Tailwind v4 Theme 映射

css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  /* ... 30+ 映射 ... */
  --color-sidebar: var(--sidebar);
  --color-sidebar-ring: var(--sidebar-ring);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

Radius 系統(基於 --radius: 0.625rem):

Token計算值像素值(約)
--radius-sm0.625rem - 4px~6px
--radius-md0.625rem - 2px~8px
--radius-lg0.625rem~10px
--radius-xl0.625rem + 4px~14px
--radius-2xl0.625rem + 8px~18px

K.3.3 Dark Mode 切換機制

css
@custom-variant dark (&:is(.dark *));

Tailwind v4 自訂 variant,使用 .dark CSS class 切換暗色模式。當外層元素具有 .dark class 時,所有子元素的 dark: variant 生效。

K.3.4 Base Layer 樣式

css
@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}

所有元素預設使用 --border 色邊框和 --ring 色 outline;body 使用 --background 背景和 --foreground 文字色。


K.4 後台 (c9-ims) — 站點主題配置

K.4.1 sites/a1/theme.ts — OKLCH 色票

每個站點配置 Light + Dark 兩套色票,包含 30 個 OKLCH 色值:

typescript
export const theme: Theme = {
  light: {
    primary: "oklch(0.546 0.245 262.881)",          // 站點主色(藍紫)
    primaryForeground: "oklch(0.985 0 0)",            // 按鈕白字
    ring: "oklch(0.546 0.245 262.881)",               // Focus ring = 主色
    sidebarPrimary: "oklch(0.546 0.245 262.881)",     // Sidebar = 主色
    // ... 其餘與預設 shadcn/ui 色值相同
  },
  dark: {
    primary: "oklch(0.623 0.214 259.815)",            // 暗色主色(較亮)
    primaryForeground: "oklch(0.205 0 0)",            // 暗底白字反轉
    sidebarPrimary: "oklch(0.623 0.214 259.815)",
    // ... 暗色版本
  },
  radius: "0.625rem",
};

ThemeColors 介面(共 30 個屬性):

分類屬性數量
基礎background, foreground2
卡片card, cardForeground2
Popoverpopover, popoverForeground2
主色primary, primaryForeground2
次色secondary, secondaryForeground2
弱化muted, mutedForeground2
強調accent, accentForeground2
危險destructive1
邊框border, input, ring3
圖表chart1 ~ chart55
Sidebarsidebar, sidebarForeground, sidebarPrimary, sidebarPrimaryForeground, sidebarAccent, sidebarAccentForeground, sidebarBorder, sidebarRing8

K.5 後台 (c9-ims) — SiteThemeInjector 元件

K.5.1 實作原始碼

typescript
"use client";

import { useConfig } from "@/config/useConfig";
import type { ThemeColors } from "@/config/types";

function themeColorsToCSS(colors: ThemeColors): Record<string, string> {
  return {
    "--background": colors.background,
    "--foreground": colors.foreground,
    "--card": colors.card,
    "--card-foreground": colors.cardForeground,
    "--primary": colors.primary,
    "--primary-foreground": colors.primaryForeground,
    // ... 共 30 個映射
    "--sidebar-ring": colors.sidebarRing,
  };
}

export function SiteThemeInjector({ children }: { children: React.ReactNode }) {
  const config = useConfig();
  const cssVars = themeColorsToCSS(config.theme.light);

  return (
    <div style={cssVars as React.CSSProperties} className="contents">
      {children}
    </div>
  );
}

K.5.2 運作機制

Providers (layout.tsx)
  → SiteConfigProvider (傳入 siteConfig)
    → SiteThemeInjector
      → useConfig() 讀取 siteConfig.theme.light
      → themeColorsToCSS() 轉換為 inline CSS variables
      → <div style={{ "--primary": "oklch(0.546 0.245 262.881)", ... }} className="contents">
        → 所有子元素繼承 CSS variables
        → shadcn/ui 元件自動讀取 var(--primary) 等變數
        → Tailwind utilities(bg-primary, text-muted-foreground)也讀取這些變數

className="contents" 的作用

display: contents 使得 wrapper div 不產生任何佈局影響,子元素的佈局行為如同直接放在父層中。這確保主題注入不會破壞頁面佈局。


K.6 主題系統架構總覽

┌─────────────────────────────────────────────────────────┐
│                    後端 (c9-be)                          │
│  site-theme 表 → { primary, accent, surface, text, ... }│
│  ↓ API 回傳                                             │
├────────────────────────┬────────────────────────────────┤
│   前台 (c9-ec)         │   後台 (c9-ims)                │
│                        │                                │
│  ● 6 組 ThemePreset    │  ● sites/a1/theme.ts           │
│    (HEX 色值)          │    (OKLCH 色值)                 │
│                        │                                │
│  ● useTheme.applyTheme │  ● SiteThemeInjector           │
│    → setProperty()     │    → inline style              │
│    → --c9-primary-*    │    → --primary, --card, ...    │
│                        │                                │
│  ● @theme 橋接         │  ● @theme inline 映射          │
│    emerald → CSS var   │    CSS var → Tailwind token    │
│                        │                                │
│  ● Nuxt UI 自動讀取    │  ● shadcn/ui 自動讀取          │
│    Tailwind emerald-*  │    var(--primary) 等           │
└────────────────────────┴────────────────────────────────┘

前台 vs 後台主題系統比較

比較項前台 (c9-ec)後台 (c9-ims)
色彩格式HEX(#10b981OKLCH(oklch(0.546 0.245 262.881)
注入方式document.documentElement.style.setProperty()React inline style(<div style={...}>
切換時機用戶手動切換 + cookie 持久化伺服器端載入站點配置時注入
變數前綴--c9-primary-{shade}--primary, --card, --sidebar
UI 框架Nuxt UI v4(Tailwind emerald 色系橋接)shadcn/ui(直接讀取 CSS variables)
暗色支援固定暗色(不支援 light/dark 切換)支援 light/dark 雙模式
預設主題Emerald(翡翠綠)a1 站點預設藍紫色
預設數量6 組內建每站點 1 組(可從後端擴充)

(全文完)