# Doodle Mini — 后端部署方案(NestJS + 自有云服务器) > 版本:v1.0 > 最后更新:2026-03-27 > **定位**:**远期 / 阶段到期后的迁移方案**(自有服务器 + NestJS)。 > **现阶段**:小程序后端以 [小程序云开发方案](./小程序云开发方案.md) 为准,详见 [技术架构设计文档](./技术架构设计文档.md) 中的阶段说明(当前阶段有效期至 **2026-09-15**)。 > 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md) | [PC-Web端技术预案](./PC-Web端技术预案.md) --- ## 一、方案决策 ### 1.1 为什么选择自建后端(何时采用) | 考量 | 说明 | | -------------- | --------------------------------------------------------------------------------- | | **零增量成本** | 已有云服务器,不新增费用;微信云开发免费额度有限,超出需持续付费 | | **灵活性强** | NestJS 是完整的 Node.js 框架,不受云开发 SDK 限制,可自由选择数据库、缓存、队列等 | | **多端友好** | RESTful API 天然支持小程序、PC Web、App 等多端接入,无需桥接层 | | **数据自主** | 数据存储在自有服务器,不锁定平台,迁移自由 | | **技术栈统一** | 前后端均使用 TypeScript,共享类型定义和数据模型,开发效率高 | ### 1.2 技术选型 | 层级 | 技术 | 说明 | | -------- | ----------------------- | ------------------------------------------------------------ | | 运行时 | Node.js 20 LTS | 长期支持版本,稳定可靠 | | 框架 | NestJS 10+ | 企业级 Node.js 框架,模块化架构,内置 DI/AOP/中间件等 | | 数据库 | MySQL 8.0 | 成熟稳定的关系型数据库,适合结构化的题型/用户数据 | | ORM | Prisma | 类型安全的 ORM,自动生成 TS 类型,迁移管理方便 | | 缓存 | Redis(可选) | 热门题型列表、配置数据缓存,降低数据库压力;初期可不引入 | | 文件存储 | 本地磁盘 + Nginx 静态 | 素材文件存服务器本地,Nginx 直接提供静态文件服务,零成本 CDN | | 进程管理 | PM2 | 守护进程、自动重启、日志管理 | | 反向代理 | Nginx | HTTPS 终止、静态文件、反向代理、Gzip 压缩 | | 容器化 | Docker + docker-compose | 可选,方便环境一致性和部署自动化 | --- ## 二、系统架构 ``` ┌───────────────────────────────────────────────────────────────┐ │ 客户端 │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 微信小程序 │ │ PC Web (远期) │ │ │ │ wx.request() │ │ axios/fetch │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ └───────────┼───────────────────────────┼───────────────────────┘ │ HTTPS │ ▼ ▼ ┌───────────────────────────────────────────────────────────────┐ │ 云服务器 │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Nginx │ │ │ │ • HTTPS 终止(Let's Encrypt 证书) │ │ │ │ • /api/* → 反向代理到 NestJS (localhost:3000) │ │ │ │ • /static/* → 直接提供静态素材文件 │ │ │ │ • Gzip 压缩、请求限流 │ │ │ └───────────┬────────────────────────┬────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌────────────────────┐ ┌──────────────────────┐ │ │ │ NestJS 应用 │ │ 静态文件目录 │ │ │ │ (PM2 守护) │ │ │ │ │ │ │ │ /data/doodle/ │ │ │ │ ┌──────────┐ │ │ ├── previews/ │ │ │ │ │ Auth │ │ │ ├── coloring/ │ │ │ │ │ Module │ │ │ ├── origami/ │ │ │ │ ├──────────┤ │ │ ├── fonts/ │ │ │ │ │ Worksheet│ │ │ └── share/ │ │ │ │ │ Module │ │ └──────────────────────┘ │ │ │ ├──────────┤ │ │ │ │ │ User │ │ │ │ │ │ Module │ │ │ │ │ ├──────────┤ │ │ │ │ │ Favorite │ │ │ │ │ │ Module │ │ │ │ │ ├──────────┤ │ │ │ │ │ Stats │ │ │ │ │ │ Module │ │ │ │ │ ├──────────┤ │ │ │ │ │ Upload │ │ │ │ │ │ Module │ │ │ │ │ └──────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────┐ │ │ │ │ │ MySQL │ │ │ │ │ │ (Prisma) │ │ │ │ │ └──────────┘ │ │ │ └────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────┘ ``` --- ## 三、项目结构 ``` doodle-server/ ├── src/ │ ├── main.ts ← 应用入口 │ ├── app.module.ts ← 根模块 │ │ │ ├── common/ ← 通用模块 │ │ ├── guards/ │ │ │ └── wx-auth.guard.ts ← 微信登录鉴权守卫 │ │ ├── interceptors/ │ │ │ └── response.interceptor.ts ← 统一响应格式 │ │ ├── filters/ │ │ │ └── http-exception.filter.ts← 统一异常处理 │ │ ├── decorators/ │ │ │ └── current-user.decorator.ts ← 获取当前用户 │ │ └── dto/ │ │ └── pagination.dto.ts ← 分页参数 │ │ │ ├── auth/ ← 鉴权模块 │ │ ├── auth.module.ts │ │ ├── auth.controller.ts ← POST /auth/wx-login │ │ └── auth.service.ts ← 微信 code2session + JWT 签发 │ │ │ ├── worksheet/ ← 题型配置模块 │ │ ├── worksheet.module.ts │ │ ├── worksheet.controller.ts ← GET /worksheets, GET /worksheets/:id │ │ ├── worksheet.service.ts │ │ └── dto/ │ │ ├── query-worksheet.dto.ts ← 查询筛选参数 │ │ └── worksheet-response.dto.ts │ │ │ ├── category/ ← 分类模块 │ │ ├── category.module.ts │ │ ├── category.controller.ts ← GET /categories │ │ └── category.service.ts │ │ │ ├── user/ ← 用户模块 │ │ ├── user.module.ts │ │ ├── user.controller.ts ← GET /user/profile, PATCH /user/profile │ │ └── user.service.ts │ │ │ ├── favorite/ ← 收藏模块 │ │ ├── favorite.module.ts │ │ ├── favorite.controller.ts ← GET/POST/DELETE /favorites │ │ └── favorite.service.ts │ │ │ ├── history/ ← 下载历史模块 │ │ ├── history.module.ts │ │ ├── history.controller.ts ← GET/POST /history │ │ └── history.service.ts │ │ │ ├── stats/ ← 统计模块 │ │ ├── stats.module.ts │ │ ├── stats.controller.ts ← POST /stats/download, GET /stats/popular │ │ └── stats.service.ts │ │ │ ├── feedback/ ← 反馈模块 │ │ ├── feedback.module.ts │ │ ├── feedback.controller.ts ← POST /feedback │ │ └── feedback.service.ts │ │ │ └── upload/ ← 素材上传模块(管理后台用) │ ├── upload.module.ts │ ├── upload.controller.ts ← POST /upload/asset │ └── upload.service.ts │ ├── prisma/ │ ├── schema.prisma ← 数据库 Schema │ └── seed.ts ← 初始数据填充脚本 │ ├── nginx/ │ └── doodle-api.conf ← Nginx 配置模板 │ ├── .env.example ← 环境变量模板 ├── docker-compose.yml ← 可选容器化部署 ├── ecosystem.config.js ← PM2 配置 ├── package.json └── tsconfig.json ``` --- ## 四、数据库设计(Prisma Schema) ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } // ─── 题型配置表 ─── model Worksheet { id String @id @default(cuid()) title String @db.VarChar(100) desc String @db.VarChar(500) category String @db.VarChar(20) // math | chinese | english | puzzle | craft subcategory String @db.VarChar(50) ageMin Int @db.SmallInt // 适龄最小值 ageMax Int @db.SmallInt // 适龄最大值 difficulty Int @db.SmallInt // 1-4 previewImage String @db.VarChar(500) tags Json // string[] isNew Boolean @default(false) isHot Boolean @default(false) sortOrder Int @default(0) downloadCount Int @default(0) status String @default("active") @db.VarChar(10) // active | draft | hidden // 模板引擎字段 template String @db.VarChar(30) // TemplateType generator String @db.VarChar(30) // GeneratorType generatorConfig Json // 生成器参数 layoutConfig Json // 排版参数 userConfigurable Json? // 用户可调整参数定义 legacyPage String? @db.VarChar(200) // 旧页面路径(迁移过渡) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt favorites Favorite[] downloadLogs DownloadLog[] @@index([category, status, sortOrder]) @@index([status, sortOrder]) @@map("worksheets") } // ─── 分类表 ─── model Category { id String @id @default(cuid()) name String @db.VarChar(50) icon String @db.VarChar(200) color String @db.VarChar(10) sortOrder Int @default(0) parentId String? @db.VarChar(30) @@index([parentId, sortOrder]) @@map("categories") } // ─── 用户表 ─── model User { id String @id @default(cuid()) openid String @unique @db.VarChar(100) unionid String? @unique @db.VarChar(100) nickName String? @db.VarChar(50) avatarUrl String? @db.VarChar(500) totalDownloads Int @default(0) createdAt DateTime @default(now()) lastActiveAt DateTime @default(now()) favorites Favorite[] downloadLogs DownloadLog[] feedbacks Feedback[] @@map("users") } // ─── 收藏表 ─── model Favorite { id String @id @default(cuid()) userId String worksheetId String createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) worksheet Worksheet @relation(fields: [worksheetId], references: [id]) @@unique([userId, worksheetId]) @@index([userId, createdAt]) @@map("favorites") } // ─── 下载日志表 ─── model DownloadLog { id String @id @default(cuid()) userId String worksheetId String params Json? // 生成参数快照 createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) worksheet Worksheet @relation(fields: [worksheetId], references: [id]) @@index([userId, createdAt]) @@index([worksheetId, createdAt]) @@map("download_logs") } // ─── 反馈表 ─── model Feedback { id String @id @default(cuid()) userId String content String @db.Text contact String? @db.VarChar(100) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@map("feedback") } // ─── 学习路线表 ─── model LearningPlan { id String @id @default(cuid()) ageMin Int @db.SmallInt ageMax Int @db.SmallInt ageLabel String @db.VarChar(20) milestones Json // string[],能力目标描述 weeks Json // WeekPlan[],4 周学习路线 @@unique([ageMin, ageMax]) @@map("learning_plans") } ``` --- ## 五、API 接口设计 ### 5.1 鉴权 微信小程序通过 `wx.login()` 获取临时 `code`,发送到后端换取 `openid`,后端签发 JWT。 ``` POST /api/auth/wx-login Body: { code: string } Response: { token: string, user: UserInfo } 流程: 小程序 wx.login() → code → POST /api/auth/wx-login { code } → 后端调用微信 code2Session API 获取 openid → 查找或创建用户记录 → 签发 JWT (payload: { userId, openid }) → 返回 { token, user } 小程序将 token 存入 wx.Storage 后续请求 Header: Authorization: Bearer ``` ### 5.2 题型配置 ``` GET /api/worksheets Query: category, subcategory, ageMin, ageMax, difficulty, status, page, pageSize Response: { items: Worksheet[], total: number } GET /api/worksheets/:id Response: Worksheet GET /api/worksheets/popular Query: limit (default 10) Response: Worksheet[] ``` ### 5.3 分类 ``` GET /api/categories Response: Category[] ``` ### 5.4 用户 ``` GET /api/user/profile ← 需登录 Response: UserInfo PATCH /api/user/profile ← 需登录 Body: { nickName?, avatarUrl? } Response: UserInfo ``` ### 5.5 收藏 ``` GET /api/favorites ← 需登录 Query: page, pageSize Response: { items: FavoriteWithWorksheet[], total: number } POST /api/favorites ← 需登录 Body: { worksheetId: string } Response: { id: string } DELETE /api/favorites/:worksheetId ← 需登录 Response: { success: true } ``` ### 5.6 下载历史 / 统计 ``` GET /api/history ← 需登录 Query: page, pageSize Response: { items: DownloadLogWithWorksheet[], total: number } POST /api/stats/download Body: { worksheetId: string, params?: object } Response: { success: true } ``` ### 5.7 反馈 ``` POST /api/feedback ← 需登录 Body: { content: string, contact?: string } Response: { id: string } ``` ### 5.8 学习路线 ``` GET /api/learning-plans Response: LearningPlan[] GET /api/learning-plans/:ageRange ← 如 "5-6" Response: LearningPlan ``` ### 5.9 统一响应格式 ```typescript // 成功 { code: 0, data: { ... }, message: "success" } // 失败 { code: 40001, // 业务错误码 data: null, message: "具体错误信息" } // 分页数据 { code: 0, data: { items: [...], total: 128, page: 1, pageSize: 20 } } ``` --- ## 六、小程序端适配层 原方案中 `platform/cloud-adapter.ts` 需要从微信云开发调用改为标准 HTTP 请求。 ```typescript // platform/cloud-adapter.ts — 改为 HTTP API 调用 const BASE_URL = 'https://api.your-domain.com/api'; class HttpCloudAdapter implements ICloudAdapter { private token: string | null = null; async login(): Promise { const { code } = await wx.login(); const res = await this.request('POST', '/auth/wx-login', { code }); this.token = res.token; wx.setStorageSync('token', res.token); return res.user; } async request(method: string, path: string, data?: any): Promise { const token = this.token || wx.getStorageSync('token'); const res = await new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, method: method as any, data, header: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, success: (res) => resolve(res.data), fail: reject, }); }); if (res.code !== 0) { throw new Error(res.message || '请求失败'); } return res.data; } // ─── 业务方法(与云开发版保持同一接口)─── async getWorksheetList( query: WorksheetQuery, ): Promise> { return this.request('GET', '/worksheets', query); } async getWorksheetDetail(id: string): Promise { return this.request('GET', `/worksheets/${id}`); } async addFavorite(worksheetId: string): Promise { await this.request('POST', '/favorites', { worksheetId }); } async removeFavorite(worksheetId: string): Promise { await this.request('DELETE', `/favorites/${worksheetId}`); } async getFavorites( page: number, pageSize: number, ): Promise> { return this.request('GET', '/favorites', { page, pageSize }); } async reportDownload(worksheetId: string, params?: object): Promise { await this.request('POST', '/stats/download', { worksheetId, params }); } async submitFeedback(content: string, contact?: string): Promise { await this.request('POST', '/feedback', { content, contact }); } } ``` 数据加载策略与原方案一致(缓存优先 + 后台更新),只是数据源从云数据库变为 HTTP API: ``` ┌────────────────────────────────────────────────────────────┐ │ 数据加载策略 │ │ │ │ 题型配置数据 (worksheets/categories) │ │ ┌────────────────────────────────────────┐ │ │ │ 优先级 1: 本地缓存(wx.Storage) │ │ │ │ 优先级 2: HTTP API 查询 │ │ │ │ 优先级 3: 前端内置兜底数据 │ ← 保证离线可用 │ │ └────────────────────────────────────────┘ │ │ │ │ 缓存策略: │ │ • 首次启动:API 拉取 → 写入本地缓存 │ │ • 后续启动:先用缓存渲染 → 后台静默更新 │ │ • 缓存有效期:24 小时 │ │ • 无网络:使用本地缓存或内置兜底 │ └────────────────────────────────────────────────────────────┘ ``` --- ## 七、静态资源 / 素材存储 不再使用微信云存储,改为服务器本地磁盘 + Nginx 直接提供静态文件服务。 ``` 服务器文件目录: /data/doodle/ ├── assets/ │ ├── previews/ ← 题型效果预览图 │ │ ├── math/ │ │ ├── chinese/ │ │ ├── english/ │ │ ├── puzzle/ │ │ └── craft/ │ ├── coloring/ ← 涂色卡线稿(SVG/PNG) │ │ ├── animals/ │ │ ├── vehicles/ │ │ ├── holidays/ │ │ └── ... │ ├── origami/ ← 折纸展开图 │ ├── stickers/ ← 贴纸素材 │ ├── maze-templates/ ← 迷宫模板数据(JSON) │ └── craft-templates/ ← 手工模板 ├── fonts/ ← 字体文件 │ ├── SimHei.ttf │ └── handwriting.ttf └── share/ ← 分享图 └── default-share.png ``` Nginx 配置中将 `/static/` 路径映射到此目录: ```nginx server { listen 443 ssl http2; server_name api.your-domain.com; ssl_certificate /etc/letsencrypt/live/api.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.your-domain.com/privkey.pem; # 静态素材,长缓存 + Gzip location /static/ { alias /data/doodle/; expires 30d; add_header Cache-Control "public, immutable"; gzip on; gzip_types image/svg+xml application/json; } # API 反向代理 location /api/ { proxy_pass http://127.0.0.1:3000; 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; } # 请求限流(防刷) limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; location /api/ { limit_req zone=api burst=50 nodelay; # ... proxy_pass 同上 } } ``` 小程序中引用素材的 URL 从 `cloud://doodle-xxx/assets/...` 变为 `https://api.your-domain.com/static/assets/...`。 --- ## 八、微信登录对接 自建后端需要自行对接微信小程序登录,但整体流程非常简单: ``` ┌──────────────────────────────────────────────────────────────┐ │ 微信登录流程 │ │ │ │ 小程序 后端 │ │ ────── ──── │ │ wx.login() → code │ │ │ │ │ │ │ └──── POST /auth/wx-login { code } ────→ │ │ │ │ │ ├── 调用微信 API ──→ │ │ │ POST https://api.weixin. │ │ │ qq.com/sns/jscode2session │ │ │ { appid, secret, code, │ │ │ grant_type } │ │ │ │ │ │ ←── { openid, session_key, │ │ │ unionid? } │ │ │ │ │ ├── 查找/创建 User 记录 │ │ ├── 签发 JWT │ │ │ │ │ ←── { token, user } ─────┘ │ │ │ │ wx.setStorageSync('token', token) │ │ 后续请求携带 Authorization: Bearer │ │ │ └──────────────────────────────────────────────────────────────┘ ``` NestJS 实现要点: ```typescript // auth/auth.service.ts 核心逻辑 @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, private readonly httpService: HttpService, ) {} async wxLogin(code: string) { // 1. 调用微信 code2Session const wxRes = await this.httpService.axiosRef.get( 'https://api.weixin.qq.com/sns/jscode2session', { params: { appid: process.env.WX_APPID, secret: process.env.WX_SECRET, js_code: code, grant_type: 'authorization_code', }, }, ); const { openid, unionid } = wxRes.data; if (!openid) throw new UnauthorizedException('微信登录失败'); // 2. 查找或创建用户 const user = await this.prisma.user.upsert({ where: { openid }, update: { lastActiveAt: new Date() }, create: { openid, unionid }, }); // 3. 签发 JWT const token = this.jwtService.sign({ sub: user.id, openid: user.openid, }); return { token, user }; } } ``` 需要在微信公众平台配置**合法域名**:`api.your-domain.com`(在「开发管理 → 开发设置 → 服务器域名 → request 合法域名」中添加)。 --- ## 九、部署方案 ### 9.1 PM2 部署(推荐,简单直接) ```javascript // ecosystem.config.js module.exports = { apps: [ { name: 'doodle-api', script: 'dist/main.js', instances: 1, // 单实例即可,轻量应用 exec_mode: 'fork', env: { NODE_ENV: 'production', PORT: 3000, }, error_file: '/var/log/doodle/error.log', out_file: '/var/log/doodle/out.log', merge_logs: true, max_memory_restart: '300M', }, ], }; ``` 部署步骤: ```bash # 1. 克隆代码 git clone /opt/doodle-server cd /opt/doodle-server # 2. 安装依赖 npm ci --production # 3. 构建 npm run build # 4. 初始化数据库 npx prisma migrate deploy npx prisma db seed # 5. 启动 pm2 start ecosystem.config.js # 6. 设置开机自启 pm2 save pm2 startup ``` ### 9.2 Docker 部署(可选) ```yaml # docker-compose.yml version: '3.8' services: api: build: . ports: - '3000:3000' environment: - DATABASE_URL=mysql://doodle:password@mysql:3306/doodle - JWT_SECRET=${JWT_SECRET} - WX_APPID=${WX_APPID} - WX_SECRET=${WX_SECRET} depends_on: - mysql volumes: - ./data/assets:/data/doodle restart: unless-stopped mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=doodle - MYSQL_USER=doodle - MYSQL_PASSWORD=${MYSQL_PASSWORD} volumes: - mysql_data:/var/lib/mysql restart: unless-stopped volumes: mysql_data: ``` ### 9.3 环境变量 ```env # .env.example NODE_ENV=production PORT=3000 # 数据库 DATABASE_URL="mysql://doodle:your_password@localhost:3306/doodle" # JWT JWT_SECRET="your-jwt-secret-key" JWT_EXPIRES_IN="7d" # 微信小程序 WX_APPID="your-wx-appid" WX_SECRET="your-wx-secret" # 静态资源 STATIC_BASE_URL="https://api.your-domain.com/static" ASSET_DIR="/data/doodle" ``` --- ## 十、开发路线 ``` Phase 1(与前端 Phase 1-2 同步,第 1-3 周) ├── NestJS 项目初始化(脚手架 + 基础配置) ├── Prisma Schema 定义 + 数据库迁移 ├── 微信登录鉴权模块(wx-login + JWT) ├── 题型配置 CRUD API(worksheets / categories) ├── 数据初始化脚本(存量题型 JSON 导入) ├── Nginx 配置 + HTTPS 证书 └── PM2 部署上线 Phase 2(与前端 Phase 2-3 同步,第 4-6 周) ├── 收藏 / 下载历史 API ├── 统计模块(下载计数、热门排行) ├── 素材上传接口(管理后台用) ├── 学习路线数据 API └── 前端 cloud-adapter 适配层完成 Phase 3(与前端 Phase 4 同步,第 7-8 周) ├── 反馈模块 ├── 用户信息完善 ├── API 性能优化(缓存策略) └── 日志 / 监控完善 ``` --- ## 十一、成本对比 ``` ┌──────────────────────────────────────────────────────────────┐ │ 成本对比 │ │ │ │ 微信云开发方案: │ │ ┌────────────────────────────────────────┐ │ │ │ 免费额度内:¥0 │ │ │ │ 超出后(DAU 3000+):¥19.9-99 元/月 │ │ │ │ 随用量增长持续增加 │ │ │ └────────────────────────────────────────┘ │ │ │ │ 自有云服务器方案(NestJS): │ │ ┌────────────────────────────────────────┐ │ │ │ 服务器:¥0(已有,无增量成本) │ │ │ │ 域名 + SSL:¥0(Let's Encrypt 免费证书)│ │ │ │ MySQL:¥0(服务器上安装,资源占用很小) │ │ │ │ 总计新增成本:¥0 │ │ │ │ │ │ │ │ 唯一成本是初始开发时间(约 3-5 天搭建) │ │ │ └────────────────────────────────────────┘ │ │ │ │ 结论:自建方案利用现有资源,长期零增量成本 ✅ │ │ │ └──────────────────────────────────────────────────────────────┘ ``` --- ## 十二、技术风险与应对 | 风险 | 影响 | 应对方案 | | ---------------- | ---------- | ------------------------------------------------------------------------------------ | | 服务器宕机 | 后端不可用 | PM2 自动重启 + 前端兜底数据保证基本可用 | | 数据库故障 | 数据丢失 | 定期备份(mysqldump cron)+ 前端本地缓存保证体验不中断 | | 微信域名校验 | 请求被拦截 | 提前在公众平台配置合法域名,开发阶段可勾选「不校验」 | | 服务器带宽不足 | 素材加载慢 | 图片压缩 + Nginx Gzip + 合理设置 Cache-Control + 远期可接入免费 CDN(如 Cloudflare) | | JWT 安全性 | token 泄露 | 设置合理过期时间 + refresh token 机制 + HTTPS 传输 | | 并发压力(远期) | 响应变慢 | 引入 Redis 缓存热点数据 + 数据库索引优化 + 按需水平扩展 |