Files
doodle-mini/docs/小程序云开发方案.md
T
2026-04-24 17:49:11 +08:00

352 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 SchemaMySQL** 字段语义对齐,便于阶段结束后迁移;云数据库为 **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 | 是 | 主键;唯一的可读业务 ID。 |
| `title` | string | 是 | 标题,建议 ≤8 字符。 |
| `subtitle` | string | 是 | 副标题,建议 ≤12 字符。 |
| `category` | string | 是 | 大类 对应categories 表中parentId=null 的分类 |
| `subcategory` | string | 是 | 子类,建议 ≤50 字符。 |
| `ageMin` | int | 是 | 适龄最小值 |
| `ageMax` | int | 是 | 适龄最大值 |
| `grade` | int | 是 | 年级(-4=托班,-3=小班 ... 0=幼小衔接,1=一年级.. |
| `difficulty` | int | 是 | 难度 1-4(1=入门,2=基础,3=进阶,4=挑战) |
| `previewImg` | string | 是 | 预览图 URL/云存储 fileID |
| `tags` | array\<string\> | 是 | 标签列表。 |
| `isNew` | bool | 是 | 是否新品,默认 `false`。 |
| `isHot` | bool | 是 | 是否热门,默认 `false`。 |
| `sortOrder` | int | 是 | 排序权重,默认 `0`。 |
| `downloads` | int | 是 | 下载次数,默认 `0`。 |
| `likes` | int | 是 | 收藏此时,默认 `0`。 |
| `status` | string | 是 | `active` \| `draft` \| `hidden`,默认 `draft`。 |
| `createdAt` | date | 是 | 创建时间。 |
| `updatedAt` | date | 是 | 更新时间。 |
后续可能需要使用的字段,现在先不用
| `template` | string | 是 | 渲染模板类型 `TemplateType`,建议 ≤30 字符。 |
| `generator` | string | 是 | 生成器类型 `GeneratorType`,建议 ≤30 字符。 |
| `generatorConfig` | object | 是 | 生成器参数(JSON 对象)。 |
| `layoutConfig` | object | 是 | 排版参数(JSON 对象)。 |
| `userConfigurable` | object \| null | 否 | 用户可调整参数定义(JSON)。 |
| `legacyPage` | string \| null | 否 | 旧页面路径,迁移过渡用,建议 ≤200 字符。 |
**索引建议**
| 索引键 | 类型 | 说明 |
| ------------------------------------------ | ---- | ---------------- |
| `{ 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 / subcategoryworksheet 内嵌字符串,非 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/月 │
│ CDN5 GB/月 │
└────────────────────────────────────────┘
预估用量(DAU 1000):
┌────────────────────────────────────────┐
│ 云数据库:~100 MB(足够) │
│ 云存储:~2 GB(素材渐增) │
│ 云函数:~3 万次/月 │
│ CDN~3 GB/月 │
│ │
│ 结论:免费额度可覆盖到 DAU 3000 左右 │
│ 超出后需升级套餐(如 19.9 元/月起) │
└────────────────────────────────────────┘
```
**建议在 2026-09-15 前**:结合控制台用量与业务目标,决定续用云开发、升级套餐或启动迁移至 [后端部署方案](./后端部署方案.md)。