# 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 <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 统一响应格式

```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<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/` 路径映射到此目录：

```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 <token>                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

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 <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 部署（可选）

```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 缓存热点数据 + 数据库索引优化 + 按需水平扩展                              |
