# Doodle Mini — 小程序云开发方案

> 版本：v1.1
> 最后更新：2026-03-27
> **适用范围**：与 [技术架构设计文档](./技术架构设计文档.md) 配套；**当前阶段**采用本文档所述微信云开发环境实现后端能力。
> **阶段截止**：与总架构文档一致，**有效期至 2026 年 9 月 15 日**（到期前需复盘用量与成本，并决定是否迁移至 [后端部署方案](./后端部署方案.md) 所述自建服务）。

---

## 一、方案定位

微信云开发提供免运维的 Serverless 后端能力（云数据库、云存储、云函数），与小程序 `wx.cloud` 原生集成，**接入与鉴权成本低**，适合在阶段内快速落地题型配置下发、用户数据与素材存储。

**与总架构文档的关系**：

- [技术架构设计文档](./技术架构设计文档.md)：描述前端分层、模板引擎、系统总览及**后端分阶段策略**（现阶段云开发 + 到期后可选 NestJS）。
- **本文档**：仅展开**云开发侧**的库表、云函数、存储与调用策略等技术细节。

**到期后选项（不在本文档展开）**：若免费额度不足或需更强多端能力，可迁移至 [后端部署方案](./后端部署方案.md)（NestJS + 自有服务器）；迁移时需做数据导出与 `cloud-adapter` → `http-adapter` 的适配，详见技术架构文档中的「可迁移」原则。

---

## 二、与自建后端对比（决策参考）

| 维度        | 微信云开发                    | 自建云服务器（NestJS） | 现阶段采用           |
| ----------- | ----------------------------- | ---------------------- | -------------------- |
| 接入成本    | ⭐ 极低（原生集成）           | ⭐⭐ 需搭建部署        | **云开发**           |
| 运维成本    | ⭐ 免运维                     | ⭐⭐ 需简单维护        | **云开发**           |
| 费用        | 免费额度内 ¥0；超出按套餐计费 | 已有服务器可零增量     | **云开发（阶段内）** |
| 小程序集成  | ⭐ 鉴权与 OpenID 一体化       | 需自建登录与 JWT       | **云开发**           |
| PC Web 支持 | 需 HTTP 云函数或桥接          | RESTful 天然支持       | 远期看 NestJS        |
| 灵活性      | 受云开发 SDK 与配额约束       | 完全自主               | 远期看 NestJS        |
| 数据迁移    | 可导出，迁移需工作量          | 标准 MySQL，自主备份   | 远期看 NestJS        |

---

## 三、云数据库集合设计

本节与 [后端部署方案](./后端部署方案.md) 中的 **Prisma Schema（MySQL）** 字段语义对齐，便于阶段结束后迁移；云数据库为 **MongoDB 兼容模型**（文档 = BSON），下列类型按 MongoDB 惯例书写。

### 3.1 设计约定

| 约定                 | 说明                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `_id`                | 每条文档主键。可由云开发自动生成，也可在写入时指定为字符串（如业务 ID、cuid）。**不要求**与微信 `openid` 相同。                                                          |
| `Date`               | 对应 BSON `Date`，云函数/控制台写入时使用 `serverDate()` 或 `new Date()`。                                                                                               |
| `openid` / `unionid` | 显式业务字段；与下节「用户标识」一致。                                                                                                                                   |
| `_openid`            | 仅当文档由**小程序端直连数据库**写入且开启用户校验时，云开发可自动注入当前用户 `openid`；若数据经**云函数**写入，通常自行维护 `openid`/`userId` 字段，不依赖 `_openid`。 |
| 命名                 | 集合名与 Prisma `@@map` 一致：`worksheets`、`categories`、`users`、`favorites`、`download_logs`、`feedback`、`learning_plans`。                                          |

### 3.2 用户标识：`openid`、`unionid` 与 `_id`

**用户的 `_id` 只能是 `openid` 吗？——不是。**

- 云数据库文档的 `_id` 默认为云开发生成的唯一 ID；也可以在 `add` 时**自定义**为字符串（例如与 [后端部署方案](./后端部署方案.md) 一致的 `cuid()`，或直接等于 `openid`）。
- **推荐**：`users` 集合使用 **独立 `openid` 字段（唯一）** + **`unionid` 字段（可选、唯一）**，`_id` 可用自动生成或自定义；与 Prisma 中 `User.id`（cuid）+ `openid` / `unionid` 的建模方式一致，迁移时映射清晰。
- **`unionid`**：同一微信开放平台下多应用用户唯一标识；需在小程序后台绑定开放平台，且用户已授权；未绑定时可能为空，字段应为可选并建**稀疏唯一索引**（仅非空值唯一）。

**`users` 集合完整字段与索引**：见 **§3.5**（与 Prisma `User` 一一对应）。

子表（收藏、下载日志、反馈）中的 `userId` 建议与 `users._id` 保持一致（若 `_id` 采用 cuid，则存 cuid；若 `_id` 即 `openid`，则存 `openid`），与 Prisma 外键语义一致。

**`unionid` 的写入**：在云函数中可通过 `cloud.getWXContext()` 读取 `UNIONID`（用户已绑定开放平台且当前会话可返回时才有值）；首次登录 upsert `users` 时写入或更新 `unionid` 字段。若仅能在服务端用 `code` 换 `session`，则使用微信 `code2Session` 返回的 `unionid` 字段（同样可能为空）。**不要**在小程序端把 `unionid` 当可信主键直接展示给第三方，存储与鉴权仍以服务端为准。

---

### 3.3 集合：`worksheets`（题型配置）

| 字段               | BSON 类型       | 必填 | 说明                                                            |
| ------------------ | --------------- | ---- | --------------------------------------------------------------- |
| `_id`              | string          | 是   | 主键；建议与后端一致用 cuid 或可读业务 ID。                     |
| `title`            | string          | 是   | 标题，建议 ≤100 字符。                                          |
| `desc`             | string          | 是   | 描述，建议 ≤500 字符。                                          |
| `category`         | string          | 是   | 大类：`math` \| `chinese` \| `english` \| `puzzle` \| `craft`。 |
| `subcategory`      | string          | 是   | 子类，建议 ≤50 字符。                                           |
| `ageMin`           | int             | 是   | 适龄最小值（与 Prisma 一致；替代原 `ageRange` 数组）。          |
| `ageMax`           | int             | 是   | 适龄最大值。                                                    |
| `difficulty`       | int             | 是   | 难度 1–4。                                                      |
| `previewImage`     | string          | 是   | 预览图 URL/云存储 fileID，建议 ≤500 字符。                      |
| `tags`             | array\<string\> | 是   | 标签列表。                                                      |
| `isNew`            | bool            | 是   | 是否新品，默认 `false`。                                        |
| `isHot`            | bool            | 是   | 是否热门，默认 `false`。                                        |
| `sortOrder`        | int             | 是   | 排序权重，默认 `0`。                                            |
| `downloadCount`    | int             | 是   | 下载次数，默认 `0`。                                            |
| `status`           | string          | 是   | `active` \| `draft` \| `hidden`，默认 `active`。                |
| `template`         | string          | 是   | 渲染模板类型 `TemplateType`，建议 ≤30 字符。                    |
| `generator`        | string          | 是   | 生成器类型 `GeneratorType`，建议 ≤30 字符。                     |
| `generatorConfig`  | object          | 是   | 生成器参数（JSON 对象）。                                       |
| `layoutConfig`     | object          | 是   | 排版参数（JSON 对象）。                                         |
| `userConfigurable` | object \| null  | 否   | 用户可调整参数定义（JSON）。                                    |
| `legacyPage`       | string \| null  | 否   | 旧页面路径，迁移过渡用，建议 ≤200 字符。                        |
| `createdAt`        | date            | 是   | 创建时间。                                                      |
| `updatedAt`        | date            | 是   | 更新时间。                                                      |

**索引建议**

| 索引键                                     | 类型 | 说明             |
| ------------------------------------------ | ---- | ---------------- |
| `{ category: 1, status: 1, sortOrder: 1 }` | 复合 | 列表筛选与排序。 |
| `{ status: 1, sortOrder: 1 }`              | 复合 | 全量上架列表。   |

---

### 3.4 集合：`categories`（分类）

对应 Prisma `Category`。

| 字段        | BSON 类型      | 必填 | 说明                              |
| ----------- | -------------- | ---- | --------------------------------- |
| `_id`       | string         | 是   | 主键。                            |
| `name`      | string         | 是   | 名称，建议 ≤50 字符。             |
| `icon`      | string         | 是   | 图标 URL/fileID，建议 ≤200 字符。 |
| `color`     | string         | 是   | 色值，建议 ≤10 字符。             |
| `sortOrder` | int            | 是   | 排序，默认 `0`。                  |
| `parentId`  | string \| null | 否   | 父分类 ID。                       |

**索引建议**：`{ parentId: 1, sortOrder: 1 }`。

---

### 3.5 集合：`users`（用户）

对应 Prisma `User`（`@@map("users")`）。**`openid` / `unionid` 与 `_id` 的取舍、`unionid` 写入方式**见 §3.2。

| 字段             | BSON 类型      | 必填 | 说明                                                                 |
| ---------------- | -------------- | ---- | -------------------------------------------------------------------- |
| `_id`            | string         | 是   | 主键；可与 Prisma `User.id` 一样使用 cuid，也可自定义为其它字符串，团队内与用户档案查询方式统一即可。 |
| `openid`         | string         | 是   | 当前小程序下微信用户标识；与 Prisma 一致建议 ≤100 字符；**业务唯一**，索引见下表。 |
| `unionid`        | string \| null | 否   | 开放平台下跨应用用户标识；建议 ≤100 字符；未绑开放平台或未返回时为空。 |
| `nickName`       | string \| null | 否   | 用户昵称；建议 ≤50 字符。                                            |
| `avatarUrl`      | string \| null | 否   | 头像 URL 或云存储 fileID；建议 ≤500 字符。                           |
| `totalDownloads` | int            | 是   | 累计下载次数，默认 `0`。                                             |
| `createdAt`      | date           | 是   | 首次创建时间。                                                       |
| `lastActiveAt`   | date           | 是   | 最近一次活跃时间（登录、打开小程序、写库等策略由实现约定）。         |

**索引建议**

| 索引键    | 类型         | 说明                                                 |
| --------- | ------------ | ---------------------------------------------------- |
| `openid`  | 唯一         | 登录与 upsert 主查。                                 |
| `unionid` | 唯一（稀疏） | 仅当存在开放平台绑定且需跨端关联时使用；空值不互斥。 |

---

### 3.6 集合：`favorites`（收藏）

对应 Prisma `Favorite`。

| 字段          | BSON 类型 | 必填 | 说明                    |
| ------------- | --------- | ---- | ----------------------- |
| `_id`         | string    | 是   | 主键。                  |
| `userId`      | string    | 是   | 关联 `users._id`。      |
| `worksheetId` | string    | 是   | 关联 `worksheets._id`。 |
| `createdAt`   | date      | 是   | 收藏时间。              |

**索引建议**

| 索引键                          | 类型 | 说明                         |
| ------------------------------- | ---- | ---------------------------- |
| `{ userId: 1, worksheetId: 1 }` | 唯一 | 同一用户同一题型仅一条收藏。 |
| `{ userId: 1, createdAt: -1 }`  | 复合 | 「我的收藏」按时间倒序。     |

> 若小程序端直连写库且使用安全规则，可额外保留 `_openid` 与 `userId` 二选一做一致性校验；以云函数为主时以 `userId` + 云函数鉴权为准。

---

### 3.7 集合：`download_logs`（下载日志）

对应 Prisma `DownloadLog`。

| 字段          | BSON 类型      | 必填 | 说明                    |
| ------------- | -------------- | ---- | ----------------------- |
| `_id`         | string         | 是   | 主键。                  |
| `userId`      | string         | 是   | 关联 `users._id`。      |
| `worksheetId` | string         | 是   | 关联 `worksheets._id`。 |
| `params`      | object \| null | 否   | 生成参数快照（JSON）。  |
| `createdAt`   | date           | 是   | 下载时间。              |

**索引建议**：`{ userId: 1, createdAt: -1 }`；`{ worksheetId: 1, createdAt: -1 }`（统计/运营）。

---

### 3.8 集合：`feedback`（用户反馈）

对应 Prisma `Feedback`。

| 字段        | BSON 类型      | 必填 | 说明                       |
| ----------- | -------------- | ---- | -------------------------- |
| `_id`       | string         | 是   | 主键。                     |
| `userId`    | string         | 是   | 关联 `users._id`。         |
| `content`   | string         | 是   | 反馈正文。                 |
| `contact`   | string \| null | 否   | 联系方式，建议 ≤100 字符。 |
| `createdAt` | date           | 是   | 提交时间。                 |

**索引建议**：`{ userId: 1 }`。

---

### 3.9 集合：`learning_plans`（学习路线）

对应 Prisma `LearningPlan`（总架构中的适龄学习路线配置）。

| 字段         | BSON 类型       | 必填 | 说明                                                   |
| ------------ | --------------- | ---- | ------------------------------------------------------ |
| `_id`        | string          | 是   | 主键。                                                 |
| `ageMin`     | int             | 是   | 适龄下限。                                             |
| `ageMax`     | int             | 是   | 适龄上限。                                             |
| `ageLabel`   | string          | 是   | 展示用年龄段文案，建议 ≤20 字符。                      |
| `milestones` | array\<string\> | 是   | 能力目标描述（JSON 数组，与 Prisma `Json` 一致）。     |
| `weeks`      | array           | 是   | 周计划结构 `WeekPlan[]`（JSON 数组，与 Prisma 一致）。 |

**索引建议**：`{ ageMin: 1, ageMax: 1 }` **唯一**，与 Prisma `@@unique([ageMin, ageMax])` 一致。

---

### 3.10 ER 关系（逻辑）

```
users (1) ──< favorites >── (N) worksheets
users (1) ──< download_logs >── (N) worksheets
users (1) ──< feedback
categories ──(可选业务关联)── worksheets.category / subcategory（worksheet 内嵌字符串，非 DB 外键）
learning_plans：独立配置表，按 ageMin/ageMax 查询
```

---

## 四、云函数设计

```
cloudfunctions/
└── doodle/
    ├── index.js                    ← 统一入口（action 路由）
    ├── package.json
    ├── src/
    │   ├── user/
    │   │   ├── getOpenId.js        ← ✅ 已有，保留
    │   │   └── updateProfile.js    ← 🆕 更新用户信息
    │   ├── worksheet/
    │   │   ├── getList.js          ← 🆕 获取题型列表（支持分类/筛选）
    │   │   ├── getDetail.js        ← 🆕 获取题型详情
    │   │   └── incrementDownload.js← 🆕 下载计数+1
    │   ├── favorite/
    │   │   ├── add.js              ← 🆕 添加收藏
    │   │   ├── remove.js           ← 🆕 取消收藏
    │   │   └── list.js             ← 🆕 我的收藏列表
    │   ├── history/
    │   │   └── list.js             ← 🆕 下载历史
    │   ├── feedback/
    │   │   └── submit.js           ← 🆕 提交反馈
    │   └── pdf/
    │       └── generate.js         ← 🆕 PDF 生成（远期增值功能，会员专属）
    └── common/
        ├── responseMiddleware.js   ← ✅ 已有，保留
        └── auth.js                 ← 🆕 鉴权中间件
```

---

## 五、云函数调用与数据加载策略

```
┌────────────────────────────────────────────────────────────┐
│                    数据加载策略                              │
│                                                            │
│  题型配置数据 (worksheets/categories)                       │
│  ┌────────────────────────────────────────┐                │
│  │  优先级 1: 本地缓存（wx.Storage）        │                │
│  │  优先级 2: 云数据库查询                  │                │
│  │  优先级 3: 前端内置兜底数据              │  ← 保证离线可用 │
│  └────────────────────────────────────────┘                │
│                                                            │
│  缓存策略：                                                │
│  • 首次启动：云端拉取 → 写入本地缓存                        │
│  • 后续启动：先用缓存渲染 → 后台静默更新                    │
│  • 缓存有效期：24 小时                                      │
│  • 无网络：使用本地缓存或内置兜底                            │
│                                                            │
│  用户数据 (favorites/history)                               │
│  ┌────────────────────────────────────────┐                │
│  │  本地优先写入 → 后台同步云端              │                │
│  │  冲突策略：以云端为准（云端时间戳更新）    │                │
│  └────────────────────────────────────────┘                │
│                                                            │
│  统计数据 (download count)                                  │
│  ┌────────────────────────────────────────┐                │
│  │  批量上报：本地累计 → 退出时/定时上报     │                │
│  │  非关键路径，允许丢失                    │                │
│  └────────────────────────────────────────┘                │
└────────────────────────────────────────────────────────────┘
```

---

## 六、云存储规划

```
云存储目录结构：
cloud://doodle-xxx.xxxx/
├── 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
```

---

## 七、费用与额度（阶段内监控）

```
免费额度（基础版 1）：
┌────────────────────────────────────────┐
│  云数据库：2 GB 存储 / 50 万次读写/天   │
│  云存储：5 GB / 2 GB 下载/天            │
│  云函数：10 万次调用/月 / 1000 GBs/月   │
│  CDN：5 GB/月                          │
└────────────────────────────────────────┘

预估用量（DAU 1000）：
┌────────────────────────────────────────┐
│  云数据库：~100 MB（足够）              │
│  云存储：~2 GB（素材渐增）              │
│  云函数：~3 万次/月                     │
│  CDN：~3 GB/月                         │
│                                        │
│  结论：免费额度可覆盖到 DAU 3000 左右    │
│  超出后需升级套餐（如 19.9 元/月起）      │
└────────────────────────────────────────┘
```

**建议在 2026-09-15 前**：结合控制台用量与业务目标，决定续用云开发、升级套餐或启动迁移至 [后端部署方案](./后端部署方案.md)。
