C9 Platform — RD 技術規格書
目標讀者:前端工程師、後端工程師、全端工程師 文件版本:2026-03-02 涵蓋範圍:c9-ec(前台)、c9-ims(後台)、c9-be(後端)三個子專案的完整技術規格
目錄
- 第 1 章:技術架構總覽
- 第 2 章:開發環境建置
- 第 3 章:前台 (c9-ec) 技術規格
- 第 4 章:後台 (c9-ims) 技術規格
- 第 5 章:後端 (c9-be) 技術規格
- 第 6 章:跨專案整合規範
- 第 7 章:部署與維運
第 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.6 | TypeScript 5 (strict) | TypeScript 5.7 (strict) |
| UI 元件庫 | Nuxt UI v4 | shadcn/ui new-york (Radix UI) | - |
| CSS | Tailwind CSS v4 | Tailwind CSS v4 | - |
| 狀態管理 | Pinia v3 | Zustand v5 + TanStack Query v5 | - |
| i18n | @nuxtjs/i18n 10.2.1 | next-intl v4.8.3 | nestjs-i18n v10.6 |
| 驗證 | Zod v4 | React Hook Form + Zod v4 | class-validator + DTOs |
| 認證 | JWT cookie (7 天) | NextAuth 5 beta (JWT) | JWT + Passport |
| HTTP 客戶端 | 原生 fetch (useHttp) | Axios 1.13.5 (apiClient) | Axios (外部呼叫) |
| 測試 | Vitest + Playwright | TypeScript check | Jest |
| 套件管理 | yarn | pnpm | yarn |
| 開發 Port | 3010 | 3011 | 8080 |
| 路由 | 檔案路由 (20 頁) | App Router (68 頁) | Controller (25 個) |
| 元件數量 | 77 個 Vue 元件 | 41 個 React 元件 | - |
| API Hook | 12 個 composables | 7 個 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.351.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 |
+-----------------+架構要點
- 前後端分離:c9-ec 和 c9-ims 各自獨立部署,透過 HTTP API 與 c9-be 溝通
- 白牌架構:透過
site-name/x-site-codeheader 區分站點,同一套後端服務支援多個品牌 - 統一 API 前綴:所有後端 API 以
/api為前綴,Swagger 文件在/api/docs - 圖片儲存:使用 Cloudflare R2 (S3-compatible),前端透過公開 URL 存取
- 快取策略:Redis 用於 JWT token version、權限快取、錯誤碼快取、賽事快取等
1.3 部署架構
Port 分配
| 服務 | 開發 Port | 正式 Port | 說明 |
|---|---|---|---|
| c9-ec | 3010 | 動態 | Nuxt SSR 伺服器 |
| c9-ims | 3011 | 8080 | Next.js 伺服器 |
| c9-be | 8080 | 8080 | NestJS API 伺服器 |
| MySQL | 3306 | 3306 | 資料庫 |
| Redis | 6379 | 6379 | 快取 |
環境變數總覽
每個專案都有獨立的環境變數配置:
| 專案 | 環境變數檔案 | 主要變數數量 |
|---|---|---|
| c9-ec | 無(使用 domainConfig 靜態配置) | - |
| c9-ims | .env.local | 5 個 |
| c9-be | .env.local | 30+ 個 |
c9-ims 環境變數
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 # 公開認證 URLc9-be 環境變數(完整模板)
# ==================== 資料庫 ====================
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-key1.4 專案間通訊
c9-ec (前台) → c9-be (後端)
| 項目 | 說明 |
|---|---|
| 傳輸協定 | HTTP/HTTPS |
| 客戶端 | 原生 fetch API,封裝在 useHttp composable |
| Base URL | 從 domainConfig[hostname].baseUrl 動態解析 |
| API 前綴 | /api |
| 認證 Header | Authorization: Bearer <JWT token> |
| 語系 Header | locales: zh-TW (從 i18n_redirected cookie 讀取) |
| 站點 Header | site-name: a1 (從 domainConfig[hostname].siteId 讀取) |
| Cookie 轉發 | SSR 時自動轉發 cookie 和 authorization headers |
請求流程:
元件 → useApi() [Facade]
→ use{Module}Api() [領域模組]
→ useHttp() / useHttpAsync() [HTTP 原語]
→ fetch() [原生 API]
→ c9-be (NestJS)c9-ims (後台) → c9-be (後端)
| 項目 | 說明 |
|---|---|
| 傳輸協定 | HTTP/HTTPS |
| 客戶端 | Axios 實例 (apiClient) |
| Base URL | 從 domainConfig[hostname].baseUrl + /api 動態解析 |
| 認證 Header | Authorization: Bearer <JWT token> (SessionSync 同步) |
| 語系 Header | locales: 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 (後端) → 外部服務
| 外部服務 | 通訊方式 | 用途 |
|---|---|---|
| BetSolutions | S2S HTTP 回調 | 遊戲投注/派彩結算 |
| RSG | S2S HTTP 回調 + DES 加解密 | 遊戲投注/派彩結算 |
| 萬通金流 | HTTP API + 回調 | ATM/信用卡入金 |
| USDT | HTTP API + 回調 | 加密貨幣入金 |
| API-Football | HTTP API (每 30 分鐘 Cron) | 即時賽事資料 |
| 台灣銀行 | tw-exchange 套件 | 即時匯率查詢 |
| Resend | HTTP API | Email 發送 |
| Twilio | HTTP API | SMS 發送 |
| Google OAuth | HTTP API | 第三方登入 |
| Telegram | Bot API | 第三方登入 |
| Cloudflare R2 | S3 API (AWS SDK) | 檔案上傳/刪除/列表 |
統一回應格式
所有 c9-be API 回應遵循統一格式:
// 成功回應
{
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 查表:
- 後端啟動時,
CommonService.onModuleInit()掃描i18n/zh-TW/*.json,建構所有數字 key 的錯誤碼 map - 前端呼叫
GET /common/enums,取得當前語系的ERROR_CODES(以 API path 為索引) - 前端收到非 200 回應時,以
ERROR_CODES[path][code]查詢可讀錯誤訊息 - 支援
:id動態路由匹配(如/api/promo/:id)
// 前端查表流程(useHttp / httpRequest 內部)
const mapped = errorCodes[data.path]?.[code]
|| errorCodes[data.path.replace(/\/[^/]+$/, '/:id')]?.[code];
if (mapped) data.message = mapped;第 2 章:開發環境建置
2.1 前置需求
必要工具
| 工具 | 最低版本 | 建議版本 | 安裝方式 |
|---|---|---|---|
| Node.js | 20.0+ | 20 LTS | nvm install 20 |
| yarn | 1.22+ | 1.22+ | npm i -g yarn |
| pnpm | 8.0+ | 9.x | npm i -g pnpm |
| MySQL | 8.0+ | 8.0+ | brew install mysql |
| Redis | 7.0+ | 7.x | brew install redis |
| gh (GitHub CLI) | - | latest | brew install gh |
| git-cz | - | latest | npm i -g git-cz |
可選工具
| 工具 | 用途 |
|---|---|
| Docker | 資料庫容器化 |
| tmux | 分屏開發 (yarn dev:split) |
| Playwright | E2E 測試瀏覽器 |
2.2 安裝步驟
第一步:Clone 與安裝根目錄
# Clone 主倉庫(含 git submodules)
git clone <repo-url> c9
cd c9
# 安裝根目錄依賴(concurrently 等)
yarn install第二步:安裝各子專案依賴
# 前台 (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 ..第三步:設定環境變數
# 後端 — 建立 .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 等設定第四步:建立資料庫
# 登入 MySQL
mysql -u root -p
# 建立資料庫
CREATE DATABASE c9 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第五步:啟動開發伺服器
# 方式一:一鍵啟動所有服務
cd c9
yarn dev
# 方式二:個別啟動
yarn dev:ec # 前台 → http://localhost:3010
yarn dev:ims # 後台 → http://localhost:3011
yarn dev:be # 後端 → http://localhost:8080/api第六步:Seed 資料庫(選填)
cd c9-be
npx ts-node scripts/seed-all.ts2.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,未設定則使用記憶體快取 |
PORT | 8080 | 伺服器監聽 Port |
NODE_ENV | 'development' | 環境模式,影響 TypeORM synchronize |
TZ | 'Asia/Seoul' | TimeService 時區 |
ADMIN_JWT_SECRET | fallback JWT_SECRET | 後台管理員 JWT 密鑰 |
R2 儲存變數
| 變數 | 說明 |
|---|---|
R2_BUCKET_NAME | R2 Bucket 名稱 |
R2_ENDPOINT | R2 API 端點 |
R2_ACCESS_KEY_ID | R2 存取金鑰 |
R2_SECRET_ACCESS_KEY | R2 密鑰 |
R2_PUBLIC_URL | R2 公開存取 URL |
Admin 預設帳號
| 變數 | 預設值 | 說明 |
|---|---|---|
ADMIN_DEFAULT_EMAIL | 'root' | 預設管理員 Email |
ADMIN_DEFAULT_PASSWORD | 'root' | 預設管理員密碼 |
ADMIN_DEFAULT_NAME | 'Root' | 預設管理員名稱 |
AdminModule 啟動時(OnModuleInit)會檢查是否已有管理員,若無則自動建立預設管理員。
OAuth 變數(選填)
| 變數 | 說明 |
|---|---|
GOOGLE_CLIENT_ID | Google OAuth Client ID |
GOOGLE_CLIENT_SECRET | Google OAuth Secret |
GOOGLE_REDIRECT_URI | Google OAuth 回調 URL |
TELEGRAM_BOT_TOKEN | Telegram Bot Token |
TELEGRAM_BOT_USERNAME | Telegram Bot Username |
Email / SMS 變數(選填)
| 變數 | 說明 |
|---|---|
RESEND_API_KEY | Resend Email API Key |
RESEND_FROM | 寄件者 Email |
TWILIO_ACCOUNT_SID | Twilio Account SID |
TWILIO_AUTH_TOKEN | Twilio Auth Token |
TWILIO_VERIFY_SERVICE_SID | Twilio Verify Service SID |
遊戲商變數(選填)
| 變數 | 說明 |
|---|---|
BS_API_URL | BetSolutions API URL |
BS_AUTH_URL | BetSolutions Auth URL |
BS_MERCHANT_ID | BetSolutions 商戶 ID |
BS_PRIVATE_KEY | BetSolutions 私鑰 |
RSG_API_URL | RSG API URL |
RSG_CLIENT_ID | RSG Client ID |
RSG_CLIENT_SECRET | RSG Client Secret |
RSG_DES_KEY | RSG DES 加密金鑰 |
RSG_DES_IV | RSG DES IV |
RSG_SYSTEM_CODE | RSG System Code |
RSG_WEB_ID | RSG Web ID |
2.4 一鍵啟動指令
根目錄指令 (c9/)
| 指令 | 說明 | 底層行為 |
|---|---|---|
yarn dev | 同時啟動三個專案 | concurrently -n ec,ims,be 彩色標籤輸出 |
yarn dev:split | tmux 分屏啟動 | 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 push | commit + 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:ims | TypeScript 型別檢查 | 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:e2e | E2E 測試 (Playwright) |
yarn test:e2e:ui | E2E 測試 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 typecheck | TypeScript 型別檢查 |
yarn lint | ESLint 檢查 |
yarn format | Prettier 格式化 |
c9-be(後端)
| 指令 | 說明 |
|---|---|
yarn dev | 開發模式 --watch (http://localhost:8080/api) |
yarn build | 正式建置 |
yarn start:prod | 正式啟動 |
yarn test | 單元測試 (Jest) |
yarn test:e2e | E2E 測試 |
yarn lint | ESLint 檢查 |
yarn format | Prettier 格式化 |
2.5 Seed 資料
後端提供 23+ 個 seed 腳本,可一鍵或個別執行:
一鍵 Seed
cd c9-be
npx ts-node scripts/seed-all.tsseed-all.ts 依序呼叫所有 seed 腳本,建立完整的測試資料集(5 個站點、30 個用戶、49 張資料表)。
個別 Seed 腳本
| 腳本 | 說明 |
|---|---|
seed-site-config.ts | 站點設定(5 個站點 + 主題) |
seed-vendor.ts | 金流群組/通道 |
seed-deposit.ts | 存款訂單 |
seed-deposit-order.ts | 存款訂單(獨立版本) |
seed-vip.ts | VIP 等級 / 反水規則 |
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 上傳 |
執行方式:
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.26 | Composition API,<script setup> 語法 |
| TypeScript | ^5.6.3 | 嚴格模式型別檢查 |
| @nuxt/ui | ^4.4.0 | 基於 Tailwind CSS v4 的 UI 元件庫 |
| @nuxtjs/i18n | 10.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 個)
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) |
| Composables | 45 | 20 業務邏輯 + 12 API + 13 型別 |
| 型別定義檔 | 13 | composables/types/ 下 |
| Plugins | 4 | 2 通用 + 2 Client-only |
| Pinia Stores | 5 | 4 Feature + 1 Facade |
| 工具函式 | 7 | utils/ 下 |
| 主題預設 | 12 | A1 (6 寶石色系) + A2 (6 金屬色系) |
| 佈局 | 2 | a1 (主要) + a2 (奢華) |
| 中介層 | 1 | auth.global.ts |
| i18n 語系 | 5 | zh-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.ts3.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 端執行
使用範例:
<!-- app.vue -->
<A2LayoutSplashScreen v-if="layoutName === 'a2'" />
<CommonSplashScreen v-else />CommonConfirmDialog
| 項目 | 說明 |
|---|---|
| 檔案 | app/components/Common/ConfirmDialog.vue |
| 用途 | 通用確認對話框,搭配 useOverlay().create() 使用 |
Props:
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
title | string | '' | 對話框標題 |
description | string | '' | 對話框描述文字 |
confirmLabel | string | '確認' | 確認按鈕文字 |
cancelLabel | string | '取消' | 取消按鈕文字 |
confirmColor | 'error' | 'primary' | 'warning' | 'primary' | 確認按鈕顏色 |
onSuccess | () => void | — | 確認後的回呼函式 |
使用範例:
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 陣列
- 顯示版權資訊
- 依
enabled和sortOrder過濾排序
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 設定 liveChatEnabled為true時注入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-hideCSS utility 隱藏捲軸 - 用於「最近遊玩」等水平排列場景
A1GameLoadMore
| 項目 | 說明 |
|---|---|
| 檔案 | app/components/A1/Game/LoadMore.vue |
| 用途 | 載入更多按鈕 |
Props:
| Prop | 型別 | 說明 |
|---|---|---|
prevCount | number | 目前顯示數量 |
nextCount | number | 下次載入數量 |
total | number | 總數量 |
step | number | 每次載入步長 |
Emits:loadMore
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) - 登入成功後檢查
redirectTocookie,有值則導向原始目標頁 - 表單驗證使用 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 確認綁定 - 使用
GoogleAuthResult和EnableGoogleAuthPayload型別
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)、遊戲類型、日期範圍
- 顯示統計摘要:總投注次數、投注金額、有效投注、輸贏
- 分頁列表顯示個別投注紀錄
- 使用
BetRecordParams和BetRecordResult型別
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 認證主頁,依狀態顯示不同內容
- 狀態:
unverified→pending→verified/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 的結構差異
| 差異點 | A1 | A2 |
|---|---|---|
| 頂部導航 | TitleBar + Sidebar | Navbar(整合式導航列) |
| 設計風格 | 深色科技遊戲風 | 奢華精品風 |
| 主題色系 | 寶石色系(emerald, amber, sky, violet, rose, cyan) | 金屬色系(champagne, roseGold, platinum, onyx, sapphire, burgundy) |
| SplashScreen | CommonSplashScreen | A2LayoutSplashScreen(獨立版本) |
A2 元件對照表
| A1 元件 | A2 對應元件 | 差異說明 |
|---|---|---|
| A1/Layout/TitleBar + Sidebar | A2/Layout/Navbar | 合併為單一導航元件 |
| A1/Layout/Footer | A2/Layout/Footer | 樣式差異 |
| A1/Layout/BottomBar | A2/Layout/BottomBar | 樣式差異 |
| A1/Layout/LiveChat | A2/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.vue | A1/Home/index 或 A2/Home/index | 否 |
/alliance | pages/alliance.vue | A1/Alliance/index 或 A2/Alliance/index | 否 |
/challenges | pages/challenges.vue | 挑戰頁面(mock data) | 否 |
/mission | pages/mission.vue | A1/Mission/index 或 A2/Mission/index | 否 |
/help | pages/help/index.vue | A1/Help/Center 或 A2/Help/Center | 否 |
/game | pages/game/index.vue | A1/Game/Lobby 或 A2/Game/Lobby | 否 |
/game/play | pages/game/play.vue | A1/Game/Play 或 A2/Game/Play | 是 |
/promo | pages/promo/index.vue | A1/Promo/Center 或 A2/Promo/Center | 否 |
/promo/[id] | pages/promo/[id].vue | A1/Promo/Detail 或 A2/Promo/Detail | 否 |
/redirect/[action] | pages/redirect/[action].vue | OAuth callback 處理 | 否 |
/user/affiliate | pages/user/affiliate.vue | A1/User/Affiliate/index | 是 |
/user/bet-record | pages/user/bet-record.vue | A1/User/BetRecord/index | 是 |
/user/deposit | pages/user/deposit.vue | A1/User/Deposit/index | 是 |
/user/inbox | pages/user/inbox.vue | A1/User/Inbox/index | 是 |
/user/kyc | pages/user/kyc.vue | A1/User/Kyc/index | 是 |
/user/setting | pages/user/setting.vue | A1/User/Setting | 是 |
/user/transaction | pages/user/transaction.vue | A1/User/Transaction/index | 是 |
/user/vip | pages/user/vip.vue | A1/User/Vip/index | 是 |
/user/wallet | pages/user/wallet.vue | A1/User/Wallet/index | 是 |
/user/withdrawal | pages/user/withdrawal.vue | A1/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.ts3.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,提供統一的請求/回應處理 |
匯出型別
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>
內部處理流程:
- 組合 URL:
baseUrl + url + buildQuery(body) - 注入 Headers:
locales、site-name、Authorization、SSR forward - 執行
middlewares.beforehook fetch()發送請求- HTTP 401 →
handle401()(清 token → 存 callback → 導回首頁 + toast) - 非 200 HTTP → 拋出 Error
- 解析 JSON → 錯誤碼攔截(
ERROR_CODES[path][code]映射 → 自訂errorMessage覆蓋 → toast) - 執行
middlewares.afterhook - 回傳 data
useHttpAsync<T>(key, url, options, asyncOptions) — SSR 版本,回傳 AsyncData<T>
內部使用 useAsyncData(key, () => useHttp(url, options), { server: true, lazy: false })
Header 注入機制
| Header | 來源 | 說明 |
|---|---|---|
locales | i18n_redirected cookie | 當前語系(預設 zh-TW) |
site-name | domainConfig.siteId | 白牌站點 ID |
Content-Type | json: true | application/json |
Authorization | token cookie | Bearer <JWT> |
cookie | SSR forward | SSR 時轉發 request cookies |
authorization | SSR forward | SSR 時轉發 authorization header |
錯誤碼三層映射
- Store 查表:
store.getEnums.ERROR_CODES[data.path][code](含:id萬用匹配) - 自訂文案覆蓋:
errorMessage(string 全覆蓋,Record 按 code 覆蓋) - Toast 顯示:
useToast().add({ description, color: 'error' })
3.4.4 useAuth — 認證狀態管理
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useAuth.ts |
| 依賴 | useStore(), useApi(), @vueuse/core |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
token | Ref<string | null> | JWT Token(cookie 持久化,48 小時) |
isLogin | ComputedRef<boolean> | 是否已登入(!!token.value) |
setToken | (v: string | null) => Promise<void> | 設定/清除 Token |
refreshUserData | () => Promise<void> | 重新取得用戶資料 |
logout | () => Promise<void> | 登出(清 token + 清 userDetail + 導回首頁) |
resendOtp | (options?) => OtpTimer | OTP 冷卻倒數計時器 |
resendOtp 回傳物件
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 解析域名設定 |
匯出
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
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 |
回傳值 — 狀態
| 名稱 | 型別 | 說明 |
|---|---|---|
currentThemeId | Ref<string> | 當前主題 ID(預設 emerald) |
currentTheme | ComputedRef<ThemePreset> | 當前主題完整物件 |
presets | Ref<ThemePreset[]> | 所有可用主題預設 |
siteCode | Ref<string> | 站點代碼 |
sitePrefix | Ref<string> | 站點前綴 |
siteLayout | Ref<string> | 站點佈局 |
siteDisplayName | Ref<string> | 站點顯示名稱 |
siteDisplayDescription | Ref<string> | 站點描述 |
supportedLocales | Ref<string[]> | 支援的語系列表 |
agentTourEnabled | Ref<boolean> | 代理導覽開關 |
agentTourIntervalSec | Ref<number> | 導覽間隔秒數(預設 604800 = 7 天) |
depositMethods | Ref<{fiat, credit, crypto}> | 存款方式開關 |
domainAssets | Ref<DomainEntry | null> | 域名素材(Logo、Favicon) |
bottomBarEnabled | Ref<boolean> | 底部導航列開關 |
bottomBarItems | Ref<BottomBarItem[]> | 底部導航項目 |
footerSections | Ref<FooterSection[]> | 頁尾區段 |
learnMoreItems | Ref<LearnMoreItem[]> | 了解更多 FAQ 項目 |
templateVariables | Ref<TemplateVariable[]> | 模板變數 |
customerServiceConfig | Ref<CustomerServiceConfig | null> | 客服設定 |
回傳值 — 方法
| 方法 | 參數 | 說明 |
|---|---|---|
setTheme | themeId: string | 切換主題(套用 CSS 變數 + 存 cookie + 更新 appConfig) |
initTheme | — | 初始化主題(從 cookie 讀取或使用預設) |
fetchSiteConfig | — | 從後端取得站點設定(主題、語系、佈局、客服等) |
fetchSiteConfig 處理邏輯
- 呼叫
getSiteConfig()API - 解析回傳的
SiteConfigResult - 更新所有
useState狀態 - 語系不一致時自動切換至第一個支援語系
- 匹配 hostname 設定域名素材(Logo、Favicon、Browser Title)
- 將 API 回傳的
activeTheme轉為ThemePreset格式並加入預設列表
3.4.7 useLayout — Modal 狀態管理
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useLayout.ts |
| 用途 | 跨元件共享 Modal 開關狀態(使用 useState) |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
loginModalOpen | Ref<boolean> | 登入 Modal 開關 |
openLoginModal | () => void | 開啟登入 Modal |
buyCryptoModalOpen | Ref<boolean> | 購買加密貨幣 Modal 開關 |
openBuyCryptoModal | () => void | 開啟購買加密貨幣 Modal |
setPasswordModalOpen | Ref<boolean> | 設定密碼 Modal 開關 |
setPasswordAccount | Ref<string> | 設定密碼的目標帳號 |
openSetPasswordModal | (account: string) => void | 開啟設定密碼 Modal |
contactModalOpen | Ref<boolean> | 聯繫客服 Modal 開關 |
openContactModal | () => void | 開啟聯繫客服 Modal |
3.4.8 useGame — 遊戲邏輯
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useGame.ts |
| 依賴 | useStore(), useI18n(), useConfig(), useApi(), gameConstants, childGame |
匯出型別
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_ENUM | object | 遊戲類型值列舉(sports:1, slot:2, live:3, ...) |
GAME_TYPE_KEY_ENUM | Record<number, string> | 遊戲類型值→鍵反查 |
provider | ProviderLogoItem[] | 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 內穩定) |
errImg | object | 圖片載入錯誤追蹤 |
3.4.9 useDevice — 裝置偵測
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useDevice.ts |
| 依賴 | @vueuse/core(useMediaQuery, useWindowSize) |
型別定義
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';參數
| 參數 | 型別 | 預設值 | 說明 |
|---|---|---|---|
desktopMinWidth | number | 768 | 桌面最小寬度門檻 |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
ua | ComputedRef<string> | User-Agent(SSR 安全) |
width | Ref<number> | 視窗寬度 |
isMobile | ComputedRef<boolean> | 手機裝置 |
isTablet | ComputedRef<boolean> | 平板裝置 |
isDesktop | ComputedRef<boolean> | 桌面裝置 |
isTouchDevice | ComputedRef<boolean> | 觸控裝置 |
isLandscape | Ref<boolean> | 橫向 |
isPortrait | Ref<boolean> | 直向 |
os | ComputedRef<OS> | 作業系統 |
isIOS, isAndroid, isMac, isWindows, isLinux | ComputedRef<boolean> | OS 旗標 |
browser | ComputedRef<Browser> | 瀏覽器 |
isSafari, isChrome, isEdge, isFirefox | ComputedRef<boolean> | 瀏覽器旗標 |
brand | ComputedRef<Brand> | 裝置品牌 |
isIPhone, isIPad, isSamsung, isPixel, isHuawei, isXiaomi, isOppo, isVivo | ComputedRef<boolean> | 品牌旗標 |
3.4.10 useCash — 金流支付
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useCash.ts |
| 依賴 | usePaymentChannels(), useExchangeRate(), useApi(), useOverlay() |
匯出型別
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) |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
channels | Ref<VendorChannel[]> | 存款通道列表 |
loadingChannels | Ref<boolean> | 載入中 |
selectedCurrency | Ref<string | undefined> | 選中幣種 |
currencyOptions | ComputedRef<SelectItem[]> | 幣種選項 |
getCurrencyOptions | (paymentMethod?) => SelectItem[] | 依支付方式過濾幣種 |
fetchDepositChannels | () => Promise<void> | 取得存款通道 |
fetchVendorChannels | () => Promise<void> | 取得金流商通道 |
bankCards | Ref<BankCard[]> | 銀行卡列表 |
fetchBankCards | (force?) => Promise<void> | 取得銀行卡(含快取) |
bankCodeData | Ref<BankCodeItem[]> | 銀行代碼列表 |
fetchBankCodeData | () => Promise<void> | 取得銀行代碼 |
creditCards | Ref<CreditCard[]> | 信用卡列表 |
fetchCreditCards | (force?) => Promise<void> | 取得信用卡(含快取) |
vendorChannels | Ref<VendorChannel[]> | 金流商通道 |
3.4.12 useExchangeRate — 匯率查詢
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useExchangeRate.ts |
| 狀態共享 | 模組級共享 |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
exchangeRateData | Ref<Record<string, any>> | 法幣匯率資料 |
loadingRate | Ref<boolean> | 載入中 |
quotedAtText | ComputedRef<string> | 報價時間文字 |
rateLookupKey | ComputedRef<string | null> | 匯率查找鍵 |
hasRateForCurrency | ComputedRef<boolean> | 是否有該幣種匯率 |
toFixedRate | (v) => string | 匯率格式化(4 位小數) |
showRate | (field: string) => string | 顯示特定欄位匯率 |
fetchExchangeRate | () => Promise<void> | 取得法幣匯率 |
cryptoRateData | Ref<Record<string, any>> | 加密貨幣匯率 |
loadingCryptoRate | Ref<boolean> | 載入中 |
fetchCryptoRate | () => Promise<void> | 取得加密貨幣匯率 |
getCryptoPrice | (coin: string) => string | 取得加密貨幣格式化價格 |
getCryptoRawPrice | (coin: string) => number | 取得加密貨幣原始價格(計算用) |
3.4.13 useAffiliate — 代理推廣
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useAffiliate.ts |
| 依賴 | useStore(), useApi() |
回傳值
| 名稱 | 參數 | 回傳 | 說明 |
|---|---|---|---|
isAgent | — | ComputedRef<boolean> | 是否為代理 |
applyAsAgent | — | Promise<{agentCode}> | 申請成為代理 |
fetchDashboard | — | Promise<AffiliateDashboard> | 取得代理儀表板 |
fetchPromoLink | — | Promise<AffiliatePromoLink> | 取得推廣連結 |
fetchDownline | AffiliateDownlineParams? | Promise<PaginatedResult> | 取得下線列表 |
fetchCommissions | AffiliateCommissionParams? | Promise<PaginatedResult> | 取得佣金紀錄 |
fetchSettlements | AffiliateSettlementParams? | Promise<PaginatedResult> | 取得結算紀錄 |
fetchSettlementDetail | id: number | Promise<AffiliateSettlementDetail> | 取得結算詳情 |
fetchBalance | — | Promise<AffiliateBalance> | 取得代理餘額 |
fetchWithdrawals | AffiliateWithdrawalParams? | Promise<PaginatedResult> | 取得提款紀錄 |
requestWithdrawal | AffiliateWithdrawalRequestPayload | Promise<ApiResponse> | 申請提款 |
fetchClickStats | AffiliateClickStatsParams? | Promise<AffiliateClickStats> | 取得點擊統計 |
fetchAllianceInfo | — | Promise<AllianceInfoResult> | 取得聯盟資訊 |
fetchReferralCodes | — | Promise<ReferralCode[]> | 取得推廣碼列表 |
createReferralCode | CreateReferralCodePayload | Promise<ApiResponse> | 建立推廣碼 |
deleteReferralCode | id: number | Promise<ApiResponse> | 刪除推廣碼 |
fetchVipMilestones | — | Promise<VipMilestoneLog[]> | 取得 VIP 里程碑紀錄 |
fetchTierInfo | — | Promise<TierInfoResult> | 取得代理等級資訊 |
3.4.14 useInbox — 站內信
| 項目 | 說明 |
|---|---|
| 檔案 | app/composables/useInbox.ts |
| 依賴 | useApi() |
回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
unreadCount | Ref<number> | 總未讀數(useState 共享) |
personalUnreadCount | Ref<number> | 個人訊息未讀數 |
systemUnreadCount | Ref<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 天後再顯示)回傳值
| 名稱 | 型別 | 說明 |
|---|---|---|
tourPhase | Ref<'highlight' | 'modal' | null> | 當前導覽階段 |
showTour | Ref<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。
匯出型別
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;
}回傳值
| 名稱 | 說明 |
|---|---|
kycState | Ref<KycState>(useState 共享) |
submitKyc | 提交 KYC(設為 pending) |
fetchKycStatus | 從 cookie 載入狀態 |
mockApprove | Mock 通過 |
mockReject | Mock 拒絕 |
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 | 路徑 | 說明 |
|---|---|---|---|
login | POST | /auth/login | 帳密登入 |
register | POST | /auth/register | 註冊 |
getUserDetailSsr | GET | /auth/user-detail | 用戶資料(SSR) |
getUserDetailCsr | GET | /auth/user-detail | 用戶資料(CSR) |
getCountryCodesSsr | GET | /auth/country-codes | 國碼列表(SSR) |
getCountryCodesCsr | GET | /auth/country-codes | 國碼列表(CSR) |
getLoginConfigSsr | GET | /auth/login-config | 登入設定(SSR) |
getLoginConfigCsr | GET | /auth/login-config | 登入設定(CSR) |
sendVerifyEmail | POST | /auth/send-verify-email | 發送 Email 驗證碼 |
checkVerifyEmail | POST | /auth/check-verify-email | 確認 Email 驗證碼 |
sendVerifyMobile | POST | /auth/send-verify-mobile | 發送手機 OTP |
checkVerifyMobile | POST | /auth/check-verify-mobile | 確認手機 OTP |
generateGoogleAuth | GET | /auth/google-auth/generate | 產生 2FA QR Code |
enableGoogleAuth | POST | /auth/google-auth/enable | 啟用 2FA |
editPassword | POST | /auth/edit-password | 修改密碼 |
setPassword | POST | /auth/set-password | 設定密碼(OAuth) |
loginGoogle | POST | /auth/login-google | Google OAuth 登入 |
loginTelegram | POST | /auth/login-telegram | Telegram 登入 |
logout | POST | /auth/logout | 登出 |
updateLocale | PATCH | /auth/locale | 更新語系偏好 |
getMascots | GET | /auth/mascots | 取得吉祥物列表 |
updateAvatar | PATCH | /auth/avatar | 更新頭像 |
bindGoogle | POST | /auth/bind-google | 綁定 Google |
bindTelegram | POST | /auth/bind-telegram | 綁定 Telegram |
useGameApi — 遊戲 API(10 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getGameProviderSsr | GET | /game/providers | 遊戲商列表(SSR) |
getGameProviderCsr | GET | /game/providers | 遊戲商列表(CSR) |
gameLaunch | POST | /game/launch | 啟動遊戲 |
gameSimulate | POST | /game/simulate | 模擬遊戲結果 |
getGameTypeConfigsCsr | GET | /game/type-configs | 遊戲分類設定 |
getRecentGamesCsr | GET | /game/recent | 最近遊玩 |
getEnumsSsr | GET | /common/enums | 枚舉(SSR) |
getEnumsCsr | GET | /common/enums | 枚舉(CSR) |
getRanking | GET | /ranking | 排行榜 |
getBetRecords | GET | /bet-record | 投注紀錄 |
useWalletApi — 錢包 API(12 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getBankCards | GET | /wallet/bank-cards | 銀行卡列表 |
addBankCard | POST | /wallet/bank-cards | 新增銀行卡(FormData) |
deleteBankCard | DELETE | /wallet/bank-cards/:id | 刪除銀行卡 |
getCreditCards | GET | /wallet/credit-cards | 信用卡列表 |
addCreditCard | POST | /wallet/credit-cards | 新增信用卡 |
deleteCreditCard | DELETE | /wallet/credit-cards/:id | 刪除信用卡 |
getCryptoAddresses | GET | /wallet/crypto-addresses | 加密錢包列表 |
addCryptoAddress | POST | /wallet/crypto-addresses | 新增加密錢包 |
deleteCryptoAddress | DELETE | /wallet/crypto-addresses/:id | 刪除加密錢包 |
getVendorChannels | GET | /vendor/channels | 金流商通道 |
wantongAddAtm | POST | /vendor/wantong/atm | 萬通 ATM 預登錄 |
wantongAddCard | POST | /vendor/wantong/card | 萬通信用卡預登錄 |
useDepositApi — 存款 API(6 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getExchangeRate | GET | /deposit/exchange-rate | 法幣匯率 |
getCryptoRate | GET | /deposit/crypto-rate | 加密貨幣匯率 |
getDepositChannels | GET | /deposit/channels | 存款通道 |
deposit | POST | /deposit | 建立存款訂單 |
getDepositOrders | GET | /deposit/orders | 存款紀錄 |
confirmDeposit | POST | /deposit/confirm | 確認存款 |
usePromoApi — 活動 API(8 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getPromos | GET | /promo | 活動列表 |
getPromoById | GET | /promo/:id | 活動詳情 |
claimPromo | POST | /promo/:id/claim | 領取活動 |
getPromoClaims | GET | /promo/claims | 領取紀錄 |
getPromoTagsCsr | GET | /promo/tags | 活動標籤 |
createPromo | POST | /promo | 建立活動(Admin) |
updatePromo | PATCH | /promo/:id | 更新活動(Admin) |
deletePromo | DELETE | /promo/:id | 刪除活動(Admin) |
useVipApi — VIP API(11 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getVipLevels | GET | /vip/levels | VIP 等級列表 |
getVipRebates | GET | /vip/rebates | 反水率列表 |
getVipStatus | GET | /vip/status | VIP 狀態 |
createVipLevel | POST | /vip/admin/levels | 建立等級(Admin) |
updateVipLevel | PATCH | /vip/admin/levels/:id | 更新等級(Admin) |
deleteVipLevel | DELETE | /vip/admin/levels/:id | 刪除等級(Admin) |
createVipRebate | POST | /vip/admin/rebates | 建立反水規則(Admin) |
updateVipRebate | PATCH | /vip/admin/rebates/:id | 更新反水規則(Admin) |
deleteVipRebate | DELETE | /vip/admin/rebates/:id | 刪除反水規則(Admin) |
triggerDailyRebate | POST | /vip/admin/trigger-daily | 觸發每日反水(Admin) |
triggerMonthlyRelegation | POST | /vip/admin/trigger-monthly | 觸發月度保級(Admin) |
useAffiliateApi — 代理 API(35 方法)
涵蓋:追蹤點擊、代理管理、儀表板、下線、佣金、結算、提款、聯盟(推廣碼/VIP 里程碑/代理等級/佣金費率)、Admin 操作。
useInboxApi — 站內信 API(7 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getInbox | GET | /inbox | 收件匣列表 |
getInboxUnreadCount | GET | /inbox/unread-count | 未讀數量 |
markInboxRead | PATCH | /inbox/:id/read | 標記已讀 |
markInboxReadAll | PATCH | /inbox/read-all | 全部已讀 |
adminSendInbox | POST | /inbox/admin/send | 發送站內信(Admin) |
adminGetInbox | GET | /inbox/admin/list | 站內信列表(Admin) |
adminDeleteInbox | DELETE | /inbox/admin/:id | 刪除站內信(Admin) |
useSiteConfigApi — 站點設定 API(7 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getSiteConfig | GET | /site-config | 取得站點設定 |
adminGetSiteConfigs | GET | /site-config/admin/list | 站點列表(Admin) |
adminUpdateSiteConfig | PATCH | /site-config/admin/:id | 更新站點(Admin) |
adminGetSiteThemes | GET | /site-config/admin/:id/themes | 主題列表(Admin) |
adminCreateSiteTheme | POST | /site-config/admin/:id/themes | 建立主題(Admin) |
adminUpdateSiteTheme | PATCH | /site-config/admin/themes/:id | 更新主題(Admin) |
adminDeleteSiteTheme | DELETE | /site-config/admin/themes/:id | 刪除主題(Admin) |
useWithdrawalApi — 提領 API(7 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
withdrawalSendCode | POST | /withdrawal/send-code | 發送提領驗證碼 |
withdrawalRequest | POST | /withdrawal/request | 建立提領申請 |
getWithdrawalList | GET | /withdrawal/list | 提領紀錄 |
getTurnoverStatus | GET | /withdrawal/turnover-status | 打碼量狀態 |
adminGetWithdrawalList | GET | /withdrawal/admin/list | 提領列表(Admin) |
adminReviewWithdrawalOrder | PATCH | /withdrawal/admin/:id/review | 審核提領(Admin) |
adminCompleteWithdrawalOrder | PATCH | /withdrawal/admin/:id/complete | 完成提領(Admin) |
useMissionApi — 任務 API(3 方法)
| 方法 | HTTP | 路徑 | 說明 |
|---|---|---|---|
getMissions | GET | /mission | 任務列表 |
getMissionClaims | GET | /mission/claims | 領取紀錄 |
claimMission | POST | /mission/:id/claim | 領取任務獎勵 |
3.5 型別定義完整文件
所有型別定義位於
app/composables/types/,共 13 個檔案。index.tsre-export 所有模組,useApiTypes.ts提供便捷別名 re-export。
3.5.1 common.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 個介面)
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 個介面)
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 — 錢包型別
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 — 存款型別
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 — 活動型別
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 型別
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 — 任務型別
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 — 站內信型別
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 個介面)
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 — 提領型別
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) |
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) |
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 使用
頁面使用範例:
<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 後綴) |
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 |
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 讀取
refCode或ref參數 - 採用 first-click-wins 策略(不覆蓋已有推廣碼)
- 存入 cookie(30 天有效期)
- Fire-and-forget 呼叫
trackClickAPI
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 | 型別 | 說明 |
|---|---|---|
isReady | ref(false) | App 是否就緒(SplashScreen 依此淡出) |
isLoading | ref(false) | 全域載入中狀態 |
doms | ref({}) | DOM 參考(保留欄位) |
enums | ref({}) | 後端枚舉快取(ERROR_CODES 等) |
| Action | 參數 | 說明 |
|---|---|---|
setIsReady | boolean | 設定就緒狀態 |
setIsRoading | boolean | 設定載入狀態(注意原始碼拼寫為 Roading) |
setDoms | Record<string, any> | 設定 DOM 參考 |
setEnums | Partial<EnumsResult> | 設定枚舉資料 |
| Getter | 型別 | 說明 |
|---|---|---|
getIsReady | ComputedRef<boolean> | |
getIsRoading | ComputedRef<boolean> | |
getDoms | ComputedRef<Record<string, any>> | |
getEnums | ComputedRef<Partial<EnumsResult>> |
3.7.3 useUserStore — 用戶狀態
| 項目 | 說明 |
|---|---|
| 檔案 | app/stores/userStore.ts |
| Store ID | 'user' |
| State | 型別 | 說明 |
|---|---|---|
userDetail | ref<Partial<UserDetail>>({}) | 用戶詳細資料 |
countryCodes | ref<CountryCode[]>([]) | 國碼列表 |
loginConfig | ref<LoginConfigResult>({ google: '' }) | 登入設定(OAuth client ID 等) |
| Action | 參數 | 說明 |
|---|---|---|
setUserDetail | Partial<UserDetail> | 設定用戶資料 |
setCountryCodes | CountryCode[] | 設定國碼列表 |
setLoginConfig | LoginConfigResult | 設定登入設定 |
3.7.4 useGameStore — 遊戲狀態
| 項目 | 說明 |
|---|---|
| 檔案 | app/stores/gameStore.ts |
| Store ID | 'game-data' |
| State | 型別 | 說明 |
|---|---|---|
gameList | Ref<GameListResult> | 遊戲列表(含 mapping、provider、狀態分類) |
gameTypeConfigs | ref<GameTypeConfig[]>([]) | 遊戲分類設定 |
3.7.5 usePromoStore — 活動狀態
| 項目 | 說明 |
|---|---|
| 檔案 | app/stores/promoStore.ts |
| Store ID | 'promo' |
| State | 型別 | 說明 |
|---|---|---|
promoTags | ref<PromoTag[]>([]) | 活動標籤列表 |
3.7.6 useMainStore — Facade Store
| 項目 | 說明 |
|---|---|
| 檔案 | app/stores/index.ts |
| Store ID | 'main' |
Facade 模式:組合所有 Feature Store 的 actions 和 getters 成單一介面。
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 自訂屬性:
@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 介面
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 組寶石色系)
| themeId | themeName | 圖示 | 預覽色 | glow |
|---|---|---|---|---|
emerald | theme.emerald | i-lucide-leaf | #34d399 | 16, 185, 129 |
amber | theme.amber | i-lucide-flame | #fbbf24 | 245, 158, 11 |
sky | theme.sky | i-lucide-cloud | #38bdf8 | 14, 165, 233 |
violet | theme.violet | i-lucide-sparkles | #a78bfa | 139, 92, 246 |
rose | theme.rose | i-lucide-heart | #fb7185 | 244, 63, 94 |
cyan | theme.cyan | i-lucide-droplets | #22d3ee | 6, 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 組金屬色系)
| themeId | themeName | 圖示 | 預覽色 | glow |
|---|---|---|---|---|
champagne | theme.champagne | i-lucide-crown | #d4a574 | 212, 165, 116 |
roseGold | theme.roseGold | i-lucide-gem | #b76e79 | 183, 110, 121 |
platinum | theme.platinum | i-lucide-shield | #8c9ead | 140, 158, 173 |
onyx | theme.onyx | i-lucide-diamond | #4a4a4a | 74, 74, 74 |
sapphire | theme.sapphire | i-lucide-hexagon | #2d5f8a | 45, 95, 138 |
burgundy | theme.burgundy | i-lucide-wine | #800020 | 128, 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() 轉換:
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:
<template>
<p>{{ $t('game.gameLobby') }}</p>
</template>Script:
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 |
vip | VIP | vip.title, vip.level, vip.progress, vip.benefits, vip.rebate |
tier | VIP 等級 | 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 |
kyc | KYC 認證 | 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自動在每個請求的localesheader 中帶入當前語系- 後端依此 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 介面
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
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
export const entries: Record<string, DomainConfigEntry> = {
// 預留給 A2 站點的域名映射
};3.10.4 解析函式
const DEFAULT_DOMAIN = 'localhost';
function resolveDomainConfig(hostname: string): DomainConfigEntry {
return domainConfig[hostname] ?? domainConfig[DEFAULT_DOMAIN]!;
}- 精確匹配 hostname
- 找不到時 fallback 至
localhost設定
3.10.5 新增站點流程
- 建立
config/domainConfig/{siteCode}.ts檔案 - 匯出
entriesRecord - 在
config/domainConfig/index.tsimport 並展開至domainConfig - 確保後端
site-config表有對應記錄
3.11 路由守衛與中介層
3.11.1 auth.global.ts — 全域認證守衛
| 項目 | 說明 |
|---|---|
| 檔案 | app/middleware/auth.global.ts |
| 類型 | 全域中介層(.global.ts 後綴自動載入) |
| 保護路徑 | /user/* |
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('/');
});攔截流程:
- 檢查目標路徑是否在
PROTECTED_PREFIXES中 - 檢查
tokencookie 是否存在 - 無 token → 存目標路徑至
redirectTocookie → 開啟登入 Modal → 導回首頁 - 登入成功後,Login Modal 讀取
redirectTocookie 並導回原始目標頁
3.12 工具函式(Utils)
3.12.1 utsFormat — 格式化工具
| 項目 | 說明 |
|---|---|
| 檔案 | app/utils/utsFormat.ts |
| 依賴 | moment-timezone, useI18n() |
| 方法 | 參數 | 回傳 | 說明 |
|---|---|---|---|
formatNumber | val, minDecimals=0, maxDecimals=2 | string | 千分位數字格式化 |
formatAmount | val | string | 固定 2 位小數金額 |
formatAmountisFinite | val | string | 安全金額格式化(非數值回傳 —) |
formatDate | dateStr, fmt='YYYY/MM/DD' | string | 日期格式化 |
formatDateTime | dateStr | string | 日期時間(YYYY-MM-DD HH:mm) |
formatDateHas | dateStr | string | 有值顯示日期時間,無值顯示 — |
formatDateShort | dateStr | string | 短日期(MM/DD + 截止) |
formatTime | isoStr | string | 時間(上午/下午 HH:mm) |
3.12.2 utsVipTier — VIP 等級樣式
| 項目 | 說明 |
|---|---|
| 檔案 | app/utils/utsVipTier.ts |
| 依賴 | useI18n() |
VipTierStyle 介面
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 |
|---|---|---|---|
bronze | text-amber-500 | noto:3rd-place-medal | warning |
gold | text-amber-400 | noto:1st-place-medal | warning |
platinum | text-blue-400 | noto:gem-stone | info |
diamond | text-purple-400 | noto:crown | error |
方法
| 方法 | 說明 |
|---|---|
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
| Key | Value | 說明 |
|---|---|---|
sports | 1 | 體育 |
slot | 2 | 老虎機 |
live | 3 | 真人 |
lottery | 4 | 彩票 |
chess | 5 | 棋牌 |
esports | 8 | 電競 |
crypto | 9 | 加密遊戲 |
fish | 10 | 捕魚 |
CHILD_GAME_TYPES
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 結構
@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 |
| 兩個 Project | unit(node 環境)+ nuxt(@nuxt/test-utils + happy-dom) |
| 指令 | 說明 |
|---|---|
yarn test | 跑全部測試 |
yarn test:unit | 只跑單元測試 |
yarn test:nuxt | 只跑元件測試 |
yarn test:watch | Watch 模式 |
3.14.2 E2E 測試(Playwright)
| 項目 | 說明 |
|---|---|
| 設定檔 | playwright.config.ts |
| 瀏覽器 | Desktop Chromium |
| 並行 | fullyParallel: true |
| CI 重試 | 2 次 |
| 指令 | 說明 |
|---|---|
yarn test:e2e | 跑全部 E2E |
yarn test:e2e:ui | UI 模式 |
3.14.3 Vitest 設定檔完整內容
// 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 的差異:
| 屬性 | unit | nuxt |
|---|---|---|
| 環境 | node(純邏輯測試) | nuxt(@nuxt/test-utils,模擬 Nuxt 運行環境) |
| DOM | 無 | happy-dom(輕量 DOM 模擬) |
| 適用場景 | 工具函式、型別、常數 | Vue 元件渲染、composable 測試 |
| 目錄 | test/unit/ | test/nuxt/ |
3.14.4 Playwright 設定檔完整內容
// 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 | ./tests | E2E 測試檔案目錄 |
fullyParallel | true | 測試案例完全並行執行 |
forbidOnly | CI 環境為 true | CI 環境禁止 .only |
retries | CI: 2, 本地: 0 | CI 環境失敗重試 2 次 |
workers | CI: 1, 本地: auto | CI 環境單 worker,本地依 CPU |
reporter | 'html' | 產生 HTML 格式報告 |
trace | 'on-first-retry' | 首次重試時收集 trace |
nuxt.rootDir | 專案根目錄 | @nuxt/test-utils 整合 |
projects | Chromium | 僅使用 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 佈局總覽
| 佈局 | 檔案 | 風格 | 說明 |
|---|---|---|---|
a1 | layouts/a1.vue | 暗色科技風 | Dashboard 式佈局,側邊欄 + 頂部導航 |
a2 | layouts/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 | 聯繫客服 |
A1LayoutLiveChat | LiveChat 腳本注入 |
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-navbar | rgba(20, 17, 16, 0.95) | 導航列背景(半透明毛玻璃) |
--a2-bg-footer | #0f0c08 | 頁尾背景 |
--a2-bg-hover | rgba(255, 255, 255, 0.04) | Hover 狀態 |
--a2-bg-active | rgba(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-subtle | rgba(255, 255, 255, 0.04) | 極細邊框 |
--a2-border-default | rgba(255, 255, 255, 0.08) | 預設邊框 |
--a2-border-strong | rgba(255, 255, 255, 0.15) | 強調邊框 |
--a2-border-light | rgba(255, 255, 255, 0.06) | 淡色邊框 |
主色調變數(暗面金色系,可被主題系統覆寫)
| 變數 | 值 | 說明 |
|---|---|---|
--a2-primary-50 | rgba(184, 148, 63, 0.06) | 極淡 |
--a2-primary-100 | rgba(184, 148, 63, 0.12) | 淡 |
--a2-primary-200 | rgba(184, 148, 63, 0.2) | 輕 |
--a2-primary-300 | rgba(184, 148, 63, 0.3) | 中淡 |
--a2-primary-400 | var(--c9-primary-400, #c99240) | 中(可被主題覆寫) |
--a2-primary-500 | var(--c9-primary-500, #b07a30) | 中強(可被主題覆寫) |
--a2-primary-600 | var(--c9-primary-600, #946328) | 強(可被主題覆寫) |
--a2-primary-700 | var(--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-sm | 0 1px 3px rgba(0, 0, 0, 0.3) |
--a2-shadow-md | 0 4px 16px rgba(0, 0, 0, 0.35) |
--a2-shadow-lg | 0 12px 40px rgba(0, 0, 0, 0.4) |
其他
| 變數 | 值 | 用途 |
|---|---|---|
--a2-radius | 1rem | 預設圓角 |
Nuxt UI 主色覆寫
A2 佈局在 .a2-layout scope 內覆寫 Nuxt UI 的 --ui-color-primary-* 變數:
.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 toggles3.15.6 佈局選擇機制
佈局由 useConfig() 解析域名時決定:
hostname → domainConfig[hostname].layout → layoutName
app.vue: const layoutName = (layout || 'a1') as 'a1' | 'a2'
→ <NuxtLayout :name="layoutName"> 動態切換佈局SplashScreen 也依佈局切換:
<A2LayoutSplashScreen v-if="layoutName === 'a2'" />
<CommonSplashScreen v-else />3.15.7 A1 vs A2 設計對比
| 特性 | A1 | A2 |
|---|---|---|
| 導航模式 | 側邊欄(UDashboardGroup) | 頂部水平導航列 |
| 色調 | 科技深藍(#0a1120) | 奢華深金(#141110) |
| 主題色 | 寶石色系(emerald/amber/sky/violet/rose/cyan) | 金屬色系(champagne/roseGold/platinum/onyx/sapphire/burgundy) |
| 邊框 | ring-white/10 | ring-[var(--a2-border-default)] |
| 文字 | text-white/80 | text-[var(--a2-text-primary)](暖白 #f5f0e8) |
| 按鈕 | bg-emerald-500 漸層 | 金色漸層(--a2-gold) |
| 圓角 | rounded-full / rounded-xl | rounded-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-sparkle | 10 個金色星點隨機閃爍(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.vue | A2/Home/index.vue |
/alliance | A1/Alliance/index.vue | A2/Alliance/index.vue |
/challenges | A1/Mission/index.vue | A2/Mission/index.vue |
/mission | A1/Mission/index.vue | A2/Mission/index.vue |
/help | A1/Help/Center.vue | A2/Help/Center.vue |
/game | A1/Game/index.vue | A2/Game/index.vue |
/game/play | A1/Game/Play.vue | A2/Game/Play.vue |
/promo | A1/Promo/Center.vue | A2/Promo/Center.vue |
/promo/[id] | A1/Promo/Detail.vue | A2/Promo/Detail.vue |
/user/affiliate | A1/User/Affiliate/index.vue | A2/User/Affiliate/index.vue |
/user/bet-record | A1/User/BetRecord/index.vue | A2/User/BetRecord/index.vue |
/user/deposit | A1/User/Deposit/index.vue | A2/User/Deposit/index.vue |
/user/inbox | A1/User/Inbox/index.vue | A2/User/Inbox/index.vue |
/user/kyc | A1/User/Kyc/index.vue | A2/User/Kyc/index.vue |
/user/setting | A1/User/Setting.vue | A2/User/Setting.vue |
/user/transaction | A1/User/Transaction/index.vue | A2/User/Transaction/index.vue |
/user/vip | A1/User/Vip/index.vue | A2/User/Vip/index.vue |
/user/wallet | A1/User/Wallet/index.vue | A2/User/Wallet/index.vue |
/user/withdrawal | A1/User/Withdrawal/index.vue | A2/User/Withdrawal/index.vue |
3.16.3 標準頁面模式(vueVersion 模式)
除 redirect/[action].vue 外,所有頁面均使用統一的 vueVersion 模式:
<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 路由滾動行為
// 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 佈局快捷鍵
Sidebar 快捷鍵
| 快捷鍵 | 動作 | 目標路徑 |
|---|---|---|
Shift+H | 首頁 | / |
Shift+G | 遊戲大廳 | /game |
Shift+P | 活動中心 | /promo |
Shift+V | VIP 中心 | /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 | 搜尋 | 開啟搜尋 |
Navbar 快捷鍵
| 快捷鍵 | 動作 |
|---|---|
M | 切換使用者選單 |
D | 存款 |
W | 提領 |
Shift+U | 個人設定 |
Shift+Y | KYC 認證 |
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 完整設定
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.port | 3010 | 開發伺服器端口 |
app.head.meta.viewport | ...user-scalable=no | 禁止使用者縮放(搭配 usePreventZoom) |
css | ['~/assets/css/global.scss', '~/assets/css/main.css'] | 全域樣式載入順序 |
3.18.3 app.config.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/ 資料夾) |
locales | 5 個語系物件 | 每個含 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 對應 |
|---|---|---|---|
A2LayoutNavbar | Layout/Navbar.vue | 頂部導航列(940 行,含動態遊戲選單、固定右側面板) | A1LayoutSidebar + a1.vue Navbar |
A2LayoutFooter | Layout/Footer.vue | 頁尾 | A1LayoutFooter |
A2LayoutBottomBar | Layout/BottomBar.vue | 行動版底部導航 | A1LayoutBottomBar |
A2LayoutLiveChat | Layout/LiveChat.vue | LiveChat 腳本注入 | A1LayoutLiveChat |
A2LayoutSplashScreen | Layout/SplashScreen.vue | 奢華啟動動畫(208 行,含 SVG 金環動畫) | CommonSplashScreen |
3.19.3 A2 Home 元件(4 個)
| 元件 | 檔案 | 用途 |
|---|---|---|
A2Home | Home/index.vue | 首頁組合(Banner + LiveSports + Promo + Lobby + Rankings + FAQ + Partners) |
A2HomeBanner | Home/Banner.vue | 首頁輪播 Banner |
A2HomeLiveSports | Home/LiveSports.vue | 即時體育賽事 |
A2HomePromo | Home/Promo.vue | 首頁活動推薦 |
A2 首頁組合結構:
A2Home
├── A2HomeBanner # Banner 輪播
├── A2HomeLiveSports # 即時賽事
├── A2HomePromo # 活動推薦
├── A2GameLobby # 遊戲大廳
├── A2GameRankList # 排行榜
├── FAQ (UAccordion) # 常見問題(優先後台設定,fallback i18n)
└── Partners Grid # 遊戲供應商 Logo 網格3.19.4 A2 Game 元件(10 個)
| 元件 | 檔案 | 用途 |
|---|---|---|
A2Game | Game/index.vue | 遊戲模組入口 |
A2GameLobby | Game/Lobby.vue | 遊戲大廳(含最近遊玩) |
A2GamePlay | Game/Play.vue | 遊戲啟動 iframe |
A2GameSearch | Game/Search.vue | 遊戲搜尋 |
A2GameProvider | Game/Provider.vue | 遊戲供應商卡片 |
A2GameRankList | Game/RankList.vue | 排行榜 |
A2GameListBar | Game/ListBar.vue | 水平捲動列表 |
A2GameLoadMore | Game/LoadMore.vue | 載入更多按鈕 |
A2GameEmpty | Game/Empty.vue | 空狀態提示 |
3.19.5 A2 Modal 元件(15 個)
| 元件 | 檔案 | 用途 |
|---|---|---|
A2ModalLogin | Modal/Login.vue | 登入 |
A2ModalRegister | Modal/Register.vue | 註冊 |
A2ModalEditPassword | Modal/EditPassword.vue | 修改密碼 |
A2ModalSetPassword | Modal/SetPassword.vue | 設定密碼 |
A2ModalTheme | Modal/Theme.vue | 主題切換 |
A2ModalLocale | Modal/Locale.vue | 語系切換 |
A2ModalVerifyUserInfo | Modal/VerifyUserInfo.vue | 用戶資訊驗證 |
A2ModalContactSupport | Modal/ContactSupport.vue | 聯繫客服 |
A2ModalBuyCrypto | Modal/BuyCrypto.vue | 購買加密貨幣引導 |
A2ModalBindGoogleAuth | Modal/BindGoogleAuth.vue | Google Authenticator 2FA |
A2ModalAddBankCard | Modal/AddBankCard.vue | 新增銀行卡 |
A2ModalAddCreditCard | Modal/AddCreditCard.vue | 新增信用卡 |
A2ModalAddCryptoAddress | Modal/AddCryptoAddress.vue | 新增加密錢包 |
A2ModalBankCardDetail | Modal/BankCardDetail.vue | 銀行卡詳情 |
A2ModalAgentTour | Modal/AgentTour.vue | 代理推廣導覽 |
3.19.6 A2 User 元件(35 個)
| 子目錄 | 數量 | 元件 |
|---|---|---|
| (根) | 1 | Setting |
BetRecord/ | 1 | index |
Deposit/ | 4 | index, Fiat, Credit, Crypto |
Withdrawal/ | 1 | index |
Wallet/ | 4 | index, Fiat, Credit, Crypto |
Transaction/ | 5 | index, Deposit, Withdrawal, Dividend, Promo |
Vip/ | 6 | index, StatusCard, LevelList, Benefits, MyRebates, RebateTable |
Affiliate/ | 7 | index, Dashboard, Downline, Commission, Settlement, Withdrawal, Alliance |
Inbox/ | 1 | index |
Kyc/ | 6 | index, StatusCard, StepBasicInfo, StepDocUpload, StepLiveness, StepReview |
3.19.7 A2 其他元件
| 元件 | 檔案 | 用途 |
|---|---|---|
A2Alliance | Alliance/index.vue | 聯盟計劃 |
A2HelpCenter | Help/Center.vue | 幫助中心 |
A2Mission | Mission/index.vue | 任務列表 |
A2PromoCenter | Promo/Center.vue | 活動中心 |
A2PromoDetail | Promo/Detail.vue | 活動詳情 |
A2PromoLinkCard | PromoLinkCard.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 完整元件數量
| 分類 | A1 | A2 | Common | 合計 |
|---|---|---|---|---|
| Layout | 5 | 5 | 0 | 10 |
| Home | 4 | 4 | 0 | 8 |
| Game | 10 | 10 | 0 | 20 |
| User | 40 | 35 | 0 | 75 |
| Modal | 16 | 15 | 0 | 31 |
| Promo | 3 | 3 | 0 | 6 |
| Alliance | 1 | 1 | 0 | 2 |
| Mission | 1 | 1 | 0 | 2 |
| Help | 1 | 1 | 0 | 2 |
| SplashScreen | 0 | 0 | 2 | 2 |
| ConfirmDialog | 0 | 0 | 0 | 0 |
| 合計 | 81 | 75 | 2 | 158 |
注:A1 多出的元件主要在 Modal(
VerifyUserInfo額外一個變體)和 User(A1 含更多分頁子元件)。
3.21.2 Composable 數量
| 分類 | 數量 |
|---|---|
| 業務邏輯 | 20 |
| API(模組化) | 12 |
| 型別定義 | 13 |
| 合計 | 45 |
3.21.3 完整專案檔案統計
| 類別 | 數量 | 說明 |
|---|---|---|
| Pages | 20 | 檔案路由 |
| Components | 158 | A1 (81) + A2 (75) + Common (2) |
| Composables | 45 | 20 業務 + 12 API + 13 型別 |
| Plugins | 4 | 2 通用 + 2 client-only |
| Stores | 5 | 4 feature + 1 facade |
| Utils | 7 | 工具函式 |
| Layouts | 2 | a1 + a2 |
| Middleware | 1 | auth.global.ts |
| Config | 3 | domainConfig/index + a1 + a2 |
| i18n Locales | 5 | zh-TW, en-US, zh-CN, th-TH, vi-VN |
| Theme Presets | 12 | A1 (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.04.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.yaml4.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
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):
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",
},
};解析函式:
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
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 個)
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 格式)
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
// 取得完整站點配置
export function useConfig(): SiteConfig;
// 快捷 Hook:取得功能開關
export function useFeatureFlags(): FeatureFlags;
// 快捷 Hook:取得主題配置
export function useSiteTheme(): Theme;4.3.7 站點註冊表
位置:src/config/siteRegistry.ts
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 | 參數 | 說明 |
|---|---|---|---|---|
getEnums | GET | /common/enums | 無 | 取得後端所有枚舉(含 ERROR_CODES) |
adminLogin | POST | /admin/login | { email: string; password: string } | 管理員登入,回傳 JWT token + admin 資訊 |
adminSendVerifyCode | POST | /admin/send-verify-code | { email: string } | 發送 Email 驗證碼(管理員註冊用) |
adminVerifyEmail | POST | /admin/verify-email | { email: string; code: string } | 驗證 Email 驗證碼 |
adminRegister | POST | /admin/register | { email, password, name, code, groupId? } | 管理員註冊(需先驗證 Email) |
getAdminProfile | GET | /admin/profile | 無 | 取得當前管理員個人資料 |
generateGoogleAuth | POST | /admin/google-auth/generate | 無 | 產生 Google Authenticator QR Code + Secret |
enableGoogleAuth | POST | /admin/google-auth/enable | { code: string } | 啟用 2FA(需輸入 TOTP 驗證碼) |
disableGoogleAuth | POST | /admin/google-auth/disable | { code: string } | 停用 2FA(需輸入 TOTP 驗證碼) |
使用範例:
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 | 參數 | 說明 |
|---|---|---|---|---|
getAdmins | GET | /admin/list | QueryParams & { keyword?, status?, groupId?, startDate?, endDate? } | 管理員列表(分頁 + 篩選) |
getAdmin | GET | /admin/:id | id: number | 取得單一管理員詳情 |
createAdmin | POST | /admin/create | { email, password, name, groupId? } | 新增管理員 |
updateAdmin | PATCH | /admin/:id | id: number, payload: Record<string, unknown> | 更新管理員(name, password, groupId, status, allowedSiteCodes) |
deleteAdmin | DELETE | /admin/:id | id: number | 刪除管理員 |
群組 CRUD
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getGroups | GET | /admin/groups/list | QueryParams & { keyword?, status?, startDate?, endDate? } | 群組列表 |
getGroup | GET | /admin/groups/:id | id: number | 取得單一群組(含權限列表) |
createGroup | POST | /admin/groups/create | { name, permissions?, description? } | 新增群組 |
updateGroup | PATCH | /admin/groups/:id | id: number, payload | 更新群組 |
deleteGroup | DELETE | /admin/groups/:id | id: number | 刪除群組 |
操作紀錄
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getAdminLogs | GET | /admin/logs/list | QueryParams & Record<string, unknown> | 管理員操作紀錄列表 |
R2 儲存操作紀錄
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getR2Logs | GET | /admin/reports/r2-logs | QueryParams & Record<string, unknown> | R2 操作日誌(上傳/刪除紀錄) |
R2 檔案管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
r2List | GET | /admin/r2/list | { prefix?, continuationToken?, maxKeys?, siteConfigId? } | 列出 R2 物件(檔案 + 資料夾) |
r2Upload | POST | /admin/r2/upload | FormData(含 file + key + siteConfigId) | 上傳檔案至 R2 |
r2Delete | POST | /admin/r2/delete | { keys: string[], siteConfigId? } | 批次刪除 R2 物件 |
r2Move | POST | /admin/r2/move | { sourceKey, destinationKey, isFolder, siteConfigId? } | 移動/重命名 R2 物件 |
r2CreateFolder | POST | /admin/r2/create-folder | { name, prefix?, siteConfigId? } | 建立 R2 資料夾 |
r2DeleteFolder | POST | /admin/r2/delete-folder | { prefix, siteConfigId? } | 刪除 R2 資料夾(含子項目) |
4.4.3 useFinanceApi — 財務管理
位置:src/hooks/api/useFinanceApi.ts
存款相關
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getDeposits | GET | /deposit | QueryParams & Record<string, unknown> | 存款列表 |
getDepositReview | GET | /admin/finance/deposit-review | QueryParams & { status?, startDate?, endDate? } | 存款審核列表 |
reviewDeposit | PATCH | /admin/finance/deposit-review/:id | id, { action: "approve"|"reject", rejectReason? } | 審核存款訂單 |
提領相關
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getWithdrawals | GET | /withdrawal/admin/list | QueryParams & Record<string, unknown> | 提領列表(前台 withdrawal 模組) |
reviewWithdrawal | POST | /withdrawal/admin/:id/review | id: string, { action, remark? } | 審核提領(前台 withdrawal 模組) |
getAdminWithdrawals | GET | /admin/finance/withdrawals | QueryParams & { status?, userId?, keyword?, startDate?, endDate? } | 提領列表(admin finance 模組) |
adminReviewWithdrawal | POST | /admin/finance/withdrawals/:id/review | id: number, { action, rejectReason? } | 審核提領 |
adminUploadWithdrawalProof | POST | /admin/finance/withdrawals/:id/upload-proof | id: number, file: File | 上傳提領憑證 |
adminCompleteWithdrawal | POST | /admin/finance/withdrawals/:id/complete | id: number | 標記提領為已完成 |
前台用戶管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getAuthUsers | GET | /admin/finance/users | QueryParams & { keyword? } | 前台用戶列表(搜尋帳號/名稱) |
getAuthUser | GET | /admin/finance/users/:id | id: number | 取得前台用戶詳情(含錢包、存提款、投注紀錄) |
updateAuthUser | PATCH | /admin/finance/users/:id | id, { name?, email?, mobile? } | 更新前台用戶資料 |
updateUserVendorGroup | PATCH | /admin/users/:userId/vendor-group | userId, vendorGroupId | 設定用戶金流群組 |
餘額調整
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
adjustBalance | POST | /admin/finance/adjust-balance | { userId, amount, type: "add"|"deduct", reason } | 手動調整用戶餘額 |
銀行卡 CRUD
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getBankCards | GET | /admin/finance/bank-cards | QueryParams & { userId?, status?, startDate?, endDate?, keyword? } | 銀行卡列表 |
createBankCard | POST | /admin/finance/bank-cards | { userId, bankCode, bankAccount, branch, holderName } | 新增銀行卡 |
updateBankCard | PATCH | /admin/finance/bank-cards/:id | id, data | 更新銀行卡 |
reviewBankCard | PATCH | /admin/finance/bank-cards/:id/review | id, { status } | 審核銀行卡 |
deleteBankCard | DELETE | /admin/finance/bank-cards/:id | id | 刪除銀行卡 |
信用卡 CRUD
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getCreditCards | GET | /admin/finance/credit-cards | QueryParams & { userId?, status?, startDate?, endDate?, keyword? } | 信用卡列表 |
createCreditCard | POST | /admin/finance/credit-cards | { userId, cardNumber, holderName, cvv, expiryDate } | 新增信用卡 |
updateCreditCard | PATCH | /admin/finance/credit-cards/:id | id, data | 更新信用卡 |
reviewCreditCard | PATCH | /admin/finance/credit-cards/:id/review | id, { status } | 審核信用卡 |
deleteCreditCard | DELETE | /admin/finance/credit-cards/:id | id | 刪除信用卡 |
加密地址 CRUD
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getCryptoAddresses | GET | /admin/finance/crypto-addresses | QueryParams & { userId?, status?, startDate?, endDate?, keyword? } | 加密地址列表 |
createCryptoAddress | POST | /admin/finance/crypto-addresses | { userId, walletName, currency?, network?, address } | 新增加密地址 |
updateCryptoAddress | PATCH | /admin/finance/crypto-addresses/:id | id, data | 更新加密地址 |
reviewCryptoAddress | PATCH | /admin/finance/crypto-addresses/:id/review | id, { status } | 審核加密地址 |
deleteCryptoAddress | DELETE | /admin/finance/crypto-addresses/:id | id | 刪除加密地址 |
金流群組管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getVendorGroups | GET | /admin/vendor-groups/list | 無 | 金流群組列表 |
createVendorGroup | POST | /admin/vendor-groups/create | { name: Record<string,string>, enabled? } | 新增金流群組 |
updateVendorGroup | PATCH | /admin/vendor-groups/:id | id, { name?, enabled? } | 更新金流群組 |
deleteVendorGroup | DELETE | /admin/vendor-groups/:id | id | 刪除金流群組 |
金流通道管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getVendorChannels | GET | /admin/vendor-channels/list | { groupId? } | 金流通道列表 |
createVendorChannel | POST | /admin/vendor-channels/create | Record<string, unknown> | 新增金流通道 |
updateVendorChannel | PATCH | /admin/vendor-channels/:id | id, data | 更新金流通道 |
deleteVendorChannel | DELETE | /admin/vendor-channels/:id | id | 刪除金流通道 |
getGroupChannelIds | GET | /admin/vendor-groups/:groupId/channels | groupId | 取得群組關聯的通道 ID 列表 |
setGroupChannels | PUT | /admin/vendor-groups/:groupId/channels | groupId, channelIds: number[] | 設定群組關聯的通道 |
4.4.4 useAffiliateApi — 代理推廣管理
位置:src/hooks/api/useAffiliateApi.ts
代理列表與管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getAffiliates | GET | /affiliate/admin/agents | QueryParams & Record<string, unknown> | 代理列表(支援 siteCode 篩選) |
createAgent | POST | /affiliate/admin/create-agent | { userId, agentCode? } | 手動綁定用戶為代理 |
setAgentTier | POST | /affiliate/admin/set-agent-tier | { agentId, tierCode } | 手動設定代理等級 |
佣金結算
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getSettlements | GET | /affiliate/admin/settlements | QueryParams & Record<string, unknown> | 佣金結算列表 |
reviewSettlement | POST | /affiliate/admin/settlements/:id/review | id, { action, rejectReason? } | 審核佣金結算 |
getSettlementRiskLogs | GET | /affiliate/admin/settlements/:id/risk-logs | id | 取得結算風控紀錄 |
代理提款
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getAffiliateWithdrawals | GET | /affiliate/admin/withdrawals | QueryParams & Record<string, unknown> | 代理提款列表 |
reviewAffiliateWithdrawal | POST | /affiliate/admin/withdrawals/:id/review | id, { action, rejectReason? } | 審核代理提款 |
completeAffiliateWithdrawal | POST | /affiliate/admin/withdrawals/:id/complete | id | 標記代理提款已完成 |
綁定管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
adminBind | POST | /affiliate/admin/bind | { memberId, agentId, action, remark? } | 手動綁定上下線 |
getBindLogs | GET | /affiliate/admin/bind-logs | QueryParams & Record<string, unknown> | 綁定紀錄列表 |
聯盟配置
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getCommissionRates | GET | /affiliate/admin/commission-rates | QueryParams | 佣金費率列表 |
upsertCommissionRate | POST | /affiliate/admin/commission-rates | Record<string, unknown> | 新增/更新佣金費率 |
getAgentTiers | GET | /affiliate/admin/agent-tiers | QueryParams | 代理等級列表 |
upsertAgentTier | POST | /affiliate/admin/agent-tiers | Record<string, unknown> | 新增/更新代理等級 |
deleteAgentTier | DELETE | /affiliate/admin/agent-tiers/:id | id | 刪除代理等級 |
getVipMilestones | GET | /affiliate/admin/vip-milestones | QueryParams | VIP 里程碑列表 |
upsertVipMilestone | POST | /affiliate/admin/vip-milestones | Record<string, unknown> | 新增/更新 VIP 里程碑 |
deleteVipMilestone | DELETE | /affiliate/admin/vip-milestones/:id | id | 刪除 VIP 里程碑 |
聯盟模板
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
previewAllianceTemplate | GET | /affiliate/admin/preview-template | 無 | 預覽聯盟預設模板資料 |
loadAllianceTemplate | POST | /affiliate/admin/load-template | Record<string, unknown> | 帶入聯盟模板(transaction 內寫入) |
4.4.5 useVipApi — VIP 等級與返水
位置:src/hooks/api/useVipApi.ts
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getVipLevels | GET | /vip/levels | QueryParams | VIP 等級列表(支援 siteCode 篩選) |
createVipLevel | POST | /vip/levels | Record<string, unknown> | 新增 VIP 等級 |
updateVipLevel | PATCH | /vip/levels/:id | id, payload | 更新 VIP 等級 |
deleteVipLevel | DELETE | /vip/levels/:id | id | 刪除 VIP 等級 |
getVipRebates | GET | /vip/rebates | QueryParams | 返水規則列表(支援 siteCode 篩選) |
createVipRebate | POST | /vip/rebates | Record<string, unknown> | 新增返水規則 |
updateVipRebate | PATCH | /vip/rebates/:id | id, payload | 更新返水規則 |
deleteVipRebate | DELETE | /vip/rebates/:id | id | 刪除返水規則 |
bulkUpsertVipRebates | POST | /vip/rebates/bulk | { items: { level, gameType, rebateRate }[] } | 批次新增/更新返水規則 |
previewVipTemplate | GET | /vip/preview-template | 無 | 預覽 VIP 預設模板資料 |
loadVipTemplate | POST | /vip/load-template | Record<string, unknown> | 帶入 VIP 模板 |
4.4.6 useGameApi — 遊戲管理
位置:src/hooks/api/useGameApi.ts
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getGameProviders | GET | /game/admin/providers | QueryParams & { gameType?, siteCode? } | 遊戲供應商列表 |
createGameProvider | POST | /game/admin/providers | Record<string, unknown>(含 siteCode) | 新增遊戲供應商 |
updateGameProvider | PATCH | /game/admin/providers/:id | id, payload | 更新遊戲供應商 |
deleteGameProvider | DELETE | /game/admin/providers/:id | id | 刪除遊戲供應商 |
getGameTypeConfigs | GET | /game/admin/type-configs | QueryParams | 遊戲分類列表 |
createGameTypeConfig | POST | /game/admin/type-configs | Record<string, unknown> | 新增遊戲分類 |
updateGameTypeConfig | PATCH | /game/admin/type-configs/:id | id, payload | 更新遊戲分類 |
deleteGameTypeConfig | DELETE | /game/admin/type-configs/:id | id | 刪除遊戲分類 |
previewGameTemplate | GET | /game/admin/preview-template | 無 | 預覽遊戲預設模板 |
loadGameTemplate | POST | /game/admin/load-template | data, params?: { siteCode? } | 帶入遊戲模板(支援指定站點) |
copyGameSiteData | POST | /game/admin/copy-site-data | { sourceSiteCode, targetSiteCode, type: "providers"|"typeConfigs" } | 跨站複製遊戲資料 |
4.4.7 useContentApi — 內容、風控、報表、站點設定
位置:src/hooks/api/useContentApi.ts
站點設定
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getSiteConfigs | GET | /site-config/admin/list | 無 | 取得所有站點列表(含主題) |
createSiteConfig | POST | /site-config/admin | { siteCode, prefix, layout?, siteName, siteDescription? } | 新增站點 |
updateSiteConfig | PATCH | /site-config/admin/:id | id, payload | 更新站點設定 |
deleteSiteConfig | DELETE | /site-config/admin/:id | id | 刪除站點 |
uploadDomainAsset | POST | /site-config/admin/:id/domain-asset | siteConfigId, FormData | 上傳域名素材(logo/favicon) |
uploadCustomerServiceIcon | POST | /site-config/admin/:id/customer-service-icon | siteConfigId, FormData | 上傳客服圖示 |
活動管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getPromos | GET | /admin/promos/list | QueryParams & { tag?, enabled?, startDate?, endDate?, siteCode?, title?, conditionType? } | 活動列表 |
getPromo | GET | /admin/promos/:id | id | 取得單一活動詳情 |
createPromo | POST | /admin/promos/create | FormData | 新增活動(含圖片上傳) |
updatePromo | PATCH | /admin/promos/:id | id, FormData | 更新活動 |
deletePromo | DELETE | /admin/promos/:id | id | 刪除活動 |
活動標籤
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getPromoTags | GET | /admin/promo-tags/list | { siteCode? } | 活動標籤列表 |
createPromoTag | POST | /admin/promo-tags/create | { name, label?, color?, sortOrder?, enabled?, siteCode? } | 新增標籤 |
updatePromoTag | PATCH | /admin/promo-tags/:id | id, data | 更新標籤 |
deletePromoTag | DELETE | /admin/promo-tags/:id | id | 刪除標籤 |
站內信
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getInboxAdmin | GET | /inbox/admin/list | QueryParams & { scope? } | 站內信列表 |
sendInbox | POST | /inbox/admin/send | Record<string, unknown> | 發送站內信 |
updateInbox | PATCH | /inbox/admin/:id | id, payload | 更新站內信 |
deleteInbox | DELETE | /inbox/admin/:id | id | 刪除站內信 |
風控管理
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getRiskIpRules | GET | /admin/risk/ip-rules | QueryParams & { type?, keyword? } | IP 黑白名單列表 |
createRiskIpRule | POST | /admin/risk/ip-rules | { ip, type, remark? } | 新增 IP 規則 |
updateRiskIpRule | PATCH | /admin/risk/ip-rules/:id | id, { ip?, type?, remark? } | 更新 IP 規則 |
deleteRiskIpRule | DELETE | /admin/risk/ip-rules/:id | id | 刪除 IP 規則 |
riskLookup | GET | /admin/risk/lookup | QueryParams & { keyword?, startDate?, endDate? } | IP/裝置指紋反查用戶 |
getLoginFailures | GET | /admin/risk/login-failures | QueryParams & { keyword?, startDate?, endDate? } | 登入失敗紀錄 |
getRiskGameBlacklist | GET | /admin/risk/game-blacklist | QueryParams & { userId?, gameType? } | 遊戲黑名單列表 |
createRiskGameBlacklist | POST | /admin/risk/game-blacklist | { userId, gameType?, productId?, remark? } | 新增遊戲黑名單 |
deleteRiskGameBlacklist | DELETE | /admin/risk/game-blacklist/:id | id | 刪除遊戲黑名單 |
報表 API
| 方法名稱 | HTTP 方法 | URL | 參數 | 說明 |
|---|---|---|---|---|
getReportPlayers | GET | /admin/reports/players | QueryParams & { keyword?, vipLevel?, startDate?, endDate?, bankAccount?, cardNumber?, cryptoAddress?, vipOp?, vipFilterLevel?, online?, siteCode? } | 玩家報表 |
getVipPlayers | GET | /admin/reports/vip-players | QueryParams & { keyword?, vipLevelMin?, vipLevelMax?, tier?, relegationStatus?, vipHold?, minBet?, maxBet?, startDate?, endDate? } | VIP 玩家報表 |
getReportBetRecords | GET | /admin/reports/bet-records | QueryParams & { userId?, keyword?, gameType?, gamePlatform?, status?, startDate?, endDate?, siteCode? } | 投注紀錄報表 |
getReportOverview | GET | /admin/reports/overview | { startDate?, endDate?, siteCode? } | 總覽報表(統計 + 每日摘要) |
getReportProfitLoss | GET | /admin/reports/profit-loss | { startDate?, endDate?, groupBy?, gameType?, siteCode? } | 損益報表 |
getReportGames | GET | /admin/reports/games | { startDate?, endDate?, gamePlatform?, gameType?, siteCode? } | 遊戲報表 |
getReportPromos | GET | /admin/reports/promos | QueryParams & { startDate?, endDate?, siteCode? } | 活動報表 |
getReportPlayerSummary | GET | /admin/reports/player-summary | QueryParams & { keyword?, sortBy?, sortOrder?, vipLevel?, startDate?, endDate?, siteCode? } | 玩家簡表 |
getReportHistory | GET | /admin/reports/history | QueryParams & { type?, startDate?, endDate? } | 歷史紀錄 |
exportReport | GET | /admin/reports/export/:type | type: string, params? | 匯出報表 CSV |
4.4.8 useApi — Facade 合併
位置:src/hooks/useApi.ts
useApi() 是向下相容的 Facade hook,合併所有 7 個領域 hook 的方法為單一扁平物件:
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 提取資料。
export function useApiQuery<TResult = unknown>(
queryKey: QueryKey,
queryFn: () => Promise<ApiResponse<TResult>>,
options?: Omit<UseQueryOptions<TResult | null, Error>, "queryKey" | "queryFn">,
): UseQueryResult<TResult | null, Error>;內部邏輯:
- 呼叫
queryFn()取得ApiResponse<TResult> - 檢查
res?.code === 200 - 回傳
res.result ?? null - 非 200 時回傳
null
使用範例:
const { data, isLoading, refetch } = useApiQuery<AdminUser>(
["admin", adminId],
() => api.getAdmin(adminId),
{ enabled: !!adminId },
);
// data: AdminUser | nulluseApiListQuery<TItem>
列表查詢封裝,自動正規化兩種後端回傳格式。
export function useApiListQuery<TItem = any>(
queryKey: unknown[],
queryFn: () => Promise<any>,
options?: { enabled?: boolean },
): {
data: { items: TItem[]; total: number };
isLoading: boolean;
refetch: () => void;
};內部邏輯:
- 使用
useApiQuery取得 raw result - 若 result 為陣列 →
items = result,total = result.length - 若 result 為物件 →
items = result.items ?? [],total = result.total ?? result.pagination?.total ?? 0
使用範例:
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。
export function useApiMutation<TData = unknown, TVariables = unknown>(
mutationFn: (variables: TVariables) => Promise<ApiResponse<TData>>,
options?: Omit<UseMutationOptions<...>, "mutationFn"> & {
invalidateKeys?: QueryKey[];
},
): UseMutationResult<ApiResponse<TData>, Error, TVariables>;內部邏輯:
- 執行
mutationFn - 成功後自動
queryClient.invalidateQueries()指定的 keys - 呼叫
options.onSuccess回調
使用範例:
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 行樣板代碼。
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; // 是否為「全部站點」模式
};內部邏輯:
- 從
useSiteFilterStore取得selectedSiteCode和sites isAllSites = selectedSiteCode === nullvisibleSites:全站模式顯示所有站,單站模式僅顯示該站activeSiteId:自動追蹤當前 Tab,若可見站點變更則自動切到第一個activeSiteCode:全站模式下回傳當前 Tab 的 siteCode(用於 API 參數),單站模式回傳undefined(由 header 自動帶)handleSiteChange:切換 Tab 時同時呼叫options.onSiteChange
完整使用範例:
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 取得 permissions 和 groupType,提供權限檢查方法。
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 的薄封裝,提供四種通知類型。
export function useNotify(): {
success: (message: string) => void;
error: (message: string) => void;
info: (message: string) => void;
warning: (message: string) => void;
};使用範例:
const notify = useNotify();
notify.success("儲存成功");
notify.error("操作失敗");4.5.5 useInitEnums — 枚舉初始化
位置:src/hooks/useInitEnums.ts
匯出 EnumInitializer React 元件(非 hook),在 Providers 掛載時自動 fetch /api/common/enums。
行為:
- 模組級 flag
enumsFetched確保整個 App 生命週期只 fetch 一次 useRef(fetching)防止 Strict Mode 下重複請求- 取得
res.data.result.ERROR_CODES後呼叫enumStore.setErrorCodes() - 後端未就緒時靜默失敗,不影響前端運作
function EnumInitializer(): null;4.5.6 useDomainConfig — 域名配置解析
位置:src/hooks/useDomainConfig.ts
Client Component 專用 hook,從瀏覽器 hostname 解析域名配置。
export function useDomainConfig(): DomainConfigEntry;
// 回傳 { baseUrl, imgUrl, siteId }內部邏輯:
- 呼叫
getHostnameClient()取得瀏覽器 hostname - 呼叫
resolveDomainConfig(hostname)查表 - 使用
useMemo([])快取結果(hostname 不會變化)
4.5.7 useR2Url — R2 圖片 URL 轉換
位置:src/hooks/useR2Url.ts
將 R2 key(路徑)轉為完整公開 URL。
export function useR2Url(): {
toR2Url: (key: string | null | undefined) => string | undefined;
};轉換規則:
| 輸入 | 輸出 |
|---|---|
null / undefined | undefined |
已是完整 URL(http:// 或 https:// 開頭) | 原樣回傳(向後相容舊資料) |
R2 key(如 a1/logoSmall.png) | ${imgUrl}/${key} |
使用範例:
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 通用型別
/** 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 — 權限系統型別
/** 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 — 管理員型別
/** 後台管理員 */
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 頁用)
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 — 財務型別
/** 銀行卡 */
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 — 代理型別
/** 代理商(含餘額) */
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 型別
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 — 遊戲型別
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 — 活動型別
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 — 報表型別
/** 玩家報表 */
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 外,還定義了以下未歸類的型別:
/** 活動標籤 */
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
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
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
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。
使用場景:
SiteSelector(Header 下拉選單)讀寫selectedSiteCodeapiClientrequest interceptor 讀取selectedSiteCode注入x-site-codeheaderuseMultiSiteTabs讀取selectedSiteCode和sites計算可見站點AdminContentWrapper以key={selectedSiteCode}強制 remount
4.8 API 客戶端 (apiClient)
位置:src/lib/apiClient.ts
4.8.1 Token 快取機制
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 項自動注入
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 名稱 | 說明 |
|---|---|---|---|
| baseURL | domainConfig[hostname] | — | 動態解析後端 API 位址 |
| site-name | domainConfig[hostname].siteId | site-name | 白牌站點識別 |
| locales | NEXT_LOCALE cookie | locales | 多語系(決定後端回傳語系) |
| Authorization | 模組級 cachedToken | Authorization | JWT Bearer Token |
| x-site-code | siteFilterStore.selectedSiteCode | x-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
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 — Store 查表:
enumStore.errorCodes[path][code]- 支援
:id動態路由匹配:/api/admin/users/123→/api/admin/users/:id
- 支援
- 層 2 — 呼叫端覆寫:
errorMessage: string→ 全覆蓋errorMessage: Record<code, msg>→ 按 code 覆蓋
- 層 3 — Toast 顯示:
toast.error(message)via sonner- 可用
errorToast: false停用
- 可用
4.9 NextAuth 認證系統
位置:src/lib/auth.ts
4.9.1 設定總覽
| 項目 | 設定值 |
|---|---|
| Provider | Credentials(email + password) |
| Session 策略 | JWT |
| Secret | "c9-ims-auth-secret-key" |
| Trust Host | true |
| 自訂登入頁 | /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 CredentialsSignin4.9.3 JWT Payload 擴展
interface ExtendedToken {
id?: string;
permissions?: string[];
groupType?: GroupType;
accessToken?: string; // 後端發的 JWT token
allowedSiteCodes?: string[] | null;
}4.9.4 Session 擴展
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:
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:
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(內嵌元件)
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 路由匹配規則
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};排除 api/、_next/、_vercel/、靜態檔案(含 .)。
4.10.3 處理邏輯
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 預設配置
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
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
職責:
- 驗證 locale 是否在支援列表中
- 解析域名配置(
resolveDomainConfig) - 載入本地站點配置(
loadSiteConfig) - 從後端 API 取得站點名稱 + 域名素材覆寫本地配置
- 產生 metadata(title + favicon)
- 包裹
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()
- URL 帶
- 多站點:不適用
- 權限:公開頁面
4.12.2 儀表板
/dashboard — 儀表板
- 路由:
src/app/[locale]/(admin)/dashboard/page.tsx - 用途:統計卡片 + 折線圖 + 柱狀圖 + 最近活動
- 元件:Card, Recharts (LineChart, BarChart), StatCard (內嵌元件)
- API:目前使用 Demo 靜態資料
- Feature Flag:
enableBilling控制收入卡片,enableAnalytics控制圖表區域 - 多站點:待實作
- 權限:登入即可存取
4.12.3 系統管理(15 個頁面)
/system/admins — 管理員列表
- 路由:
src/app/[locale]/(admin)/system/admins/page.tsx - 用途:管理員 CRUD 列表
- 元件:SimpleTable, FilterBar, StatusBadge, Badge
- API:
getAdmins(),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 - 用途:三步驟管理員註冊(發送驗證碼 → 驗證 → 註冊)
- API:
adminSendVerifyCode(),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)
- API:
getAdmin(),updateAdmin(),getGroups(),getSiteConfigs() - 多站點:不需要(但可設定管理員的 allowedSiteCodes)
- 權限:
canWrite("admin")
/system/groups — 群組列表
- 路由:
src/app/[locale]/(admin)/system/groups/page.tsx - 用途:管理群組 CRUD 列表
- API:
getGroups(),deleteGroup() - 多站點:不需要(全站共用)
- 權限:
canRead("admin-group")
/system/groups/new — 新增群組
- 路由:
src/app/[locale]/(admin)/system/groups/new/page.tsx - 用途:建立新群組 + 權限矩陣設定
- API:
createGroup() - 表單:useState + Zod safeParse (createGroupSchema)
- 權限:
canWrite("admin-group")
/system/groups/[id] — 編輯群組
- 路由:
src/app/[locale]/(admin)/system/groups/[id]/page.tsx - 用途:編輯群組名稱、描述、狀態、權限矩陣
- API:
getGroup(),updateGroup() - 權限:
canWrite("admin-group")
/system/logs — 操作紀錄
- 路由:
src/app/[locale]/(admin)/system/logs/page.tsx - 用途:管理員操作紀錄查詢
- API:
getAdminLogs() - 多站點:不需要(全站共用)
- 權限:
canRead("admin-log")
/system/site-config — 站點基本設定
- 路由:
src/app/[locale]/(admin)/system/site-config/page.tsx - 用途:站點 CRUD + 站點設定子頁面(OAuth, 遊戲商, 服務商, 代理導覽)
- API:
getSiteConfigs(),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 寫入
- API:
getSiteConfigs(),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)
- API:
getSiteConfigs(),updateSiteConfig(),uploadCustomerServiceIcon() - 多站點:已完成
- 權限:
canRead("site-config")
/system/cloud-storage — 雲端儲存
- 路由:
src/app/[locale]/(admin)/system/cloud-storage/page.tsx - 用途:R2 檔案管理(瀏覽、上傳、刪除、移動、建立資料夾)
- API:
r2List(),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
- API:
getR2Logs() - 多站點:已完成(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")
/system/layout-footer — 頁尾
- 路由:
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、代理資訊)
- API:
getReportPlayers(),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 - 用途:玩家完整資料(個人資訊、錢包、存提款紀錄、投注紀錄、登入日誌)
- API:
getAuthUser() - 子元件:EditPlayerInfoDialog, AddBankCardDialog, EditBankCardDialog, AddCreditCardDialog, EditCreditCardDialog, AddCryptoAddressDialog, EditCryptoAddressDialog
- 多站點:不需要(詳情頁)
- 權限:
canRead("user")
/players/new-registrations — 新註冊玩家
- 路由:
src/app/[locale]/(admin)/players/new-registrations/page.tsx - 用途:近期新註冊用戶列表
- API:
getReportPlayers(),getVipLevels() - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("user")
/players/online — 在線玩家
- 路由:
src/app/[locale]/(admin)/players/online/page.tsx - 用途:目前在線的玩家列表
- API:
getReportPlayers({ online: true }) - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("user")
/players/login-failures — 登入失敗紀錄
- 路由:
src/app/[locale]/(admin)/players/login-failures/page.tsx - 用途:登入失敗紀錄查詢
- API:
getLoginFailures() - 篩選: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 列表
- API:
getPromos(),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 - 用途:建立新活動
- API:
createPromo(),getPromoTags() - 元件:PromoForm (多語系標題/內容/圖片)
- 多站點:不需要(由列表頁帶入 siteCode)
- 權限:
canWrite("promo")
/activity/promos/[id] — 編輯活動
- 路由:
src/app/[locale]/(admin)/activity/promos/[id]/page.tsx - 用途:編輯活動詳情
- API:
getPromo(),updatePromo(),getPromoTags() - 子元件:PromoForm, PromoPreview
- 多站點:不需要(詳情頁)
- 權限:
canWrite("promo")
/activity/tags — 活動標籤
- 路由:
src/app/[locale]/(admin)/activity/tags/page.tsx - 用途:活動標籤 CRUD
- API:
getPromoTags(),createPromoTag(),updatePromoTag(),deletePromoTag() - 多站點:已完成(SiteTabs)
- 權限:
canRead("promo-tag")
4.12.7 站內信(2 個頁面)
/mail/inbox — 站內信管理
- 路由:
src/app/[locale]/(admin)/mail/inbox/page.tsx - 用途:站內信列表 + 發送/編輯/刪除
- API:
getInboxAdmin(),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 - 用途:搜尋用戶 → 選擇 → 加減餘額
- API:
getAuthUsers(),adjustBalance() - 表單:useState + Zod safeParse (adjustBalanceSchema)
- 多站點:已完成(SiteTabs,手動展開模式)
- 權限:
canWrite("finance")
/finance/deposit-settings — 入金設定
- 路由:
src/app/[locale]/(admin)/finance/deposit-settings/page.tsx - 用途:金流群組 + 金流通道管理
- API:
getVendorGroups(),createVendorGroup(),getVendorChannels(),createVendorChannel(),setGroupChannels() - 子元件:VendorGroupSection, VendorChannelSection, GroupDialog, ChannelDialog
- 多站點:已完成(SiteTabs)
- 權限:
canRead("vendor")
/finance/deposit-review — 存款審核
- 路由:
src/app/[locale]/(admin)/finance/deposit-review/page.tsx - 用途:存款訂單審核列表
- API:
getDepositReview(),reviewDeposit() - 篩選:orderId, userId, keyword, paymentMethod (select), status (select), startDate, endDate
- 多站點:已完成(SiteTabs)
- 權限:
canRead("deposit")
/finance/withdrawals — 提領審核
- 路由:
src/app/[locale]/(admin)/finance/withdrawals/page.tsx - 用途:提領訂單三階段審核(待審核 → 已核准 → 已完成)
- API:
getAdminWithdrawals(),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 - API:
getBankCards(),reviewBankCard() - 篩選:keyword, userId, status (select), bankCode, holderName, startDate, endDate
- 多站點:已完成(SiteTabs)
- 權限:
canRead("finance")
/finance/credit-cards — 信用卡列表
- 路由:
src/app/[locale]/(admin)/finance/credit-cards/page.tsx - API:
getCreditCards(),reviewCreditCard() - 篩選:keyword, userId, status (select), holderName, startDate, endDate
- 多站點:已完成(SiteTabs)
- 權限:
canRead("finance")
/finance/crypto-addresses — 加密地址列表
- 路由:
src/app/[locale]/(admin)/finance/crypto-addresses/page.tsx - API:
getCryptoAddresses(),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 + 模板帶入 + 同預設站點 + 跨站複製
- API:
getGameProviders(),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 + 模板帶入 + 同預設站點 + 跨站複製
- API:
getGameTypeConfigs(),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 + 模板帶入 + 同預設站點
- API:
getVipLevels(),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
- API:
getVipRebates(),bulkUpsertVipRebates(),getVipLevels() - 多站點:已完成(模式 A + 同預設站點)
- 權限:
canRead("vip")
/vip/players — VIP 玩家
- 路由:
src/app/[locale]/(admin)/vip/players/page.tsx - 用途:VIP 玩家管理列表
- API:
getVipPlayers() - 篩選: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 達標獎勵)
- API:
getVipMilestones()(來自 useAffiliateApi) - 多站點:已完成(SiteTabs)
- 權限:
canRead("vip")
4.12.11 報表(7 個頁面)
所有報表頁面均使用 useMultiSiteTabs hook + ExportButton CSV 匯出。
/reports/overview — 總覽報表
- 路由:
src/app/[locale]/(admin)/reports/overview/page.tsx - 用途:統計卡片(用戶數、存款、提領、投注、盈虧)+ 每日摘要表格
- API:
getReportOverview() - 篩選:startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/players — 玩家報表
- 路由:
src/app/[locale]/(admin)/reports/players/page.tsx - API:
getReportPlayers() - 篩選:keyword, vipLevel (select 0-15), startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/player-summary — 玩家簡表
- 路由:
src/app/[locale]/(admin)/reports/player-summary/page.tsx - API:
getReportPlayerSummary() - 篩選:keyword, vipLevel (select), sortBy, sortOrder, startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/bet-records — 投注紀錄
- 路由:
src/app/[locale]/(admin)/reports/bet-records/page.tsx - API:
getReportBetRecords() - 篩選:keyword, gameType (select), gamePlatform (select), status (select), startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/games — 遊戲報表
- 路由:
src/app/[locale]/(admin)/reports/games/page.tsx - API:
getReportGames() - 篩選:gameType (select), gamePlatform (select), startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/profit-loss — 損益報表
- 路由:
src/app/[locale]/(admin)/reports/profit-loss/page.tsx - API:
getReportProfitLoss() - 篩選:groupBy (日/週/月 select), gameType (select), startDate, endDate
- 多站點:已完成
- 權限:
canRead("report")
/reports/promos — 活動報表
- 路由:
src/app/[locale]/(admin)/reports/promos/page.tsx - API:
getReportPromos() - 篩選: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
- API:
getRiskIpRules(),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/手機反查用戶
- API:
riskLookup() - 篩選:keyword (統一搜尋欄)
- 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("risk")
/risk-control/game-blacklist — 遊戲黑名單
- 路由:
src/app/[locale]/(admin)/risk-control/game-blacklist/page.tsx - 用途:遊戲黑名單 CRUD(支援全封鎖/類型封鎖/特定遊戲封鎖)
- API:
getRiskGameBlacklist(),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 - API:
getAffiliates(),createAgent(),setAgentTier() - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("affiliate")
/affiliate/commission-rates — 佣金費率
- 路由:
src/app/[locale]/(admin)/affiliate/commission-rates/page.tsx - 用途:按代理等級 x 遊戲類型配置佣金費率
- API:
getCommissionRates(),upsertCommissionRate() - 多站點:已完成(SiteTabs)
- 權限:
canRead("affiliate")
/affiliate/settlements — 結算紀錄
- 路由:
src/app/[locale]/(admin)/affiliate/settlements/page.tsx - API:
getSettlements(),reviewSettlement(),getSettlementRiskLogs() - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("affiliate")
/affiliate/aff-withdrawals — 代理提領
- 路由:
src/app/[locale]/(admin)/affiliate/aff-withdrawals/page.tsx - API:
getAffiliateWithdrawals(),reviewAffiliateWithdrawal(),completeAffiliateWithdrawal() - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("affiliate")
/affiliate/bind-logs — 綁定紀錄
- 路由:
src/app/[locale]/(admin)/affiliate/bind-logs/page.tsx - API:
getBindLogs(),adminBind() - 多站點:已完成(useMultiSiteTabs)
- 權限:
canRead("affiliate")
/affiliate/agent-tiers — 代理層級
- 路由:
src/app/[locale]/(admin)/affiliate/agent-tiers/page.tsx - API:
getAgentTiers(),upsertAgentTier(),deleteAgentTier() - 多站點:已完成(SiteTabs)
- 權限:
canRead("affiliate")
/affiliate/vip-milestones — VIP 里程碑(聯盟)
- 路由:
src/app/[locale]/(admin)/affiliate/vip-milestones/page.tsx - API:
getVipMilestones(),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 Flag:
enableRBAC - 多站點:不適用
/users — 用戶管理列表
- 路由:
src/app/[locale]/(admin)/users/page.tsx - Feature Flag:
enableUserManagement - 多站點:不需要
/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.ts | 1 | 1 | 登入表單驗證 |
admin.ts | 4 | 4 | 管理員三步驟建立 + 編輯驗證 |
group.ts | 2 | 2 | 群組建立 + 編輯驗證 |
finance.ts | 1 | 1 | 餘額調整驗證 |
promo.ts | 1 | 1 | 活動建立/編輯驗證 |
user.ts | 2 | 2 | 用戶建立 + 編輯驗證 |
role.ts | 2 + 1 子 Schema | 2 | 角色建立 + 編輯驗證 |
4.13.2 使用模式
後台有兩種表單驗證模式:
模式 A:React Hook Form + zodResolver(推薦,用於登入頁等正式表單)
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 表單)
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
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 使用。
| 欄位 | 型別 | 驗證規則 | 錯誤訊息 |
|---|---|---|---|
email | string | email() 格式檢查 | "請輸入有效的 Email" |
password | string | min(6) 最少 6 字元 | "密碼至少 6 個字元" |
4.13.4 Admin Schema — admin.ts
檔案:src/lib/validations/admin.ts
後台管理員建立採用三步驟流程(送驗證碼 → 驗證 → 註冊),每步驟各有獨立 Schema:
adminSendCodeSchema(步驟 1:送驗證碼)
export const adminSendCodeSchema = z.object({
email: z.string().email("請輸入有效的 Email"),
});
export type AdminSendCodeInput = z.infer<typeof adminSendCodeSchema>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
email | string | email() | 管理員 Email,用於發送驗證碼 |
adminVerifyCodeSchema(步驟 2:驗證碼確認)
export const adminVerifyCodeSchema = z.object({
email: z.string().email("請輸入有效的 Email"),
code: z.string().min(1, "請輸入驗證碼"),
});
export type AdminVerifyCodeInput = z.infer<typeof adminVerifyCodeSchema>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
email | string | email() | 同步驟 1 的 Email |
code | string | min(1) | Email 驗證碼 |
adminRegisterSchema(步驟 3:註冊)
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>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
email | string | email() | 管理員 Email |
password | string | min(6) | 登入密碼 |
name | string | min(2) | 管理員顯示名稱 |
code | string | min(1) | Email 驗證碼 |
groupId | string? | optional() | 所屬群組 ID(可選) |
updateAdminSchema(編輯管理員)
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>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
name | string | min(2) | 管理員顯示名稱 |
password | string | "" | union([min(6), literal("")]) | 密碼(空字串表示不修改) |
groupId | string? | optional() | 所屬群組 ID |
status | string | 必填 | 狀態值("0" 或 "1") |
allowedSiteCodes | string[]? | optional() | 可存取的站點代碼列表(多站點 RBAC) |
特殊設計:password 欄位使用 z.union() 允許空字串,這是因為編輯管理員時密碼為選填欄位 — 若留空表示不修改密碼,若填寫則需至少 6 字元。
4.13.5 Group Schema — group.ts
檔案:src/lib/validations/group.ts
createGroupSchema(建立群組)
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>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
name | string | min(1) | 群組名稱 |
description | string? | optional() | 群組描述 |
permissions | string[] | min(1) | 權限列表(格式:"module:action") |
updateGroupSchema(編輯群組)
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
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 | 操作類型:加值 / 扣款 |
amount | string | min(1) | 調整金額(字串型別,前端轉 number) |
reason | string | min(1) | 調整原因(必填,用於審計追蹤) |
設計說明:amount 使用 string 而非 number 是因為 HTML Input 元素的 value 屬性始終為字串,在提交時由 API 層轉換為 number。
4.13.7 Promo Schema — promo.ts
檔案:src/lib/validations/promo.ts
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>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
tag | string | min(1) | 活動標籤 |
startTime | string | min(1) | 活動開始時間 |
endTime | string | min(1) | 活動結束時間 |
conditionType | string | min(1) | 條件類型(存款/投注等) |
conditionValue | string | 可為空 | 條件數值 |
rewardAmount | string | min(1) | 獎勵金額(USD) |
turnoverMultiplier | string | 可為空 | 打碼倍率 |
maxClaims | string | 可為空 | 最大領取次數 |
4.13.8 User Schema — user.ts
檔案:src/lib/validations/user.ts
createUserSchema(建立用戶)
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>;| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
email | string | email() | 用戶 Email |
name | string | min(2).max(50) | 用戶名稱(2-50 字元) |
roleId | string | min(1) | 指定角色 ID |
status | "active" | "inactive" | enum | 用戶狀態 |
updateUserSchema(編輯用戶)
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
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(權限項目)
| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
resource | enum | 5 種資源 | dashboard, users, roles, billing, analytics |
actions | enum[] | 至少 1 個 | read, create, update, delete |
createRoleSchema
| 欄位 | 型別 | 驗證規則 | 說明 |
|---|---|---|---|
name | string | min(2).max(30) | 角色名稱 |
description | string? | max(200).optional() | 角色描述 |
permissions | permissionSchema[] | min(1) | 權限配置陣列 |
4.13.10 型別推導慣例
所有 Schema 都使用 z.infer<typeof schema> 產生對應的 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 頁面的標準資料表格元件,支援伺服器端分頁。
泛型介面
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;
}渲染邏輯
- 載入中(
loading = true):顯示置中的Loader2旋轉動畫 - 空資料(
data.length === 0):顯示emptyText提示 - 正常渲染:
- 使用 shadcn/ui
Table元件群組(Table,TableHeader,TableBody,TableRow,TableHead,TableCell) - 外層包裹
rounded-lg border - 支援
onRowClick點擊行,自動加上cursor-pointer - 支援
rowClassName動態行樣式(例如 VIP 返水頁面的 amber 高亮)
- 使用 shadcn/ui
- 分頁:底部渲染
Pagination元件
使用範例
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 切換元件,支援「同預設站點」複製功能。
介面定義
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;
}關鍵特性
- Tab 顯示格式:站點名稱 +
(siteCode / prefix)+ 預設站標記 Badge - 預設站判斷:
configs[0]為預設站(第一筆始終為預設站點) - 複製按鈕:只在非預設站 Tab 啟用時顯示,需同時提供
copyLabel和onCopyFromDefault - 使用 shadcn/ui Tabs:
Tabs+TabsList+TabsTrigger,支援 flex-wrap 自動換行 - 空列表保護:
configs.length === 0時回傳null
複製按鈕渲染條件
{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用途:所有列表頁面的篩選欄位元件,支援三種欄位類型。
欄位型別定義
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
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
}關鍵行為
- Enter 鍵搜尋:text 和 date 欄位支援 Enter 鍵觸發
onSearch - 重置邏輯:清空所有欄位為空字串,然後呼叫
onReset - Select 特殊處理:空值使用
"__all__"佔位符(因 shadcn Select 不接受空字串 value) - 按鈕對齊:搜尋/重置按鈕上方有隱藏的
<span className="h-4">與 Label 高度對齊
使用範例
<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 標籤。
預設色彩映射
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 介面
interface StatusBadgeProps {
status: string | number; // 狀態值
label: string; // 顯示文字
colorMap?: Record<string | number, string>; // 自訂色彩映射
className?: string; // 額外 CSS
}色彩查找流程
- 使用傳入的
colorMap(若有)或DEFAULT_STATUS_COLORS - 從 map 中查找
status對應的色彩名稱(如"emerald") - 從
COLOR_MAP中取得對應的 Tailwind CSS classes - 若找不到,預設使用
amber
常見使用場景
| 場景 | status 值 | colorMap |
|---|---|---|
| 管理員狀態 | 0 / 1 | { 1: "emerald", 0: "rose" } |
| 存款訂單狀態 | 0 / 1 / 2 | 預設(amber/emerald/rose) |
| 提款狀態 | "pending" / "approved" / "completed" | 預設 |
| Google Auth | 0 / 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 定義
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;
}使用方式
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 實例:
// providers.tsx
<ConfirmDialog />4.14.6 Pagination — 分頁元件
檔案:src/components/shared/pagination.tsx用途:SimpleTable 的分頁導航元件。
Props 介面
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 介面
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 組(以分隔線區隔):
- 文字格式:Bold / Italic / Underline / Strikethrough
- 標題:H1 / H2 / H3
- 列表:Bullet List / Ordered List
- 對齊:Left / Center / Right
- 插入:Link / Image / Text Color
- 歷史: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 介面
interface ExportButtonProps {
reportType: string; // 報表類型(如 "players", "bet-records")
params?: Record<string, unknown>; // 篩選參數
}匯出流程
- 點擊按鈕,設定
exporting = true(顯示 Loader2 spinner) - 呼叫
api.exportReport(reportType, params) - 從回應中取得
{ csv, filename } - 建立 Blob(加 BOM
\uFEFF確保 Excel 正確開啟 UTF-8) - 建立臨時
<a>下載連結,觸發瀏覽器下載 - 釋放 Object URL
4.14.9 LoadingSpinner — 載入動畫
檔案:src/components/shared/loadingSpinner.tsx用途:全頁或區塊載入指示器。
Props 介面
interface LoadingSpinnerProps {
className?: string;
size?: "sm" | "md" | "lg"; // 預設 "md"
}尺寸對照
| size | CSS classes |
|---|---|
sm | h-4 w-4 |
md | h-8 w-8 |
lg | h-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 管理的「帶入模板」功能,允許使用者預覽模板資料並在確認前編輯。
型別定義
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 介面
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;
}關鍵特性
- 多分區 Tab:每個
TemplateSection渲染為一個 Tab,Tab 上顯示筆數 Badge - 可編輯欄位:
editable: true的欄位渲染為Input,支援即時編輯 - 資料同步:開啟 Dialog 時將
sections.data拷貝到內部editDatastate(使用syncedRef防止重複同步) - 確認提交:
onConfirm(editData)回傳所有(含已編輯的)資料 - 最大寬度:
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 子元件
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 配置
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) |
導航群組顯示邏輯
每個群組同時檢查兩個條件:
- Feature Flag 檢查:
useFeatureFlags()取得站點功能開關 - RBAC 權限 檢查:
usePermissions()取得使用者權限
// 範例: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用途:頁面頂部導航列,包含站點選擇、語系切換、個人資料。
包含功能
- SiteSelector:多站點切換下拉選單(見 4.15.4)
- 語系切換:
DropdownMenu列出LOCALE_LABELS,使用router.replace(pathname, { locale })切換 - 個人資料:顯示 session user 資訊 + 登出按鈕
- Google Auth:Google Authenticator 2FA 設定按鈕(開啟
GoogleAuthDialog) - Admin Profile 查詢:使用
useApiQuery(["admin-profile"], ...)取得 Google Auth 狀態
4.15.4 SiteSelector — 站點切換選擇器
檔案:src/components/layout/SiteSelector.tsx用途:Header 中的站點切換下拉選單。
渲染條件
sites.length <= 1:單站點模式,不渲染- 路由匹配
HIDE_SITE_SELECTOR_ROUTES:不渲染
隱藏路由列表
const HIDE_SITE_SELECTOR_ROUTES = [
"/system/admins",
"/system/groups",
"/system/logs",
];這些頁面為全站共用設定,不區分站點。
切換行為
const handleSelect = useCallback((code: string | null) => {
if (code === selectedSiteCode) return;
setSelectedSiteCode(code); // 更新 siteFilterStore
queryClient.removeQueries(); // 清除 TanStack Query cache
}, [selectedSiteCode, setSelectedSiteCode, queryClient]);重要:切換站點時呼叫 queryClient.removeQueries() 清除所有快取,確保不會顯示前一站點的資料。配合 AdminContentWrapper 的 key={selectedSiteCode} 強制 remount,所有子頁面會重新拉取資料。
4.15.5 AdminContentWrapper — 管理頁面包裹器
檔案:src/components/layout/AdminContentWrapper.tsx用途:透過 React key 強制 remount,實現站點切換時自動重新載入資料。
export function AdminContentWrapper({ children }: { children: React.ReactNode }) {
const selectedSiteCode = useSiteFilterStore((s) => s.selectedSiteCode);
return <div key={selectedSiteCode ?? "__all__"}>{children}</div>;
}原理:當 selectedSiteCode 改變時,key 值改變,React 會卸載舊的子元件樹並建立新的,等同於頁面完全重新載入。所有 useState、useApiQuery 等 hooks 都會重新初始化。
4.15.6 SiteFilterInitializer — 站點列表初始化
檔案:src/components/layout/SiteFilterInitializer.tsx用途:啟動時拉取後端站點列表,填入 siteFilterStore。
執行流程
- 監聽
useSession()的status - 當
status === "authenticated"時執行 - 呼叫
api.getSiteConfigs()(GET/site-config/admin/list) - 根據管理員的
allowedSiteCodes過濾可見站點 - 將結果映射為
{ id, siteCode, prefix, siteName }填入 store
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 Variable | ThemeColors 屬性 | 用途 |
|---|---|---|
--background | background | 頁面背景 |
--foreground | foreground | 文字顏色 |
--card | card | Card 背景 |
--primary | primary | 主要色 |
--secondary | secondary | 次要色 |
--muted | muted | 柔和色 |
--accent | accent | 強調色 |
--destructive | destructive | 危險操作色 |
--border | border | 邊框色 |
--input | input | 輸入框邊框色 |
--ring | ring | Focus ring 色 |
--chart-1 ~ --chart-5 | chart1 ~ chart5 | Recharts 圖表色 |
--sidebar | sidebar | 側邊欄背景 |
--sidebar-foreground | sidebarForeground | 側邊欄文字 |
--sidebar-primary | sidebarPrimary | 側邊欄主色 |
--sidebar-accent | sidebarAccent | 側邊欄強調色 |
--sidebar-border | sidebarBorder | 側邊欄邊框 |
--sidebar-ring | sidebarRing | 側邊欄 focus ring |
注入方式:使用 <div style={cssVars as React.CSSProperties} className="contents">,className="contents" 確保不產生額外的 DOM 層級。
4.15.8 LocaleGuard — 語系保護
檔案:src/components/layout/localeGuard.tsx用途:確保當前語系在站點支援的語系列表中。
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用途:路由切換後自動將捲軸滾回頂部。
三種偵測機制
- MutationObserver:偵測
[data-scroll-region]元素的 children 變化 - history.pushState 攔截:Monkey-patch
history.pushState攔截 SPA 導航 - 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, usePathnamerouting.ts — 路由定義
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 — 請求配置
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 使用。
navigation.ts — Locale-Aware 導航
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);重要規則:所有頁面必須使用 @/i18n/navigation 匯出的 Link、useRouter、usePathname,不可使用 next/navigation 的版本(否則會丟失 locale 資訊)。
4.16.3 訊息檔案格式
語系檔案使用扁平 dot-notation 格式,而非巢狀 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 模式
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,大括號 {} 會被解析為變數佔位符:
// 錯誤 — 會觸發 IntlError: FORMATTING_ERROR
"format": "格式為 {JSON}"
// 正確 — 使用單引號轉義大括號
"format": "格式為 '{'JSON'}'"4.16.6 多語系工具函式
檔案:src/lib/locales.ts
// 語系顯示名稱對照表
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 語系切換流程
- Header DropdownMenu 列出所有語系(使用
LOCALE_LABELS) - 使用者點選目標語系
- 呼叫
router.replace(pathname, { locale: nextLocale }) - next-intl middleware 處理 cookie 設定 + 重新載入頁面
getRequestConfig載入對應語系檔案LocaleGuard驗證語系是否在站點supportedLocales中
4.17 多站點開發完整模式
4.17.1 模式 A:列表頁(使用 useMultiSiteTabs)
適用於大多數 CRUD 列表頁面(47+ 個頁面已採用此模式)。
完整範例
"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 API | PUT/PATCH 各站設定 |
典型結構
// 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 |
| 子元件定義 | 小型子元件可定義在同檔案(如 StatCard 在 dashboard/page.tsx 內) |
4.18.2 命名規範
| 類型 | 規範 | 範例 |
|---|---|---|
| React 元件 | PascalCase | SimpleTable, FilterBar, StatusBadge |
| 元件檔案 | camelCase.tsx | simpleTable.tsx, filterBar.tsx |
| 佈局元件檔案 | PascalCase.tsx | AdminContentWrapper.tsx, SiteSelector.tsx |
| 頁面檔案 | page.tsx | Next.js 慣例 |
| Hooks | use{Feature} | useApi, useNotify, usePermissions |
| API Hooks | use{Domain}Api | useAdminApi, useFinanceApi |
| Store 檔案 | {feature}Store.ts | uiStore.ts, enumStore.ts |
| Validation | {domain}.ts | admin.ts, group.ts |
| Type 檔案 | 單數名詞 | admin.ts, affiliate.ts |
| i18n keys | dot-notation | "system.admins.title" |
4.18.3 Import 規範
// 正確 — 使用 @ 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 + safeParse4.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 多站點開發必備檢查清單
新增頁面時,請確認以下項目:
- 是否需要多站點支援?(全站共用設定不需要)
- 使用
useMultiSiteTabshook 取得站點狀態 - API 呼叫的 Query Key 包含
activeSiteCode - API 參數包含
siteCode(全站模式時) - 渲染
SiteTabs元件 - 是否需要「同預設站點」功能?
- 後端 Entity 是否有
siteCode欄位? - 後端 Controller 是否使用
@AdminSiteCode()裝飾器?
4.18.7 i18n 開發規範
- 新增翻譯時必須同步更新全部 5 個語系檔案
- 使用
useTranslations("namespace")取得翻譯函式 - 通用翻譯用
useTranslations("common") - 避免硬寫文字在元件中
- 大括號需轉義:
'{'xxx'}' - 錯誤訊息不硬寫,使用
enumStore.errorCodes查表
第 5 章:後端 (c9-be) 技術規格
5.1 完整 Entity 文件(49 張資料表)
本章節詳細記錄 c9-be 後端專案中所有 49 個 TypeORM Entity 的完整欄位定義,包含資料型別、是否可為空、預設值、註解、索引、唯一約束與關聯關係。
5.1.1 用戶認證模組
AuthUser(auth-user)
前台用戶主表,包含帳號、密碼、餘額、VIP、代理、OAuth 等完整欄位。每站帳號獨立(透過 siteCode 區分)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
account | varchar(50) | NO | - | 帳號 |
password | varchar(255) | YES | null | 密碼 (bcrypt),OAuth 用戶可為空 |
name | varchar(50) | NO | - | 暱稱 |
email | varchar(100) | YES | null | 電子信箱 |
emailVerified | tinyint(1) | NO | 0 | 信箱已驗證 (1=已驗證, 0=未驗證) |
mobile | varchar(30) | YES | null | 手機號碼 |
mobileVerified | tinyint(1) | NO | 0 | 手機已驗證 |
avatar | varchar(500) | YES | null | 頭像 URL |
balance | decimal(18,6) | NO | 0.000000 | 餘額 (USD) |
frozenBalance | decimal(18,6) | NO | 0.000000 | 凍結餘額 (USD) |
totalDeposit | decimal(18,6) | NO | 0.000000 | 累計存款 (USD) |
totalWithdrawal | decimal(18,6) | NO | 0.000000 | 累計提款 (USD) |
totalEffectiveBet | decimal(18,6) | NO | 0.000000 | 累計有效投注 (USD) |
totalWinLose | decimal(18,6) | NO | 0.000000 | 累計輸贏 (USD) |
vipLevel | int | NO | 1 | VIP 等級 |
vipHold | tinyint(1) | NO | 0 | VIP 鎖定(0=未鎖, 1=已鎖) |
monthlyBet | decimal(18,6) | NO | 0.000000 | 當月累計投注 (USD) |
isAgent | tinyint(1) | NO | 0 | 是否為代理 |
agentCode | varchar(30) | YES | null | 代理推廣碼 |
agentLevel | int | NO | 0 | 代理層級 (0=非代理, 1/2/3=層級) |
parentAgentId | int | YES | null | 上線代理 ID |
google | varchar(100) | YES | null | Google OAuth sub |
telegram | varchar(100) | YES | null | Telegram ID |
vendorGroupId | int | YES | null | 所屬金流群組 ID |
tokenVersion | int | NO | 0 | Token 版本號(遞增可強制登出) |
googleAuthSecret | varchar(32) | YES | null | Google Authenticator Secret |
googleAuthEnabled | tinyint(1) | NO | 0 | Google Auth 啟用狀態 |
status | tinyint(1) | NO | 1 | 帳號狀態 (1=啟用, 0=停用) |
lastLoginIp | varchar(45) | YES | null | 最後登入 IP |
lastLoginAt | datetime | YES | null | 最後登入時間 |
lastActivityAt | datetime | YES | null | 最後活動時間(節流 60s 更新) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 (CreateDateColumn) |
updatedAt | datetime | NO | auto | 更新時間 (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(多對一:金流群組,vendorGroupIdFK,onDelete: SET NULL)
AuthUserLoginLog(auth-user-login-log)
用戶登入/登出紀錄表,記錄每次登入動作、IP、裝置資訊。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | YES | null | 用戶 ID (FK → auth-user) |
action | varchar(20) | NO | - | 動作 (LOGIN / LOGOUT / LOGIN_FAIL / DEL / UNCAPTURED) |
ip | varchar(45) | YES | null | 登入 IP |
device | varchar(500) | YES | null | 裝置指紋或 User-Agent |
account | varchar(100) | YES | null | 登入帳號(登入失敗時記錄輸入值) |
lastUse | datetime | YES | null | 最後使用時間 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:
(userId, lastUse)— 複合索引ip— IP 反查用
關聯:
ManyToOne → AuthUser(userIdFK,onDelete: CASCADE)
5.1.2 遊戲模組
GameProvider(game-provider)
遊戲供應商表,記錄每個站點可用的遊戲及其配置。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
gameCode | varchar(50) | NO | - | 遊戲碼(如 slot-betsolutions) |
providerCode | varchar(30) | NO | - | 供應商代碼 (betsolutions / rsg) |
gameType | int | NO | - | 遊戲類型 (1=SPORTS, 2=SLOT, 3=LIVE, 4=LOTTERY, 5=CHESS, 8=ESPORTS, 9=CRYPTO, 10=FISH) |
productId | int | YES | null | 產品 ID(供應商端的遊戲 ID) |
label | json | YES | null | 遊戲名稱 (多語系 JSON) |
icon | varchar(255) | YES | null | 圖示路徑 |
sortOrder | int | NO | 0 | 排序權重 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
meta | json | YES | null | 額外設定(遊戲商特定參數) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, gameCode) — 同站遊戲碼唯一
索引:siteCode
GameTypeConfig(game-type-config)
遊戲分類配置表,定義每個站點的遊戲分類及其顯示設定。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
gameType | int | NO | - | 遊戲類型編號 |
typeKey | varchar(30) | NO | - | 類型代碼(如 sports, slot) |
label | json | YES | null | 分類名稱 (多語系 JSON) |
icon | varchar(255) | YES | null | 圖示路徑 |
sortOrder | int | NO | 0 | 排序權重 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, gameType) — 同站遊戲類型唯一
索引:siteCode
GameTransaction(game-transaction)
遊戲交易紀錄表,記錄每一筆遊戲內的轉帳(下注/派彩)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
provider | varchar(30) | NO | - | 供應商代碼 |
transactionId | varchar(100) | NO | - | 交易 ID(供應商端唯一) |
type | varchar(20) | NO | - | 交易類型 (bet / win / refund / rollback) |
amount | decimal(18,6) | NO | 0 | 交易金額 (USD) |
balanceAfter | decimal(18,6) | NO | 0 | 交易後餘額 (USD) |
roundId | varchar(100) | YES | null | 遊戲回合 ID |
gameId | varchar(50) | YES | null | 遊戲 ID |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:transactionId — 交易 ID 全域唯一
索引:userId, siteCode
GamePlayLog(game-play-log)
遊戲遊玩紀錄表,UPSERT 方式記錄用戶最近遊玩的遊戲。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
gameCode | varchar(50) | NO | - | 遊戲碼 |
productId | int | YES | null | 產品 ID |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 首次遊玩時間 |
updatedAt | datetime | NO | auto | 最後遊玩時間 |
唯一約束:(siteCode, userId, gameCode, productId) — 同站用戶+遊戲唯一
索引:siteCode
5.1.3 投注紀錄模組
BetOrder(bet-order)
注單主表,記錄每一筆投注的完整資訊。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
gameType | varchar(20) | NO | - | 遊戲類型 (sports/slot/live/lottery/chess/esports/crypto/fish) |
gamePlatform | varchar(30) | NO | - | 遊戲平台代碼 |
gameNumber | varchar(100) | NO | - | 遊戲單號(供應商端唯一) |
betAmount | decimal(18,6) | NO | 0 | 投注金額 (USD) |
betEffective | decimal(18,6) | NO | 0 | 有效投注金額 (USD),依 TURNOVER_WEIGHT 計算 |
winLose | decimal(18,6) | NO | 0 | 輸贏金額 (USD) |
status | varchar(20) | NO | 'valid' | 注單狀態 (valid / invalid / cancelled) |
odds | decimal(10,2) | YES | null | 賠率 |
gameName | varchar(100) | YES | null | 遊戲名稱 |
betDatetime | datetime | YES | null | 下注時間 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:gameNumber — 遊戲單號全域唯一
索引:userId, siteCode
關聯:OneToMany → BetDetail(一對多:注單明細)
BetDetail(bet-detail)
注單明細表,記錄每個回合的細項。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
orderId | int | NO | - | 注單 ID (FK → bet-order) |
roundNo | varchar(100) | YES | null | 回合編號 |
betAmount | decimal(18,6) | NO | 0 | 投注金額 |
winLose | decimal(18,6) | NO | 0 | 輸贏金額 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
關聯:ManyToOne → BetOrder(orderId FK,onDelete: CASCADE)
5.1.4 VIP 模組
VipLevel(vip-level)
VIP 等級定義表,每站獨立配置,等級數量可自由擴充。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
level | int | NO | - | VIP 等級編號 |
name | json | NO | - | 等級名稱 (多語系,如 {"zh-TW":"青銅 I","en-US":"Bronze I"}) |
tier | varchar(20) | NO | - | 階級 (bronze / gold / platinum / diamond) |
minChip | decimal(18,6) | NO | 0 | 升級所需最低累計籌碼 (USD) |
relegationChip | decimal(18,6) | NO | 0 | 保級所需籌碼 (USD) |
sortOrder | int | NO | 0 | 排序權重 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, level) — 同站 VIP 等級唯一
索引:siteCode
VipRebate(vip-rebate)
VIP 返水規則表,定義各等級各遊戲類型的返水比例。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
level | int | NO | - | VIP 等級 |
gameType | varchar(20) | NO | - | 遊戲類型 (sports/slot/live/lottery/chess/esports/crypto/fish) |
rebateRate | decimal(5,2) | NO | 0 | 返水比例 (%) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, level, gameType) — 同站等級+遊戲類型唯一
索引:level, siteCode
VipRebateLog(vip-rebate-log)
VIP 反水發放紀錄表,記錄每日反水結算結果。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
settleDate | varchar(10) | NO | - | 結算日期 (YYYY-MM-DD) |
vipLevel | int | NO | - | 結算時 VIP 等級 |
gameType | varchar(20) | NO | - | 遊戲類型 |
dailyEffective | decimal(18,6) | NO | 0 | 當日有效投注 (USD) |
rebateRate | decimal(5,2) | NO | 0 | 返水比例 (%) |
rebateAmount | decimal(18,6) | NO | 0 | 返水金額 (USD) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, settleDate, siteCode
5.1.5 後台管理模組
AdminUser(admin-user)
後台管理員帳號表,全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
email | varchar(100) | NO | - | 管理員 Email(唯一) |
emailVerified | tinyint(1) | NO | 0 | 信箱已驗證 (1=已驗證, 0=未驗證) |
password | varchar(255) | NO | - | 密碼 (bcrypt) |
name | varchar(50) | NO | - | 管理員名稱 |
groupId | int | YES | null | 所屬群組 ID (FK → admin-group) |
status | tinyint(1) | NO | 1 | 啟用狀態 (1=啟用, 0=停用) |
lastLoginIp | varchar(45) | YES | null | 最後登入 IP |
lastLoginAt | datetime | YES | null | 最後登入時間 |
tokenVersion | int | NO | 0 | Token 版本號 |
googleAuthSecret | varchar(32) | YES | null | Google Authenticator Secret |
googleAuthEnabled | tinyint(1) | NO | 0 | Google Auth 啟用狀態 |
allowedSiteCodes | json | YES | null | 可管理的站點代碼列表,null=全站點 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:email — Email 全域唯一
索引:groupId
關聯:ManyToOne → AdminGroup(groupId FK,onDelete: SET NULL)
AdminGroup(admin-group)
管理員群組表,定義群組類型與權限。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
type | varchar(20) | NO | - | 群組類型 (root / super_admin / general_admin / custom) |
name | varchar(50) | NO | - | 群組名稱 |
permissions | json | YES | null | 權限列表 (JSON 陣列,如 ["admin:read","admin:write"]) |
description | varchar(255) | YES | null | 群組說明 |
status | tinyint(1) | NO | 1 | 啟用狀態 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
關聯:OneToMany → AdminUser(一對多:管理員列表)
AdminOperationLog(admin-operation-log)
管理員操作紀錄表。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
adminId | int | NO | - | 管理員 ID (FK → admin-user) |
module | varchar(50) | NO | - | 模組名稱 |
action | varchar(50) | NO | - | 操作動作 (create / update / delete / review) |
targetId | varchar(50) | YES | null | 操作對象 ID |
ip | varchar(45) | YES | null | 操作 IP |
userAgent | varchar(500) | YES | null | 瀏覽器 User-Agent |
method | varchar(10) | YES | null | HTTP 方法 |
path | varchar(255) | YES | null | API 路徑 |
detail | json | YES | null | 操作詳情 |
summary | varchar(500) | YES | null | 操作摘要 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:adminId
關聯:ManyToOne → AdminUser(adminId FK)
5.1.6 風控模組
RiskIpRule(risk-ip-rule)
IP 黑白名單規則表,每站獨立配置。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
ip | varchar(45) | NO | - | IP 位址 |
type | varchar(20) | NO | - | 規則類型 (blacklist / whitelist) |
remark | varchar(255) | YES | null | 備註 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:siteCode
RiskGameBlacklist(risk-game-blacklist)
遊戲黑名單表,封鎖特定用戶的遊戲存取。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
gameType | varchar(20) | YES | null | 遊戲類型(null=封鎖所有遊戲) |
productId | int | YES | null | 產品 ID(null=封鎖該類型所有遊戲) |
remark | varchar(255) | YES | null | 備註 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
封鎖規則:
gameType=null, productId=null→ 封鎖用戶所有遊戲gameType='slot', productId=null→ 封鎖用戶該類型所有遊戲gameType='slot', productId=123→ 封鎖用戶特定遊戲
索引:userId, siteCode
5.1.7 代理推廣模組
AffiliateCommission(affiliate-commission)
代理佣金表,記錄每筆投注產生的佣金。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
memberId | int | NO | - | 下線會員 ID |
betOrderId | int | YES | null | 關聯注單 ID |
agentLevel | int | NO | 1 | 代理層級 (1/2/3) |
gameType | varchar(20) | NO | - | 遊戲類型 |
netLoss | decimal(18,6) | NO | 0 | 下線淨輸 (USD) |
commissionRate | decimal(5,2) | NO | 0 | 佣金比例 (%) |
commissionAmount | decimal(18,6) | NO | 0 | 佣金金額 (USD) |
weekStart | date | YES | null | 結算週起始日 |
weekEnd | date | YES | null | 結算週結束日 |
settlementId | int | YES | null | 結算單 ID (FK) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:agentId, memberId, siteCode
AffiliateSettlement(affiliate-settlement)
代理佣金結算表,記錄每週結算結果。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
weekStart | date | NO | - | 結算週起始日(週一) |
weekEnd | date | NO | - | 結算週結束日(週日) |
activeMemberCount | int | NO | 0 | 活躍下線數 |
totalBetAmount | decimal(18,6) | NO | 0 | 下線總投注 (USD) |
totalNetLoss | decimal(18,6) | NO | 0 | 下線總淨輸 (USD) |
level1Commission | decimal(18,6) | NO | 0 | 一級佣金 (USD) |
level2Commission | decimal(18,6) | NO | 0 | 二級佣金 (USD) |
level3Commission | decimal(18,6) | NO | 0 | 三級佣金 (USD) |
totalCommission | decimal(18,6) | NO | 0 | 總佣金 (USD) |
platformFee | decimal(18,6) | NO | 0 | 平台費 (USD) |
finalAmount | decimal(18,6) | NO | 0 | 實際發放金額 (USD) |
status | varchar(20) | NO | 'pending' | 狀態 (pending / approved / rejected / paid) |
riskFlagged | tinyint(1) | NO | 0 | 是否被風控標記 |
riskReasons | json | YES | null | 風控標記原因 |
reviewedBy | varchar(100) | YES | null | 審核人 |
reviewedAt | datetime | YES | null | 審核時間 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, agentId, weekStart) — 同站代理+週起始唯一
索引:agentId, siteCode
AffiliateBalance(affiliate-balance)
代理餘額表,記錄每個代理的可用餘額與累計收益。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID(唯一) |
available | decimal(18,6) | NO | 0 | 可用餘額 (USD) |
frozen | decimal(18,6) | NO | 0 | 凍結金額 (USD) |
totalEarned | decimal(18,6) | NO | 0 | 累計收益 (USD) |
totalWithdrawn | decimal(18,6) | NO | 0 | 累計已提款 (USD) |
agentTier | varchar(20) | YES | 'bronze' | 代理等級 (bronze/silver/gold/platinum) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:agentId — 代理 ID 全域唯一
AffiliateWithdrawal(affiliate-withdrawal)
代理提款申請表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
amount | decimal(18,6) | NO | - | 提款金額 (USD) |
method | varchar(20) | NO | - | 提款方式 (crypto / bank) |
bankCardId | int | YES | null | 銀行卡 ID |
cryptoAddressId | int | YES | null | 加密錢包 ID |
status | varchar(20) | NO | 'pending' | 狀態 (pending / approved / rejected / completed) |
rejectReason | varchar(255) | YES | null | 拒絕原因 |
reviewedBy | varchar(100) | YES | null | 審核人 |
reviewedAt | datetime | YES | null | 審核時間 |
completedAt | datetime | YES | null | 完成時間 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:agentId, siteCode
AffiliateClick(affiliate-click)
代理推廣連結點擊追蹤表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
refCode | varchar(30) | NO | - | 推廣碼 |
ip | varchar(45) | YES | null | 點擊 IP |
userAgent | varchar(500) | YES | null | 瀏覽器 User-Agent |
referrer | varchar(500) | YES | null | 來源頁面 URL |
converted | tinyint(1) | NO | 0 | 是否已轉化 (0=未轉, 1=已轉) |
convertedUserId | int | YES | null | 轉化後的用戶 ID |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:agentId, refCode, siteCode
AffiliateBindLog(affiliate-bind-log)
代理綁定紀錄表,記錄上下線綁定/解綁動作。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
memberId | int | NO | - | 會員 ID |
agentId | int | NO | - | 代理 ID |
refCode | varchar(30) | YES | null | 推廣碼 |
action | varchar(20) | NO | - | 動作 (bind / unbind / rebind) |
ip | varchar(45) | YES | null | 操作 IP |
device | varchar(500) | YES | null | 裝置資訊 |
operatorAccount | varchar(100) | YES | null | 操作者帳號(管理員手動綁定時記錄) |
remark | varchar(255) | YES | null | 備註 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:memberId, agentId, siteCode
AffiliateRiskLog(affiliate-risk-log)
結算風控紀錄表,記錄結算時偵測到的異常。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
settlementId | int | NO | - | 結算單 ID |
agentId | int | NO | - | 代理 ID |
memberId | int | YES | null | 會員 ID |
riskType | varchar(50) | NO | - | 風控類型 |
detail | json | YES | null | 風控詳情 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:settlementId, siteCode
5.1.8 聯盟系統模組
AllianceCommissionRate(alliance-commission-rate)
聯盟佣金費率表,定義各代理等級各遊戲類型的佣金比例。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentTier | varchar(20) | NO | - | 代理等級 (bronze/silver/gold/platinum) |
agentLevel | int | NO | - | 代理層級 (1/2/3) |
gameType | varchar(20) | NO | - | 遊戲類型 |
commissionRate | decimal(5,2) | NO | 0 | 佣金比例 (%) |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(agentTier, agentLevel, gameType) — 等級+層級+遊戲類型唯一
AllianceAgentTier(alliance-agent-tier)
代理等級定義表。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
tierCode | varchar(20) | NO | - | 等級代碼(唯一,如 bronze/silver/gold/platinum) |
tierName | json | YES | null | 等級名稱 (多語系) |
minTotalEarned | decimal(18,6) | NO | 0 | 最低累計收益門檻 (USD) |
minActiveMembers | int | NO | 0 | 最低活躍下線數 |
sortOrder | int | NO | 0 | 排序權重 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:tierCode — 等級代碼唯一
AllianceVipMilestone(alliance-vip-milestone)
VIP 里程碑定義表,下線 VIP 等級達標時的獎勵設定。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
vipLevel | int | NO | - | VIP 等級門檻(唯一) |
bonusAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
description | json | YES | null | 說明 (多語系) |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:vipLevel — VIP 等級唯一
AllianceVipMilestoneLog(alliance-vip-milestone-log)
VIP 里程碑發放紀錄表。含 siteCode。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
memberId | int | NO | - | 會員 ID |
vipLevel | int | NO | - | VIP 等級 |
bonusAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
milestoneId | int | YES | null | 里程碑定義 ID |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, agentId, memberId, vipLevel) — 同站代理+會員+VIP 唯一
AllianceReferralCode(alliance-referral-code)
聯盟推廣碼表,每個代理最多 10 個推廣碼。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
agentId | int | NO | - | 代理 ID |
code | varchar(30) | NO | - | 推廣碼 |
label | varchar(50) | YES | null | 渠道標籤 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
convertCount | int | NO | 0 | 轉化數 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, code) — 同站推廣碼唯一
5.1.9 金流模組
DepositOrder(deposit-order)
存款訂單表,記錄每一筆存款的完整資訊。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
channelId | int | YES | null | 金流通道 ID |
subOrder | varchar(30) | NO | - | 商家訂單編號(唯一) |
orderAmount | decimal(18,6) | NO | 0 | 訂單金額 (USD) |
vendorAmount | decimal(18,6) | NO | 0 | 金流商收款金額(當地幣別) |
paymentMethod | varchar(20) | NO | - | 支付方式 (fiat / credit / crypto) |
status | varchar(20) | NO | 'pending' | 訂單狀態 (pending / created / paid / failed / cancelled) |
exchangeRate | decimal(18,10) | YES | null | 匯率 |
resultUrl | text | YES | null | 金流商回傳的支付頁面 URL |
callbackData | json | YES | null | 金流商回調原始資料 |
proofImage | varchar(500) | YES | null | 付款證明圖片 URL |
expectedCode | varchar(20) | YES | null | ATM 預期收款銀行代碼 |
expectedAccount | varchar(30) | YES | null | ATM 預期收款帳號 |
userCardLastValue | varchar(10) | YES | null | 信用卡末五碼 |
payerName | varchar(50) | YES | null | 付款人姓名 |
payerMobile | varchar(30) | YES | null | 付款人手機 |
payerEmail | varchar(100) | YES | null | 付款人 Email |
reviewedBy | varchar(100) | YES | null | 審核人 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:subOrder — 商家訂單編號全域唯一
索引:userId, siteCode
VendorGroup(vendor-group)
金流群組表,用於將用戶分配至不同的金流通道組合。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
name | json | NO | - | 群組名稱 (多語系) |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:siteCode
VendorChannel(vendor-channel)
金流通道表,儲存各金流商的連線設定。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
name | json | NO | - | 通道名稱 (多語系) |
storeCode | varchar(50) | NO | - | 商店代碼 |
secret1 | varchar(255) | NO | - | 金鑰 1 |
secret2 | varchar(255) | YES | null | 金鑰 2 |
secret3 | varchar(255) | YES | null | 金鑰 3 |
secret4 | varchar(255) | YES | null | 金鑰 4 |
currency | varchar(10) | NO | - | 幣別 (TWD/USD/CNY/THB/VND) |
paymentMethods | simple-array | NO | - | 支持的支付方式 (fiat,credit,crypto) |
paymentAddress | varchar(255) | YES | null | USDT 收款地址 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:siteCode
VendorGroupChannel(vendor-group-channel)
金流群組與通道的多對多關聯表。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
groupId | int | NO | - | 群組 ID (FK → vendor-group) |
channelId | int | NO | - | 通道 ID (FK → vendor-channel) |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(groupId, channelId) — 群組+通道唯一
關聯:
ManyToOne → VendorGroup(groupIdFK,onDelete: CASCADE)ManyToOne → VendorChannel(channelIdFK,onDelete: CASCADE)
5.1.10 錢包模組
BankCard(bank-card)
銀行卡表,儲存用戶的銀行帳戶資訊。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
bankCode | varchar(10) | NO | - | 銀行代碼 |
bankAccount | varchar(30) | NO | - | 銀行帳號 |
branch | varchar(50) | NO | - | 分行名稱 |
holderName | varchar(50) | NO | - | 持卡人姓名 |
idCardFront | varchar(500) | YES | null | 身分證正面 (R2 path) |
idCardBack | varchar(500) | YES | null | 身分證反面 (R2 path) |
passbookCover | varchar(500) | YES | null | 存摺封面 (R2 path) |
status | tinyint(1) | NO | 0 | 審核狀態 (0=待審核, 1=通過, 2=拒絕) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, siteCode
CreditCard(credit-card)
信用卡表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
cardNumber | varchar(20) | NO | - | 卡號 |
holderName | varchar(50) | NO | - | 持卡人姓名 |
cvv | varchar(5) | NO | - | CVV |
expiryDate | varchar(10) | NO | - | 有效期 (MM/YY) |
status | tinyint(1) | NO | 0 | 審核狀態 (0=待審核, 1=通過, 2=拒絕) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, siteCode
CryptoAddress(crypto-address)
加密貨幣錢包地址表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
walletName | varchar(100) | NO | - | 錢包名稱 |
currency | varchar(10) | NO | 'USDT' | 幣種 |
network | varchar(20) | NO | 'TRC-20' | 網路協議 |
address | varchar(255) | NO | - | 錢包地址 |
status | tinyint(1) | NO | 0 | 審核狀態 (0=待審核, 1=通過, 2=拒絕) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, siteCode
5.1.11 活動模組
Promo(promo)
活動促銷表,記錄所有優惠活動。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
title | json | NO | - | 活動標題 (多語系) |
content | json | NO | - | 活動內容 (多語系 HTML) |
actionHtml | text | YES | null | 渲染連結/按鈕 HTML |
imgPc | json | YES | null | PC 版橫幅圖片 (多語系 URL) |
imgMobile | json | YES | null | 手機版橫幅圖片 (多語系 URL) |
startTime | datetime | NO | - | 活動開始時間 |
endTime | datetime | NO | - | 活動結束時間 |
tag | varchar(30) | NO | - | 活動標籤 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
conditionType | varchar(30) | NO | 'none' | 領取條件類型 (none/deposit_threshold/vip_level/first_deposit) |
conditionValue | varchar(50) | YES | '0' | 條件門檻值 |
rewardAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
turnoverMultiplier | decimal(10,2) | NO | 0 | 打碼量倍數 |
maxClaims | int | NO | 0 | 最大領取總數 (0=無限) |
claimedCount | int | NO | 0 | 已領取數 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:siteCode
PromoClaim(promo-claim)
活動領取紀錄表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
promoId | int | NO | - | 活動 ID |
userId | int | NO | - | 用戶 ID |
rewardAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
requiredTurnover | decimal(18,6) | NO | 0 | 所需打碼量 (USD) |
completedTurnover | decimal(18,6) | NO | 0 | 已完成打碼量 (USD) |
turnoverCompleted | tinyint(1) | NO | 0 | 打碼量是否已完成 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, promoId, userId) — 同站活動+用戶唯一(每人每活動限領一次)
PromoTag(promo-tag)
活動標籤表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
name | varchar(30) | NO | - | 標籤代碼 |
label | json | YES | null | 標籤名稱 (多語系) |
color | varchar(20) | YES | null | 顏色色碼 |
sortOrder | int | NO | 0 | 排序權重 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, name) — 同站標籤名唯一
5.1.12 站內信模組
Notification(notification)
站內信表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | YES | null | 目標用戶 ID(null=全站廣播) |
title | json | NO | - | 通知標題 (多語系) |
content | json | NO | - | 通知內容 (多語系 HTML) |
category | varchar(20) | NO | - | 分類 (system / promo) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, siteCode
NotificationRead(notification-read)
通知已讀紀錄表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
notificationId | int | NO | - | 通知 ID |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, userId, notificationId) — 同站用戶+通知唯一
5.1.13 站點配置模組
SiteConfig(site-config)
站點設定表,儲存每個白牌站點的完整配置。此表的 siteCode 欄位本身就是站點定義(非多站篩選用途)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
siteCode | varchar(30) | NO | - | 站點代碼(唯一) |
prefix | varchar(30) | NO | - | 白牌前綴(唯一,對應 R2 路徑) |
layout | varchar(30) | YES | 'a1' | 前台模板代碼 |
siteName | json | NO | - | 站點名稱 (多語系) |
siteDescription | json | YES | null | 站點介紹 (多語系) |
supportedLocales | json | YES | null | 支援語系列表 |
activeThemeId | int | YES | null | 當前啟用主題 ID |
mascots | json | YES | null | 吉祥物列表 (R2 URLs) |
bottomBarConfig | json | YES | null | 前台底部導航列配置 |
footerConfig | json | YES | null | 前台頁尾配置 |
learnMoreConfig | json | YES | null | 了解更多 FAQ 配置 (多語系 title+content 陣列) |
customerServiceConfig | json | YES | null | 客服管道配置 (8 種管道 + LiveChat) |
domains | json | YES | null | 域名設置 (hostname, protocol 等) |
oauthProviders | json | YES | null | 三方登入配置 (Google/Telegram) |
gameProviders | json | YES | null | 遊戲商配置 |
serviceProviders | json | YES | null | 服務商配置 (SMS/Email) |
templateVariables | json | YES | null | 模板變數 |
notificationConfig | json | YES | null | 通知設定 |
depositMethods | json | YES | null | 存款方式設定 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:siteCode — 站點代碼唯一、prefix — 前綴唯一
關聯:OneToMany → SiteTheme(一對多:主題列表)
SiteTheme(site-theme)
站點主題表,定義各站點的色彩方案。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
themeId | varchar(50) | NO | - | 主題識別碼(唯一) |
themeName | json | NO | - | 主題名稱 (多語系) |
primary | json | NO | - | 主色系 (base/dark/light/glow) |
accent | json | NO | - | 強調色 (gold/info/violet/cyan/error) |
surface | json | NO | - | 表面色 (page/navbar/card/modal/sidebar) |
text | json | NO | - | 文字色 (primary/secondary/muted/hint) |
border | json | NO | - | 邊框色 (subtle/default/strong) |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteConfigId | int | NO | - | 站點配置 ID (FK → site-config) |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:themeId — 主題識別碼唯一
關聯:ManyToOne → SiteConfig(siteConfigId FK,onDelete: CASCADE)
5.1.14 提領模組
WithdrawalOrder(withdrawal-order)
提領訂單表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
amount | decimal(18,6) | NO | - | 提領金額 (USD) |
cryptoAddressId | int | YES | null | 加密錢包 ID |
address | varchar(255) | YES | null | 提領地址 |
network | varchar(20) | YES | null | 網路協議 |
status | varchar(20) | NO | 'pending' | 狀態 (pending / approved / rejected / completed) |
rejectReason | varchar(255) | YES | null | 拒絕原因 |
reviewedBy | varchar(100) | YES | null | 審核人 |
proofKey | varchar(500) | YES | null | 代付證明 R2 key |
proofOriginalName | varchar(255) | YES | null | 代付證明原始檔名 |
completedBy | varchar(100) | YES | null | 完成出款操作者 |
completedAt | datetime | YES | null | 完成時間 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:userId, siteCode
5.1.15 排行榜模組
RankList(rank-list)
排行榜表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
gameName | varchar(100) | NO | - | 遊戲名稱 |
betAmount | decimal(18,6) | NO | 0 | 投注金額 (USD) |
multiplier | decimal(10,2) | NO | 0 | 倍率 |
payout | decimal(18,6) | NO | 0 | 派彩金額 (USD) |
isAnonymous | tinyint(1) | NO | 0 | 是否匿名 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:siteCode
5.1.16 任務模組
Mission(mission)
任務定義表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
category | varchar(20) | NO | - | 任務類別 (deposit / bet) |
periodType | varchar(10) | NO | - | 週期類型 (daily / weekly / monthly) |
tier | int | NO | - | 任務階層(同類同週期的不同門檻) |
threshold | decimal(18,6) | NO | 0 | 門檻值 (USD) |
rewardAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
vipRequired | int | NO | 0 | VIP 等級要求(0=無要求) |
turnoverMultiplier | decimal(10,2) | NO | 0 | 打碼量倍數 |
enabled | tinyint(1) | NO | 1 | 啟用狀態 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, category, periodType, tier) — 同站類別+週期+層級唯一
MissionProgress(mission-progress)
任務進度表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
userId | int | NO | - | 用戶 ID |
periodType | varchar(10) | NO | - | 週期類型 |
periodKey | varchar(20) | NO | - | 週期標識 (如 2026-03-02, 2026-W10) |
depositTotal | decimal(18,6) | NO | 0 | 累計存款 (USD) |
betTotal | decimal(18,6) | NO | 0 | 累計投注 (USD) |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, userId, periodType, periodKey) — 同站用戶+週期唯一
MissionClaim(mission-claim)
任務領取紀錄表。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
missionId | int | NO | - | 任務 ID |
userId | int | NO | - | 用戶 ID |
periodKey | varchar(20) | NO | - | 週期標識 |
rewardAmount | decimal(18,6) | NO | 0 | 獎勵金額 (USD) |
requiredTurnover | decimal(18,6) | NO | 0 | 所需打碼量 (USD) |
completedTurnover | decimal(18,6) | NO | 0 | 已完成打碼量 (USD) |
turnoverCompleted | tinyint(1) | NO | 0 | 打碼量是否已完成 |
siteCode | varchar(30) | NO | 'C9' | 所屬站點代碼 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
唯一約束:(siteCode, missionId, userId, periodKey) — 同站任務+用戶+週期唯一
5.1.17 R2 儲存模組
R2OperationLog(r2-operation-log)
R2 雲端儲存操作紀錄表。全站共用(不含 siteCode)。
| 欄位 | 型別 | Nullable | Default | Comment / 說明 |
|---|---|---|---|---|
id | int (PrimaryGeneratedColumn) | NO | auto | 主鍵 |
adminId | int | NO | - | 管理員 ID (FK → admin-user) |
action | varchar(30) | NO | - | 操作動作 (upload / delete / move / create-folder / delete-folder) |
module | varchar(30) | NO | - | 模組名稱 |
objectKey | varchar(500) | NO | - | 物件 Key |
originalName | varchar(255) | YES | null | 原始檔名 |
fileSize | int | YES | null | 檔案大小 (bytes) |
mimeType | varchar(100) | YES | null | MIME 類型 |
ip | varchar(45) | YES | null | 操作 IP |
userAgent | varchar(500) | YES | null | 瀏覽器 User-Agent |
detail | json | YES | null | 操作詳情 |
createdAt | datetime | NO | auto | 建立時間 |
updatedAt | datetime | NO | auto | 更新時間 |
索引:adminId
關聯:ManyToOne → AdminUser(adminId FK)
5.1.18 Entity 統計總表
| 分類 | 含 siteCode | 不含 siteCode | 合計 |
|---|---|---|---|
| 用戶認證 | 2 | 0 | 2 |
| 遊戲 | 4 | 0 | 4 |
| 投注紀錄 | 2 | 0 | 2 |
| VIP | 3 | 0 | 3 |
| 後台管理 | 0 | 3 | 3 |
| 風控 | 2 | 0 | 2 |
| 代理推廣 | 7 | 0 | 7 |
| 聯盟系統 | 2 | 3 | 5 |
| 金流 | 3 | 1 | 4 |
| 錢包 | 3 | 0 | 3 |
| 活動 | 3 | 0 | 3 |
| 站內信 | 2 | 0 | 2 |
| 站點配置 | 0 | 2 | 2 |
| 提領 | 1 | 0 | 1 |
| 排行榜 | 1 | 0 | 1 |
| 任務 | 3 | 0 | 3 |
| R2 儲存 | 0 | 1 | 1 |
| 合計 | 37 | 12 | 49 |
5.2 完整 API 端點文件(205+ 個端點)
本章節詳細記錄所有 API 端點的 HTTP 方法、路由、Guards、Decorators、請求參數/Body、回應格式。所有路由均有
/api全域前綴。
5.2.1 認證模組(Auth)— 17 個端點
Controller: AuthController路由前綴: /api/authSwagger Tag: Auth
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 1 | POST | /auth/register | - | - | 用戶註冊 |
| 2 | POST | /auth/login | - | - | 用戶登入 |
| 3 | POST | /auth/login/google | - | - | Google OAuth 登入 |
| 4 | POST | /auth/login/telegram | - | - | Telegram 登入 |
| 5 | GET | /auth/user-detail | JwtAuthGuard | - | 取得用戶詳細資料 |
| 6 | GET | /auth/balance | JwtAuthGuard | - | 取得用戶餘額 |
| 7 | PATCH | /auth/profile | JwtAuthGuard | - | 更新個人資料 |
| 8 | POST | /auth/change-password | JwtAuthGuard | - | 變更密碼 |
| 9 | POST | /auth/set-password | JwtAuthGuard | - | 首次設定密碼(OAuth 用戶) |
| 10 | POST | /auth/send-email-verify | JwtAuthGuard | - | 發送 Email 驗證碼 |
| 11 | POST | /auth/verify-email | JwtAuthGuard | - | 驗證 Email |
| 12 | POST | /auth/send-mobile-verify | JwtAuthGuard | - | 發送手機驗證碼 |
| 13 | POST | /auth/verify-mobile | JwtAuthGuard | - | 驗證手機號碼 |
| 14 | POST | /auth/upload-avatar | JwtAuthGuard | - | 上傳頭像 (multipart) |
| 15 | POST | /auth/bind/google | JwtAuthGuard | - | 綁定 Google 帳號 |
| 16 | POST | /auth/unbind/google | JwtAuthGuard | - | 解綁 Google 帳號 |
| 17 | POST | /auth/bind/telegram | JwtAuthGuard | - | 綁定 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 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /game/providers | OptionalJwtAuthGuard | 遊戲供應商列表(支援 gameType 篩選) |
| 2 | POST | /game/launch | JwtAuthGuard | 啟動遊戲(回傳遊戲 URL) |
| 3 | POST | /game/simulate | JwtAuthGuard | 模擬遊戲結果(開發測試用) |
| 4 | POST | /game/demo | - | 試玩模式(不需登入) |
| 5 | GET | /game/recent | JwtAuthGuard | 最近遊玩的遊戲列表 |
Admin 端點(11 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 6 | GET | /game/admin/providers | AdminJwtAuthGuard | - | 遊戲供應商列表 (Admin) |
| 7 | POST | /game/admin/providers | AdminJwtAuthGuard | - | 新增遊戲供應商 |
| 8 | PATCH | /game/admin/providers/:id | AdminJwtAuthGuard | - | 更新遊戲供應商 |
| 9 | DELETE | /game/admin/providers/:id | AdminJwtAuthGuard | - | 刪除遊戲供應商 |
| 10 | GET | /game/admin/type-configs | AdminJwtAuthGuard | - | 遊戲分類列表 |
| 11 | POST | /game/admin/type-configs | AdminJwtAuthGuard | - | 新增遊戲分類 |
| 12 | PATCH | /game/admin/type-configs/:id | AdminJwtAuthGuard | - | 更新遊戲分類 |
| 13 | DELETE | /game/admin/type-configs/:id | AdminJwtAuthGuard | - | 刪除遊戲分類 |
| 14 | GET | /game/admin/preview-template | AdminJwtAuthGuard | - | 預覽模板 |
| 15 | POST | /game/admin/load-template | AdminJwtAuthGuard | - | 帶入模板(按 @AdminSiteCode 寫入) |
| 16 | POST | /game/admin/copy-site-data | AdminJwtAuthGuard | - | 跨站複製 |
S2S 回調端點(2 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 17 | POST | /game/betsolutions/callback | - | BetSolutions S2S 回調(@ApiExcludeEndpoint) |
| 18 | POST | /game/rsg/callback | - | RSG S2S 回調(DES 加解密,@ApiExcludeEndpoint) |
POST /game/launch 業務邏輯:
- 驗證用戶 JWT
- 檢查
AdminRiskService.isUserBlockedFromGame()遊戲黑名單(錯誤碼 5010) - 取得對應 provider 的 launch URL
- UPSERT
game-play-log記錄遊玩紀錄 - 回傳遊戲 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 級)
| # | Method | Route | 說明 |
|---|---|---|---|
| 1 | POST | /wallet/bank-card/add | 新增銀行卡(multipart: idCardFront/idCardBack/passbookCover) |
| 2 | GET | /wallet/bank-card/list | 取得銀行卡列表 |
| 3 | DELETE | /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 級)
| # | Method | Route | 說明 |
|---|---|---|---|
| 1 | POST | /wallet/credit-card/add | 新增信用卡 |
| 2 | GET | /wallet/credit-card/list | 取得信用卡列表 |
| 3 | DELETE | /wallet/credit-card/:id | 刪除信用卡 |
加密錢包(CryptoAddress)— 3 個端點
Controller: CryptoAddressController路由前綴: /api/wallet/crypto-addressGuards: JwtAuthGuard(Controller 級)
| # | Method | Route | 說明 |
|---|---|---|---|
| 1 | POST | /wallet/crypto-address/add | 新增加密錢包地址 |
| 2 | GET | /wallet/crypto-address/list | 取得加密錢包列表 |
| 3 | DELETE | /wallet/crypto-address/:id | 刪除加密錢包地址 |
5.2.4 金流模組(Vendor)— 6 個端點
Controller: VendorController路由前綴: /api/vendor
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /vendor/channels | JwtAuthGuard | 取得可用金流通道列表(依語系/群組篩選) |
萬通金流(Wantong)— 2 個端點
Controller: WantongController路由前綴: /api/vendor/wantong
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 2 | POST | /vendor/wantong/add-atm | JwtAuthGuard | ATM 入金 |
| 3 | POST | /vendor/wantong/add-card | JwtAuthGuard | 信用卡入金 |
| 4 | POST | /vendor/wantong/callback | - | 萬通回調(@ApiExcludeEndpoint) |
USDT(加密貨幣)— 1 個端點
Controller: UsdtController路由前綴: /api/vendor/usdt
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 5 | POST | /vendor/usdt/callback | - | USDT 回調(@ApiExcludeEndpoint) |
5.2.5 存款模組(Deposit)— 4 個端點
Controller: DepositController路由前綴: /api/deposit
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /deposit/exchange-rate | - | 取得台幣即時匯率 |
| 2 | GET | /deposit/crypto-rate | - | 取得加密貨幣匯率 |
| 3 | GET | /deposit/channels | JwtAuthGuard | 取得可用存款通道 |
| 4 | POST | /deposit | JwtAuthGuard | 建立存款訂單 |
| 5 | POST | /deposit/confirm | JwtAuthGuard | 確認存款(上傳付款證明) |
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 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /vip/levels | - | VIP 等級列表(含 siteCode 篩選) |
| 2 | GET | /vip/rebates | - | VIP 返水規則列表 |
| 3 | GET | /vip/user-status | JwtAuthGuard | 用戶 VIP 狀態 |
Admin 端點(10 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 4 | GET | /vip/admin/levels | AdminJwtAuthGuard, PermissionsGuard | vip:read | VIP 等級列表 (Admin) |
| 5 | POST | /vip/admin/levels | AdminJwtAuthGuard, PermissionsGuard | vip:write | 新增 VIP 等級 |
| 6 | PATCH | /vip/admin/levels/:id | AdminJwtAuthGuard, PermissionsGuard | vip:write | 更新 VIP 等級 |
| 7 | DELETE | /vip/admin/levels/:id | AdminJwtAuthGuard, PermissionsGuard | vip:write | 刪除 VIP 等級 |
| 8 | GET | /vip/admin/rebates | AdminJwtAuthGuard, PermissionsGuard | vip:read | 返水規則列表 (Admin) |
| 9 | POST | /vip/admin/rebates/bulk | AdminJwtAuthGuard, PermissionsGuard | vip:write | 批次新增/更新返水規則 (Bulk Upsert) |
| 10 | GET | /vip/admin/preview-template | AdminJwtAuthGuard | - | 預覽模板 |
| 11 | POST | /vip/admin/load-template | AdminJwtAuthGuard | - | 帶入模板 |
| 12 | POST | /vip/admin/copy-site-data | AdminJwtAuthGuard | - | 跨站複製 |
| 13 | POST | /vip/admin/trigger-settlement | AdminJwtAuthGuard | - | 手動觸發反水結算 |
5.2.7 活動模組(Promo)— 7 個端點
Controller: PromoController路由前綴: /api/promo
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /promo/list | OptionalJwtAuthGuard | 活動列表(前台,activeOnly) |
| 2 | GET | /promo/:id | OptionalJwtAuthGuard | 活動詳情 |
| 3 | GET | /promo/claims | JwtAuthGuard | 我的活動領取紀錄 |
| 4 | GET | /promo/tags | - | 活動標籤列表 |
| 5 | POST | /promo/:id/claim | JwtAuthGuard | 領取活動獎勵 |
POST /promo/:id/claim 業務邏輯:
- 驗證用戶是否滿足領取條件(conditionType 檢查)
- 檢查是否已領取(unique constraint: siteCode + promoId + userId)
- 檢查 maxClaims 上限
- 發放獎勵金額至用戶餘額
- 設定打碼量要求(rewardAmount * turnoverMultiplier)
5.2.8 排行榜模組(Ranking)— 1 個端點
Controller: RankingController路由前綴: /api/ranking
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /ranking | - | 排行榜列表 |
5.2.9 投注紀錄模組(BetRecord)— 2 個端點
Controller: BetRecordController路由前綴: /api/bet-record
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /bet-record/orders | JwtAuthGuard | 注單列表(支援 gameType/status/日期篩選) |
| 2 | GET | /bet-record/orders/:id | JwtAuthGuard | 注單詳情(含明細) |
5.2.10 代理推廣模組(Affiliate)— 39 個端點
Controller: AffiliateController路由前綴: /api/affiliate
前台代理端點(21 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | POST | /affiliate/track-click | - | 追蹤推廣連結點擊 |
| 2 | POST | /affiliate/apply | JwtAuthGuard | 申請成為代理 |
| 3 | GET | /affiliate/tour | JwtAuthGuard | 代理導覽資訊 |
| 4 | GET | /affiliate/dashboard | JwtAuthGuard | 代理儀表板 |
| 5 | GET | /affiliate/downline | JwtAuthGuard | 下線列表 |
| 6 | GET | /affiliate/commissions | JwtAuthGuard | 佣金紀錄 |
| 7 | GET | /affiliate/settlements | JwtAuthGuard | 結算紀錄 |
| 8 | GET | /affiliate/balance | JwtAuthGuard | 代理餘額 |
| 9 | POST | /affiliate/withdraw | JwtAuthGuard | 代理提款申請 |
| 10 | GET | /affiliate/withdrawals | JwtAuthGuard | 代理提款紀錄 |
| 11 | GET | /affiliate/alliance-info | - | 聯盟資訊(公開) |
| 12 | GET | /affiliate/tier-info | - | 代理等級資訊(公開) |
| 13 | GET | /affiliate/vip-milestones | - | VIP 里程碑(公開) |
| 14 | GET | /affiliate/referral-codes | JwtAuthGuard | 我的推廣碼列表 |
| 15 | POST | /affiliate/referral-codes | JwtAuthGuard | 新增推廣碼(最多 10 個) |
| 16 | PATCH | /affiliate/referral-codes/:id | JwtAuthGuard | 更新推廣碼 |
| 17 | DELETE | /affiliate/referral-codes/:id | JwtAuthGuard | 刪除推廣碼 |
Admin 代理管理端點(17 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 18 | GET | /affiliate/admin/agents | AdminJwtAuthGuard, PermissionsGuard | affiliate:read | 代理列表 |
| 19 | POST | /affiliate/admin/create-agent | AdminJwtAuthGuard, PermissionsGuard | affiliate:write | 手動綁定用戶為代理 |
| 20 | GET | /affiliate/admin/settlements | AdminJwtAuthGuard, PermissionsGuard | affiliate:read | 佣金結算列表 |
| 21 | POST | /affiliate/admin/settlements/:id/review | AdminJwtAuthGuard, PermissionsGuard | affiliate:write | 結算審核 (approve/reject) |
| 22 | GET | /affiliate/admin/settlements/:id/risk-logs | AdminJwtAuthGuard, PermissionsGuard | affiliate:read | 結算風控紀錄 |
| 23 | GET | /affiliate/admin/withdrawals | AdminJwtAuthGuard, PermissionsGuard | affiliate:read | 代理提款列表 |
| 24 | POST | /affiliate/admin/withdrawals/:id/review | AdminJwtAuthGuard, PermissionsGuard | affiliate:write | 提款審核 |
| 25 | POST | /affiliate/admin/withdrawals/:id/complete | AdminJwtAuthGuard, PermissionsGuard | affiliate:write | 提款完成 |
| 26 | POST | /affiliate/admin/bind | AdminJwtAuthGuard, PermissionsGuard | affiliate:write | 手動綁定上下線 |
| 27 | GET | /affiliate/admin/bind-logs | AdminJwtAuthGuard, PermissionsGuard | affiliate:read | 綁定紀錄 |
| 28 | GET | /affiliate/admin/commission-rates | AdminJwtAuthGuard | - | 佣金費率列表 |
| 29 | POST | /affiliate/admin/commission-rates | AdminJwtAuthGuard | - | 新增/更新佣金費率 |
| 30 | DELETE | /affiliate/admin/commission-rates/:id | AdminJwtAuthGuard | - | 刪除佣金費率 |
| 31 | GET | /affiliate/admin/vip-milestones | AdminJwtAuthGuard | - | VIP 里程碑列表 |
| 32 | POST | /affiliate/admin/vip-milestones | AdminJwtAuthGuard | - | 新增/更新 VIP 里程碑 |
| 33 | DELETE | /affiliate/admin/vip-milestones/:id | AdminJwtAuthGuard | - | 刪除 VIP 里程碑 |
| 34 | GET | /affiliate/admin/agent-tiers | AdminJwtAuthGuard | - | 代理等級列表 |
| 35 | POST | /affiliate/admin/agent-tiers | AdminJwtAuthGuard | - | 新增/更新代理等級 |
| 36 | DELETE | /affiliate/admin/agent-tiers/:id | AdminJwtAuthGuard | - | 刪除代理等級 |
| 37 | GET | /affiliate/admin/preview-template | AdminJwtAuthGuard | - | 預覽模板 |
| 38 | POST | /affiliate/admin/load-template | AdminJwtAuthGuard | - | 帶入模板 |
| 39 | POST | /affiliate/admin/set-agent-tier | AdminJwtAuthGuard | - | 手動調整代理等級 |
| 40 | POST | /affiliate/admin/trigger-settlement | AdminJwtAuthGuard | - | 手動觸發週結 |
| 41 | POST | /affiliate/admin/trigger-daily-settlement | AdminJwtAuthGuard | - | 手動觸發日結 |
5.2.11 站內信模組(Inbox)— 7 個端點
Controller: InboxController路由前綴: /api/inbox
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 1 | GET | /inbox/list | JwtAuthGuard | - | 用戶收件匣列表 |
| 2 | GET | /inbox/unread-count | JwtAuthGuard | - | 未讀通知數量 |
| 3 | POST | /inbox/:id/read | JwtAuthGuard | - | 標記通知為已讀 |
| 4 | POST | /inbox/read-all | JwtAuthGuard | - | 全部標記為已讀 |
| 5 | POST | /inbox/admin/send | AdminJwtAuthGuard, PermissionsGuard | - | 管理員發送通知 |
| 6 | PATCH | /inbox/admin/:id | AdminJwtAuthGuard, PermissionsGuard | - | 更新通知 |
| 7 | GET | /inbox/admin/list | AdminJwtAuthGuard, PermissionsGuard | - | 管理員通知列表 |
| 8 | DELETE | /inbox/admin/:id | AdminJwtAuthGuard, PermissionsGuard | - | 刪除通知 |
5.2.12 站點設定模組(SiteConfig)— 12 個端點
Controller: SiteConfigController路由前綴: /api/site-config
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /site-config | - | 取得當前站點公開配置 |
| 2 | GET | /site-config/admin/list | AdminJwtAuthGuard | 站點列表(含主題) |
| 3 | POST | /site-config/admin | AdminJwtAuthGuard | 新增站點 |
| 4 | PATCH | /site-config/admin/:id | AdminJwtAuthGuard | 更新站點設定 |
| 5 | DELETE | /site-config/admin/:id | AdminJwtAuthGuard | 刪除站點(cascade 刪主題) |
| 6 | GET | /site-config/admin/:siteConfigId/themes | AdminJwtAuthGuard | 主題列表 |
| 7 | POST | /site-config/admin/:siteConfigId/themes | AdminJwtAuthGuard | 新增主題 |
| 8 | PATCH | /site-config/admin/themes/:id | AdminJwtAuthGuard | 更新主題 |
| 9 | DELETE | /site-config/admin/themes/:id | AdminJwtAuthGuard | 刪除主題 |
| 10 | POST | /site-config/admin/:id/domain-asset | AdminJwtAuthGuard | 上傳域名素材 |
| 11 | POST | /site-config/admin/:id/customer-service-icon | AdminJwtAuthGuard | 上傳客服圖示 |
| 12 | PATCH | /site-config/admin/:siteConfigId/mascots | AdminJwtAuthGuard | 更新吉祥物 |
| 13 | GET | /site-config/admin/:siteCode/customer-service | AdminJwtAuthGuard | 取得客服設定 |
5.2.13 提領模組(Withdrawal)— 7 個端點
Controller: WithdrawalController路由前綴: /api/withdrawal
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 1 | POST | /withdrawal/send-code | JwtAuthGuard | - | 發送提領驗證碼 |
| 2 | POST | /withdrawal/request | JwtAuthGuard | - | 申請提領 |
| 3 | GET | /withdrawal/list | JwtAuthGuard | - | 我的提領紀錄 |
| 4 | GET | /withdrawal/turnover-status | JwtAuthGuard | - | 打碼量狀態 |
POST /withdrawal/request
- Body:
RequestWithdrawalDto—{ amount: number, cryptoAddressId: number, verifyCode: string } - 業務邏輯: 驗證信箱驗證碼 → 檢查餘額充足 → 凍結餘額 → 建立提領訂單
5.2.14 任務模組(Mission)— 3 個端點
Controller: MissionController路由前綴: /api/mission
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /mission/list | JwtAuthGuard | 任務列表(含進度) |
| 2 | GET | /mission/claims | JwtAuthGuard | 任務領取紀錄 |
| 3 | POST | /mission/:id/claim | JwtAuthGuard | 領取任務獎勵 |
5.2.15 其他模組
公用端點(Common)— 1 個端點
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /common/enums | - | 取得列舉資料 + ERROR_CODES(依語系) |
即時體育(LiveSports)— 1 個端點
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | /live-sports | - | 即時賽事列表(Redis 快取,30min 更新) |
健康檢查(App)— 1 個端點
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 1 | GET | / | - | Health check |
5.2.16 後台管理模組(Admin)— 90 個端點
Controller: AdminController路由前綴: /api/adminSwagger Tag: Admin
認證管理(4 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 1 | POST | /admin/login | - | - | 管理員登入 |
| 2 | POST | /admin/register | - | - | 管理員註冊 |
| 3 | POST | /admin/send-verify-code | - | - | 發送驗證碼 |
| 4 | POST | /admin/verify-email | - | - | 驗證 Email |
個人資料 + 2FA(4 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 5 | GET | /admin/profile | AdminJwtAuthGuard | 取得管理員個人資料 |
| 6 | PATCH | /admin/profile | AdminJwtAuthGuard | 更新管理員個人資料 |
| 7 | POST | /admin/google-auth/setup | AdminJwtAuthGuard | 設定 Google Authenticator(回傳 QR Code) |
| 8 | POST | /admin/google-auth/verify | AdminJwtAuthGuard | 驗證 TOTP 啟用 2FA |
| 9 | POST | /admin/google-auth/disable | AdminJwtAuthGuard | 停用 2FA |
權限(1 個)
| # | Method | Route | Guards | 說明 |
|---|---|---|---|---|
| 10 | GET | /admin/permissions/all | AdminJwtAuthGuard | 取得所有權限列表 |
管理員 CRUD(5 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 11 | GET | /admin/list | AdminJwtAuthGuard, PermissionsGuard | admin:read | 管理員列表 |
| 12 | GET | /admin/:id | AdminJwtAuthGuard, PermissionsGuard | admin:read | 取得管理員詳情 |
| 13 | POST | /admin/create | AdminJwtAuthGuard, PermissionsGuard | admin:write | 新增管理員 |
| 14 | PATCH | /admin/:id | AdminJwtAuthGuard, PermissionsGuard | admin:write | 更新管理員 |
| 15 | DELETE | /admin/:id | AdminJwtAuthGuard, PermissionsGuard | admin:write | 刪除管理員 |
群組 CRUD(5 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 16 | GET | /admin/groups/list | AdminJwtAuthGuard, PermissionsGuard | admin-group:read | 群組列表 |
| 17 | GET | /admin/groups/:id | AdminJwtAuthGuard, PermissionsGuard | admin-group:read | 群組詳情 |
| 18 | POST | /admin/groups/create | AdminJwtAuthGuard, PermissionsGuard | admin-group:write | 新增群組 |
| 19 | PATCH | /admin/groups/:id | AdminJwtAuthGuard, PermissionsGuard | admin-group:write | 更新群組 |
| 20 | DELETE | /admin/groups/:id | AdminJwtAuthGuard, PermissionsGuard | admin-group:write | 刪除群組 |
操作紀錄(1 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 21 | GET | /admin/logs/list | AdminJwtAuthGuard, PermissionsGuard | admin-log:read | 操作紀錄列表 |
活動管理(8 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 22 | GET | /admin/promos/list | AdminJwtAuthGuard, PermissionsGuard | promo:read | 活動列表 |
| 23 | GET | /admin/promos/:id | AdminJwtAuthGuard, PermissionsGuard | promo:read | 活動詳情 |
| 24 | POST | /admin/promos/create | AdminJwtAuthGuard, PermissionsGuard | promo:write | 新增活動 (multipart) |
| 25 | PATCH | /admin/promos/:id | AdminJwtAuthGuard, PermissionsGuard | promo:write | 更新活動 (multipart) |
| 26 | DELETE | /admin/promos/:id | AdminJwtAuthGuard, PermissionsGuard | promo:write | 刪除活動 |
| 27 | GET | /admin/promo-tags/list | AdminJwtAuthGuard, PermissionsGuard | promo-tag:read | 標籤列表 |
| 28 | POST | /admin/promo-tags/create | AdminJwtAuthGuard, PermissionsGuard | promo-tag:write | 新增標籤 |
| 29 | PATCH | /admin/promo-tags/:id | AdminJwtAuthGuard, PermissionsGuard | promo-tag:write | 更新標籤 |
| 30 | DELETE | /admin/promo-tags/:id | AdminJwtAuthGuard, PermissionsGuard | promo-tag:write | 刪除標籤 |
財務管理(26 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 31 | GET | /admin/finance/deposit-review | AdminJwtAuthGuard, PermissionsGuard | - | 存款審核列表 |
| 32 | PATCH | /admin/finance/deposit-review/:id | AdminJwtAuthGuard, PermissionsGuard | - | 審核存款 (approve/reject) |
| 33 | GET | /admin/finance/users | AdminJwtAuthGuard, PermissionsGuard | user:read | 前台用戶列表 |
| 34 | GET | /admin/finance/users/:id | AdminJwtAuthGuard, PermissionsGuard | user:read | 用戶詳情 |
| 35 | PATCH | /admin/finance/users/:id | AdminJwtAuthGuard, PermissionsGuard | user:write | 更新用戶資料 |
| 36 | PATCH | /admin/users/:userId/vendor-group | AdminJwtAuthGuard, PermissionsGuard | user:write | 更新用戶金流群組 |
| 37 | POST | /admin/finance/adjust-balance | AdminJwtAuthGuard, PermissionsGuard | finance:write | 人工調節餘額 |
| 38-42 | - | /admin/finance/bank-cards/* | AdminJwtAuthGuard, PermissionsGuard | finance:read/write | 銀行卡 CRUD + 審核 (5 個) |
| 43-47 | - | /admin/finance/credit-cards/* | AdminJwtAuthGuard, PermissionsGuard | finance:read/write | 信用卡 CRUD + 審核 (5 個) |
| 48-52 | - | /admin/finance/crypto-addresses/* | AdminJwtAuthGuard, PermissionsGuard | finance:read/write | 加密錢包 CRUD + 審核 (5 個) |
| 53-56 | - | /admin/finance/withdrawals/* | AdminJwtAuthGuard, PermissionsGuard | withdrawal:read/write | 提領列表、審核、上傳憑證、完成 (4 個) |
金流商管理(9 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 57 | GET | /admin/vendor-groups/list | AdminJwtAuthGuard, PermissionsGuard | vendor:read | 金流群組列表 |
| 58 | POST | /admin/vendor-groups/create | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 新增金流群組 |
| 59 | PATCH | /admin/vendor-groups/:id | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 更新金流群組 |
| 60 | DELETE | /admin/vendor-groups/:id | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 刪除金流群組 |
| 61 | GET | /admin/vendor-groups/:id/channels | AdminJwtAuthGuard, PermissionsGuard | vendor:read | 群組通道列表 |
| 62 | PUT | /admin/vendor-groups/:id/channels | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 設定群組通道 |
| 63 | GET | /admin/vendor-channels/list | AdminJwtAuthGuard, PermissionsGuard | vendor:read | 金流通道列表 |
| 64 | POST | /admin/vendor-channels/create | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 新增金流通道 |
| 65 | PATCH | /admin/vendor-channels/:id | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 更新金流通道 |
| 66 | DELETE | /admin/vendor-channels/:id | AdminJwtAuthGuard, PermissionsGuard | vendor:write | 刪除金流通道 |
報表(10 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 67 | GET | /admin/reports/players | AdminJwtAuthGuard, PermissionsGuard | report:read | 玩家報表(25+ 篩選參數) |
| 68 | GET | /admin/reports/vip-players | AdminJwtAuthGuard, PermissionsGuard | report:read | VIP 玩家報表 |
| 69 | GET | /admin/reports/bet-records | AdminJwtAuthGuard, PermissionsGuard | report:read | 投注紀錄報表 |
| 70 | GET | /admin/reports/overview | AdminJwtAuthGuard, PermissionsGuard | report:read | 總體報表 |
| 71 | GET | /admin/reports/profit-loss | AdminJwtAuthGuard, PermissionsGuard | report:read | 損益報表 |
| 72 | GET | /admin/reports/games | AdminJwtAuthGuard, PermissionsGuard | report:read | 遊戲報表 |
| 73 | GET | /admin/reports/promos | AdminJwtAuthGuard, PermissionsGuard | report:read | 優惠報表 |
| 74 | GET | /admin/reports/player-summary | AdminJwtAuthGuard, PermissionsGuard | report:read | 玩家簡表 |
| 75 | GET | /admin/reports/r2-logs | AdminJwtAuthGuard, PermissionsGuard | report:read | R2 操作紀錄 |
| 76 | GET | /admin/reports/export/:type | AdminJwtAuthGuard, PermissionsGuard | report:read | 匯出報表 (CSV) |
風控管理(8 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 77 | GET | /admin/risk/ip-rules | AdminJwtAuthGuard, PermissionsGuard | risk:read | IP 規則列表 |
| 78 | POST | /admin/risk/ip-rules | AdminJwtAuthGuard, PermissionsGuard | risk:write | 新增 IP 規則 |
| 79 | PATCH | /admin/risk/ip-rules/:id | AdminJwtAuthGuard, PermissionsGuard | risk:write | 更新 IP 規則 |
| 80 | DELETE | /admin/risk/ip-rules/:id | AdminJwtAuthGuard, PermissionsGuard | risk:write | 刪除 IP 規則 |
| 81 | GET | /admin/risk/login-failures | AdminJwtAuthGuard, PermissionsGuard | risk:read | 登入失敗列表 |
| 82 | GET | /admin/risk/lookup | AdminJwtAuthGuard, PermissionsGuard | risk:read | IP/FP 檢查 |
| 83 | GET | /admin/risk/game-blacklist | AdminJwtAuthGuard, PermissionsGuard | risk:read | 遊戲黑名單列表 |
| 84 | POST | /admin/risk/game-blacklist | AdminJwtAuthGuard, PermissionsGuard | risk:write | 新增遊戲黑名單 |
| 85 | DELETE | /admin/risk/game-blacklist/:id | AdminJwtAuthGuard, PermissionsGuard | risk:write | 刪除遊戲黑名單 |
R2 儲存管理(6 個)
| # | Method | Route | Guards | Permission | 說明 |
|---|---|---|---|---|---|
| 86 | GET | /admin/r2/list | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 檔案列表 |
| 87 | POST | /admin/r2/upload | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 上傳檔案 (multipart, 最大 50MB) |
| 88 | POST | /admin/r2/delete | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 批次刪除 |
| 89 | POST | /admin/r2/move | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 移動檔案/資料夾 |
| 90 | POST | /admin/r2/create-folder | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 建立資料夾 |
| 91 | POST | /admin/r2/delete-folder | AdminJwtAuthGuard, PermissionsGuard | storage:manage | R2 刪除資料夾 (遞迴) |
5.3 完整 DTO 文件(53 個 DTO)
本章節記錄所有 Data Transfer Object 的完整欄位定義,包含驗證規則(class-validator)與 Swagger 描述。
5.3.1 認證模組 DTO(7 個)
RegisterDto
檔案: src/modules/auth/dto/register.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
account | string | @IsString() | YES | 帳號 |
password | string | @IsString(), @MinLength(6) | YES | 密碼(至少 6 碼) |
name | string | @IsString() | YES | 暱稱 |
refCode | string | @IsOptional(), @IsString() | NO | 推廣碼 |
device | string | @IsOptional(), @IsString() | NO | 裝置指紋(FingerprintJS) |
LoginDto
檔案: src/modules/auth/dto/login.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
account | string | @IsString() | YES | 帳號 |
password | string | @IsString(), @MinLength(6) | YES | 密碼 |
device | string | @IsOptional(), @IsString() | NO | 裝置指紋 |
LoginTelegramDto
檔案: src/modules/auth/dto/login-telegram.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
id | number | @IsInt() | YES | Telegram User ID |
first_name | string | @IsString() | YES | 名字 |
last_name | string | @IsOptional(), @IsString() | NO | 姓氏 |
username | string | @IsOptional(), @IsString() | NO | Telegram Username |
photo_url | string | @IsOptional(), @IsString() | NO | 頭像 URL |
auth_date | number | @IsInt() | YES | 認證時間戳 |
hash | string | @IsString() | YES | HMAC-SHA256 驗證 Hash |
device | string | @IsOptional(), @IsString() | NO | 裝置指紋 |
SetPasswordDto
檔案: src/modules/auth/dto/set-password.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
password | string | @IsString(), @MinLength(6) | YES | 新密碼 |
SendEmailVerifyDto
檔案: src/modules/auth/dto/send-email-verify.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | 目標 Email |
SendMobileVerifyDto
檔案: src/modules/auth/dto/send-mobile-verify.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
mobile | string | @IsString() | YES | 手機號碼 |
countryCode | string | @IsOptional(), @IsString() | NO | 國碼 |
PhoneValidateDto
檔案: src/modules/auth/dto/phone-validate.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
mobile | string | @IsString() | YES | 手機號碼 |
code | string | @IsString() | YES | 驗證碼 |
5.3.2 後台管理模組 DTO(10 個)
AdminLoginDto
檔案: src/modules/admin/dto/admin-login.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | 管理員 Email |
password | string | @IsString(), @MinLength(6) | YES | 密碼 |
RegisterAdminDto
檔案: src/modules/admin/dto/register-admin.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | |
password | string | @IsString(), @MinLength(6) | YES | 密碼 |
name | string | @IsString() | YES | 名稱 |
verifyCode | string | @IsString() | YES | 驗證碼 |
CreateAdminDto
檔案: src/modules/admin/dto/create-admin.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | |
password | string | @IsString(), @MinLength(6) | YES | 密碼 |
name | string | @IsString() | YES | 名稱 |
groupId | number | @IsOptional(), @IsInt() | NO | 群組 ID |
allowedSiteCodes | string[] | @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
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
name | string | @IsString() | YES | 群組名稱 |
type | string | @IsString(), @IsIn([...]) | YES | 群組類型 |
permissions | string[] | @IsOptional(), @IsArray() | NO | 權限列表 |
description | string | @IsOptional(), @IsString() | NO | 說明 |
UpdateAdminGroupDto
- 繼承
PartialType(CreateAdminGroupDto)
QueryAdminLogDto
檔案: src/modules/admin/dto/query-admin-log.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
page | number | @IsOptional(), @IsInt() | NO | 頁碼 |
pageSize | number | @IsOptional(), @IsInt() | NO | 每頁筆數 |
adminId | number | @IsOptional(), @IsInt() | NO | 管理員 ID 篩選 |
module | string | @IsOptional(), @IsString() | NO | 模組篩選 |
action | string | @IsOptional(), @IsString() | NO | 動作篩選 |
startDate | string | @IsOptional(), @IsString() | NO | 起始日期 |
endDate | string | @IsOptional(), @IsString() | NO | 結束日期 |
SendVerifyCodeDto
檔案: src/modules/admin/dto/send-verify-code.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | 目標 Email |
VerifyEmailDto
檔案: src/modules/admin/dto/verify-email.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
email | string | @IsEmail() | YES | |
code | string | @IsString() | YES | 驗證碼 |
GoogleAuthCodeDto
檔案: src/modules/admin/dto/google-auth-code.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
code | string | @IsString() | YES | 6 位 TOTP 驗證碼 |
5.3.3 存款模組 DTO(1 個)
DepositDto
檔案: src/modules/deposit/dto/deposit.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
channelId | number | @IsInt() | YES | 金流通道 ID |
paymentMethod | string | @IsString(), @IsIn(['fiat','credit','crypto']) | YES | 支付方式 |
subOrder | string | @IsString(), @MinLength(8), @MaxLength(24) | YES | 商家訂單編號 |
orderAmount | number | @IsNumber({ maxDecimalPlaces: 6 }), @Min(0.000001) | YES | 訂單金額 (USDT) |
expectedCode | string | @IsOptional(), @IsString() | NO | ATM 銀行代碼 |
expectedAccount | string | @IsOptional(), @IsString() | NO | ATM 帳號 |
userCardLastValue | string | @IsOptional(), @IsString() | NO | 信用卡末五碼 |
productDes | string | @IsOptional(), @IsString() | NO | 商品描述 |
msg | string | @IsOptional(), @IsString() | NO | 備註 |
payerName | string | @IsOptional(), @IsString() | NO | 付款人姓名 |
payerMobile | string | @IsOptional(), @IsString() | NO | 付款人手機 |
payerEmail | string | @IsOptional(), @IsString() | NO | 付款人 Email |
5.3.4 VIP 模組 DTO(4 個)
CreateVipLevelDto
檔案: src/modules/vip/dto/create-vip-level.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
level | number | @IsInt(), @Min(1) | YES | VIP 等級編號 |
name | Record<string, string> | @Transform(JSON.parse), @IsObject() | YES | 等級名稱(多語系) |
tier | string | @IsString(), @IsIn(['bronze','gold','platinum','diamond']) | YES | 階級 |
minChip | number | @IsNumber(), @Min(0) | YES | 升級所需最低籌碼 |
relegationChip | number | @IsNumber(), @Min(0) | YES | 保級所需籌碼 |
sortOrder | number | @IsOptional(), @IsInt() | NO | 排序權重 |
enabled | number | @IsOptional(), @IsInt() | NO | 啟用狀態 |
UpdateVipLevelDto
PartialType(CreateVipLevelDto)— 所有欄位選填
CreateVipRebateDto
檔案: src/modules/vip/dto/create-vip-rebate.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
level | number | @IsInt() | YES | VIP 等級 |
gameType | string | @IsString() | YES | 遊戲類型 |
rebateRate | number | @IsNumber() | YES | 返水比例 (%) |
UpdateVipRebateDto
PartialType(CreateVipRebateDto)— 所有欄位選填
5.3.5 活動模組 DTO(2 個)
CreatePromoDto
檔案: src/modules/promo/dto/create-promo.dto.ts
| 欄位 | 型別 | 驗證規則 | 必填 | 說明 |
|---|---|---|---|---|
title | Record<string, string> | @Transform(JSON.parse), @IsObject() | YES | 活動標題(多語系) |
content | Record<string, string> | @Transform(JSON.parse), @IsObject() | YES | 活動內容(多語系 HTML) |
actionHtml | string | @IsOptional(), @IsString() | NO | 渲染連結/按鈕 HTML |
startTime | string | @IsDateString() | YES | 開始時間 (ISO 8601) |
endTime | string | @IsDateString() | YES | 結束時間 (ISO 8601) |
tag | string | @IsString(), @MaxLength(30) | YES | 活動標籤 |
enabled | number | @IsOptional(), @Transform(Number), @IsInt() | NO | 啟用狀態 |
conditionType | string | @IsString(), @IsIn([...]) | YES | 領取條件類型 |
conditionValue | string | @IsOptional(), @IsString() | NO | 條件門檻值 |
rewardAmount | number | @Transform(Number), @IsNumber() | YES | 獎勵金額 (USD) |
maxClaims | number | @IsOptional(), @Transform(Number), @IsInt() | NO | 最大領取數 |
turnoverMultiplier | number | @IsOptional(), @Transform(Number), @IsNumber() | NO | 打碼量倍數 |
UpdatePromoDto
PartialType(CreatePromoDto)— 所有欄位選填
5.3.6 代理推廣模組 DTO(12 個)
TrackClickDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
refCode | string | YES | 推廣碼 |
referrer | string | NO | 來源頁面 |
ApplyAgentDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agentCode | string | NO | 自訂推廣碼(3-20 碼,不填自動產生) |
CreateAgentDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
userId | number | YES | 用戶 ID |
agentCode | string | NO | 代理碼 |
QueryCommissionsDto / QuerySettlementsDto / QueryDownlineDto
- 均為分頁+篩選 DTO,包含
page,pageSize,startDate,endDate等篩選欄位
RequestWithdrawalDto(代理提款)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
amount | number | YES | 提款金額 |
method | string | YES | 提款方式 (crypto/bank) |
bankCardId | number | NO | 銀行卡 ID |
cryptoAddressId | number | NO | 加密錢包 ID |
ReviewSettlementDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
action | string | YES | 動作 (approve/reject) |
rejectReason | string | NO | 拒絕原因 |
ReviewWithdrawalDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
action | string | YES | 動作 (approve/reject) |
rejectReason | string | NO | 拒絕原因 |
AdminBindDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
memberId | number | YES | 會員 ID |
agentId | number | YES | 代理 ID |
remark | string | NO | 備註 |
CreateReferralCodeDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
code | string | YES | 推廣碼(3-30 英數字,@Matches(/^[a-zA-Z0-9]+$/)) |
label | string | NO | 渠道標籤(最大 50 碼) |
SetAgentTierDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agentId | number | YES | 代理 ID |
tierCode | string | YES | 等級代碼 |
UpsertCommissionRateDto / UpsertVipMilestoneDto / UpsertAgentTierDto
- 批次新增/更新用 DTO,用於「帶入模板」功能
5.3.7 錢包模組 DTO(3 個)
AddBankCardDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
bankCode | string | YES | 銀行代碼 |
bankAccount | string | YES | 銀行帳號 |
branch | string | YES | 分行名稱 |
holderName | string | YES | 持卡人姓名 |
AddCreditCardDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
cardNumber | string | YES | 卡號 |
holderName | string | YES | 持卡人姓名 |
cvv | string | YES | CVV |
expiryDate | string | YES | 有效期 (MM/YY) |
AddCryptoAddressDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
walletName | string | YES | 錢包名稱 |
currency | string | NO | 幣種(預設 USDT) |
network | string | NO | 網路(預設 TRC-20) |
address | string | YES | 錢包地址 |
5.3.8 金流模組 DTO(4 個)
AddAtmDto(萬通 ATM)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
subOrder | string | YES | 訂單編號 |
orderAmount | number | YES | 金額 |
expectedCode | string | YES | 銀行代碼 |
expectedAccount | string | YES | 帳號 |
AddCardDto(萬通信用卡)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
subOrder | string | YES | 訂單編號 |
orderAmount | number | YES | 金額 |
userCardLastValue | string | YES | 卡號末五碼 |
WantongCallbackDto / UsdtCallbackDto
- S2S 回調用 DTO,接收金流商回調參數
5.3.9 站點設定模組 DTO(4 個)
CreateSiteConfigDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
siteCode | string | YES | 站點代碼(唯一,最大 30 碼) |
prefix | string | YES | 白牌前綴(唯一) |
layout | string | NO | 前台模板代碼(預設 a1) |
siteName | Record<string, string> | YES | 站點名稱(多語系) |
siteDescription | Record<string, string> | NO | 站點介紹(多語系) |
supportedLocales | string[] | NO | 支援語系 |
UpdateSiteConfigDto
PartialType(CreateSiteConfigDto)— 所有欄位選填
CreateSiteThemeDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
themeId | string | YES | 主題識別碼(最大 50 碼) |
themeName | Record<string, string> | YES | 主題名稱(多語系) |
primary | Record<string, string> | YES | 主色系 (base/dark/light/glow) |
accent | Record<string, string> | YES | 強調色 |
surface | Record<string, any> | YES | 表面色 |
text | Record<string, string> | YES | 文字色 |
border | Record<string, string> | YES | 邊框色 |
enabled | number | NO | 啟用狀態 |
UpdateSiteThemeDto
PartialType(CreateSiteThemeDto)— 所有欄位選填
5.3.10 站內信模組 DTO(1 個)
AdminSendNotificationDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
userId | number | NO | 目標用戶 ID(不傳=全站廣播) |
title | Record<string, string> | YES | 通知標題(多語系) |
content | Record<string, string> | YES | 通知內容(多語系 HTML) |
category | string | YES | 分類 (system / promo) |
5.3.11 提領模組 DTO(2 個)
RequestWithdrawalDto(前台提領)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
amount | number | YES | 提領金額 (USD) |
cryptoAddressId | number | YES | 加密錢包 ID |
verifyCode | string | YES | 郵箱驗證碼(6 碼) |
ReviewWithdrawalDto(Admin 審核)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
action | string | YES | 動作 (approve / reject) |
rejectReason | string | NO | 拒絕原因 |
5.3.12 R2 模組 DTO(1 個)
QueryR2LogDto
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
page | number | NO | 頁碼 |
pageSize | number | NO | 每頁筆數 |
action | string | NO | 操作類型篩選 |
adminId | number | NO | 管理員 ID 篩選 |
keyword | string | NO | 關鍵字搜尋 |
startDate | string | NO | 起始日期 |
endDate | string | NO | 結束日期 |
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-groupToken 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 表:
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,結合多個工具函式:
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() 封鎖檢查邏輯:
- 查詢
risk-game-blacklist表 - 條件:
userId匹配 AND(gameType IS NULLORgameType匹配)AND(productId IS NULLORproductId匹配) - 若有匹配記錄則返回
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?) → void | UPSERT game-play-log |
afterBetSettlement() | (userId, betOrder) → void | 投注結算後連鎖觸發 |
adminListProviders() | (siteCode?) → GameProvider[] | Admin 供應商列表 |
adminCreateProvider() | (dto, siteCode?) → GameProvider | Admin 新增供應商 |
copyGameSiteData() | (source, target, type) → void | 跨站複製(transaction 內先刪後插) |
loadTemplate() | (siteCode?) → void | 帶入模板 |
投注結算後連鎖觸發
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() | () → void | Cron: 每日反水結算 |
monthlyRelegationCheck() | () → void | Cron: 月度保級檢查 |
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() | () → void | Cron: 每週一 03:00 佣金週結 |
dailySettlement() | () → void | Cron: 每日 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() 回傳結構
{
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 位小數:
Math.floor(value * 1e6) / 1e6parsePagination(query: any): { skip: number, take: number }
檔案: src/utils/pagination.ts
正規化分頁參數:
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 加入日期區間篩選:
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()裝飾器宣告。所有排程邏輯分散在VipService、AffiliateSettlementService、LiveSportsService三個 Service 中。
5.5.1 每日反水結算 (Daily Rebate Settlement)
| 項目 | 說明 |
|---|---|
| Cron 表達式 | 0 5 0 * * * |
| 執行時間 | 每日 00:05 |
| 所在 Service | VipService (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) / 1e6dailyEffective:用戶昨日該遊戲類型的有效投注總額rebateRate:該站點 VIP 等級對應遊戲類型的反水比率 (%)- 結果使用
truncateUsd()無條件捨去至小數 6 位
反水規則對照表 (預設模板)
| VIP 等級 | sports | slot | live | lottery | chess | esports | crypto | fish |
|---|---|---|---|---|---|---|---|---|
| 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 紀錄欄位
| 欄位 | 範例值 | 說明 |
|---|---|---|
| userId | 42 | 用戶 ID |
| settleDate | "2026-03-01" | 結算日期 (昨日) |
| vipLevel | 5 | 結算時的 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 |
| 所在 Service | VipService (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 |
| 所在 Service | AffiliateSettlementService (src/modules/affiliate/affiliate-settlement.service.ts) |
| 方法名稱 | handleWeeklySettlementCron() → settleWeek() → settleRange() |
| 影響資料表 | affiliate-settlement、affiliate-commission、affiliate-balance、affiliate-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()
// 優先順序:
// 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 |
| 所在 Service | AffiliateSettlementService (src/modules/affiliate/affiliate-settlement.service.ts) |
| 方法名稱 | handleDailySettlementCron() → settleDay() → settleRange() |
| 影響資料表 | affiliate-settlement、affiliate-commission、affiliate-balance、affiliate-risk-log |
| 多站點 | 同週結 |
流程
與週結共用 settleRange() 核心邏輯,差異如下:
| 差異點 | 週結 | 日結 |
|---|---|---|
| periodType | 'weekly' | 'daily' |
| 日期範圍 | 上週一 ~ 上週日 | 昨日一天 |
| 冪等 key | weekStart + 'weekly' | dayStr + 'daily' |
| 執行時間 | 每週一 03:00 | 每日 03:30 |
日結時間計算
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 分鐘 |
| 所在 Service | LiveSportsService (src/modules/live-sports/live-sports.service.ts) |
| 方法名稱 | refreshCache() |
| 影響資料表 | 無 (僅寫入 Redis 快取) |
| 外部 API | API-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.sporti18n 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:05 | VipService | handleDailyRebateCron() |
| 2 | 月度保級檢查 | 0 0 1 1 * * | 每月 1 號 01:00 | VipService | handleMonthlyRelegationCron() |
| 3 | 代理佣金週結 | 0 0 3 * * 1 | 每週一 03:00 | AffiliateSettlementService | handleWeeklySettlementCron() |
| 4 | 代理佣金日結 | 0 30 3 * * * | 每日 03:30 | AffiliateSettlementService | handleDailySettlementCron() |
| 5 | 即時賽事快取 | 0 */30 * * * * | 每 30 分鐘 | LiveSportsService | refreshCache() |
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 筆/站)站點定義
| siteCode | prefix | 站點名稱 | 主題色 | 主題名稱 |
|---|---|---|---|---|
| C9 | c9 | C9 娛樂城 | #10b981 (翡翠綠) | Emerald |
| B1 | b1 | 寶盈娛樂城 | #3b82f6 (皇家藍) | Royal Blue |
| B2 | b2 | 星際娛樂城 | #8b5cf6 (星際紫) | Star Purple |
| B3 | b3 | 皇冠娛樂城 | #f59e0b (皇冠金) | Crown Gold |
| B4 | b4 | 鳳凰娛樂城 | #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 輔助函數
/** 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 筆 |
| 使用 UPSERT | ON 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 需求 | 打碼倍率 |
|---|---|---|---|---|---|---|
| deposit | daily | 1 | $10 | $0.30 | 0 | 3x |
| deposit | daily | 2 | $50 | $0.80 | 0 | 3x |
| deposit | daily | 3 | $100 | $1.50 | 0 | 5x |
| deposit | daily | 4 | $200 | $3.00 | VIP 3 | 5x |
| deposit | daily | 5 | $300 | $6.00 | VIP 5 | 8x |
| bet | daily | 1 | $20 | $0.30 | 0 | 3x |
| ... | 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 腳本完整清單
| # | 腳本檔案 | 類型 | 用途 | 影響的資料表 |
|---|---|---|---|---|
| 1 | seed-all.ts | 主入口 | 全資料表假資料 (5 站 × 30 用戶) | 37+ 張表 |
| 2 | seed-site-config.ts | 獨立種子 | 站點設定 + 主題 + 佈局 | site-config, site-theme |
| 3 | seed-vendor.ts | 獨立種子 | 金流群組/通道 | vendor-group, vendor-channel, vendor-group-channel |
| 4 | seed-deposit.ts | 獨立種子 | 存款訂單 | deposit-order |
| 5 | seed-deposit-order.ts | 獨立種子 | 存款訂單 (獨立版) | deposit-order |
| 6 | seed-vip.ts | 獨立種子 | VIP 等級 + 反水 | vip-level, vip-rebate |
| 7 | seed-bet-record.ts | 獨立種子 | 投注紀錄 | bet-order, bet-detail |
| 8 | seed-ranking.ts | 獨立種子 | 排行榜 | rank-list |
| 9 | seed-ranking-users.ts | 獨立種子 | 排行榜用戶 | rank-list |
| 10 | seed-inbox.ts | 獨立種子 | 站內信 | notification, notification-read |
| 11 | seed-mission.ts | 獨立種子 | 任務定義 | mission |
| 12 | seed-withdrawal.ts | 獨立種子 | 提領訂單 | withdrawal-order |
| 13 | seed-learn-more.ts | 獨立種子 | 了解更多 FAQ | site-config (learnMoreConfig) |
| 14 | seed-layout-defaults.ts | 獨立種子 | 前台佈局配置 | site-config (bottomBarConfig, footerConfig) |
| 15 | seed-agent-promo.ts | 獨立種子 | 代理活動 | promo, promo-claim |
| 16 | seed-merchants.ts | 獨立種子 | 商戶資料 | - |
| 17 | assign-all-channels.ts | 工具 | 金流通道全分配 | vendor-group-channel |
| 18 | clear-deposit.ts | 工具 | 清除存款資料 | deposit-order |
| 19 | generate-promo-images.ts | 素材生成 | 活動橫幅圖片 → R2 | (R2 儲存) |
| 20 | generate-mascot-avatars.ts | 素材生成 | 吉祥物頭像 → R2 | (R2 儲存) |
| 21 | generate-mascots.mjs | 素材生成 | 吉祥物圖片 (ESM) | (R2 儲存) |
| 22 | cleanup-promo.sql | SQL 清理 | 清理活動資料 | promo, promo-claim |
5.6.7 腳本共用模式
DataSource 初始化
所有 TypeScript 種子腳本使用相同的 DataSource 初始化模式:
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 約束,清空表格需遵循正確順序 (先刪子表再刪父表):
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 成功回應
{
"code": 200,
"message": "ok",
"result": { /* 實際資料 */ },
"timestamp": 1709369200000,
"path": "/api/auth/user-detail"
}6.1.2 業務錯誤回應
{
"code": 2001,
"message": "帳號已存在",
"data": null,
"timestamp": 1709369200000,
"path": "/api/auth/register"
}HTTP Status 維持 200(僅 401 例外)。
6.1.3 未授權回應
{
"code": 401,
"message": "Unauthorized",
"data": null,
"timestamp": 1709369200000,
"path": "/api/auth/user-detail"
}HTTP Status 為 401。
6.1.4 各專案判斷方式
| 專案 | 成功判斷 | 錯誤處理 |
|---|---|---|
| c9-ec | res?.code === 200 && res.result | useHttp 自動查 ERROR_CODES + toast |
| c9-ims | res?.code === 200 | httpRequest() 三層映射 + sonner toast |
| c9-be | Controller 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/i18n | no_prefix (cookie: i18n_redirected) | locales header |
| c9-ims | next-intl | no_prefix (cookie: NEXT_LOCALE) | locales header |
| c9-be | nestjs-i18n | HeaderResolver | 讀取 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 多語系欄位
{
"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 天 |
| 後台 IMS | NextAuth 5 (JWT) | Credentials Provider | session |
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 傳遞機制
| 場景 | Header | Decorator | 說明 |
|---|---|---|---|
| 前台 API | site-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-ec | hostname → domainConfig → siteId | config/domainConfig/a1.ts |
| c9-ims | hostname → domainConfig → siteId + NEXT_PUBLIC_SITE_ID | config/domainConfig/a1.ts |
| c9-be | SITE_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 + redirect6.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)
- 在後台 IMS 或 Swagger 呼叫
POST /site-config/admin建立新站點 - 設定
siteCode,prefix,siteName,supportedLocales - 建立站點主題
POST /site-config/admin/:siteConfigId/themes - 執行
seed-all.ts為新站點產生基礎資料(VIP 等級、遊戲商等)
6.8.2 前台 (c9-ec)
- 在
config/domainConfig/新增站點檔案(如a2.ts) - 設定 hostname →
{ baseUrl, imgUrl, siteId, layout }映射 - 在
config/domainConfig/index.tsimport 新站點
6.8.3 後台 (c9-ims)
- 在
sites/新增站點目錄(如sites/a2/) - 建立
config.ts(features, theme)+theme.ts(OKLCH 色票) - 在
config/domainConfig/新增 hostname 映射 - 在
config/siteRegistry.ts新增站點 ID 白名單
第 7 章:部署與維運
7.1 部署架構
7.1.1 開發環境 Port 分配
| 服務 | Port | 說明 |
|---|---|---|
| c9-ec | 3010 | Nuxt dev server |
| c9-ims | 3011 | Next.js Turbopack dev server |
| c9-be | 8080 | NestJS API server |
| MySQL | 3306 | 資料庫 |
| Redis | 6379 | 快取 |
| Swagger UI | 8080/api/docs | API 文件 |
7.1.2 生產環境 (Zeabur)
| 服務 | 域名 | 說明 |
|---|---|---|
| c9-ec | c9-ec.zeabur.app | 前台 |
| c9-be | c9-be.zeabur.app | 後端 API |
| MySQL | Zeabur MySQL | 雲端資料庫 |
| Redis | Zeabur Redis | 雲端快取 |
| R2 | Cloudflare R2 | 圖片/檔案儲存 |
7.2 一鍵啟動
7.2.1 同時啟動三個專案
# 根目錄
cd c9 && yarn devconcurrently 同時啟動,彩色標籤區分:
[ec]藍色 — 前台[ims]綠色 — 後台[be]黃色 — 後端
7.2.2 單獨啟動
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-ec | yarn build | yarn preview |
| c9-ims | yarn build:a1 | yarn start:a1 |
| c9-be | yarn build | yarn start:prod |
7.3.2 資料庫初始化
# 1. 確保 MySQL 已啟動且資料庫存在
# 2. 設定 .env.local 環境變數
# 3. 啟動後端(TypeORM synchronize=dev 自動建表)
cd c9-be && yarn dev
# 4. 產生假資料(可選)
npx ts-node scripts/seed-all.ts7.4 一鍵 Commit / Push
7.4.1 統一 Commit
yarn commit # 互動式選擇 type + 輸入訊息,依序 commit 有變更的子專案流程:
- 掃描三個子專案是否有變更
- 互動式選擇 commit type (feat/fix/refactor/style/perf/docs/test/chore)
- 選填 scope + 輸入 commit message
- 依序
git add . && git commit每個有變更的子專案
7.4.2 統一 Push
yarn push # commit + push(有變更才處理)
yarn push:all # 只推送未 push 的 commit
yarn push:ec # 只推送 c9-ec
yarn push:ims # 只推送 c9-ims
yarn push:be # 只推送 c9-be7.5 GitHub 倉庫
| 專案 | 倉庫 | 分支 |
|---|---|---|
| c9-ec | git@github.com:zxc38380166/c9-ec.git | master |
| c9-ims | git@github.com:zxc38380166/c9-ims.git | master |
| c9-be | git@github.com:zxc38380166/c9-be.git | master |
7.6 測試策略
7.6.1 各專案測試
| 專案 | 單元測試 | 元件測試 | E2E 測試 | 型別檢查 |
|---|---|---|---|---|
| c9-ec | Vitest (node) | Vitest + @nuxt/test-utils (happy-dom) | Playwright (Chromium) | - |
| c9-ims | - | - | - | yarn typecheck (tsc --noEmit) |
| c9-be | Jest | - | supertest | - |
7.6.2 測試指令
c9-ec:
yarn test # 跑全部 (Vitest)
yarn test:unit # 只跑單元測試
yarn test:nuxt # 只跑元件測試
yarn test:e2e # E2E (Playwright)c9-ims:
yarn typecheck # TypeScript 型別檢查
yarn lint # ESLint 檢查c9-be:
yarn test # 單元測試 (Jest)
yarn test:e2e # E2E 測試
yarn test:cov # 測試覆蓋率7.7 程式碼品質工具
| 工具 | c9-ec | c9-ims | c9-be |
|---|---|---|---|
| ESLint | @nuxt/eslint | eslint-config-next | @nestjs/eslint-config |
| Prettier | - | prettier | prettier |
| TypeScript | 5.6 | 5 (strict) | 5.7 (strict) |
# 各專案
yarn lint # 程式碼檢查
yarn format # 格式化 (Prettier)7.8 工具依賴
| 工具 | 用途 | 安裝方式 |
|---|---|---|
concurrently | 同時啟動多個專案 | 已在根目錄 devDependencies |
git-cz | 互動式 git commit | npm i -g git-cz |
gh | GitHub CLI (建立/刪除倉庫) | brew install gh |
pnpm | c9-ims 套件管理 | npm i -g pnpm |
7.9 參考文件索引
| 文件 | 位置 | 用途 |
|---|---|---|
CLAUDE.md | 根目錄 + 各子專案 | AI 助手參考指南 |
PROJECT_API.md | c9-ec/public/ + c9-be/ | 205+ 個 API 端點完整參考 |
PROJECT_API_DOC.md | c9-be/ | 12,000+ 行人類可讀 API 文件 |
PROJECT_SPEC.md | c9-ec/public/ + c9-be/ | 全端專案規格書 |
PROJECT_SPEC_RD.md | docs/ | RD 技術規格書(本文件) |
| Swagger UI | http://localhost:8080/api/docs | 互動式 API 文件 |
附錄
附錄 A:完整環境變數參考
A.1 c9-be 環境變數(.env.local)
| 變數名 | 型別 | 預設值 | 說明 |
|---|---|---|---|
DB_HOST | string | localhost | MySQL 主機位址 |
DB_PORT | number | 3306 | MySQL 端口 |
DB_USERNAME | string | root | MySQL 使用者 |
DB_PASSWORD | string | — | MySQL 密碼 |
DB_DATABASE | string | c9_db | MySQL 資料庫名稱 |
DB_SYNCHRONIZE | boolean | true | TypeORM 自動同步(僅開發環境) |
JWT_SECRET | string | — | 前台用戶 JWT 簽名密鑰 |
JWT_ADMIN_SECRET | string | — | 後台管理員 JWT 簽名密鑰 |
JWT_EXPIRE_DAYS | number | 7 | JWT 過期天數 |
SITE_CODE | string | C9 | 預設站點代碼 |
REDIS_HOST | string | localhost | Redis 主機位址 |
REDIS_PORT | number | 6379 | Redis 端口 |
REDIS_PASSWORD | string | — | Redis 密碼(可選) |
R2_ACCOUNT_ID | string | — | Cloudflare R2 帳號 ID |
R2_ACCESS_KEY_ID | string | — | R2 存取金鑰 ID |
R2_SECRET_ACCESS_KEY | string | — | R2 存取金鑰密碼 |
R2_BUCKET_NAME | string | c9-assets | R2 Bucket 名稱 |
R2_PUBLIC_URL | string | — | R2 公開存取 URL |
RESEND_API_KEY | string | — | Resend Email API Key |
RESEND_FROM_EMAIL | string | — | 寄件者 Email |
TWILIO_ACCOUNT_SID | string | — | Twilio 帳號 SID |
TWILIO_AUTH_TOKEN | string | — | Twilio 認證 Token |
TWILIO_PHONE_NUMBER | string | — | Twilio 發送手機號碼 |
GOOGLE_CLIENT_ID | string | — | Google OAuth Client ID |
GOOGLE_CLIENT_SECRET | string | — | Google OAuth Client Secret |
WANTONG_MERCHANT_ID | string | — | 萬通金流商戶 ID |
WANTONG_API_KEY | string | — | 萬通金流 API Key |
WANTONG_CALLBACK_URL | string | — | 萬通金流回調 URL |
USDT_MERCHANT_ID | string | — | USDT 支付商戶 ID |
USDT_API_KEY | string | — | USDT 支付 API Key |
USDT_CALLBACK_URL | string | — | USDT 支付回調 URL |
API_FOOTBALL_KEY | string | — | API-Football 密鑰 |
BETSOLUTIONS_MERCHANT_ID | string | — | BetSolutions 商戶 ID |
BETSOLUTIONS_PRIVATE_KEY | string | — | BetSolutions 私鑰 |
RSG_OPERATOR_TOKEN | string | — | RSG 運營商 Token |
RSG_SECRET_KEY | string | — | RSG DES 加解密 Key |
TELEGRAM_BOT_TOKEN | string | — | Telegram Bot Token |
A.2 c9-ims 環境變數(.env.local)
| 變數名 | 型別 | 預設值 | 說明 |
|---|---|---|---|
NEXTAUTH_URL | string | http://localhost:3011 | NextAuth 回調 URL |
NEXTAUTH_SECRET | string | c9-ims-auth-secret-key | NextAuth 加密密鑰 |
NEXT_PUBLIC_SITE_ID | string | a1 | 當前站點 ID(對應 domainConfig) |
NEXT_PUBLIC_API_URL | string | http://localhost:8080 | 後端 API URL(SSR fallback) |
NEXT_PUBLIC_R2_URL | string | — | R2 公開存取 URL |
A.3 c9-ec 環境變數
c9-ec 不使用 .env 檔案,所有配置透過程式碼中的 domainConfig 靜態定義:
// 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/register | Email 已被使用 |
| 2003 | /api/auth/register | 推薦碼無效 |
| 2004 | /api/auth/register | 推薦碼已過期 |
| 2010 | /api/auth/login | 帳號或密碼錯誤 |
| 2011 | /api/auth/login | 帳號已被停用 |
| 2012 | /api/auth/login | 需要 2FA 驗證碼 |
| 2013 | /api/auth/login | 2FA 驗證碼錯誤 |
| 2020 | /api/auth/send-otp | OTP 發送過於頻繁 |
| 2021 | /api/auth/verify-otp | OTP 驗證碼錯誤 |
| 2022 | /api/auth/verify-otp | OTP 已過期 |
| 2030 | /api/auth/change-password | 原密碼錯誤 |
| 2040 | /api/auth/bind-google | Google 帳號已綁定其他用戶 |
| 2041 | /api/auth/bind-telegram | Telegram 帳號已綁定其他用戶 |
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/request | OTP 驗證失敗 |
| 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/login | 2FA 驗證碼錯誤 |
| 8010 | /api/admin/register | Email 已存在 |
| 8020 | /api/admin/google-auth/verify | Google 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-blacklistC.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-detail | JWT | 取得用戶資料 |
| POST | /api/auth/change-password | JWT | 修改密碼 |
| POST | /api/auth/send-otp | JWT | 發送 Email OTP |
| POST | /api/auth/verify-otp | JWT | 驗證 OTP |
| POST | /api/auth/bind-google | JWT | 綁定 Google |
| POST | /api/auth/unbind-google | JWT | 解綁 Google |
| POST | /api/auth/bind-telegram | JWT | 綁定 Telegram |
| POST | /api/auth/unbind-telegram | JWT | 解綁 Telegram |
| POST | /api/auth/setup-2fa | JWT | 設定 2FA |
| POST | /api/auth/verify-2fa | JWT | 驗證 2FA |
| POST | /api/auth/disable-2fa | JWT | 停用 2FA |
| GET | /api/auth/avatars | JWT | 取得頭像列表 |
| PATCH | /api/auth/avatar | JWT | 更新頭像 |
遊戲操作
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| GET | /api/game/providers | Optional | 遊戲供應商列表 |
| GET | /api/game/type-configs | Optional | 遊戲分類列表 |
| POST | /api/game/launch | JWT | 啟動遊戲 |
| POST | /api/game/demo | - | 試玩遊戲 |
| GET | /api/game/recent | JWT | 最近遊玩 |
| GET | /api/game/favorites | JWT | 收藏遊戲 |
| POST | /api/game/favorite | JWT | 加入收藏 |
| DELETE | /api/game/favorite/:id | JWT | 移除收藏 |
存款/提款
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| POST | /api/deposit | JWT | 提交存款 |
| GET | /api/deposit/orders | JWT | 存款紀錄 |
| GET | /api/deposit/exchange-rate | - | 即時匯率 |
| GET | /api/deposit/crypto-rate | - | 加密貨幣匯率 |
| POST | /api/withdrawal/request | JWT | 提交提款 |
| GET | /api/withdrawal/list | JWT | 提款紀錄 |
| GET | /api/withdrawal/turnover-status | JWT | 打碼量狀態 |
錢包管理
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| GET | /api/wallet/bank-cards | JWT | 銀行卡列表 |
| POST | /api/wallet/bank-card | JWT | 新增銀行卡 |
| DELETE | /api/wallet/bank-card/:id | JWT | 刪除銀行卡 |
| GET | /api/wallet/credit-cards | JWT | 信用卡列表 |
| POST | /api/wallet/credit-card | JWT | 新增信用卡 |
| DELETE | /api/wallet/credit-card/:id | JWT | 刪除信用卡 |
| GET | /api/wallet/crypto-addresses | JWT | 加密地址列表 |
| POST | /api/wallet/crypto-address | JWT | 新增加密地址 |
| DELETE | /api/wallet/crypto-address/:id | JWT | 刪除加密地址 |
VIP 系統
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| GET | /api/vip/status | JWT | VIP 狀態 |
| GET | /api/vip/levels | Optional | VIP 等級列表 |
| GET | /api/vip/rebates | Optional | 反水率列表 |
| GET | /api/vip/rebate-history | JWT | 反水紀錄 |
代理系統
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| POST | /api/affiliate/apply | JWT | 申請成為代理 |
| GET | /api/affiliate/dashboard | JWT | 代理儀表板 |
| GET | /api/affiliate/downlines | JWT | 下線列表 |
| GET | /api/affiliate/commissions | JWT | 佣金紀錄 |
| GET | /api/affiliate/settlements | JWT | 結算紀錄 |
| GET | /api/affiliate/balance | JWT | 代理餘額 |
| POST | /api/affiliate/withdrawal | JWT | 代理提款 |
| GET | /api/affiliate/withdrawals | JWT | 代理提款紀錄 |
站內信
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| GET | /api/inbox/list | JWT | 站內信列表 |
| GET | /api/inbox/:id | JWT | 站內信詳情 |
| POST | /api/inbox/read | JWT | 標記已讀 |
| POST | /api/inbox/read-all | JWT | 全部已讀 |
| DELETE | /api/inbox/:id | JWT | 刪除站內信 |
| GET | /api/inbox/unread-count | JWT | 未讀數量 |
活動/任務
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| GET | /api/promo/list | Optional | 活動列表 |
| GET | /api/promo/:id | Optional | 活動詳情 |
| POST | /api/promo/claim | JWT | 領取活動 |
| GET | /api/promo/claims | JWT | 領取紀錄 |
| GET | /api/promo/tags | - | 活動標籤 |
| GET | /api/mission/list | JWT | 任務列表 |
| POST | /api/mission/claim | JWT | 領取任務獎勵 |
D.2 後台 Admin API(按模組)
認證
| 方法 | 端點 | Guard | 說明 |
|---|---|---|---|
| POST | /api/admin/login | - | 管理員登入 |
| POST | /api/admin/register | AdminJWT | 建立管理員 |
| GET | /api/admin/profile | AdminJWT | 取得個人資料 |
| PATCH | /api/admin/profile | AdminJWT | 更新個人資料 |
| POST | /api/admin/google-auth/generate | AdminJWT | 產生 2FA QR Code |
| POST | /api/admin/google-auth/verify | AdminJWT | 驗證 2FA |
| POST | /api/admin/google-auth/disable | AdminJWT | 停用 2FA |
管理員 CRUD
| 方法 | 端點 | Guard | Permission | 說明 |
|---|---|---|---|---|
| GET | /api/admin/list | AdminJWT | admin:read | 管理員列表 |
| POST | /api/admin/create | AdminJWT | admin:write | 建立管理員 |
| GET | /api/admin/:id | AdminJWT | admin:read | 管理員詳情 |
| PATCH | /api/admin/:id | AdminJWT | admin:write | 更新管理員 |
| DELETE | /api/admin/:id | AdminJWT | admin:write | 刪除管理員 |
群組 CRUD
| 方法 | 端點 | Guard | Permission | 說明 |
|---|---|---|---|---|
| GET | /api/admin/groups/list | AdminJWT | admin-group:read | 群組列表 |
| POST | /api/admin/groups/create | AdminJWT | admin-group:write | 建立群組 |
| GET | /api/admin/groups/:id | AdminJWT | admin-group:read | 群組詳情 |
| PATCH | /api/admin/groups/:id | AdminJWT | admin-group:write | 更新群組 |
| DELETE | /api/admin/groups/:id | AdminJWT | admin-group:write | 刪除群組 |
| GET | /api/admin/permissions/all | AdminJWT | - | 權限清單 |
報表
| 方法 | 端點 | Guard | Permission | 說明 |
|---|---|---|---|---|
| GET | /api/admin/reports/players | AdminJWT | report:read | 玩家報表 |
| GET | /api/admin/reports/vip-players | AdminJWT | report:read | VIP 玩家 |
| GET | /api/admin/reports/bet-records | AdminJWT | report:read | 投注紀錄 |
| GET | /api/admin/reports/overview | AdminJWT | report:read | 總覽報表 |
| GET | /api/admin/reports/profit-loss | AdminJWT | report:read | 損益報表 |
| GET | /api/admin/reports/games | AdminJWT | report:read | 遊戲報表 |
| GET | /api/admin/reports/promos | AdminJWT | report:read | 活動報表 |
| GET | /api/admin/reports/player-summary | AdminJWT | report:read | 玩家簡表 |
| GET | /api/admin/reports/r2-logs | AdminJWT | report:read | R2 操作紀錄 |
| GET | /api/admin/reports/export | AdminJWT | report:read | CSV 匯出 |
附錄 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.ts 中 i18n.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
解法:
- 確認
SessionSync元件有正確同步 token 到apiClient - 檢查後端 JWT_ADMIN_SECRET 與前端登入時使用的是否一致
- 確認 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 簽名驗證失敗
解法:
- BetSolutions:確認
BETSOLUTIONS_PRIVATE_KEY與遊戲商提供的一致 - RSG:確認
RSG_SECRET_KEY用於 DES 加解密,且 key 長度正確
Cron Job 未執行
解法:確認 @nestjs/schedule 在 app.module.ts 中有 import ScheduleModule.forRoot()。查看日誌中是否有 [Scheduler] 標記。
R2 上傳失敗
解法:確認環境變數 R2_ACCOUNT_ID、R2_ACCESS_KEY_ID、R2_SECRET_ACCESS_KEY、R2_BUCKET_NAME 都已正確設定。
E.5 跨專案整合問題
前端收到 code !== 200 但沒有顯示錯誤訊息
解法:
- 確認後端
GET /common/enums能正常回傳 ERROR_CODES - 確認前端啟動時有呼叫
getEnumsCsr()或EnumInitializer已掛載 - 檢查 path 匹配:前端使用的 URL 路徑需與後端
path欄位一致
多站點資料未隔離
解法:
- 確認後端 Entity 有
siteCode欄位且有@Index() - 確認 Service 中的 QueryBuilder 有
.andWhere('alias.siteCode = :siteCode') - 確認 Controller 使用
@AdminSiteCode()裝飾器 - 確認前台 API 有傳送
site-nameheader
新增站點後前台/後台看不到
解法:
- 後端:確認
site-config表中已新增站點記錄 - 前台:確認
domainConfig中有新站點的域名映射 - 後台:確認
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 更具感知均勻性 |
| Composable | Vue 3 的可組合函式,用於封裝可重用邏輯 |
| Hook | React 中的可重用邏輯封裝函式 |
| Guard | NestJS 的路由守衛,用於認證和授權 |
| Decorator | NestJS 的裝飾器,用於標記路由元資料 |
| DTO | Data Transfer Object,定義 API 請求/回應的資料結構 |
| Entity | TypeORM 的資料庫實體,對應一張資料表 |
| Interceptor | NestJS 的攔截器,用於統一包裝回應格式 |
| Subscriber | TypeORM 的事件訂閱器,用於 Entity 生命週期鉤子 |
| QueryBuilder | TypeORM 的查詢建構器,用於複雜 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 後綴自動載入) |
| 觸發時機 | 每次路由切換時自動執行 |
保護路徑定義:
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('/') 導回首頁完整原始碼:
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 |
公開路徑白名單:
const PUBLIC_PATHS = ["/login"];認證流程:
請求進入
→ 解析 pathname
→ 判斷是否為公開路徑
→ 偵測 HTTPS(含反向代理 x-forwarded-proto 檢查)
→ getToken() 讀取 NextAuth JWT
→ 未登入 + 非公開路徑:
redirect → /login?expired=1(帶 expired 參數顯示 toast)
→ 已登入 + 在登入頁:
redirect → /dashboard
→ 其他情況:
交由 next-intl middleware 處理 i18n 路由完整原始碼:
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):
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 |
| Provider | Credentials 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 CredentialsSigninJWT Callback — Token 擴展:
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 — 回傳擴展:
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):
| 擴展介面 | 新增欄位 | 說明 |
|---|---|---|
User | permissions?: string[] | 權限陣列 |
User | groupType?: GroupType | 群組類型 |
User | accessToken?: string | 後端 JWT Token |
User | allowedSiteCodes?: string[] | null | 允許存取的站點 |
Session | accessToken: string | 傳遞至客戶端的 JWT |
G.4 後端 (c9-be) — Guards(路由守衛)
後端共有 4 個 Guard,分為前台認證與後台認證兩組。
G.4.1 JwtAuthGuard — 前台用戶認證
| 項目 | 說明 |
|---|---|
| 檔案路徑 | c9-be/src/modules/auth/guards/jwt-auth.guard.ts |
| Strategy | AuthGuard('jwt') |
| 用途 | 保護前台用戶端點(如個人資料、存款、提領等) |
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}對應 Strategy(auth/strategies/jwt.strategy.ts):
| 設定項 | 值 |
|---|---|
| Strategy name | 'jwt'(Passport 預設) |
| Secret | JWT_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 |
| Strategy | AuthGuard('jwt') |
| 用途 | 某些端點可選認證(如遊戲列表:登入用戶顯示最近遊玩) |
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
handleRequest(_err: any, user: any) {
return user || null; // 未認證不報錯,回傳 null
}
}與 JwtAuthGuard 的差異:
| 比較項 | JwtAuthGuard | OptionalJwtAuthGuard |
|---|---|---|
| 未帶 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 |
| Strategy | AuthGuard('admin-jwt') |
| 用途 | 保護所有後台管理端點 |
@Injectable()
export class AdminJwtAuthGuard extends AuthGuard('admin-jwt') {}對應 Strategy(admin/strategies/admin-jwt.strategy.ts):
| 設定項 | 值 |
|---|---|
| Strategy name | 'admin-jwt'(自定名稱與前台區分) |
| Secret | ADMIN_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_MODULES | 16 個模組:admin, admin-group, admin-log, user, deposit, withdrawal, promo, promo-tag, affiliate, vip, game, risk, report, vendor, finance, site-config |
PERM_ACTIONS | 2 種操作:read, write |
ALL_PERMISSIONS | 32 個權限 key(16 模組 x 2 操作)— ROOT 專用 |
SUPER_ADMIN_PERMISSIONS | 30 個(排除 site-config:read, site-config:write) |
GENERAL_ADMIN_PERMISSIONS | 15 個(僅 read,排除 site-config) |
GROUP_TYPES | root, super_admin, general_admin, custom |
G.5 後端 (c9-be) — 自定義 Decorators
G.5.1 @AdminSiteCode() — 後台站點篩選
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;
},
);| 優先順序 | 來源 | 回傳值 |
|---|---|---|
| 1 | x-site-code request header | 特定站點代碼 (string) |
| 2 | siteCode query parameter | 特定站點代碼 (string) |
| 3 | 皆無 | null(代表全站) |
G.5.2 @SiteName() — 前台站點名稱
export const SiteName = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string => {
const req = ctx.switchToHttp().getRequest();
return req.headers['site-name'] || process.env.SITE_CODE || 'default';
},
);| 優先順序 | 來源 | 說明 |
|---|---|---|
| 1 | site-name request header | 前台 HTTP Client 注入 |
| 2 | SITE_CODE 環境變數 | 後端預設站點 |
| 3 | 'default' | 最終 fallback |
G.5.3 SiteCodeSubscriber — TypeORM 自動填入 siteCode
@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 條件 | 行為 |
|---|---|---|
beforeInsert | 有 siteCode 欄位且值為空 | 自動填入 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 Unauthorized | 401 | 唯一保持原始 status 的情況 |
| 其他所有狀態 | 200 | 業務錯誤統一用 200 + code 區分 |
回應 body 結構:
{
"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 回傳格式 |
標準包裝格式:
{
"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.ts | VIP 等級/反水種子資料 | 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 執行環境
npx ts-node scripts/seed-all.ts環境變數載入順序:
.env(基礎設定).env.local(覆寫,含資料庫密碼等敏感資訊)
資料庫連線:直接使用 TypeORM DataSource,不經過 NestJS Module。
H.2.2 站點定義(5 個站點)
| 站點代碼 | 前綴 | 名稱 | 主題色 | 說明 |
|---|---|---|---|---|
C9 | c9 | C9 娛樂城 | #10b981 (翡翠綠) | 主站 |
B1 | b1 | 寶盈娛樂城 | #3b82f6 (皇家藍) | 白牌站 1 |
B2 | b2 | 星際娛樂城 | #8b5cf6 (星際紫) | 白牌站 2 |
B3 | b3 | 皇冠娛樂城 | #f59e0b (皇冠金) | 白牌站 3 |
B4 | b4 | 鳳凰娛樂城 | #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-provider | 3 | slot-betsolutions, crypto-betsolutions, slot-rsg |
vip-level | 15 | 青銅 I-VI、黃金 I-III、白金 I-III、鑽石 I-III |
alliance-agent-tier | 4 | bronze, silver, gold, platinum |
alliance-commission-rate | 108 | 4 等級 x 3 層級 x (1 通用 + 8 遊戲類型) |
alliance-vip-milestone | 5 | VIP 3/5/7/10/13 達標獎勵 |
Phase 2: 站點設定與主題
| 資料表 | 筆數 | 說明 |
|---|---|---|
site-config | 5 | 每站一筆,含 learnMoreConfig JSON |
site-theme | 5 | 每站一組主題,含 primary/accent/surface/text/border |
Phase 3: 每站資料(每站獨立生成,以下為每站的資料量)
| 資料表 | 每站筆數 | 說明 |
|---|---|---|
auth-user | 30 | 6 代理 + 24 會員,密碼 = 帳號 |
auth-user-login-log | ~90-150 | 每人 2-5 筆登入紀錄 |
vendor-group | 3 | 預設/VIP/測試 |
vendor-channel | 3 | 萬通支付/USDT 支付/測試通道 |
vendor-group-channel | 4 | 群組-通道關聯 |
bank-card | 20 | 含各種審核狀態 |
credit-card | 20 | 含各種審核狀態 |
crypto-address | 20 | TRC-20/ERC-20/BEP-20 |
deposit-order | ~60-120 | 代理 3-5 筆、會員 1-3 筆 |
withdrawal-order | 15 | 含各種狀態 |
bet-order | ~60-150 | 每人 2-5 筆注單 |
bet-detail | ~300-750 | 每注單 1-10 筆明細 |
game-transaction | 50 | bet/win/cancel/jackpot |
game-play-log | ~20-30 | 最近遊玩紀錄(UPSERT) |
vip-rebate | 120 | 15 等級 x 8 遊戲類型 |
vip-rebate-log | 30 | 反水發放紀錄 |
rank-list | 30 | 排行榜紀錄 |
promo-tag | 2 | 代理/熱門 |
promo | 2 | 代理招募/週末加碼 |
promo-claim | ~8-10 | 活動領取紀錄 |
notification | 15 | 8 全站通知 + 7 個人通知 |
notification-read | ~15-20 | 已讀紀錄 |
mission | 30 | 2 類別 x 3 週期 x 5 層級 |
mission-progress | ~40-60 | 任務進度 |
mission-claim | ~10-15 | 任務領取 |
risk-game-blacklist | 3 | 風控封鎖紀錄 |
affiliate-balance | 6 | 每個代理一筆 |
alliance-referral-code | 12 | 每代理 2 個推廣碼 |
affiliate-click | 30 | 點擊追蹤紀錄 |
affiliate-settlement | 18 | 每代理 3 週結算 |
affiliate-commission | ~36-90 | 每結算 2-5 筆佣金 |
affiliate-withdrawal | ~5-10 | 代理提款 |
affiliate-bind-log | ~10-15 | 綁定紀錄 |
H.2.4 預設測試帳號
| 帳號 | 密碼 | 角色 | 站點 | 說明 |
|---|---|---|---|---|
otis01 | otis01 | 代理 + 會員 | C9 | 第一位用戶(ID=1) |
c9james02 | c9james02 | 代理 | C9 | 自動生成帳號 |
c9oliver03 ~ c9owen30 | 同帳號 | 會員 | C9 | 每站 30 人 |
b1james01 ~ b1owen30 | 同帳號 | 各角色 | B1 | B1 站用戶 |
b2james01 ~ b2owen30 | 同帳號 | 各角色 | B2 | B2 站用戶 |
b3james01 ~ b3owen30 | 同帳號 | 各角色 | B3 | B3 站用戶 |
b4james01 ~ b4owen30 | 同帳號 | 各角色 | B4 | B4 站用戶 |
重要:所有測試帳號的密碼等於帳號名稱(經 bcrypt hash 後儲存)。
後台管理員帳號由 AdminModule.onModuleInit() 自動建立(非 seed 腳本),預設為:
- Email:
ADMIN_DEFAULT_EMAIL環境變數(預設root) - Password:
ADMIN_DEFAULT_PASSWORD環境變數(預設root)
H.2.5 VIP 等級預設資料
| 等級 | 名稱 | Tier | 累積投注門檻 (USD) | 保級投注 (USD) |
|---|---|---|---|---|
| 1 | 青銅 I | bronze | 0 | 0 |
| 2 | 青銅 II | bronze | 3,600 | 200 |
| 3 | 青銅 III | bronze | 12,000 | 500 |
| 4 | 青銅 IV | bronze | 60,000 | 2,000 |
| 5 | 青銅 V | bronze | 180,000 | 5,000 |
| 6 | 青銅 VI | bronze | 360,000 | 20,000 |
| 7 | 黃金 I | gold | 660,000 | 58,000 |
| 8 | 黃金 II | gold | 2,400,000 | 150,000 |
| 9 | 黃金 III | gold | 5,000,000 | 250,000 |
| 10 | 白金 I | platinum | 10,000,000 | 500,000 |
| 11 | 白金 II | platinum | 30,000,000 | 1,500,000 |
| 12 | 白金 III | platinum | 60,000,000 | 2,500,000 |
| 13 | 鑽石 I | diamond | 100,000,000 | 5,000,000 |
| 14 | 鑽石 II | diamond | 500,000,000 | 12,500,000 |
| 15 | 鑽石 III | diamond | 960,000,000 | 25,000,000 |
H.2.6 反水率預設表(15 等級 x 8 遊戲類型,單位 %)
| 等級 | sports | slot | live | lottery | chess | esports | crypto | fish |
|---|---|---|---|---|---|---|---|---|
| 1 | 0.20 | 0.50 | 0.50 | 0.50 | 0.50 | 0.30 | 0.50 | 0.50 |
| 7 | 0.50 | 0.80 | 0.70 | 0.70 | 0.70 | 0.50 | 0.70 | 0.70 |
| 10 | 0.65 | 1.00 | 0.80 | 0.80 | 0.80 | 0.60 | 0.80 | 0.80 |
| 15 | 0.90 | 1.50 | 1.00 | 1.10 | 1.10 | 0.80 | 1.10 | 1.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.ts | R2 上傳 | 使用 Sharp 生成活動橫幅圖片 |
generate-mascot-avatars.ts | R2 上傳 | 讀取 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.sql | SQL 腳本清理活動相關資料 |
附錄 I:Nuxt 與 Next.js 配置完整參考
本附錄詳細說明前台 (c9-ec) 與後台 (c9-ims) 的框架配置檔案。
I.1 前台 (c9-ec) — nuxt.config.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' },
],
},
});I.1.1 配置項說明
| 配置項 | 值 | 說明 |
|---|---|---|
compatibilityDate | '2025-07-15' | Nuxt 4 向後相容日期 |
devtools.enabled | true | 啟用 Vue DevTools |
devServer.port | 3010 | 開發伺服器 port |
app.head.meta | viewport | 禁止用戶縮放(行動裝置最佳化) |
I.1.2 Nuxt 模組說明(9 個)
| 模組 | 版本 | 用途 |
|---|---|---|
@nuxt/eslint | - | ESLint 整合 |
@nuxt/hints | - | 效能最佳化提示 |
@nuxt/image | 2.0.0 | 圖片最佳化(WebP、響應式) |
@nuxt/scripts | 0.13.2 | 第三方腳本管理 |
@nuxt/test-utils | 3.23.0 | 測試工具(Vitest + Playwright 整合) |
@nuxt/ui | v4.4.0 | UI 元件庫(基於 Tailwind CSS v4) |
@nuxtjs/i18n | 10.2.1 | 多語系支援 |
@nuxt/content | v3 | 內容管理(Markdown → Vue) |
@pinia/nuxt | 0.11.3 | Pinia 狀態管理整合 |
I.1.3 CSS 載入順序
~/assets/css/global.scss— 全域 SCSS(目前為空,保留擴充)~/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
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 配置項說明
| 配置項 | 值 | 說明 |
|---|---|---|
reactStrictMode | false | 停用 React Strict Mode(避免 double render 影響 API 呼叫) |
withNextIntl plugin | ./src/i18n/request.ts | next-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, usePathnameI.2.3 站點配置(sites/a1/config.ts)
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 | 預設值 | 控制功能 |
|---|---|---|
enableAnalytics | false | 分析報表模組 |
enableBilling | false | 帳單模組 |
enableUserManagement | false | 用戶管理獨立頁面 |
enableRBAC | false | 角色權限管理獨立頁面 |
enableI18nSwitcher | true | Header 語系切換器 |
enableSystemManagement | true | 系統管理(管理員/群組/紀錄/站點設定) |
enableFinanceManagement | true | 財務管理(餘額/銀行卡/提領/入金設定) |
enableAffiliateManagement | true | 代理管理(含聯盟系統) |
enableVipManagement | true | VIP 管理(等級/返水/里程碑) |
enableReports | true | 報表資訊(7 個子頁面) |
enableGameManagement | true | 遊戲管理(遊戲商/類型設定) |
enableRiskControl | true | 風控設置(IP 規則/檢查/遊戲黑名單) |
enablePlayerManagement | true | 玩家管理(全部/新註冊/線上/登入失敗) |
I.3 後台 (c9-ims) — NextAuth 完整配置
I.3.1 設定摘要
| 設定項 | 值 |
|---|---|
| Secret | "c9-ims-auth-secret-key" |
| Trust Host | true |
| Session Strategy | jwt |
| Provider | Credentials(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 同步 accessTokenI.4 後端 (c9-be) — TypeORM 配置
I.4.1 config/typeorm.config.ts
| 設定項 | 值 | 說明 |
|---|---|---|
type | 'mysql' | MySQL 資料庫 |
host | DB_HOST env | 資料庫主機 |
port | DB_PORT env | 資料庫 port |
charset | 'utf8mb4' | 支援 emoji 等 4-byte 字元 |
timezone | '+08:00' | 統一 UTC+8 時區 |
autoLoadEntities | true | 自動載入所有 forFeature() 註冊的 Entity |
synchronize | isDev only | 僅開發環境自動同步 schema |
logging | false | 不輸出 SQL 日誌 |
subscribers | [SiteCodeSubscriber] | 自動填入 siteCode |
附錄 J:測試架構與指令參考
本附錄詳細說明 C9 平台的測試架構、配置檔案、以及測試指令。
J.1 前台 (c9-ec) — 測試架構
前台使用三層測試策略:Vitest 單元測試、Nuxt 元件測試、Playwright E2E 測試。
J.1.1 Vitest 配置(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',
},
},
},
}),
],
},
})兩個 Test Project:
| Project | 環境 | 測試目錄 | 用途 |
|---|---|---|---|
unit | node | test/unit/ | 純邏輯單元測試(工具函式、API 邏輯) |
nuxt | nuxt (happy-dom) | test/nuxt/ | Vue 元件測試(含 Nuxt 自動匯入、composable) |
Nuxt 元件測試環境特點:
| 特點 | 說明 |
|---|---|
| DOM 環境 | happy-dom(比 jsdom 更快) |
| 自動匯入 | Nuxt composables(useRoute、useState 等)自動可用 |
| 模組模擬 | @nuxt/test-utils 自動 mock Nuxt modules |
| rootDir | 指向專案根目錄,確保路徑正確解析 |
J.1.2 Playwright 配置(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 | './tests' | E2E 測試目錄 |
fullyParallel | true | 完全平行執行 |
forbidOnly | CI 環境為 true | 禁止 .only() 進入 CI |
retries | CI=2, 本地=0 | CI 環境重試 2 次 |
workers | CI=1, 本地=自動 | CI 環境單工作者 |
reporter | 'html' | 生成 HTML 報告 |
trace | 'on-first-retry' | 首次重試時收集追蹤 |
projects | Chromium only | 僅 Desktop Chrome |
J.1.3 前台測試指令
| 指令 | 說明 |
|---|---|
yarn test | 執行全部 Vitest 測試(unit + nuxt) |
yarn test:watch | Watch 模式(檔案變更自動重跑) |
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):
{
"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 測試檔案匹配 |
transform | ts-jest | TypeScript 轉譯 |
J.2.2 E2E 測試範例
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:watch | Watch 模式 |
yarn test:cov | 執行測試 + 覆蓋率報告 |
yarn test:e2e | 執行 E2E 測試 |
yarn test:debug | Debug 模式測試 |
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 --coverage | yarn test:cov |
| CI 整合 | Playwright retries=2, workers=1 | Jest 標準配置 |
附錄 K:CSS 變數與主題系統完整參考
本附錄詳細說明 C9 平台的主題系統實作,包含前台 CSS 變數橋接、後台 OKLCH 色彩系統、以及動態主題切換機制。
K.1 前台 (c9-ec) — CSS 變數橋接系統
前台使用「CSS 變數橋接」模式:將 Tailwind CSS 的 emerald 色系透過 CSS custom properties 覆寫,使得運行時可動態切換主題色。
K.1.1 main.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-glow | 16, 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 | 翡翠綠 | #34d399 | i-lucide-leaf | 16, 185, 129 |
amber | 琥珀金 | #fbbf24 | i-lucide-flame | 245, 158, 11 |
sky | 天空藍 | #38bdf8 | i-lucide-cloud | 14, 165, 233 |
violet | 薰衣紫 | #a78bfa | i-lucide-sparkles | 139, 92, 246 |
rose | 玫瑰紅 | #fb7185 | i-lucide-heart | 244, 63, 94 |
cyan | 青色 | #22d3ee | i-lucide-droplets | 6, 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 值 | 用途 |
|---|---|---|
--background | oklch(1 0 0) | 頁面背景(純白) |
--foreground | oklch(0.145 0 0) | 主要文字(深灰) |
--card | oklch(1 0 0) | 卡片背景 |
--card-foreground | oklch(0.145 0 0) | 卡片文字 |
--primary | oklch(0.205 0 0) | 主要按鈕/連結 |
--primary-foreground | oklch(0.985 0 0) | 主要按鈕文字 |
--secondary | oklch(0.97 0 0) | 次要元素背景 |
--muted | oklch(0.97 0 0) | 弱化元素背景 |
--muted-foreground | oklch(0.556 0 0) | 弱化文字 |
--accent | oklch(0.97 0 0) | 強調元素 |
--destructive | oklch(0.577 0.245 27.325) | 危險操作(紅色) |
--border | oklch(0.922 0 0) | 邊框 |
--input | oklch(0.922 0 0) | 輸入框邊框 |
--ring | oklch(0.708 0 0) | Focus ring |
--chart-1 ~ --chart-5 | 各色 | 圖表色系(5 色) |
--sidebar | oklch(0.985 0 0) | Sidebar 背景 |
--sidebar-primary | oklch(0.205 0 0) | Sidebar 主色 |
--sidebar-border | oklch(0.922 0 0) | Sidebar 邊框 |
Dark Mode(.dark)— 30 個變數:
| 變數 | OKLCH 值 | 與 Light 差異 |
|---|---|---|
--background | oklch(0.145 0 0) | 深色背景 |
--foreground | oklch(0.985 0 0) | 白色文字 |
--card | oklch(0.205 0 0) | 深灰卡片 |
--primary | oklch(0.922 0 0) | 淺色主色(反轉) |
--secondary | oklch(0.269 0 0) | 深灰次要 |
--muted-foreground | oklch(0.708 0 0) | 較亮的弱化文字 |
--destructive | oklch(0.704 0.191 22.216) | 較亮的紅色 |
--border | oklch(1 0 0 / 10%) | 半透明白邊 |
--input | oklch(1 0 0 / 15%) | 半透明白輸入框 |
--sidebar | oklch(0.205 0 0) | 深色 sidebar |
--sidebar-primary | oklch(0.488 0.243 264.376) | 藍紫色主色 |
K.3.2 Tailwind v4 Theme 映射
@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-sm | 0.625rem - 4px | ~6px |
--radius-md | 0.625rem - 2px | ~8px |
--radius-lg | 0.625rem | ~10px |
--radius-xl | 0.625rem + 4px | ~14px |
--radius-2xl | 0.625rem + 8px | ~18px |
K.3.3 Dark Mode 切換機制
@custom-variant dark (&:is(.dark *));Tailwind v4 自訂 variant,使用 .dark CSS class 切換暗色模式。當外層元素具有 .dark class 時,所有子元素的 dark: variant 生效。
K.3.4 Base Layer 樣式
@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 色值:
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, foreground | 2 |
| 卡片 | card, cardForeground | 2 |
| Popover | popover, popoverForeground | 2 |
| 主色 | primary, primaryForeground | 2 |
| 次色 | secondary, secondaryForeground | 2 |
| 弱化 | muted, mutedForeground | 2 |
| 強調 | accent, accentForeground | 2 |
| 危險 | destructive | 1 |
| 邊框 | border, input, ring | 3 |
| 圖表 | chart1 ~ chart5 | 5 |
| Sidebar | sidebar, sidebarForeground, sidebarPrimary, sidebarPrimaryForeground, sidebarAccent, sidebarAccentForeground, sidebarBorder, sidebarRing | 8 |
K.5 後台 (c9-ims) — SiteThemeInjector 元件
K.5.1 實作原始碼
"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(#10b981) | OKLCH(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 組(可從後端擴充) |
(全文完)