35 KiB
35 KiB
Doodle Mini — 后端部署方案(NestJS + 自有云服务器)
版本:v1.0 最后更新:2026-03-27 定位:远期 / 阶段到期后的迁移方案(自有服务器 + NestJS)。
现阶段:小程序后端以 小程序云开发方案 为准,详见 技术架构设计文档 中的阶段说明(当前阶段有效期至 2026-09-15)。 配套文档:技术架构设计文档 | 小程序云开发方案 | 产品设计文档 | PC-Web端技术预案
一、方案决策
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)
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 <token>
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 统一响应格式
// 成功
{
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 请求。
// 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<UserInfo> {
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<T>(method: string, path: string, data?: any): Promise<T> {
const token = this.token || wx.getStorageSync('token');
const res = await new Promise<any>((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<PaginatedResult<Worksheet>> {
return this.request('GET', '/worksheets', query);
}
async getWorksheetDetail(id: string): Promise<Worksheet> {
return this.request('GET', `/worksheets/${id}`);
}
async addFavorite(worksheetId: string): Promise<void> {
await this.request('POST', '/favorites', { worksheetId });
}
async removeFavorite(worksheetId: string): Promise<void> {
await this.request('DELETE', `/favorites/${worksheetId}`);
}
async getFavorites(
page: number,
pageSize: number,
): Promise<PaginatedResult<Favorite>> {
return this.request('GET', '/favorites', { page, pageSize });
}
async reportDownload(worksheetId: string, params?: object): Promise<void> {
await this.request('POST', '/stats/download', { worksheetId, params });
}
async submitFeedback(content: string, contact?: string): Promise<void> {
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/ 路径映射到此目录:
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 <token> │
│ │
└──────────────────────────────────────────────────────────────┘
NestJS 实现要点:
// 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 部署(推荐,简单直接)
// 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',
},
],
};
部署步骤:
# 1. 克隆代码
git clone <repo-url> /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 部署(可选)
# 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.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 缓存热点数据 + 数据库索引优化 + 按需水平扩展 |