diff --git a/apps/admin/src/i18n/pages/en.ts b/apps/admin/src/i18n/pages/en.ts index ee4ce17..305e954 100644 --- a/apps/admin/src/i18n/pages/en.ts +++ b/apps/admin/src/i18n/pages/en.ts @@ -388,6 +388,42 @@ const adminPages: Record = { 'audit.action.CREATE_STAFF': 'Create staff account', 'audit.action.UPDATE_STAFF': 'Edit staff account', 'audit.action.DELETE_STAFF': 'Delete staff account', + 'audit.action.DELETE_PLAYER': 'Delete player', + 'audit.action.PURGE_UNUSED_FILES': 'Purge unused media', + 'audit.action.FORGOT_PASSWORD_RESET': 'Player forgot-password reset', + 'audit.action.CREATE_LEAGUE': 'Create league', + 'audit.action.UPDATE_LEAGUE': 'Update league', + 'audit.action.ARCHIVE_LEAGUE': 'Archive league', + 'audit.action.CREATE_TEAM': 'Create team', + 'audit.action.CREATE_MATCH': 'Create match', + 'audit.action.UPDATE_MATCH': 'Update match', + 'audit.action.DELETE_MATCH': 'Delete match', + 'audit.action.ARCHIVE_MATCH': 'Archive match', + 'audit.action.IMPORT_MATCHES': 'Import matches', + 'audit.action.PUBLISH_MATCH': 'Publish match', + 'audit.action.UNPUBLISH_MATCH': 'Unpublish match', + 'audit.action.CLOSE_MATCH': 'Close match', + 'audit.action.REOPEN_MATCH': 'Reopen match', + 'audit.action.CANCEL_MATCH': 'Cancel match', + 'audit.action.CREATE_MARKET_TEMPLATE': 'Create market template', + 'audit.action.UPDATE_MARKET_TEMPLATE': 'Update market template', + 'audit.action.DUPLICATE_MARKET_TEMPLATE': 'Duplicate market template', + 'audit.action.SET_DEFAULT_MARKET_TEMPLATE': 'Set default market template', + 'audit.action.GENERATE_MATCH_MARKETS': 'Generate match markets', + 'audit.action.APPLY_MARKET_TEMPLATE': 'Apply market template', + 'audit.action.BULK_SAVE_MATCH_MARKETS': 'Bulk save match markets', + 'audit.action.UPDATE_MATCH_ODDS': 'Update match odds', + 'audit.action.UPDATE_MARKET': 'Update market', + 'audit.action.UPDATE_SELECTION': 'Update selection', + 'audit.action.CREATE_OUTRIGHT': 'Create outright', + 'audit.action.UPDATE_OUTRIGHT': 'Update outright', + 'audit.action.UPDATE_OUTRIGHT_ODDS': 'Update outright odds', + 'audit.action.ADD_OUTRIGHT_SELECTION': 'Add outright selection', + 'audit.action.BATCH_ADD_OUTRIGHT_SELECTIONS': 'Batch add outright selections', + 'audit.action.UPDATE_OUTRIGHT_SELECTION': 'Update outright selection', + 'audit.action.REMOVE_OUTRIGHT_SELECTION': 'Remove outright selection', + 'audit.action.IMPORT_WC2026_OUTRIGHT': 'Import WC2026 outright', + 'audit.module.CATALOG': 'Match catalog', 'audit.module.USERS': 'Players', 'audit.module.AGENTS': 'Agents', 'audit.module.SYSTEM': 'System', @@ -395,6 +431,8 @@ const adminPages: Record = { 'audit.module.SETTLEMENT': 'Settlement', 'audit.module.CASHBACK': 'Cashback', 'audit.module.STAFF': 'Staff', + 'audit.module.MEDIA': 'Media library', + 'audit.module.identity': 'Identity', 'cashback.start_date': 'Start date', 'cashback.end_date': 'End date', diff --git a/apps/admin/src/i18n/pages/zh.ts b/apps/admin/src/i18n/pages/zh.ts index 27cbabc..67a09cf 100644 --- a/apps/admin/src/i18n/pages/zh.ts +++ b/apps/admin/src/i18n/pages/zh.ts @@ -389,6 +389,42 @@ const adminPages: Record = { 'audit.action.CREATE_STAFF': '创建后台账号', 'audit.action.UPDATE_STAFF': '编辑后台账号', 'audit.action.DELETE_STAFF': '删除后台账号', + 'audit.action.DELETE_PLAYER': '删除玩家', + 'audit.action.PURGE_UNUSED_FILES': '清理未引用媒体', + 'audit.action.FORGOT_PASSWORD_RESET': '玩家找回密码', + 'audit.action.CREATE_LEAGUE': '新建联赛', + 'audit.action.UPDATE_LEAGUE': '更新联赛', + 'audit.action.ARCHIVE_LEAGUE': '归档联赛', + 'audit.action.CREATE_TEAM': '新建球队', + 'audit.action.CREATE_MATCH': '新建赛事', + 'audit.action.UPDATE_MATCH': '更新赛事', + 'audit.action.DELETE_MATCH': '删除赛事', + 'audit.action.ARCHIVE_MATCH': '归档赛事', + 'audit.action.IMPORT_MATCHES': '批量导入赛事', + 'audit.action.PUBLISH_MATCH': '发布赛事', + 'audit.action.UNPUBLISH_MATCH': '下架赛事', + 'audit.action.CLOSE_MATCH': '封盘赛事', + 'audit.action.REOPEN_MATCH': '重新开放赛事', + 'audit.action.CANCEL_MATCH': '取消赛事', + 'audit.action.CREATE_MARKET_TEMPLATE': '新建盘口模板', + 'audit.action.UPDATE_MARKET_TEMPLATE': '更新盘口模板', + 'audit.action.DUPLICATE_MARKET_TEMPLATE': '复制盘口模板', + 'audit.action.SET_DEFAULT_MARKET_TEMPLATE': '设为默认盘口模板', + 'audit.action.GENERATE_MATCH_MARKETS': '生成赛事盘口', + 'audit.action.APPLY_MARKET_TEMPLATE': '应用盘口模板', + 'audit.action.BULK_SAVE_MATCH_MARKETS': '批量保存赛事盘口', + 'audit.action.UPDATE_MATCH_ODDS': '更新赛事赔率', + 'audit.action.UPDATE_MARKET': '更新盘口', + 'audit.action.UPDATE_SELECTION': '更新选项', + 'audit.action.CREATE_OUTRIGHT': '新建冠军盘', + 'audit.action.UPDATE_OUTRIGHT': '更新冠军盘', + 'audit.action.UPDATE_OUTRIGHT_ODDS': '更新冠军盘赔率', + 'audit.action.ADD_OUTRIGHT_SELECTION': '添加冠军盘选项', + 'audit.action.BATCH_ADD_OUTRIGHT_SELECTIONS': '批量添加冠军盘选项', + 'audit.action.UPDATE_OUTRIGHT_SELECTION': '更新冠军盘选项', + 'audit.action.REMOVE_OUTRIGHT_SELECTION': '移除冠军盘选项', + 'audit.action.IMPORT_WC2026_OUTRIGHT': '导入世界杯冠军盘', + 'audit.module.CATALOG': '赛事管理', 'audit.module.USERS': '玩家', 'audit.module.AGENTS': '代理', 'audit.module.SYSTEM': '系统', @@ -396,6 +432,8 @@ const adminPages: Record = { 'audit.module.SETTLEMENT': '结算', 'audit.module.CASHBACK': '返水', 'audit.module.STAFF': '后台账号', + 'audit.module.MEDIA': '媒体库', + 'audit.module.identity': '身份认证', 'cashback.start_date': '开始日期', 'cashback.end_date': '结束日期', diff --git a/apps/admin/src/utils/session-hydrate.ts b/apps/admin/src/utils/session-hydrate.ts index 88345e0..b629b7b 100644 --- a/apps/admin/src/utils/session-hydrate.ts +++ b/apps/admin/src/utils/session-hydrate.ts @@ -74,6 +74,7 @@ export async function hydrateStaffSession(): Promise { maxAgentLevel: typeof raw.maxAgentLevel === 'number' ? raw.maxAgentLevel : null, canManageSubAgents: raw.canManageSubAgents === true, inviteCode: raw.inviteCode ?? null, + visibleMenus: raw.visibleMenus ?? null, }); lastHydrateAt = Date.now(); return true; diff --git a/apps/player/src/i18n/en-US.ts b/apps/player/src/i18n/en-US.ts index ac47641..e58f996 100644 --- a/apps/player/src/i18n/en-US.ts +++ b/apps/player/src/i18n/en-US.ts @@ -124,6 +124,9 @@ export default { stats_stake: 'Total Stake', stats_return: 'Total Return', cashbacked: 'Cashbacked', + profit: 'Profit', + return_incl_stake: 'Return {amount} (incl. stake)', + stake_to_return: '{stake} × {odds} = {amount}', }, auth: { login: 'Login', @@ -471,7 +474,6 @@ export default { place_failed: 'Bet failed', }, profile: { - settings: 'Settings', edit: 'Edit Profile', language: 'Language', avatar: 'Avatar', diff --git a/apps/player/src/i18n/ms-MY.ts b/apps/player/src/i18n/ms-MY.ts index 2f49b53..ae32182 100644 --- a/apps/player/src/i18n/ms-MY.ts +++ b/apps/player/src/i18n/ms-MY.ts @@ -130,6 +130,9 @@ export default { stats_stake: 'Jumlah Taruhan', stats_return: 'Jumlah Pulangan', cashbacked: 'Rebat dibayar', + profit: 'Keuntungan', + return_incl_stake: 'Pulangan {amount} (termasuk jumlah)', + stake_to_return: '{stake} × {odds} = {amount}', }, auth: { login: 'Log Masuk', @@ -477,7 +480,6 @@ export default { place_failed: 'Pertaruhan gagal', }, profile: { - settings: 'Tetapan', edit: 'Edit Profil', language: 'Bahasa', avatar: 'Avatar', diff --git a/apps/player/src/i18n/zh-CN.ts b/apps/player/src/i18n/zh-CN.ts index ac212c4..1a8cbb2 100644 --- a/apps/player/src/i18n/zh-CN.ts +++ b/apps/player/src/i18n/zh-CN.ts @@ -124,6 +124,9 @@ export default { stats_stake: '总投注额', stats_return: '总回报', cashbacked: '已回水', + profit: '净赢', + return_incl_stake: '回报 {amount}(含本金)', + stake_to_return: '{stake} × {odds} = {amount}', }, auth: { login: '登录', @@ -471,7 +474,6 @@ export default { place_failed: '下注失败', }, profile: { - settings: '设置', edit: '修改资料', language: '语言', avatar: '选择头像', diff --git a/docs/四套主题Docker部署任务.md b/docs/四套主题Docker部署任务.md new file mode 100644 index 0000000..29bc27b --- /dev/null +++ b/docs/四套主题Docker部署任务.md @@ -0,0 +1,594 @@ +# 四套主题 Docker 生产部署任务 + +> **目标**:在同一台服务器、**同一套 Docker Compose 栈**内,同时运行 4 套玩家端主题(4 个 player 容器 + 1 套共用 api/admin/db)。 +> **适用分支**:`main`(暗金)、`theme-2`(Pinnacle 蓝白)、`theme-3`(统一移动端视觉)、`theme-4`(海军蓝暗色极简)。 +> **关联文档**:[Docker部署指南.md](./Docker部署指南.md)、[docker/镜像构建与导出.md](./docker/镜像构建与导出.md)、[AGENTS.md](../AGENTS.md)「主题分支」章节。 + +--- + +## 一、架构总览 + +```text +域名 A(主站) → 127.0.0.1:8082 → thebet365-player 镜像 tag: main +域名 B(theme-2) → 127.0.0.1:8083 → thebet365-player2 镜像 tag: theme-2 +域名 C(theme-3) → 127.0.0.1:8084 → thebet365-player3 镜像 tag: theme-3 +域名 D(theme-4) → 127.0.0.1:8085 → thebet365-player4 镜像 tag: theme-4 + +管理后台 → 127.0.0.1:8081 → thebet365-admin 镜像 tag: latest(或统一 IMAGE_TAG) + ↓ + thebet365-api(Docker 内网,不映射宿主机端口) + ↓ + thebet365-postgres + thebet365-redis +``` + +| 要点 | 说明 | +|------|------| +| 不是「一个容器四套皮肤」 | 是 **4 个 player 容器**,各跑各自构建好的静态资源 | +| 后端共用 | api / postgres / redis / uploads **只有一套**,账号与余额互通 | +| 本地开发 | 仍只需 `pnpm dev:player`(`:5173`),**切分支**预览不同主题;不必本地开 4 端口 | +| `deploy-update.sh` | 默认只维护 **一套** api/player/admin;多主题 player2~4 需 **单独 load + compose up** | +| 邀请注册链接 | 当前仅 `VITE_PLAYER_URL` 单域名;需 **阶段 F** 实现后台按主题选链接(见第十三节) | + +--- + +## 二、任务清单(实施顺序) + +按顺序勾选,下次上线可直接照着做。 + +### 阶段 A — 仓库改动(一次性) + +- [ ] **A1** 新增 `docker-compose.themes.yml`(player2 / player3 / player4 服务定义) +- [ ] **A2** 更新 `.env.docker.example`(`PLAYER2_*` ~ `PLAYER4_*`、`CORS_ORIGINS` 示例) +- [ ] **A3** 更新 `docker-compose.prod.yml` 中 `player` 使用 `PLAYER_IMAGE_TAG`(与 `IMAGE_TAG` 解耦,见下文片段) +- [ ] **A4**(可选)在 `docs/Docker部署指南.md` 增加「多主题」小节链接到本文 +- [ ] **A5**(可选)在 `AGENTS.md` 文档索引增加本文链接 + +### 阶段 B — 各主题分支同步 main 功能(发版前每个 theme 分支各做一遍) + +- [ ] **B1** 在 `theme-2` / `theme-3` / `theme-4` 同步 API、Admin、shared、迁移(**禁止**整目录覆盖 player) +- [ ] **B2** Player 侧只做逻辑/i18n **增量合并**,保留各分支 `styles.css` 与主题资源 +- [ ] **B3** 各分支本地 `pnpm build` 或 `pnpm dev:player` 冒烟 +- [ ] **B4** `main` 分支照常维护;若 api/admin 有变更,四个 player 镜像可共用同一 api/admin 包 + +### 阶段 C — 本地打镜像(Windows 构建机) + +- [ ] **C1** 四个分支分别打 **player** 镜像(tag 互不相同) +- [ ] **C2** 打 **api + admin** 一包(在 `main` 或任意已同步分支,tag 如 `latest`) +- [ ] **C3** 核对 `.manifest.txt` 中 `git_commit` 与分支一致 +- [ ] **C4** 上传 tar 到服务器(勿覆盖 `.env.docker`) + +### 阶段 D — 服务器首次启用多主题 + +- [ ] **D1** 备份:执行 `deploy-update` 前会自动备份;首次改 compose 前建议手动 `./scripts/backup-prod.sh` +- [ ] **D2** `docker load` 四个 player tar + api/admin tar +- [ ] **D3** 编辑 `.env.docker`(端口、镜像 tag、CORS、域名) +- [ ] **D4** `docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d` +- [ ] **D5** 宝塔为 4 个玩家域名 + 1 个管理域名配置反代 +- [ ] **D6** 执行下方「验收清单」 + +### 阶段 E — 日常更新(重复) + +- [ ] 仅换某一主题 UI → 只 rebuild/load 对应 player tag → `up -d playerN` +- [ ] API/Admin/迁移变更 → `deploy-update.sh` 更新 api/admin;必要时四个 player 无需重建 +- [ ] 发版后强刷浏览器 / 清 CDN + +### 阶段 F — 邀请链接按主题选择(功能开发,可与 D 并行) + +- [ ] **F1** API:`SystemConfig` 存四套玩家站域名配置(见第十三节) +- [ ] **F2** 管理端「全局设置」可编辑主题名称 + 公网 URL + 默认项 +- [ ] **F3** 邀请面板 / 邀请历史:下拉选择主题后再复制注册链接 +- [ ] **F4** 代理端邀请弹窗同样可读主题列表并选择(只读,不能改 URL) +- [ ] **F5** 部署四套主题后,在全局设置填齐 4 个 https 域名并验收邀请链接 + +--- + +## 三、仓库待实现:`docker-compose.themes.yml` + +> **任务 A1**:在仓库根目录新建此文件,与 `docker-compose.prod.yml` 合并使用。 + +```yaml +# 四套主题扩展 — 与 docker-compose.prod.yml 一起使用: +# docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d + +services: + player2: + image: thebet365-player:${PLAYER2_IMAGE_TAG:-theme-2} + container_name: thebet365-player2 + depends_on: + api: + condition: service_healthy + ports: + - '${BIND_ADDR:-127.0.0.1}:${PLAYER2_PORT:-8083}:80' + healthcheck: + test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1'] + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + restart: unless-stopped + networks: + - thebet365 + + player3: + image: thebet365-player:${PLAYER3_IMAGE_TAG:-theme-3} + container_name: thebet365-player3 + depends_on: + api: + condition: service_healthy + ports: + - '${BIND_ADDR:-127.0.0.1}:${PLAYER3_PORT:-8084}:80' + healthcheck: + test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1'] + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + restart: unless-stopped + networks: + - thebet365 + + player4: + image: thebet365-player:${PLAYER4_IMAGE_TAG:-theme-4} + container_name: thebet365-player4 + depends_on: + api: + condition: service_healthy + ports: + - '${BIND_ADDR:-127.0.0.1}:${PLAYER4_PORT:-8085}:80' + healthcheck: + test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1'] + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + restart: unless-stopped + networks: + - thebet365 +``` + +### 任务 A3:调整 `docker-compose.prod.yml` 的 `player` 服务 + +将 `player.image` 从 `${IMAGE_TAG}` 改为独立变量(避免与 api/admin 的 `IMAGE_TAG` 绑死): + +```yaml + player: + image: thebet365-player:${PLAYER_IMAGE_TAG:-main} + # build / ports / healthcheck 等其余保持不变 +``` + +> `player2~4` **不要**写 `build:`,生产只 load 预构建镜像。 + +--- + +## 四、`.env.docker` 配置模板 + +> **任务 A2**:追加到 `.env.docker.example`;服务器 `.env.docker` 按真实域名填写。 + +```env +# ── 共用后端 ── +IMAGE_TAG=latest +ADMIN_PORT=8081 +BIND_ADDR=127.0.0.1 + +# 管理端邀请链接默认指向的主玩家站(勿带末尾 /) +VITE_PLAYER_URL=https://www.example.com + +# 四个玩家站域名都需列入(逗号分隔,无空格或统一 trim) +CORS_ORIGINS=https://www.example.com,https://theme2.example.com,https://theme3.example.com,https://theme4.example.com,https://admin.example.com + +# ── 四套 player 镜像 tag 与宿主机端口 ── +PLAYER_IMAGE_TAG=main +PLAYER_PORT=8082 + +PLAYER2_IMAGE_TAG=theme-2 +PLAYER2_PORT=8083 + +PLAYER3_IMAGE_TAG=theme-3 +PLAYER3_PORT=8084 + +PLAYER4_IMAGE_TAG=theme-4 +PLAYER4_PORT=8085 +``` + +修改 `CORS_ORIGINS` 后必须重启 api: + +```bash +docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker restart api +``` + +--- + +## 五、分支合并代码(main → theme-*) + +> **发版前**在每个 `theme-*` 分支执行。目标:功能与 main 一致,**皮肤不变**。 + +### 5.1 禁止事项 + +| 禁止 | 原因 | +|------|------| +| `git merge main` 后直接发 player | 易把 main 暗金样式大量带入 | +| `git checkout main -- apps/player/` | 整目录覆盖会冲掉主题 CSS/组件 | +| `git checkout main -- apps/player/src/i18n/` | 会冲掉主题分支已有措辞 | + +### 5.2 推荐:整目录同步(API / Admin / shared / 迁移) + +在目标主题分支上(示例 `theme-2`): + +```bash +git checkout theme-2 +git fetch origin + +# 同步后端与共享包(可按需增减路径) +git checkout origin/main -- apps/api +git checkout origin/main -- apps/admin +git checkout origin/main -- packages/shared +git checkout origin/main -- pnpm-lock.yaml +git checkout origin/main -- pnpm-workspace.yaml + +# 若有新迁移,务必带上 +git checkout origin/main -- apps/api/prisma/migrations +git checkout origin/main -- apps/api/prisma/schema.prisma + +git status # 确认没有误改 apps/player 大段样式文件 +git commit -m "sync: api/admin/shared/migrations from main" +``` + +### 5.3 Player:只做逻辑与文案增量 + +**方式 1 — 按文件 cherry-pick(有明确 commit 时)** + +```bash +git log origin/main --oneline -- apps/player/src/composables apps/player/src/stores +git cherry-pick # 冲突时保留 theme 分支的 styles / 主题组件 +``` + +**方式 2 — 按目录选择性 checkout(仅非 UI 目录)** + +```bash +# 示例:只同步 stores、composables、api 封装(执行前确认路径在 main 有变更) +git checkout origin/main -- apps/player/src/stores +git checkout origin/main -- apps/player/src/composables +git checkout origin/main -- apps/player/src/api +``` + +**方式 3 — i18n 只合并新增 key** + +- 对比 `apps/player/src/i18n/*.ts`,手工把 main **新增的 key** 补进三语文件 +- 不要用 main 文件整文件覆盖 + +### 5.4 合并后本地验证(每个 theme 分支) + +```bash +pnpm install +pnpm db:generate # 若 schema 有变 +pnpm --filter @thebet365/shared build +pnpm --filter @thebet365/player build +pnpm dev:api # 另开终端 +pnpm dev:player # http://localhost:5173 目视确认仍是本主题皮肤 +``` + +### 5.5 main 分支 + +- 日常功能开发在 `main` 完成后再同步到各 theme 分支 +- `main` 自身 player 镜像 tag 建议使用 `main` 或 `latest`(与 `.env.docker` 中 `PLAYER_IMAGE_TAG` 一致即可) + +--- + +## 六、构建与上传镜像 + +### 6.1 四个 player 镜像(Windows CMD) + +```bat +cd C:\path\to\thebet365 + +git checkout main +docs\docker\build-and-export-player.bat --tag main + +git checkout theme-2 +docs\docker\build-and-export-player.bat --tag theme-2 + +git checkout theme-3 +docs\docker\build-and-export-player.bat --tag theme-3 + +git checkout theme-4 +docs\docker\build-and-export-player.bat --tag theme-4 +``` + +产物(项目根目录,已在 `.gitignore`): + +| 文件 | 加载后镜像名 | +|------|----------------| +| `thebet365-player-main.tar` | `thebet365-player:main` | +| `thebet365-player-theme-2.tar` | `thebet365-player:theme-2` | +| `thebet365-player-theme-3.tar` | `thebet365-player:theme-3` | +| `thebet365-player-theme-4.tar` | `thebet365-player:theme-4` | + +### 6.2 api + admin 一包 + +```bat +git checkout main +docs\docker\build-and-export-images.bat --tag latest +``` + +或 Linux:`./docs/docker/build-and-export-images.sh --tag latest` + +### 6.3 上传至服务器 + +目录示例:`/www/wwwroot/thebet365` + +| 必传 | 说明 | +|------|------| +| 四个 player tar | 四套皮肤 | +| `thebet365-images-latest.tar`(或带版本 tag) | api + admin | +| `docker-compose.prod.yml` | 若仓库有更新 | +| `docker-compose.themes.yml` | **首次多主题必传** | +| `scripts/` | 若部署脚本有更新 | + +**勿覆盖**:`.env.docker`、Docker 数据卷、`backups/` + +--- + +## 七、服务器部署命令 + +### 7.1 首次启用四套主题 + +```bash +cd /www/wwwroot/thebet365 +chmod +x scripts/*.sh + +# 导入全部镜像 +docker load -i thebet365-images-latest.tar +docker load -i thebet365-player-main.tar +docker load -i thebet365-player-theme-2.tar +docker load -i thebet365-player-theme-3.tar +docker load -i thebet365-player-theme-4.tar + +# 编辑 .env.docker(见第四节模板)后启动 +docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d +``` + +若当前环境已由 `deploy-first.sh` 跑通,只需 **load 新 player 镜像 + 合并 compose + up -d player2 player3 player4**,并调整 `.env.docker`。 + +### 7.2 更新共用 api/admin + +仍用现有脚本(**不会**自动更新 player2~4): + +```bash +./scripts/deploy-update.sh --images thebet365-images-latest.tar --tag latest +``` + +之后确认多主题 compose 仍生效: + +```bash +docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d +``` + +### 7.3 只更新某一主题(例如 theme-4) + +```bash +docker load -i thebet365-player-theme-4.tar +docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d player4 +``` + +其他主题容器不受影响。 + +### 7.4 回滚某一 player + +```bash +# 加载旧 tag 的 tar 后 +docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d player3 +``` + +api 回滚仍用 `./scripts/rollback.sh --to `(**不**回滚数据库)。 + +--- + +## 八、宝塔 / Nginx 反代 + +每个玩家站 **只反代到对应端口**,容器内已处理 `/api` 与 `/uploads`,宝塔无需再写 API 规则。 + +| 宝塔网站 | `proxy_pass` | +|----------|----------------| +| 主站(main) | `http://127.0.0.1:8082` | +| theme-2 站 | `http://127.0.0.1:8083` | +| theme-3 站 | `http://127.0.0.1:8084` | +| theme-4 站 | `http://127.0.0.1:8085` | +| 管理后台 | `http://127.0.0.1:8081` | + +示例: + +```nginx +location / { + proxy_pass http://127.0.0.1:8082; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +四套站点分别申请 SSL;`CORS_ORIGINS` 使用 **https** 域名。 + +--- + +## 九、验收清单 + +部署完成后逐项检查: + +- [ ] `docker compose ... ps` — api、admin、player、player2、player3、player4 均为 **healthy** +- [ ] 四个玩家域名首页 UI 皮肤互不相同 +- [ ] 各站登录同一账号,余额一致 +- [ ] 各站下注、充值、站内信、公告正常 +- [ ] 管理后台 `:8081` 正常;员工菜单与 Inbox 开关已配置(见 [Docker部署指南.md](./Docker部署指南.md) 第八节步骤 5) +- [ ] `npx prisma migrate status`(在 api 容器内)无 pending 迁移 +- [ ] 浏览器 Network 无 CORS 报错(若有,检查 `CORS_ORIGINS` 并 restart api) +- [ ] theme-4 独有组件(如悬浮客服)仅在 theme-4 站出现 +- [ ] **阶段 F 完成后**:邀请面板切换四套主题,复制链接分别打开对应域名 `/register?code=...` + +--- + +## 十、常见问题 + +| 现象 | 处理 | +|------|------| +| 某站 502 | `docker compose ps` 看对应 player 是否 healthy;`docker logs thebet365-player3` | +| 接口 401/403 仅某一域名 | 检查 `CORS_ORIGINS` 是否包含该 https 域名 | +| 四套变成同一皮肤 | 检查是否四个 tag 打错或 `.env.docker` 的 `PLAYER*_IMAGE_TAG` 写错 | +| `deploy-update` 后 player2 消失 | 脚本未管理 themes compose;重新 `up -d` 并带 `-f docker-compose.themes.yml` | +| 邀请链接域名不对 | **未做阶段 F**:改 `VITE_PLAYER_URL` 重建 admin(仅一个默认站);**已做阶段 F**:到「全局设置 → 玩家主题站点」核对 URL,邀请面板选对应主题 | +| 构建 ENOENT `public/球员` | 清理 `packages/shared/public` 下除 `flags`、`players` 外的中文目录后重试 | + +--- + +## 十一、与本地开发的区别 + +| 场景 | 做法 | +|------|------| +| 日常改某一主题 UI | 切到对应分支 → `pnpm dev:player`(5173) | +| 本地同时对比 4 套 | 可选:多开终端 `--port 5173/5174/5175/5176`(非必须) | +| 生产 4 套并存 | 4 个 player 容器 + 4 个端口 + 4 个域名 | + +--- + +## 十二、邀请链接按主题选择(阶段 F 详细设计) + +> **现状**:`apps/admin/src/utils/invite-link.ts` 用构建时注入的 `VITE_PLAYER_URL` 拼链接;四套主题并存时后台无法选择注册落地页。 +> **目标**:管理员(及代理)生成/复制邀请链接时,**下拉选择**要落地的玩家主题站;配置存数据库,**改域名不必重建 admin 镜像**。 + +### 12.1 行为说明 + +| 角色 | 能力 | +|------|------| +| 平台管理员(`settings.manage`) | 在「全局设置」维护主题列表:显示名、公网 URL、是否启用、哪一项为默认 | +| 管理员 / 代理 | 在「邀请」弹窗选主题 → 复制链接 `{所选站}/register?code=xxx` | +| 玩家 | 无感知;任意主题站注册,同一邀请码、同一后端 | + +邀请码本身与主题无关(仍走现有 `InvitesService`);**仅注册链接里的域名**随所选主题变化。 + +### 12.2 数据模型(`SystemConfig`) + +配置键:`player.theme_sites` +值:JSON 字符串,结构示例: + +```json +{ + "themes": [ + { "id": "main", "label": "暗金主站", "baseUrl": "https://www.example.com", "enabled": true, "isDefault": true }, + { "id": "theme-2", "label": "Pinnacle 蓝白", "baseUrl": "https://theme2.example.com", "enabled": true, "isDefault": false }, + { "id": "theme-3", "label": "统一移动端", "baseUrl": "https://theme3.example.com", "enabled": true, "isDefault": false }, + { "id": "theme-4", "label": "海军蓝极简", "baseUrl": "https://theme4.example.com", "enabled": true, "isDefault": false } + ] +} +``` + +规则: + +- `id` 与 Git 分支 / Docker 镜像 tag 对齐(`main`、`theme-2`、`theme-3`、`theme-4`) +- `baseUrl` **勿带末尾 `/`**;保存时 API 侧 trim + 校验 `https?://` +- 有且仅有一个 `isDefault: true`;未配置时 API 回退 `VITE_PLAYER_URL`(兼容旧环境) +- `enabled: false` 的主题不出现在邀请下拉里 + +### 12.3 API 任务(`apps/api`) + +**F1 — `SystemConfigService` 新增:** + +```typescript +// apps/api/src/shared/config/system-config.service.ts +getPlayerThemeSites(): Promise +updatePlayerThemeSites(data): Promise +listEnabledPlayerThemes(): Promise // 仅 id/label/baseUrl/isDefault +``` + +**F1 — Admin 控制器:** + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/admin/settings/player-themes` | `settings.manage` | 完整配置(含 disabled) | +| PUT | `/admin/settings/player-themes` | `settings.manage` | 保存;写审计 `UPDATE_PLAYER_THEME_SITES` | +| GET | `/admin/player-themes/options` | 登录员工/代理即可 | 仅 `enabled` 主题,供邀请 UI | + +代理门户若走 `/manage/*`,在 `auth.controller` 或 manage 控制器增加同等 **GET options**(复用 service)。 + +**校验:** + +- 至少保留 1 个 `enabled` 主题 +- URL 格式合法;禁止重复 `baseUrl` +- 更新后无需重启 api(读 DB) + +### 12.4 管理端任务(`apps/admin`) + +**F2 — `GlobalSettingsView.vue` 增加卡片「玩家主题站点」:** + +- 表格编辑:label、baseUrl、enabled、设为默认 +- 保存调用 `PUT /admin/settings/player-themes` +- 与第四节四个域名保持一致(部署后首次必填) + +**F3 — 邀请 UI:** + +涉及文件: + +- `apps/admin/src/utils/invite-link.ts` — 改为 `buildPlayerRegisterUrl(code, baseUrl?)` +- `apps/admin/src/components/InviteCodePanel.vue` — 加载 options,`el-select` 选主题,`registerUrl` 随选择变化 +- `apps/admin/src/components/InviteHistoryPanel.vue` — 复制链接前同样选主题(或记住上次选择) + +交互建议: + +- 默认选中 API 返回的 `isDefault` 主题 +- `localStorage` 键 `admin_invite_theme_id` 记住上次选择(可选) +- 无配置时回退 `VITE_PLAYER_URL`,与现网行为一致 + +**F4 — i18n(三语同步):** + +在 `admin-messages.ts` / `admin-pages*.ts` 增加 key,例如: + +- `invite.theme_site` — 注册落地主题 +- `invite.theme_site_hint` — 选择玩家看到的注册页面风格 +- `settings.player_themes` — 玩家主题站点 +- `settings.player_themes_hint` — 与四套 Docker 玩家域名对应 + +### 12.5 与 Docker 四套主题的对应关系 + +部署完四套 player 后,在 **全局设置** 填: + +| id | 对应容器 | 示例 baseUrl | +|----|----------|--------------| +| `main` | `thebet365-player` :8082 | `https://www.example.com` | +| `theme-2` | `thebet365-player2` :8083 | `https://theme2.example.com` | +| `theme-3` | `thebet365-player3` :8084 | `https://theme3.example.com` | +| `theme-4` | `thebet365-player4` :8085 | `https://theme4.example.com` | + +`VITE_PLAYER_URL` 仍可保留为 **构建默认值** 与 **未配置 DB 时的回退**;多主题上线后以 DB 配置为准。 + +### 12.6 阶段 F 验收 + +- [ ] 全局设置保存 4 个 URL 后刷新仍生效 +- [ ] 邀请面板切换主题,链接域名随之变化,邀请码不变 +- [ ] 四套链接均可打开注册页并带 `code` 参数 +- [ ] 代理账号邀请弹窗可选主题(不能进全局设置改 URL) +- [ ] 禁用某主题后下拉里消失 +- [ ] 未配置 DB 时行为与现网一致(`VITE_PLAYER_URL`) + +### 12.7 实施顺序建议 + +```text +F1 API 配置读写 → F2 全局设置页 → F3 邀请面板 → F4 代理端 → 部署四套主题后填 URL → 12.6 验收 +``` + +可与 **阶段 A~D(Docker 多 player)** 并行开发;功能不依赖 player2~4 已上线,但验收需要四个域名可访问。 + +--- + +## 十三、文档维护 + +| 变更类型 | 更新本文 | +|----------|----------| +| 新增 theme-5 | 复制 player4 模式,追加端口 8086 | +| 修改 deploy 脚本支持多 player | 更新第七节命令 | +| 分支合并策略变化 | 更新第五节 | + +--- + +**下次实施入口**: + +- **基础设施**:阶段 A → B → C → D → 第九节验收 +- **邀请多主题**:阶段 F(第十二节)可与 A~D 并行,四套域名就绪后做 12.6 验收