From b929fd01c0939154468d693d94f19c54dfbb9d31 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Thu, 18 Jun 2026 14:35:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=8F=91=E7=89=88=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3=E5=B9=B6?= =?UTF-8?q?=E6=B8=85=E7=90=86=E8=BF=87=E6=97=B6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充本地打包到线上更新的分步说明;部署脚本对示例密钥改为警告而非阻断。 --- docs/Docker部署指南.md | 221 +++++++++-- docs/chuanglan-sms-js-guide.md | 695 --------------------------------- docs/docker/镜像构建与导出.md | 2 + docs/主题分支功能同步任务.md | 221 ----------- docs/玩家端缺失功能分析.md | 156 -------- docs/短信调试与日志说明.md | 3 +- scripts/deploy-lib.sh | 8 +- 7 files changed, 204 insertions(+), 1102 deletions(-) delete mode 100644 docs/chuanglan-sms-js-guide.md delete mode 100644 docs/主题分支功能同步任务.md delete mode 100644 docs/玩家端缺失功能分析.md diff --git a/docs/Docker部署指南.md b/docs/Docker部署指南.md index 0d7414d..dd6a166 100644 --- a/docs/Docker部署指南.md +++ b/docs/Docker部署指南.md @@ -226,55 +226,226 @@ pnpm docker:ps --- -## 八、后续更新部署 +## 八、推荐发版流程:本地打包 → 上传 → 线上更新 -**推荐:先删旧代码再解压新 zip**(避免 `packages/shared/public/球员` 等中文目录残留导致 Vite 构建失败)。 +**日常更新推荐走这条链路**,不在服务器上编译,省 CPU/内存,发版包可重复部署。 -推荐主流程是:本地或构建机生成版本化镜像包 → 上传 tar 与 manifest → 服务器执行 `deploy-update.sh --images ... --tag ...`。详细步骤见:[docker/镜像构建与导出.md](./docker/镜像构建与导出.md)(脚本位于 `docs/docker/build-and-export-images.ps1` / `build-and-export-images.sh`)。 +```text +本地 Windows/Mac 宝塔/SCP 上传 服务器终端 +───────────────── ─────────────── ───────────────── +checkout 目标分支 → 只传 tar + manifest → deploy-update.sh +build 三端镜像 到 /www/wwwroot/thebet365 自动备份+迁移+重启 +导出 .tar + .manifest 不要覆盖 .env.docker +``` -### 方式 A:服务器直接拉代码并构建 +镜像构建细节见:[docker/镜像构建与导出.md](./docker/镜像构建与导出.md)。 + +--- + +### 步骤 1:本地打包镜像 + +#### 1.1 前置条件 + +- 已安装 **Docker Desktop**(Windows/Mac)或 Linux Docker +- 在项目根目录,且已 `git checkout` 到要发布的分支(如 `main`、`theme-4`) + +#### 1.2 确认环境文件 + +本地需有 `.env.docker`(可从 `.env.docker.example` 复制)。构建 **admin** 镜像时会读取其中的 `VITE_PLAYER_URL`(玩家站公网地址,用于邀请链接)。若玩家域名有变,**改 `.env.docker` 后需重新打包 admin**。 + +#### 1.3 执行构建脚本 + +**Windows(推荐 CMD):** + +```bat +cd C:\path\to\thebet365 +docs\docker\build-and-export-images.bat --tag latest +``` + +带版本号(便于回滚追溯): + +```bat +docs\docker\build-and-export-images.bat --tag v20260618 +``` + +**Linux / macOS / Git Bash:** + +```bash +cd /path/to/thebet365 +chmod +x docs/docker/build-and-export-images.sh +./docs/docker/build-and-export-images.sh --tag latest +``` + +首次或发版建议不加 `--use-cache`(默认全量构建)。仅重新导出已有镜像时: + +```bat +docs\docker\build-and-export-images.bat --export-only --tag latest +``` + +#### 1.4 构建产物(在项目根目录) + +| 文件 | 说明 | +|------|------| +| `thebet365-images-.tar` | 含 `api` / `player` / `admin` 三个镜像,约 200–300 MB | +| `thebet365-images-.manifest.txt` | 记录 tag、构建时间、`git_commit`、镜像 ID、tar SHA-256 | + +示例:`thebet365-images-latest.tar`、`thebet365-images-latest.manifest.txt` + +> 这两个文件已在 `.gitignore` 中,**不要提交到 Git**。 + +--- + +### 步骤 2:上传到服务器 + +#### 2.1 上传目标目录 + +服务器项目目录,例如: + +```text +/www/wwwroot/thebet365 +``` + +#### 2.2 本次更新需要上传的文件 + +| 文件 | 是否必传 | +|------|----------| +| `thebet365-images-.tar` | **必传** | +| `thebet365-images-.manifest.txt` | 建议传(便于核对版本) | + +可用 **宝塔 → 文件 → 上传**,或 SCP: + +```bash +scp thebet365-images-latest.tar thebet365-images-latest.manifest.txt \ + root@你的服务器IP:/www/wwwroot/thebet365/ +``` + +#### 2.3 不要覆盖的文件 + +| 文件/目录 | 说明 | +|-----------|------| +| **`.env.docker`** | 生产密钥、端口、域名配置;**保留服务器上原有文件** | +| `postgres_data` 等 Docker 卷 | 数据库与上传文件,不在文件管理里替换 | + +服务器目录里应**早已存在**(首次部署时上传过):`docker-compose.prod.yml`、`scripts/`、`docker/` 等。仅换镜像时**不必**重传整份代码。 + +若部署脚本有更新(如 `scripts/deploy-lib.sh`),可单独上传覆盖 `scripts/` 目录。 + +--- + +### 步骤 3:服务器执行更新脚本 + +SSH 或 **宝塔 → 终端**,进入项目目录: ```bash cd /www/wwwroot/thebet365 -./scripts/deploy-update.sh --pull +chmod +x scripts/*.sh +./scripts/deploy-update.sh --images thebet365-images-latest.tar --tag latest ``` -### 方式 B:上传 zip 后在服务器构建 +`--tag` 必须与打包时一致(上例为 `latest`;若本地用了 `--tag v20260618`,这里也要写 `v20260618`)。 -保留原来的 `.env.docker`,替换代码后执行: +#### 脚本会自动完成(按顺序) + +1. 检查 `.env.docker`(缺少密钥会报错;示例密码仅**警告**,不阻断) +2. 启动并等待 PostgreSQL / Redis 就绪 +3. **更新前备份** → `./backups/thebet365-db-pre-update-<时间>.sql.gz` 与 `thebet365-uploads-pre-update-<时间>.tar.gz` +4. `docker load -i` 导入镜像包 +5. 用新 API 镜像执行 `prisma migrate deploy`(数据库结构变更) +6. 重启/替换 `api`、`player`、`admin` 容器 +7. 等待健康检查通过,执行 `prisma migrate status` +8. 将本次发布写入 `.deploy/current-release.env`(含备份路径) + +#### `.env.docker` 会被改什么? + +- **不会**整文件覆盖,**不会**改 `JWT_SECRET`、`POSTGRES_PASSWORD` 等 +- 部署结束时可能只更新一行:`IMAGE_TAG=<本次 tag>` + +#### 首次用镜像包部署(新机器) + +若服务器从未部署过,用首次脚本: ```bash -cd /www/wwwroot/thebet365 -./scripts/deploy-update.sh +./scripts/deploy-first.sh --images thebet365-images-latest.tar --tag latest ``` -### 方式 C:上传已构建镜像包 +--- + +### 步骤 4:验证是否成功 ```bash -cd /www/wwwroot/thebet365 -./scripts/deploy-update.sh --images thebet365-images-v1.2.3.tar --tag v1.2.3 +# 容器状态(api / player / admin 应为 healthy) +docker compose -f docker-compose.prod.yml --env-file .env.docker ps + +# 迁移是否全部应用 +docker compose -f docker-compose.prod.yml --env-file .env.docker exec api \ + sh -c 'cd /app/apps/api && npx prisma migrate status' + +# 本次发布记录与备份路径 +cat .deploy/current-release.env + +# 今天是否生成了新备份 +ls -lh backups/ | tail -5 + +# API 日志(无报错即可) +docker compose -f docker-compose.prod.yml --env-file .env.docker logs --tail=50 api ``` -更新脚本默认会: +浏览器:玩家站、管理站各访问一次;必要时强刷或清 CDN 缓存。 -- 先备份 PostgreSQL 与 uploads 到 `./backups/`,并生成 `.sha256` -- 构建或加载指定 tag 的新镜像 -- 使用新 API 镜像执行 `prisma migrate deploy` -- 启动/替换 API、玩家端、管理端容器 -- 等待 API、玩家端、管理端健康检查通过 -- 执行 `prisma migrate status` 检查数据库迁移状态 -- 将当前发布写入 `.deploy/current-release.env`,并保留上一次发布到 `.deploy/previous-release.env` +--- -除非已经手工确认有其他备份,否则不要使用 `--no-backup`。 +### 步骤 5:部署后管理端配置(镜像不会自动开启) + +若本次更新含站内邮箱、员工菜单等新功能,需在**管理后台**手动配置: + +1. **员工管理** → 给员工勾选可见菜单(`visibleMenus`) +2. **内容管理** → 开启 Inbox(站内邮箱)及充值/Banner/公告通知开关 +3. 验证:充值审核后玩家收到站内信、侧栏充值待审角标、在线人数等 + +--- + +### 备份说明 + +| 项目 | 说明 | +|------|------| +| **何时备份** | 执行 `deploy-update.sh` 时,在**加载新镜像、跑迁移之前** | +| **备份内容** | 更新前一刻的 PostgreSQL 全库 + `uploads` 用户上传文件 | +| **存放位置** | `/www/wwwroot/thebet365/backups/` | +| **文件命名** | `thebet365-db-pre-update-YYYYMMDD-HHMMSS.sql.gz`、`thebet365-uploads-pre-update-....tar.gz` | +| **记录位置** | `.deploy/current-release.env` 中的 `db_backup=`、`uploads_backup=` | + +手动备份(不更新时也可执行): + +```bash +./scripts/backup-db.sh +./scripts/backup-prod.sh --prefix manual +``` + +**恢复数据库**(仅出问题时):先 `stop api`,再将 `.sql.gz` 导入 postgres,最后 `start api`。项目无一键恢复脚本,需手工操作;详见下方回滚说明。 + +--- + +### 其他更新方式(备选) + +| 方式 | 适用场景 | 命令 | +|------|----------|------| +| **A:服务器拉代码构建** | 服务器性能足够、不用传 tar | `./scripts/deploy-update.sh --pull` | +| **B:上传 zip 后服务器构建** | 无 Git、在服务器编译 | 替换代码后 `./scripts/deploy-update.sh` | + +方式 B 上传代码时**保留** `.env.docker`;若用 zip 覆盖,注意清理旧的中文目录 `packages/shared/public/球员`(见第九节故障排查)。 + +--- ### 回滚应用镜像 ```bash cd /www/wwwroot/thebet365 -./scripts/rollback.sh --to v1.2.2 +./scripts/rollback.sh --to <旧tag> ``` -回滚脚本只切换 `api` / `player` / `admin` 镜像 tag,不自动恢复数据库。若新版本包含不可逆迁移或已写入不兼容数据,需要先按 `backups/` 中的 `.sql.gz` 备份手工恢复 PostgreSQL,再执行镜像回滚。 +`rollback.sh` **只切换** `api` / `player` / `admin` 镜像 tag,**不自动恢复数据库**。若新版本已执行不可逆迁移,需先从 `backups/` 中选取 `pre-update` 的 `.sql.gz` 手工恢复 PostgreSQL,再回滚镜像。 + +除非已确认另有备份,否则不要使用 `--no-backup` 跳过自动备份。 --- @@ -329,10 +500,10 @@ docker compose -f docker-compose.prod.yml --env-file .env.docker build --no-cach ## 十、与本地开发的区别 -| 场景 | 命令 | +| 场景 | 命令 / 文档 | |------|------| | 本地开发(仅 DB 用 Docker) | `docker compose up -d` + `pnpm dev` | | 生产首次部署 | `./scripts/deploy-first.sh` | -| 生产后续更新 | `./scripts/deploy-update.sh` | +| 生产后续更新(推荐) | 本地打包 → 上传 tar → `./scripts/deploy-update.sh --images ... --tag ...`(见第八节) | 相关文档:[项目启动指南.md](./项目启动指南.md) diff --git a/docs/chuanglan-sms-js-guide.md b/docs/chuanglan-sms-js-guide.md deleted file mode 100644 index 9812d72..0000000 --- a/docs/chuanglan-sms-js-guide.md +++ /dev/null @@ -1,695 +0,0 @@ -# 创蓝短信(Chuanglan)TypeScript 全栈接入指南 - -> 适用场景:**全新独立项目**,TypeScript 全栈(Next.js / Remix / Nuxt 等),直连创蓝 API。 -> 服务商:创蓝 253 云通讯(国际短信网关) -> API Endpoint:`https://sgap.253.com/send/sms` -> 签名算法参考:`babylive-backend` 中 `ChuanglanClient.java` + `SignUtil.java`(已验证可用) - ---- - -## 1. 整体架构 - -创蓝 `account` / `password` 是服务端密钥,**只能在服务端调用**,浏览器/客户端绝不直接接触创蓝。 - -``` -┌─────────────┐ POST /api/sms/send ┌──────────────────┐ POST + sign ┌─────────────┐ -│ 前端页面 │ ──────────────────────────▶│ TS 服务端 │ ──────────────────▶│ 创蓝 API │ -│ (React 等) │◀────────────────────────── │ API Route / tRPC │◀────────────────── │ 253.com │ -└─────────────┘ { sessionId } └──────────────────┘ messageId └─────────────┘ - │ - ▼ - Redis / KV 缓存 - (验证码 + 频控) -``` - -职责划分: - -| 层 | 职责 | -|----|------| -| 前端 | 收集手机号、触发发送、倒计时 60s、提交验证码 + `sessionId` | -| 服务端 API | 频控、生成验证码、调创蓝、存/验缓存 | -| `lib/chuanglan` | 签名 + HTTP 请求,不含业务逻辑 | -| Redis | 验证码存储(5 分钟 TTL)、手机号/IP 频控(60 秒 TTL) | - ---- - -## 2. 环境变量 - -```bash -# .env.local(勿提交 Git) - -CHUANGLAN_ACCOUNT=your_account -CHUANGLAN_PASSWORD=your_password -CHUANGLAN_ENDPOINT=https://sgap.253.com/send/sms -CHUANGLAN_CONNECT_TIMEOUT_MS=10000 -CHUANGLAN_READ_TIMEOUT_MS=10000 - -# 验证码业务 -SMS_CODE_TTL_SECONDS=300 # 5 分钟 -SMS_RATE_LIMIT_SECONDS=60 # 发送冷却 - -REDIS_URL=redis://127.0.0.1:6379 -``` - -创蓝账号信息可向运维索取(与 babylive-backend `application.yml` 中 `chuanglan.*` 同源)。 - ---- - -## 3. 推荐目录结构 - -以 Next.js App Router 为例,其他 TS 全栈框架可平移 `lib/` 与 `types/`: - -``` -src/ -├── lib/ -│ ├── chuanglan/ -│ │ ├── client.ts # 创蓝 HTTP Client -│ │ ├── sign.ts # MD5 签名 -│ │ └── config.ts # 读取环境变量 -│ └── sms/ -│ ├── templates.ts # 多语言短信模板 -│ ├── code.ts # 验证码生成 -│ └── service.ts # 发送 / 校验业务 -├── app/ -│ └── api/ -│ └── sms/ -│ ├── send/route.ts -│ └── verify/route.ts -├── types/ -│ └── sms.ts -└── hooks/ - └── use-sms-code.ts # 前端发送 + 倒计时 -``` - ---- - -## 4. 创蓝 API 协议 - -### 4.1 请求 - -**Method:** `POST` -**URL:** `https://sgap.253.com/send/sms` - -**Headers:** - -| Header | 说明 | -|--------|------| -| `Content-Type` | `application/json` | -| `nonce` | 毫秒时间戳字符串,如 `1718000000123` | -| `sign` | MD5 签名,见 4.2 | - -**Body:** - -```json -{ - "account": "your_account", - "mobile": "8613800138000", - "msg": "您的验证码是:123456。5分钟内有效。", - "uid": "optional-session-id" -} -``` - -| 字段 | 必填 | 说明 | -|------|------|------| -| account | 是 | 创蓝账号 | -| mobile | 是 | 目标手机号,建议带国家码 | -| msg | 是 | 短信正文 | -| uid | 否 | 自定义 ID,建议传本次验证码会话 ID | - -> `nonce` 参与签名,放 Header,**不进 Body**。 - -### 4.2 签名算法 - -1. 取 Body 全部字段 + `nonce`,组成键值对 -2. 按 key **字典序升序**(等价 Java `TreeMap`) -3. 依次拼接 `key + value`,**跳过空值**(`null` / `""` / 纯空白) -4. 末尾追加 `password` -5. 整体做 **MD5**,输出 **32 位小写** hex - -``` -sign = md5("account" + account + "mobile" + mobile + "msg" + msg + "nonce" + nonce + password) -``` - -### 4.3 响应 - -成功(`code === "0"`): - -```json -{ - "code": "0", - "message": "提交成功", - "data": { "messageId": "162575412960104448" } -} -``` - -失败时 `code` 为非 `"0"` 字符串,`message` 为错误描述。 - ---- - -## 5. TypeScript 类型 - -```typescript -// src/types/sms.ts - -export type SmsLang = 'zh' | 'en' | 'vi' | 'ms' | 'kh'; - -export interface ChuanglanSendBody { - account: string; - mobile: string; - msg: string; - uid?: string; -} - -export interface ChuanglanSendResponse { - code: string; - message: string; - data?: { messageId: string }; -} - -export interface SmsSendResult { - success: boolean; - code: string; - message: string; - messageId?: string; -} - -export interface SendSmsCodeRequest { - phone: string; - lang?: SmsLang; -} - -export interface SendSmsCodeResponse { - sessionId: string; -} - -export interface VerifySmsCodeRequest { - phone: string; - code: string; - sessionId: string; -} - -export interface VerifySmsCodeResponse { - ok: true; -} -``` - ---- - -## 6. 服务端实现 - -### 6.1 配置 - -```typescript -// src/lib/chuanglan/config.ts - -function required(name: string): string { - const v = process.env[name]; - if (!v) throw new Error(`Missing env: ${name}`); - return v; -} - -export const chuanglanConfig = { - account: required('CHUANGLAN_ACCOUNT'), - password: required('CHUANGLAN_PASSWORD'), - endpoint: process.env.CHUANGLAN_ENDPOINT ?? 'https://sgap.253.com/send/sms', - connectTimeoutMs: Number(process.env.CHUANGLAN_CONNECT_TIMEOUT_MS ?? 10_000), - readTimeoutMs: Number(process.env.CHUANGLAN_READ_TIMEOUT_MS ?? 10_000), -} as const; - -export const smsConfig = { - codeTtlSeconds: Number(process.env.SMS_CODE_TTL_SECONDS ?? 300), - rateLimitSeconds: Number(process.env.SMS_RATE_LIMIT_SECONDS ?? 60), -} as const; -``` - -### 6.2 签名 - -```typescript -// src/lib/chuanglan/sign.ts -import crypto from 'node:crypto'; - -export function generateChuanglanSign( - password: string, - params: Record, -): string { - const raw = Object.keys(params) - .sort() - .reduce((acc, key) => { - const value = params[key]; - if (value != null && value.trim() !== '') { - return acc + key + value; - } - return acc; - }, ''); - - return crypto.createHash('md5').update(raw + password, 'utf8').digest('hex').toLowerCase(); -} -``` - -### 6.3 创蓝 Client - -```typescript -// src/lib/chuanglan/client.ts -import { chuanglanConfig } from './config'; -import { generateChuanglanSign } from './sign'; -import type { ChuanglanSendResponse, SmsSendResult } from '@/types/sms'; - -export async function sendChuanglanSms( - mobile: string, - msg: string, - uid?: string, -): Promise { - const nonce = String(Date.now()); - - const body: Record = { - account: chuanglanConfig.account, - mobile, - msg, - }; - if (uid) body.uid = uid; - - const sign = generateChuanglanSign(chuanglanConfig.password, { ...body, nonce }); - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), chuanglanConfig.readTimeoutMs); - - try { - const res = await fetch(chuanglanConfig.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - nonce, - sign, - }, - body: JSON.stringify(body), - signal: controller.signal, - }); - - const data = (await res.json()) as ChuanglanSendResponse; - - if (data.code === '0') { - return { - success: true, - code: data.code, - message: 'OK', - messageId: data.data?.messageId, - }; - } - - return { success: false, code: data.code, message: data.message }; - } catch (err) { - const message = err instanceof Error ? err.message : 'Unknown error'; - return { success: false, code: 'HTTP_ERROR', message }; - } finally { - clearTimeout(timer); - } -} -``` - -### 6.4 短信模板 - -与 babylive-backend `sms.verify` 配置一致: - -```typescript -// src/lib/sms/templates.ts -import type { SmsLang } from '@/types/sms'; - -const TEMPLATES: Record = { - default: '您的验证码是:{code}。5分钟内有效。', - zh: '您的验证码是:{code}。5分钟内有效。', - en: 'Your verification code is {code}. Valid for 5 minutes.', - vi: 'Mã xác minh của bạn là {code}. Có hiệu lực trong 5 phút.', - ms: 'Kod pengesahan anda ialah {code}. Sah selama 5 minit.', - kh: 'កូដផ្ទៀងផ្ទាត់របស់អ្នកគឺ {code} ។ មានសុពលភាពរយៈពេល ៥ នាទី។', -}; - -export function renderVerifySms(lang: SmsLang | undefined, code: string): string { - const key = lang?.trim() || 'zh'; - const tpl = TEMPLATES[key] ?? TEMPLATES.default ?? TEMPLATES.zh; - return tpl.replace('{code}', code); -} -``` - -```typescript -// src/lib/sms/code.ts - -export function generateSixDigitCode(): string { - return String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0'); -} -``` - -### 6.5 业务 Service(Redis) - -```typescript -// src/lib/sms/service.ts -import { randomUUID } from 'node:crypto'; -import { sendChuanglanSms } from '@/lib/chuanglan/client'; -import { smsConfig } from '@/lib/chuanglan/config'; -import { generateSixDigitCode } from './code'; -import { renderVerifySms } from './templates'; -import type { SmsLang } from '@/types/sms'; - -// 按项目替换为 ioredis / @upstash/redis 等 -import { redis } from '@/lib/redis'; - -const codeKey = (sessionId: string) => `sms:code:${sessionId}`; -const phoneRateKey = (phone: string) => `sms:rate:phone:${phone}`; -const ipRateKey = (ip: string) => `sms:rate:ip:${ip}`; - -export class SmsRateLimitError extends Error { - constructor() { - super('发送太频繁,请60秒后再试'); - this.name = 'SmsRateLimitError'; - } -} - -export class SmsSendError extends Error { - code: string; - constructor(code: string, message: string) { - super(message); - this.name = 'SmsSendError'; - this.code = code; - } -} - -export async function sendVerifyCode(params: { - phone: string; - lang?: SmsLang; - clientIp: string; -}): Promise<{ sessionId: string }> { - const { phone, lang, clientIp } = params; - - const [phoneLimited, ipLimited] = await Promise.all([ - redis.exists(phoneRateKey(phone)), - redis.exists(ipRateKey(clientIp)), - ]); - if (phoneLimited || ipLimited) throw new SmsRateLimitError(); - - const code = generateSixDigitCode(); - const sessionId = randomUUID(); - const msg = renderVerifySms(lang, code); - - const result = await sendChuanglanSms(phone, msg, sessionId); - if (!result.success) { - throw new SmsSendError(result.code, result.message); - } - - await Promise.all([ - redis.set(codeKey(sessionId), JSON.stringify({ phone, code }), 'EX', smsConfig.codeTtlSeconds), - redis.set(phoneRateKey(phone), '1', 'EX', smsConfig.rateLimitSeconds), - redis.set(ipRateKey(clientIp), '1', 'EX', smsConfig.rateLimitSeconds), - ]); - - return { sessionId }; -} - -export async function verifyCode(params: { - phone: string; - code: string; - sessionId: string; -}): Promise { - const raw = await redis.get(codeKey(params.sessionId)); - if (!raw) throw new Error('验证码已过期'); - - const cached = JSON.parse(raw) as { phone: string; code: string }; - if (cached.phone !== params.phone || cached.code !== params.code) { - throw new Error('验证码错误'); - } - - await redis.del(codeKey(params.sessionId)); -} -``` - ---- - -## 7. API Route(Next.js 示例) - -### 7.1 发送验证码 - -```typescript -// src/app/api/sms/send/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { sendVerifyCode, SmsRateLimitError, SmsSendError } from '@/lib/sms/service'; -import type { SendSmsCodeRequest } from '@/types/sms'; - -function getClientIp(req: NextRequest): string { - return ( - req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() - || req.headers.get('x-real-ip') - || '0.0.0.0' - ); -} - -export async function POST(req: NextRequest) { - const body = (await req.json()) as SendSmsCodeRequest; - - if (!body.phone?.trim()) { - return NextResponse.json({ message: 'phone 必填' }, { status: 400 }); - } - - try { - const { sessionId } = await sendVerifyCode({ - phone: body.phone.trim(), - lang: body.lang, - clientIp: getClientIp(req), - }); - return NextResponse.json({ sessionId }); - } catch (err) { - if (err instanceof SmsRateLimitError) { - return NextResponse.json({ message: err.message }, { status: 429 }); - } - if (err instanceof SmsSendError) { - return NextResponse.json({ message: err.message, code: err.code }, { status: 502 }); - } - return NextResponse.json({ message: '服务器错误' }, { status: 500 }); - } -} -``` - -### 7.2 校验验证码 - -```typescript -// src/app/api/sms/verify/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { verifyCode } from '@/lib/sms/service'; -import type { VerifySmsCodeRequest } from '@/types/sms'; - -export async function POST(req: NextRequest) { - const body = (await req.json()) as VerifySmsCodeRequest; - - if (!body.phone || !body.code || !body.sessionId) { - return NextResponse.json({ message: '参数不完整' }, { status: 400 }); - } - - try { - await verifyCode(body); - return NextResponse.json({ ok: true }); - } catch (err) { - const message = err instanceof Error ? err.message : '校验失败'; - return NextResponse.json({ message }, { status: 400 }); - } -} -``` - -### 7.3 对外 API 契约 - -**发送** - -``` -POST /api/sms/send -Content-Type: application/json - -{ "phone": "8613800138000", "lang": "zh" } - -→ 200 { "sessionId": "uuid" } -→ 429 { "message": "发送太频繁,请60秒后再试" } -→ 502 { "message": "...", "code": "创蓝错误码" } -``` - -**校验** - -``` -POST /api/sms/verify -Content-Type: application/json - -{ "phone": "8613800138000", "code": "123456", "sessionId": "uuid" } - -→ 200 { "ok": true } -→ 400 { "message": "验证码错误或已过期" } -``` - ---- - -## 8. 前端调用 - -### 8.1 API Client - -```typescript -// src/lib/api/sms.ts -import type { SendSmsCodeResponse, SmsLang, VerifySmsCodeResponse } from '@/types/sms'; - -export async function sendSmsCode(phone: string, lang: SmsLang = 'zh'): Promise { - const res = await fetch('/api/sms/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ phone, lang }), - }); - - const json = await res.json(); - if (!res.ok) throw new Error(json.message ?? '发送失败'); - return (json as SendSmsCodeResponse).sessionId; -} - -export async function verifySmsCode( - phone: string, - code: string, - sessionId: string, -): Promise { - const res = await fetch('/api/sms/verify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ phone, code, sessionId }), - }); - - const json = await res.json(); - if (!res.ok) throw new Error(json.message ?? '校验失败'); - void json as VerifySmsCodeResponse; -} -``` - -### 8.2 React Hook 示例 - -```typescript -// src/hooks/use-sms-code.ts -'use client'; - -import { useCallback, useRef, useState } from 'react'; -import { sendSmsCode } from '@/lib/api/sms'; -import type { SmsLang } from '@/types/sms'; - -const COOLDOWN_SECONDS = 60; - -export function useSmsCode(lang: SmsLang = 'zh') { - const [sessionId, setSessionId] = useState(null); - const [countdown, setCountdown] = useState(0); - const [sending, setSending] = useState(false); - const [error, setError] = useState(null); - const timerRef = useRef | null>(null); - - const startCountdown = useCallback(() => { - setCountdown(COOLDOWN_SECONDS); - timerRef.current = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { - if (timerRef.current) clearInterval(timerRef.current); - return 0; - } - return prev - 1; - }); - }, 1000); - }, []); - - const send = useCallback(async (phone: string) => { - if (countdown > 0 || sending) return; - setSending(true); - setError(null); - try { - const id = await sendSmsCode(phone, lang); - setSessionId(id); - startCountdown(); - } catch (err) { - setError(err instanceof Error ? err.message : '发送失败'); - } finally { - setSending(false); - } - }, [countdown, sending, lang, startCountdown]); - - return { sessionId, countdown, sending, error, send }; -} -``` - -页面中使用: - -```tsx -const { sessionId, countdown, sending, error, send } = useSmsCode('zh'); - - - -// 提交表单时带上 sessionId + code 调 /api/sms/verify 或合并进登录/注册接口 -``` - ---- - -## 9. 手机号格式 - -- 国际短信建议带国家码:`8613800138000`(`86` + 11 位) -- 前端可在提交前统一格式化,或在 `service.ts` 中做 normalize -- 创蓝账号为国际网关(`sgap.253.com`),非中国大陆号段需确认创蓝侧已开通对应路由 - ---- - -## 10. 业务规则(与 babylive 对齐) - -| 规则 | 值 | -|------|-----| -| 验证码位数 | 6 位数字 | -| 验证码有效期 | 5 分钟 | -| 同手机号冷却 | 60 秒 | -| 同 IP 冷却 | 60 秒 | -| 校验成功后 | 立即删除缓存(一次性) | - ---- - -## 11. 签名自测 - -接入后用固定参数验证签名是否与 Java 端一致: - -```typescript -import { generateChuanglanSign } from '@/lib/chuanglan/sign'; - -const sign = generateChuanglanSign('your_password', { - account: 'your_account', - mobile: '8613800138000', - msg: '您的验证码是:123456。5分钟内有效。', - uid: 'test-session-001', - nonce: '1718000000123', -}); - -console.log(sign); -// 应与 Java SignUtil.generateSign 输出完全相同 -``` - -检查清单: - -- [ ] key 字典序排序 -- [ ] 空 `uid` 不参与签名 -- [ ] `nonce` 在 Header + 签名参数,不在 Body -- [ ] MD5 32 位小写 -- [ ] UTF-8 编码 - ---- - -## 12. 安全与运维 - -1. `CHUANGLAN_*` 仅服务端环境变量,不进 `NEXT_PUBLIC_*` -2. 日志中手机号脱敏、禁止打印验证码明文 -3. 生产环境 Redis 必开;无 Redis 时不可用内存 Map(Serverless 多实例会失效) -4. `uid` / `sessionId` 建议用 UUID,便于与创蓝 `messageId` 对账 -5. 监控创蓝 `code` 分布与 `HTTP_ERROR` 比例 - ---- - -## 13. 接入步骤速查 - -``` -1. 配置 .env.local(创蓝账号 + Redis) -2. 复制 lib/chuanglan/*(sign + client) -3. 复制 lib/sms/*(templates + service) -4. 添加 /api/sms/send 与 /api/sms/verify -5. 前端 useSmsCode + 表单提交携带 sessionId -6. 跑签名自测,发一条真实短信验证 -``` - -新项目按此文档从零接入即可,**无需依赖 babylive-backend 运行时**;签名算法以该仓库 `ChuanglanClient.java` 为准。 diff --git a/docs/docker/镜像构建与导出.md b/docs/docker/镜像构建与导出.md index 1ae5aa1..3717c7b 100644 --- a/docs/docker/镜像构建与导出.md +++ b/docs/docker/镜像构建与导出.md @@ -139,6 +139,8 @@ docs\docker\build-and-export-images.bat --export-only ## 五、上传到服务器并部署 +完整分步说明(本地打包 → 上传 → 终端执行 → 验证)见上级文档 **[Docker部署指南.md 第八节](../Docker部署指南.md#八推荐发版流程本地打包--上传--线上更新)**。 + ### 1. 上传 将以下内容传到服务器同一目录(如 `/www/wwwroot/thebet365`): diff --git a/docs/主题分支功能同步任务.md b/docs/主题分支功能同步任务.md deleted file mode 100644 index d62f9b8..0000000 --- a/docs/主题分支功能同步任务.md +++ /dev/null @@ -1,221 +0,0 @@ -# 主题分支功能同步任务 - -> 基于 main 分支 commit `f9343b0`(站内邮箱、在线状态、员工菜单权限与内容管理增强)及前后关联提交的分析。 -> **原则**:API / Admin / Shared 可原样同步;Player 端只做逻辑合并 + 各主题样式适配,**禁止**整文件从 main checkout 覆盖已有 player 页面。 - ---- - -## 分支现状 - -| 分支 | 相对 main 缺失 | 已有等价功能 | 主题特征 | -|------|----------------|--------------|----------| -| **theme-2** | `f9343b0` 全部 + `f34fe54`(赛事搜索) | `9f1d263` ≈ `e9a23de` 的 admin/API/投注逻辑 | Pinnacle 蓝白浅色 | -| **theme-3** | 同 theme-2 | `7832257` ≈ `e9a23de` | 统一移动端视觉 | -| **theme-4** | 仅 `f9343b0` | `d3ca849` 已含 `e9a23de` 的 admin/API + 单串关 | 海军蓝暗色极简 | - -**结论**:三个 theme 分支都需要同步 **f9343b0**;theme-2/3 额外需要 **f34fe54** 的赛事搜索。`e9a23de` 在 theme 分支已通过 cherry-pick 或 `d3ca849` 覆盖,admin/API 侧无缺口。 - ---- - -## 任务清单 - -- [ ] **sync-api-admin**:三个 theme 分支从 main checkout `apps/api`、`apps/admin`、`packages/shared/src/api-errors.ts`、相关 docs -- [ ] **player-new-files**:复制 player 新文件(composables / views / components),按各 theme CSS 变量改样式 -- [ ] **player-merge-existing**:手工合并 MainLayout、BetSlipDrawer、HomeView、router、i18n 增量 -- [ ] **theme23-football-search**:theme-2/3 额外合并 `f34fe54` FootballView 赛事搜索逻辑 -- [ ] **verify-per-theme**:按分支跑 build + 邮箱 / 充值 / 菜单 / 在线 / 搜索功能验证 - ---- - -## 一、可直接整目录同步(三个分支相同) - -与玩家主题无关,**建议从 main 原样 checkout**: - -### API 全量(含迁移) - -- `apps/api/prisma/schema.prisma` + 迁移: - - `20260617083520_add_visible_menus` - - `20260617120000_player_messages` -- 新域: - - `apps/api/src/domains/player-messages/` - - `apps/api/src/domains/presence/` -- 主要改动:`admin.controller.ts`、`player.controller.ts`、`deposit.service.ts`、`admin-staff.service.ts`、`users.service.ts`、`system-config.service.ts`、`matches.service.ts`(含 `listUpcomingPublished`、`getSelectionsOdds`)、`content.service.ts` 及对应 spec - -**功能清单**: - -- 玩家站内消息 CRUD + 充值审核自动通知 -- Redis 在线心跳 + 管理端在线人数 -- 员工 `visibleMenus` + 删除保护 -- Inbox 系统配置(开关、充值 / Banner / 公告通知) -- 待审充值 count API、home 返回 `inboxEnabled` / `upcomingMatches` - -### Admin 全量 - -Admin 在 repo 内**不随 theme 分支分化**,以下全部同步: - -- 新组件:`AdminPlayerStatusCell.vue`、`ContentImageField.vue`、`ContentRichEditor.vue` -- 新 composable:`useDepositPendingCount.ts` -- 核心页面:`StaffManage.vue`(可见菜单勾选)、`Contents.vue`(富文本 + INBOX_NOTIFY 配置)、`ManageLayout.vue`(菜单过滤 + 充值角标) -- 其它:`DashboardPlayers.vue`、`Users.vue`、`auth.ts`、全套 i18n -- `App.vue` 中 Element Plus 链接按钮 hover 修复(管理端 UI 修复,非玩家主题) - -### Shared + 文档 - -- `packages/shared/src/api-errors.ts`(及 `.js`):`CANNOT_DELETE_SELF`、`STAFF_NOT_FOUND`、`MESSAGE_NOT_FOUND` 等 -- `docs/玩家端缺失功能分析.md` - -### 可选 - -- 删除 `.agents/skills/**` — 与功能无关,可随 main 清理或保留 - ---- - -## 二、明确不同步(主题专属) - -| 路径 | 原因 | -|------|------| -| `apps/player/src/styles.css` | 各 theme CSS 变量不同(main 暗金 / theme-2 蓝白 / theme-4 海军白强调) | -| `apps/player/index.html` | theme-color、meta 随主题 | -| `apps/player/src/assets/**` | banner、empty 图、vs.png 等 | -| 各 theme 已有 player 组件/页面样式 diff | BetSlipDrawer、ProfileView、WalletView 等布局与配色 | -| theme-4 独有:`VsBadge.vue`、`WalletBalanceCard.vue`、`walletStats.ts` | theme-4 钱包 UX;**不要**同步 main 对这些文件的删除 | -| theme-2/3 独有:`ec51b67` / `0d43085` 赛事详情 8px 间距 | 保留 | -| `packages/shared/public/favicon.svg`、`site.webmanifest` | 品牌 / 主题资源,按分支保留 | - ---- - -## 三、Player 端:逻辑合并 + 样式适配 - -**禁止**对已有 player 文件执行 `git checkout main -- ` 整文件覆盖。 - -### 3.1 新增文件(复制后做样式 pass) - -| 文件 | 说明 | 样式注意 | -|------|------|----------| -| `ConfirmDialog.vue` | 通用确认弹窗 | main 写死暗金渐变;theme-2 用 `--primary`/`--bg-card`;theme-4 用白色强调变量 | -| `CustomerServicePanel.vue` | 客服面板 | 依赖 theme panel 样式 | -| `MessageListPanel.vue` | 消息列表 | 含 `#141414`、`var(--gold)`;theme-2 无 `--gold` | -| `InboxHubView.vue` | 邮箱中心 | 同上 | -| `MessageDetailView.vue` | 消息详情 | 配合 `utils/html.ts` | -| `AnnouncementListView.vue` | 公告列表 | 按 theme 变量写 scoped CSS | -| `AnnouncementDetailView.vue` | 公告详情 | 同上 | -| `useDepositNotifications.ts` | 充值结果轮询 | 纯逻辑,可直接复制 | -| `useInboxFeature.ts` | 邮箱开关路由 | 纯逻辑 | -| `usePlayerMessages.ts` | 消息 API | 纯逻辑 | -| `usePresencePing.ts` | 在线心跳 | 纯逻辑 | -| `utils/html.ts` | HTML sanitize | 纯逻辑 | - -**删除**:`CustomerServiceModal.vue`(三个 theme 均需删) - -### 3.2 已有文件 — 只合并 script / 路由 / 模板结构 - -| 文件 | 需合并的功能 | -|------|-------------| -| `router/index.ts` | 新增 `announcements`、`announcements/:id`、`messages`、`messages/:id` | -| `usePlayerHome.ts` | `homeRaw`、`upcomingMatches`、`announcementItems`、inbox 字段 | -| `MainLayout.vue` | inbox/deposit/presence composables;Hub 替代客服 Modal;路由可见性规则;**保留 theme CSS** | -| `BetSlipDrawer.vue` | 赔率轮询、`ConfirmDialog` 清空确认、`oddsDeltas` 变更提示 | -| `betSlip.ts` | `updateSelectionOdds()`(theme-4 已有,theme-2/3 需补) | -| `AnnouncementMarquee.vue` | 点击跳转 `/announcements/:id` | -| `BannerCarousel.vue` | 推广链接行为 | -| `HomeView.vue` | 近期赛事 Tab(`upcoming` / `upcomingMatches`) | -| `FootballView.vue` | theme-2/3:`f34fe54` 搜索、`?search=`、双视图 | -| `ProfileView.vue` | 邮箱 / 消息入口 | -| `RechargeHistoryView.vue` / `RechargeView.vue` | minor 逻辑 | -| `constants/defaultBanner.ts` | 删除硬编码 fallback | -| i18n 三语文件 | **只合并新增 key**,勿覆盖 theme 已有文案 | - -### 3.3 功能数据流 - -```mermaid -flowchart LR - subgraph admin [Admin] - StaffManage[StaffManage visibleMenus] - Contents[Contents INBOX_NOTIFY] - DepositReview[Deposit approve/reject] - end - subgraph api [API] - PlayerMessages[player-messages] - Presence[presence Redis] - SystemConfig[inbox settings] - end - subgraph player [Player per theme] - MainLayout[MainLayout Hub] - InboxHub[InboxHubView] - PresencePing[presence ping] - end - StaffManage --> api - Contents --> SystemConfig - DepositReview --> PlayerMessages - SystemConfig --> MainLayout - PlayerMessages --> InboxHub - Presence --> PresencePing -``` - ---- - -## 四、各分支差异要点 - -### theme-2 / theme-3 - -- **必做**:第一节 API/Admin/Shared + 第三节 Player 合并 -- **额外**:`f34fe54` 的 `FootballView.vue` 赛事搜索 -- **已有**:单串关 / 盘口状态 cherry-pick;admin `match-form.ts` 与 main 无 diff -- **保留**:8px 间距 commit、各自 Pinnacle / 移动端样式 - -### theme-4 - -- **必做**:第一节 + 第三节(以 theme-4 组件结构为基准,如 WalletView、VsBadge) -- **跳过或按需**:`f34fe54`(对比 FootballView 是否已有搜索) -- **已有**:`d3ca849` 含 e9a23de + `updateSelectionOdds` -- **注意**:不要同步 main 对 `WalletBalanceCard` / `VsBadge` 的删除 - ---- - -## 五、推荐操作顺序(每个 theme 分支) - -```bash -git checkout theme-X -git checkout main -- apps/api apps/admin packages/shared/src/api-errors.ts docs/玩家端缺失功能分析.md -pnpm db:migrate -pnpm --filter @thebet365/api test -pnpm --filter @thebet365/admin build -``` - -随后按顺序: - -1. Player 纯逻辑文件(composables、utils、router、i18n 增量、删 Modal) -2. Player 新 UI 文件(复制 + 按 `styles.css` 变量改 scoped CSS) -3. Player 已有文件手工 merge(MainLayout → BetSlipDrawer → HomeView → FootballView) - -### 验证清单 - -- [ ] 员工登录后侧栏按 `visibleMenus` 过滤 -- [ ] 充值审核后玩家收到站内信(开关打开时) -- [ ] Hub 未读角标、公告详情、inbox 关闭时客服 fallback -- [ ] 管理端在线人数、充值待审角标 -- [ ] theme-2/3:首页「近期」Tab、赛事页搜索 -- [ ] 投注单:清空确认、赔率变更提示 - ---- - -## 六、工作量与风险 - -| 类别 | 文件约数 | 方式 | 风险 | -|------|----------|------|------| -| API + Admin + Shared | ~62 | 原样 checkout | 低 | -| Player 新文件 + composables | ~15 | 复制 + 样式 pass | 中 | -| Player 已有文件 merge | ~12 | 手工 | 高 | -| **每个 theme 分支** | | | **约 2~4h**(theme-4 略少) | - -**不建议**对 theme 分支使用 `git merge main` 或 `git cherry-pick f9343b0` 整 commit — 会把 main 暗金 player 样式大量带入,冲突面超过 50 个 player 文件。 - ---- - -## 参考 commit - -| Commit | 说明 | -|--------|------| -| `f9343b0` | 站内邮箱、在线状态、员工菜单、内容管理增强(**三 theme 必同步**) | -| `f34fe54` | FootballView 赛事搜索(**theme-2/3 额外**) | -| `e9a23de` | 单串关持久化、盘口状态(theme 分支已 cherry-pick 或含于 d3ca849) | diff --git a/docs/玩家端缺失功能分析.md b/docs/玩家端缺失功能分析.md deleted file mode 100644 index fda6de4..0000000 --- a/docs/玩家端缺失功能分析.md +++ /dev/null @@ -1,156 +0,0 @@ -## 玩家端缺失功能分析 - -> 基于 PRD v1.2 要求 + 行业通用标准,对比当前代码实际实现情况 -> 分析日期:2026-06-17 - ---- - -### 一、PRD 明确要求但尚未完整实现的功能 - -#### 1. 首页「今日赛事」板块 - -PRD 4.4 节明确要求首页同时展示「热门赛事」和「今日赛事」两个板块。当前 HomeView.vue 只渲染了 `hotMatches`,缺少按当天开赛时间筛选的「今日赛事」列表。后端 `/player/home` 接口实际已返回 `todayMatches` 数据,前端未消费。 - -#### 2. 公告详情页 - -PRD 17.2 节将公告定义为"可点击查看详情的完整内容",与走马灯(短文案滚动)是两种不同的内容形态。当前只有 AnnouncementMarquee 走马灯组件,点击后没有跳转到公告详情页查看完整正文。 - -#### 3. 投注规则独立页面 - -PRD 3.3 节要求"投注规则说明必须三语,减少争议"。当前规则内容硬编码在 ProfileView.vue 的 5 段 i18n 翻译文本中(rules_p1 ~ rules_p5),不可由后台动态维护,也没有独立的规则页面。建议做成后台可编辑、前台可独立访问的规则中心页面。 - ---- - -### 二、业务闭环缺失的关键功能 - -#### 4. 提款 / 出金 - -当前已实现充值(Recharge)全流程——银行转账、USDT、截图上传、审核、入账。但完全没有提款/出金功能。玩家赢了钱却无法主动申请提款,这在业务闭环上是最大的缺口。至少需要:提款申请页面、提款方式选择、金额输入、提款记录与状态追踪。 - -#### 5. 注单结算/派彩通知 - -玩家下注后,赛事结算完成时没有任何主动通知机制。玩家只能反复刷"我的投注"才知道结果。需要:注单结算后推送通知(站内消息或 WebSocket 实时推送),至少在下注成功页和注单列表中有明确的"等待开奖"与"已开奖"状态区分。 - -#### 6. 充值/提款状态通知 - -充值订单审核通过或被拒绝后,玩家没有收到即时通知。目前只能在充值历史页面手动查看状态变化。 - ---- - -### 三、体验层面的缺失功能 - -#### 7. 赛事 / 球队搜索 - -当前赛事浏览路径只有:联赛筛选 → 时间筛选 → 列表滚动。当赛事数量多时,玩家无法通过关键词搜索球队名或联赛名快速定位比赛。需要在赛事列表页或首页增加搜索框。 - -#### 8. 投注单清空确认 - -BetSlipDrawer 中清空投注单没有二次确认弹窗,容易误操作导致已选择的投注项丢失。 - -#### 9. 投注单赔率实时更新提示 - -PRD 7.3 节要求"赔率已变化,拒绝下注,提示重新确认"。当前提交时校验赔率版本会报错,但在投注单停留期间没有实时检测赔率变化并主动标记(如高亮变化项、显示新旧赔率对比),玩家只有在点提交时才知道赔率变了。 - -#### 10. 盘口封盘倒计时 - -赛事列表和详情页没有显示距离封盘的倒计时。玩家不知道赛事何时关闭投注,影响下注决策。 - -#### 11. 快捷金额按钮自定义 - -PRD 4.3 节桌面端布局提到"快捷投注金额"。当前 BetSlipDrawer 中有固定的快捷金额按钮(Min/5、+50、+100、Max),但不能由后台配置或让玩家自定义。 - -#### 12. 我的投注 - 按赛事/时间筛选 - -MyBetsView 目前支持按状态筛选(全部/赢/输/待结算/走水),缺少按赛事、按时间范围筛选的能力,注单多了之后查找困难。 - -#### 13. 注单详情页 - 缺少分享/截图功能 - -玩家无法分享或保存注单截图。在社交平台传播场景中,注单截图是常见的拉新素材。 - ---- - -### 四、安全与合规相关缺失 - -#### 14. 负责任博彩 / 自我限额 - -PRD 18.1 节提到玩家每日投注上限和派彩上限由后台配置,但前台没有向玩家展示当前限额使用情况(如"今日已投注 X / 上限 Y"),也没有自我设置存款限额、投注限额、冷静期等负责任博彩工具。对于面向合法市场的平台,这通常是合规要求。 - -#### 15. 登录安全 - 缺少二次验证 - -玩家端登录只有密码 + 人机验证(RobotVerify),没有可选的短信验证码二次验证或 TOTP,安全性偏弱。 - -#### 16. 会话管理 - -玩家无法查看当前活跃会话(哪些设备在登录),也无法远程踢出其他设备的会话。 - ---- - -### 五、运营与增长相关缺失 - -#### 17. 邀请好友入口 - -后端已有完整的邀请码系统(生成邀请码、设置返水比例、邀请列表、撤销),但玩家前台没有"邀请好友"入口页面,无法查看自己的邀请码、邀请链接、邀请记录和奖励。 - -#### 18. 活动 / 优惠专区 - -没有活动页面或优惠专区。运营无法通过前台展示限时活动、新人奖励、充值优惠等信息。目前只能依赖 Banner 和公告,缺乏独立的活动落地页。 - -#### 19. VIP / 等级体系 - -没有玩家等级体系。成熟平台通常根据投注量、充值额等划分 VIP 等级,不同等级享受不同返水比例、专属客服、提款加速等权益。 - ---- - -### 六、数据辅助功能缺失 - -#### 20. 赛事数据 / 历史战绩 - -赛事详情页只展示盘口和赔率,没有历史交锋记录、球队近期战绩、联赛积分榜等辅助数据。这些数据可以帮助玩家做投注决策,也是提升用户粘性的手段。 - -#### 21. 投注统计 / 个人报表 - -虽然有 BetStatsPanel 显示注单数/赢/输/待结算的基础统计,但缺少更丰富的个人投注报表:按时间段统计、按玩法统计、盈亏趋势图、返水收入统计等。 - ---- - -### 七、其他体验优化建议 - -| 编号 | 功能 | 说明 | -|---|---|---| -| 22 | 赛事收藏 / 关注 | 玩家可以收藏感兴趣的赛事,快速筛选查看 | -| 23 | 多赔率格式切换 | PRD 二期提到支持 Decimal/Malay/HK/Indo/American 格式,当前只有十进制 | -| 24 | PWA / 添加到主屏幕 | 移动端 H5 支持 PWA 安装提示,提升留存 | -| 25 | 网络状态提示 | 弱网或断网时显示明确提示,避免下注失败无感知 | -| 26 | 暗色/亮色主题切换 | 当前只有深色主题,没有亮色模式 | -| 27 | 系统公告弹窗 | 维护、重大赛事等场景的全屏弹窗通知,目前只有走马灯 | -| 28 | 盘口排序偏好 | 允许玩家调整盘口分组默认排序(如把让球放在独赢前面) | - ---- - -### 优先级建议 - -**P0 - 必须补齐(影响业务闭环):** -- 提款功能(#4) -- 邀请好友入口(#17)— 后端已就绪,只差前端页面 -- 注单结算通知(#5) - -**P1 - 高优先级(影响核心体验):** -- 今日赛事板块(#1)— 后端数据已有,前端未消费 -- 赛事搜索(#7) -- 投注规则独立页面(#3) -- 公告详情页(#2) -- 负责任博彩 / 限额展示(#14) - -**P2 - 中优先级(体验优化):** -- 盘口封盘倒计时(#10) -- 赔率变化实时标记(#9) -- 注单筛选增强(#12) -- 投注单清空确认(#8) -- 充值/提款状态通知(#6) - -**P3 - 低优先级(长期迭代):** -- VIP 等级体系(#19) -- 活动专区(#18) -- 赛事数据/历史战绩(#20) -- 多赔率格式(#23) -- PWA(#24) diff --git a/docs/短信调试与日志说明.md b/docs/短信调试与日志说明.md index 9218164..3e5327c 100644 --- a/docs/短信调试与日志说明.md +++ b/docs/短信调试与日志说明.md @@ -2,8 +2,7 @@ 本文档说明 thebet365 **玩家端短信验证码**(注册 / 找回密码)在后端的日志行为,以及如何与创蓝控制台对账、排查「收不到码」问题。 -相关代码:`apps/api/src/domains/identity/sms/` -创蓝接入总览见 [chuanglan-sms-js-guide.md](./chuanglan-sms-js-guide.md)。 +相关代码:`apps/api/src/domains/identity/sms/`(`SmsService`、`ChuanglanClient`) --- diff --git a/scripts/deploy-lib.sh b/scripts/deploy-lib.sh index 44d238a..6bcb41e 100755 --- a/scripts/deploy-lib.sh +++ b/scripts/deploy-lib.sh @@ -156,9 +156,11 @@ validate_prod_env() { [ -n "$postgres_password" ] || die ".env.docker 缺少 POSTGRES_PASSWORD" [ -n "$jwt_secret" ] || die ".env.docker 缺少 JWT_SECRET" - if [ "$allow_defaults" != "true" ]; then - [ "$postgres_password" != "thebet365" ] || die "POSTGRES_PASSWORD 仍是示例值;如确为测试环境,请加 --allow-default-secrets" - [ "$jwt_secret" != "change-me-in-production-use-long-random-string" ] || die "JWT_SECRET 仍是示例值;如确为测试环境,请加 --allow-default-secrets" + if [ "$postgres_password" = "thebet365" ]; then + warn "POSTGRES_PASSWORD 仍是示例值 thebet365,生产环境建议尽快修改" + fi + if [ "$jwt_secret" = "change-me-in-production-use-long-random-string" ]; then + warn "JWT_SECRET 仍是示例值,生产环境建议尽快修改" fi if [ "$seed_database" = "true" ]; then