Files
doodle-mini/docs/后端部署方案.md
2026-03-27 17:31:52 +08:00

35 KiB
Raw Permalink Blame History

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 APIworksheets / 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:¥0Let's Encrypt 免费证书)│                  │
│  │  MySQL:¥0(服务器上安装,资源占用很小) │                  │
│  │  总计新增成本:¥0                       │                  │
│  │                                        │                  │
│  │  唯一成本是初始开发时间(约 3-5 天搭建) │                  │
│  └────────────────────────────────────────┘                  │
│                                                              │
│  结论:自建方案利用现有资源,长期零增量成本 ✅                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

十二、技术风险与应对

风险 影响 应对方案
服务器宕机 后端不可用 PM2 自动重启 + 前端兜底数据保证基本可用
数据库故障 数据丢失 定期备份(mysqldump cron)+ 前端本地缓存保证体验不中断
微信域名校验 请求被拦截 提前在公众平台配置合法域名,开发阶段可勾选「不校验」
服务器带宽不足 素材加载慢 图片压缩 + Nginx Gzip + 合理设置 Cache-Control + 远期可接入免费 CDN(如 Cloudflare
JWT 安全性 token 泄露 设置合理过期时间 + refresh token 机制 + HTTPS 传输
并发压力(远期) 响应变慢 引入 Redis 缓存热点数据 + 数据库索引优化 + 按需水平扩展