feat:更新core 和首页代码

This commit is contained in:
R524809
2026-03-27 17:31:52 +08:00
parent dc5e057cb3
commit 1b45ba7d11
44 changed files with 2891 additions and 1087 deletions
+168 -33
View File
@@ -21,7 +21,7 @@
- 小程序 DAU 稳定 > 1000
- 用户反馈中"想在电脑上用"的需求频繁出现
- 内容积累到 100+ 种题型
- 模板引擎已稳定运行,core/ 层与 wx.* 完全解耦
- 模板引擎已稳定运行,core/ 层与 wx.\* 完全解耦
---
@@ -56,12 +56,12 @@ PC Web 技术栈:
PC Web 需要一个 API 层来访问小程序云开发的数据。三种方案对比:
| 方案 | 说明 | 成本 | 推荐度 |
|------|------|------|--------|
| **A: 云开发 HTTP API** | 微信云开发提供的 HTTP 触发器,Web 直接调用 | 零额外成本 | ⭐⭐⭐⭐⭐ |
| **B: Cloudflare Workers** | 轻量 Serverless 代理层,转发请求到云开发 | 极低(免费额度充裕) | ⭐⭐⭐⭐ |
| **C: Vercel Serverless** | Next.js API Routes 做代理 | 低(Vercel 免费额度) | ⭐⭐⭐ |
| **D: 独立 Node.js 服务** | 自建 Express/Fastify 服务,独立数据库 | 较高(¥50-100/月+) | ⭐⭐ |
| 方案 | 说明 | 成本 | 推荐度 |
| ------------------------- | ------------------------------------------ | --------------------- | ---------- |
| **A: 云开发 HTTP API** | 微信云开发提供的 HTTP 触发器,Web 直接调用 | 零额外成本 | ⭐⭐⭐⭐⭐ |
| **B: Cloudflare Workers** | 轻量 Serverless 代理层,转发请求到云开发 | 极低(免费额度充裕) | ⭐⭐⭐⭐ |
| **C: Vercel Serverless** | Next.js API Routes 做代理 | 低(Vercel 免费额度) | ⭐⭐⭐ |
| **D: 独立 Node.js 服务** | 自建 Express/Fastify 服务,独立数据库 | 较高(¥50-100/月+ | ⭐⭐ |
**推荐路径**:先用方案 A(零成本),若遇到限制再升级到方案 B。
@@ -104,44 +104,179 @@ doodle/
当前不需要搭建 Monorepo,只需在小程序项目中做好分层即可:
1.`core/` 作为独立目录组织代码
2. 确保 `core/` 内**不 import 任何 wx.* API**
2. 确保 `core/` 内**不 import 任何 wx.\* API**
3. 模板引擎、生成器、数据模型全部放在 `core/`
4. 未来迁移时,将 `core/` 提取为独立 npm 包即可
### 4.3 平台适配层映射
| 适配器 | 小程序实现 | Web 实现 |
|--------|-----------|---------|
| CanvasAdapter | `wx.createSelectorQuery` + Canvas 2D | `document.getElementById` + Canvas 2D |
| ImageAdapter | `canvas.createImage()` | `new Image()` |
| StorageAdapter | `wx.setStorageSync` / `wx.getStorageSync` | `localStorage` |
| CloudAdapter | `wx.cloud.callFunction` | HTTP fetch 到云开发 API |
| ShareAdapter | `onShareAppMessage` / `onShareTimeline` | Web Share API / 复制链接 |
| ExportAdapter | `canvasToTempFilePath``saveImageToPhotosAlbum` | `canvas.toBlob()` → 下载 / jsPDF |
| 适配器 | 小程序实现 | Web 实现 |
| -------------- | ------------------------------------------------- | ------------------------------------- |
| CanvasAdapter | `wx.createSelectorQuery` + Canvas 2D | `document.getElementById` + Canvas 2D |
| ImageAdapter | `canvas.createImage()` | `new Image()` |
| StorageAdapter | `wx.setStorageSync` / `wx.getStorageSync` | `localStorage` |
| CloudAdapter | `wx.cloud.callFunction` | HTTP fetch 到云开发 API |
| ShareAdapter | `onShareAppMessage` / `onShareTimeline` | Web Share API / 复制链接 |
| ExportAdapter | `canvasToTempFilePath``saveImageToPhotosAlbum` | `canvas.toBlob()` → 下载 / jsPDF |
---
## 五、Web 端特有功能
| 功能 | 说明 |
|------|------|
| **直接下载 PDF** | jsPDF 前端生成,无需保存到相册 |
| **批量下载** | 一次生成多张 → 打包 ZIP 下载 |
| **打印预览** | 浏览器原生 `window.print()` + CSS @media print |
| **SEO** | SSR/SSG 生成题型介绍页,搜索引擎可抓取 |
| **用户登录** | 微信扫码登录 / 手机号 + 验证码 |
| **分享** | 生成题型页面 URL,可直接分享链接 |
| 功能 | 说明 |
| ---------------- | ---------------------------------------------- |
| **直接下载 PDF** | jsPDF 前端生成,无需保存到相册 |
| **批量下载** | 一次生成多张 → 打包 ZIP 下载 |
| **打印预览** | 浏览器原生 `window.print()` + CSS @media print |
| **SEO** | SSR/SSG 生成题型介绍页,搜索引擎可抓取 |
| **用户登录** | 微信扫码登录 / 手机号 + 验证码 |
| **分享** | 生成题型页面 URL,可直接分享链接 |
---
## 六、开发估算
| 阶段 | 内容 | 预估人时 |
|------|------|---------|
| 环境搭建 | Monorepo + Next.js 项目 + core 包提取 | ~16h |
| 平台适配层 | 5 个适配器的 Web 实现 | ~16h |
| UI 开发 | 首页 + 分类页 + 生成页 + 我的 | ~40h |
| 用户登录 | 微信扫码 + 手机号登录 | ~16h |
| SEO 优化 | SSG 题型页 + sitemap + meta | ~8h |
| 测试与上线 | 联调 + 部署 Vercel | ~8h |
| **总计** | | **~104h(约 3-4 周全职)** |
| 阶段 | 内容 | 预估人时 |
| ---------- | ------------------------------------- | -------------------------- |
| 环境搭建 | Monorepo + Next.js 项目 + core 包提取 | ~16h |
| 平台适配层 | 5 个适配器的 Web 实现 | ~16h |
| UI 开发 | 首页 + 分类页 + 生成页 + 我的 | ~40h |
| 用户登录 | 微信扫码 + 手机号登录 | ~16h |
| SEO 优化 | SSG 题型页 + sitemap + meta | ~8h |
| 测试与上线 | 联调 + 部署 Vercel | ~8h |
| **总计** | | **~104h(约 3-4 周全职)** |
---
## 八、PC Web 端技术预案(远期)
> PC Web 端属于远期规划,详细方案见独立文档:[PC-Web端技术预案](./PC-Web端技术预案.md)
>
> **当前阶段的准备**:只需确保 `core/` 层内不 import 任何 `wx.*` API,未来迁移时提取为独立 npm 包即可。
---
## 九、前端重构路线图
### Phase 1:基础重构(第 1-2 周)
```
优先级:🔴 关键
目标:不改变现有功能,优化代码结构
1. 修复技术债务
├── 统一云函数名称(callCloud 中 robotaxi → doodle
├── 清理 cloudfunctions/doodle/index.js 中无效的模块引用
├── 移除 debug 页中指向不存在页面的死链
└── 清理未使用的代码和资源
2. 建立 core/ 目录
├── 将 constants/ 迁移到 core/data/
├── 将纯工具函数迁移到 core/utils/
├── 将 DrawService 基类提取到 core/draw/
└── 定义统一的 WorksheetType 数据模型
3. 建立 platform/ 目录
├── 抽取 canvas-adapter(封装 wx Canvas API
├── 抽取 storage-adapter(封装 wx.Storage
└── 抽取 image-adapter(封装图片加载)
4. 统一 WXML 模板
└── 合并 mathPages 和 focusPages 的重复 canvas-page-template
```
### Phase 2:模板引擎 + 首页重构 + 配置化(第 3-5 周)
```
优先级:🟡 重要(模板引擎是后续扩展的基础)
1. 模板引擎核心开发
├── 实现 BaseTemplateRenderer 基类
├── 实现 TemplateEngine 入口(配置解析 + 渲染器/生成器调度)
├── 实现 RendererRegistry / GeneratorRegistry
├── 开发通用 worksheet 页面(pages/worksheet/worksheet
│ └── 根据 JSON 配置动态渲染参数面板 + Canvas 预览
├── 优先实现 3 种高复用模板渲染器:
│ ├── grid-exercise(网格计算型,覆盖 ~15 种现有数学题)
│ ├── full-page-asset(全幅素材型,覆盖涂色卡/折纸等新内容)
│ └── tracing-writing(描红书写型,覆盖练字/字母/拼音)
└── 将 2-3 种现有题型试点迁移到模板引擎(验证可行性)
2. 数据模型升级
├── WorksheetConfig 增加 template/generator/layoutConfig 等字段
├── 定义 Category 模型
├── 重构 MATH_FUNCTION_TYPES / FOCUS_FUNCTION_TYPES 为统一 JSON 格式
└── 编写存量题型的 JSON 配置映射
3. 首页重构
├── 新建 pages/home/ 替代原四个 Tab 入口页
├── 实现分类标签栏 + 搜索 + 推荐区
├── 实现年龄筛选、难度筛选
└── 使用新的 worksheet-card 组件(带预览图)
4. TabBar 重构
└── 发现 | 分龄 | 收藏 | 我的
5. 云数据库初始化(现阶段)
├── 建表:worksheets(含模板引擎字段)、categories
├── 编写数据初始化脚本(存量题型 JSON 导入)
└── 实现前端数据加载(缓存优先 + 云端更新,见 小程序云开发方案.md)
```
### Phase 3:模板扩展 + 新内容接入(第 6-8 周)
```
优先级:🟡 重要
1. 补全剩余模板渲染器
├── match-connect(配对连线型)
├── grid-coloring(网格涂色型)
├── card-layout(卡片排列型)
├── sequence-pattern(序列/排序型)
└── special-graphic(时钟/特殊图形型)
2. 新增题型(通过 JSON 配置 + 素材上传,大部分无需写新代码)
├── 数学:时钟练习(需 special-graphic 渲染器)
├── 语文:拼音练习(复用 tracing-writing
├── 英语:字母描红(复用 tracing-writing + letter-tracing 生成器)
├── 英语:字母闪卡(复用 card-layout + static-asset
├── 益智:控笔练习(复用 tracing-writing
└── 创意:涂色卡(复用 full-page-asset,仅需上传素材)
3. 存量题型批量迁移
├── 批次 1grid-exercise 类 (~15 种)
├── 批次 2match-connect 类 (~6 种)
└── 批次 3grid-coloring 类 (~8 种)
4. 完善新分包
├── english/ 分包
├── puzzle/ 分包
└── craft/ 分包
```
### Phase 4:体验与功能升级(第 9-10 周)
```
优先级:🟢 增强
1. 用户体系
├── 微信登录
├── 收藏功能(本地 + 云端同步)
└── 下载历史
2. 打印体验
├── 打印指南页面
├── 批量生成图片(多张保存到相册)
└── 客户端渲染性能优化
3. 运营能力
├── 数据埋点完善
├── 下载统计展示(热门排行)
└── 用户反馈入口
4. 远期增值功能
└── PDF 导出(会员专属,云函数合并 PNG 为 PDF;迁移后可为 NestJS
```
---
+125
View File
@@ -0,0 +1,125 @@
# SVG 路径分析 - 汉字笔画解析
## 📝 概述
本文档详细解析汉字练字贴中使用的 SVG 路径数据,帮助理解汉字笔画的数字化表示方法。
## 🎯 示例路径分析
### 路径片段
```
"M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z"
```
## 🔧 SVG 路径命令
### 基本命令
- **M** = MoveTo(移动到)- 开始一个新的子路径,不绘制线条
- **L** = LineTo(画线到)- 从当前位置画直线到指定点
- **Z** = ClosePath(闭合路径)- 闭合当前子路径,连接起点和终点
### 坐标系统
- 使用 54x54 的视窗坐标系
- 坐标原点 (0,0) 在左上角
- X 轴向右递增,Y 轴向下递增
- 坐标范围:0-54
## 📊 逐点分析
| 命令 | 坐标 | 描述 | 移动方向 |
| ---- | -------------- | -------------- | ---------- |
| M | (15.66, 8.1) | 起始点 | - |
| L | (15.66, 7.56) | 向上移动 | ↑ |
| L | (17.28, 4.86) | 向右上移动 | ↗ |
| L | (13.5, 1.62) | 向左上移动 | ↖ |
| L | (14.04, 7.56) | 向右下移动 | ↘ |
| L | (14.04, 8.1) | 向下移动 | ↓ |
| L | (9.18, 32.4) | 向左下大幅移动 | ↙ |
| L | (16.2, 36.18) | 向右下移动 | ↘ |
| L | (23.76, 42.12) | 向右下移动 | ↘ |
| L | (23.76, 38.88) | 向上移动 | ↑ |
| L | (10.8, 31.86) | 向左上移动 | ↖ |
| Z | - | 闭合路径 | 回到起始点 |
## 🖌️ 笔画特征分析
### 形状特征
1. **起始区域**:在坐标 (15.66, 8.1) 附近,位于画布上方
2. **转折点**:包含多个转折,形成复杂的曲线
3. **主要移动**:从上方区域移动到下方区域
4. **闭合性**:使用 Z 命令闭合路径
### 书法特征
- **复杂笔画**:包含多个方向的线条变化
- **曲线特征**:有明显的弯曲和转折
- **可能类型**:撇捺组合、弯钩笔画或复合笔画
## 🎨 在汉字中的应用
### 汉字"好"的笔画结构
这个路径很可能是汉字"好"字中的某个重要笔画:
#### 可能的组成部分
1. **女字旁**:左边的"女"字的一部分
2. **子字旁**:右边的"子"字的一部分
3. **复合笔画**:多个基本笔画的组合
### 笔画顺序
在汉字书写中,这个笔画可能:
- 不是第一笔(起始位置不在左上角)
- 包含多个书写方向的变化
- 需要一定的书写技巧
## 🔍 技术实现
### 在代码中的使用
```typescript
// SVG 路径数据
const strokes = [
'M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z',
// ... 更多笔画
];
// 绘制函数
function drawSvgPathCommands(ctx, pathD, offsetX, offsetY, scale) {
// 解析并绘制 SVG 路径
}
```
### 坐标变换
- **缩放**:根据田字格大小进行缩放
- **偏移**:根据田字格位置进行偏移
- **适配**:适配不同的显示尺寸
## 📚 相关文档
- [SVG 路径规范](https://www.w3.org/TR/SVG/paths.html)
- [汉字笔画分类](https://zh.wikipedia.org/wiki/汉字笔画)
- [田字格练字原理](./tianzige-principles.md)
## 🎯 总结
这个 SVG 路径片段展示了汉字数字化的精确表示方法:
1. **精确性**:每个转折点都有精确的坐标
2. **完整性**:完整描述了笔画的形状和走向
3. **可缩放性**:通过坐标系统支持不同尺寸的显示
4. **书法性**:保持了汉字书法的艺术特征
通过这种数字化表示,我们可以在计算机上精确重现汉字的书写过程,为练字教学提供技术支持。
---
_文档创建时间:2024年_
_最后更新:2024年_
+902
View File
@@ -0,0 +1,902 @@
# 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 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 缓存热点数据 + 数据库索引优化 + 按需水平扩展 |
+348
View File
@@ -0,0 +1,348 @@
# 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 | 是 | 主键;建议与后端一致用 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 / 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)。
+205 -438
View File
@@ -1,8 +1,10 @@
# Doodle Mini - 技术架构设计文档
> 版本:v3.0
> 最后更新:2026-03-24
> 配套文档:[现有功能清单](./现有功能清单.md) | [产品设计文档](./产品设计文档.md) | [PC-Web端技术预案](./PC-Web端技术预案.md)
> 版本:v3.2
> 最后更新:2026-03-27
> 配套文档:[现有功能清单](./现有功能清单.md) | [产品设计文档](./产品设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [后端部署方案](./后端部署方案.md) | [PC-Web端技术预案](./PC-Web端技术预案.md)
**后端阶段说明(重要)**:**现阶段**后端能力采用**微信小程序云开发**(云数据库、云存储、云函数),与 [小程序云开发方案](./小程序云开发方案.md) 一致。**当前阶段有效期至 2026 年 9 月 15 日**(到期前需结合用量、成本与产品路线,决定续用云开发、升级套餐或迁移至 [后端部署方案](./后端部署方案.md) 所述 NestJS + 自有服务器方案)。前端仍通过 `platform/cloud-adapter` 对接 `wx.cloud`,迁移时再抽象为 HTTP 适配层。
---
@@ -12,7 +14,7 @@
| 原则 | 说明 |
| ------------ | ------------------------------------------------------------------------------------------------- |
| **成本优先** | 优先使用微信云开发,避免自建服务器的运维和费用 |
| **成本优先** | 现阶段用云开发免费额度控制成本;**2026-09-15** 前复盘是否续用或迁移自建;核心逻辑仍保持前端执行 |
| **渐进增强** | 先小程序跑通,后续再扩展 PC Web,不为未来过度设计 |
| **前端为主** | Canvas 渲染、随机生成、PNG 导出等核心逻辑保持前端执行,零服务器成本;后端仅做必要的数据存储和服务 |
| **可迁移** | 核心绘制逻辑与平台 API 解耦,为将来迁移 Web 做准备 |
@@ -38,9 +40,10 @@
│ 需要后端(轻量级) │
├─────────────────────────────────────────────────────────┤
│ 📦 内容配置远程下发(热更新题型/分类,无需发版) │
│ 📦 静态素材存储与 CDN 分发(涂色卡线稿、折纸模板等) │
│ 📦 静态素材存储与分发(涂色卡线稿、折纸模板等)
│ 📦 用户数据同步(收藏、历史记录跨设备) │
│ 📦 下载统计 / 热度排名 │
│ 📦 用户标识 / 鉴权(云开发侧 OpenID 等) │
│ 📦 PDF 多页合并导出(远期增值功能,非核心路径) │
│ 📦 未来 PC Web 端的 API 服务 │
└─────────────────────────────────────────────────────────┘
@@ -48,24 +51,29 @@
#### 方案对比与选择
| 维度 | 微信云开发 | 自云服务器 | 决策 |
| ----------- | -------------------------------------------------------- | -------------------- | --------- |
| 接入成本 | ⭐ 极低(已有基础) | ⭐⭐⭐ 需搭建部署 | 云开发 ✅ |
| 运维成本 | ⭐ 免运维 | ⭐⭐⭐ 需监控/维护 | 云开发 ✅ |
| 费用 | 免费额度足够早期(数据库 2GB/存储 5GB/云函数 10万次/月) | 最低约 ¥50-100/月 | 云开发 ✅ |
| 小程序集成 | ⭐ 原生集成,鉴权零成本 | ⭐⭐ 需对接登录/鉴权 | 云开发 ✅ |
| PC Web 支持 | ⭐⭐ 需通过 HTTP API 桥接 | ⭐ 天然支持 | 自建 ✅ |
| 灵活性 | ⭐⭐ 受限于云开发 SDK | ⭐ 完全自由 | 自建 ✅ |
| 数据迁移 | ⭐⭐ 可导出但不方便 | ⭐ 标准数据库 | 自建 ✅ |
| 维度 | 微信云开发 | 自云服务器NestJS | 现阶段决策(至 2026-09-15 |
| ----------- | ----------------------------- | --------------------------- | --------------------------- |
| 接入成本 | ⭐ 极低(原生集成) | ⭐⭐ 需搭建部署(~3-5天) | **云开发 ✅** |
| 运维成本 | ⭐ 免运维 | ⭐⭐ 需简单维护(PM2 守护) | **云开发 ✅** |
| 费用 | 免费额度内 ¥0;超出按套餐计费 | 已有服务器可零增量 | **云开发(阶段内)✅** |
| 小程序集成 | ⭐ 鉴权零成本 | ⭐⭐ 需对接微信登录+JWT | **云开发 ✅** |
| PC Web 支持 | ⭐⭐ 需通过 HTTP API 桥接 | ⭐ RESTful API 天然支持 | 远期再评估 NestJS |
| 灵活性 | ⭐⭐ 受限于云开发 SDK | ⭐ 完全自由 | 到期后按需评估 |
| 数据迁移 | ⭐⭐ 可导出 | ⭐ 标准 MySQL | 到期后按需评估 |
**最终决策:近期使用微信云开发,远期按需引入轻量后端**
**最终决策(分阶段)**
```
Phase 1-3(当前~8周):100% 微信云开发
└── 云数据库 + 云存储 + 云函数,零服务器成本
现阶段(至 2026-09-15):微信小程序云开发
└── 云数据库 + 云存储 + 云函数;详情见 → 小程序云开发方案.md
远期(需要 PC Web 时):按需引入轻量后端
└── 详见 PC-Web端技术预案.md
阶段到期后:在 2026-09-15 前复盘,可选路径包括
① 续用云开发(含升级套餐)
② 迁移至 NestJS + 自有服务器 → 后端部署方案.md
③ 混合方案(按模块拆分)
平台适配层保持抽象:当前 cloud-adapterwx.cloud),迁移时可替换为 http-adapter
业务服务层接口尽量不变(见 platform/ 设计)。
```
---
@@ -100,13 +108,13 @@ Phase 1-3(当前~8周):100% 微信云开发
│ │ 🔑 此层可直接在未来 PC Web 项目中 import 复用 │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────┬────────────────────────────────────┘
wx.cloud / callFunction
┌────────────────────────────────────────────────────────────────┐
│ 后端服务层
│ 后端服务层(现阶段:微信云开发)
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 微信云开发(零服务器成本) │ │
│ │ 微信云开发 │ │
│ │ │ │
│ │ ┌────────────┐ ┌──────────┐ ┌─────────────┐ │ │
│ │ │ 云数据库 │ │ 云存储 │ │ 云函数 │ │ │
@@ -121,11 +129,13 @@ Phase 1-3(当前~8周):100% 微信云开发
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CDN (腾讯云) │ │
│ │ CDN(云开发资源) │ │
│ │ 素材图片 / 字体文件 / 预览图 / 分享图 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
PC Web 远期方案见 → PC-Web端技术预案.md
云开发详细设计 → 小程序云开发方案.md
│ 阶段到期后可选迁移 → 后端部署方案.md │
│ PC Web 远期方案 → PC-Web端技术预案.md │
└────────────────────────────────────────────────────────────────┘
```
@@ -165,22 +175,16 @@ miniprogram/
```
miniprogram/
├── core/ ← 🆕 共享核心层(平台无关)
├── core/ ← 🆕 共享核心层(平台无关,可被主包/分包复用
│ ├── models/ ← 数据模型定义
│ │ ├── worksheet.ts ← WorksheetType 统一模型
│ │ ├── category.ts ← 分类模型
│ │ ├── user.ts ← 用户数据模型
│ │ └── print-config.ts ← 打印配置模型
│ ├── generators/ ← 题目生成算法(纯逻辑
│ │ ├── math/ 数学题目生成器
│ │ │ ├── addition.ts
├── subtraction.ts
│ │ │ ├── number-sequence.ts
│ │ │ └── ...
│ │ ├── focus/ ← 专注力题目生成器
│ │ ├── chinese/ ← 语文题目生成器
│ │ └── english/ ← 英语题目生成器
│ ├── draw/ ← 绘制服务(Canvas 2D API,平台无关)
│ ├── generators/ ← 题目生成算法(仅保留跨分包通用部分
│ │ ├── common/公共算法(如随机、序列、通用算术)
│ │ └── registry.ts ← 生成器注册与装配
├── draw/ ← 绘制服务(仅保留跨分包通用部分)
│ │ ├── base-draw.ts ← 基础绘制(纸面、页眉、网格等)
│ │ ├── template-engine.ts ← 🆕 模板引擎入口
│ │ ├── renderer-registry.ts ← 🆕 渲染器注册表
@@ -194,10 +198,7 @@ miniprogram/
│ │ │ ├── full-page-asset.ts
│ │ │ ├── sequence-pattern.ts
│ │ │ └── special-graphic.ts
│ │ ── legacy/ ← 存量专用绘制服务(渐进迁移后移除
│ │ │ ├── math-draw/
│ │ │ ├── focus-draw/
│ │ │ └── chinese-draw/
│ │ ── legacy/ ← 存量中仍可复用的通用兼容层(逐步收敛
│ ├── data/ ← 内容配置数据
│ │ ├── worksheets.ts ← 所有题型定义(本地兜底)
│ │ ├── categories.ts ← 分类定义
@@ -211,7 +212,7 @@ miniprogram/
├── platform/ ← 🆕 平台适配层
│ ├── canvas-adapter.ts ← Canvas API 适配(wx ↔ Web
│ ├── storage-adapter.ts ← 存储适配(wxStorage ↔ localStorage
│ ├── cloud-adapter.ts ← 云服务适配(wx.cloud ↔ HTTP
│ ├── cloud-adapter.ts ← 云服务适配(wx.cloud;迁移 NestJS 后可换 http-adapter
│ ├── image-adapter.ts ← 图片加载适配
│ └── share-adapter.ts ← 分享能力适配
@@ -228,22 +229,27 @@ miniprogram/
│ ├── app-store.ts ← 全局状态(用户/配置/主题)
│ └── page-store.ts ← 页面级状态工具
├── pages/ ← 重构后的主包页面
│ ├── home/ ← 发现首页(合并原 4 个 Tab 入口)
│ ├── category/ ← 分类详情页
│ ├── worksheet/ ← 统一的题型生成/预览页
├── pages/ ← 主包(受 2MB 限制,只放高频入口与通用页)
│ ├── home/ ← 发现首页
│ ├── category/ ← 分类详情页(仅列表,不承载重绘制逻辑)
│ ├── profile/ ← 我的
── guide/ ← 打印指南
│ ├── favorites/ ← 收藏列表
│ ├── history/ ← 下载历史
│ └── settings/ ← 设置
── guide/ ← 打印指南
├── subpackages/ ← 🆕 按品类分包
│ ├── math/ ← 数学题型页面
── chinese/语文题型页面
│ ├── english/ ← 英语题型页面
│ ├── puzzle/益智游戏页面
│ └── craft/ 创意手工页面
├── mathPages/ ← 数学分包
│ ├── ... ← 数学题型页面
── shared/ 仅数学分包依赖(service/templates/assets/constants/draw/generators
├── chinesPages/ ← 语文分包(命名可按实现统一为 chinesePages
│ ├── ... 识字/练字及后续语文题型页面
│ └── shared/仅语文分包依赖(含语文专用 draw/generators
├── englishPages/ ← 英语分包
│ ├── ... ← 字母/拼读等页面
│ └── shared/ ← 仅英语分包依赖(含英语专用 draw/generators
├── puzzlePages/ ← 益智分包
│ ├── ... ← 迷宫/找不同/连线等页面
│ └── shared/ ← 仅益智分包依赖(含益智专用 draw/generators
├── craftPages/ ← 创意手工分包
│ ├── ... ← 涂色卡/折纸/手工模板页面
│ └── shared/ ← 仅创意手工分包依赖(含手工专用 draw/generators
├── components/ ← UI 组件
│ ├── shared/ ← 通用组件
@@ -265,6 +271,14 @@ miniprogram/
└── style/ ← 全局样式
```
> 分包约束(新增):为控制主包体积与首屏加载,**题型页面、重绘制逻辑、题型私有素材、题型私有工具、题型私有 draw 与 generators** 必须下沉到对应分包;仅该分包使用的依赖统一放到该分包的 `shared/` 目录中(延续当前 `mathPages/shared`、`focusPages/shared` 的组织方式)。
>
> 归属原则(draw / generators):
>
> 1. **跨分包复用**(2 个及以上分包共用)→ 放 `core/draw`、`core/generators`
> 2. **仅单一分包使用** → 放对应 `<xxx>Pages/shared/draw`、`<xxx>Pages/shared/generators`
> 3. 如后续从“单分包”演进为“跨分包”,再从分包 `shared` 上提到 `core`,避免过早抽象导致主包膨胀。
### 3.3 分层职责
```
@@ -534,71 +548,71 @@ interface WorksheetConfig {
status: 'active' | 'draft' | 'hidden';
// ─── 模板引擎配置(渲染用)───
template: TemplateType; // 使用哪个排版模板
generator: GeneratorType; // 使用哪个数据生成器
generatorConfig: Record<string, any>; // 生成器参数(不同生成器不同)
layoutConfig: LayoutConfig; // 排版参数
userConfigurable?: UserConfigField[]; // 用户可调整的参数定义
template: TemplateType; // 使用哪个排版模板
generator: GeneratorType; // 使用哪个数据生成器
generatorConfig: Record<string, any>; // 生成器参数(不同生成器不同)
layoutConfig: LayoutConfig; // 排版参数
userConfigurable?: UserConfigField[]; // 用户可调整的参数定义
}
/**
* 排版模板枚举(前端代码实现,新增需发版)
*/
type TemplateType =
| 'grid-exercise' // 模式 A:网格计算型
| 'match-connect' // 模式 B:配对连线型
| 'grid-coloring' // 模式 C:网格涂色型
| 'card-layout' // 模式 D:卡片排列型
| 'tracing-writing' // 模式 E:描红书写型
| 'full-page-asset' // 模式 F:全幅素材型
| 'sequence-pattern' // 模式 G:序列/排序型
| 'special-graphic'; // 模式 H:时钟/特殊图形型
| 'grid-exercise' // 模式 A:网格计算型
| 'match-connect' // 模式 B:配对连线型
| 'grid-coloring' // 模式 C:网格涂色型
| 'card-layout' // 模式 D:卡片排列型
| 'tracing-writing' // 模式 E:描红书写型
| 'full-page-asset' // 模式 F:全幅素材型
| 'sequence-pattern' // 模式 G:序列/排序型
| 'special-graphic'; // 模式 H:时钟/特殊图形型
/**
* 数据生成器枚举(前端代码实现,新增需发版)
*/
type GeneratorType =
| 'arithmetic' // 加减乘除运算题
| 'number-sequence' // 数列/排序
| 'number-decompose' // 数的分与合
| 'counting' // 数数类
| 'comparison' // 比大小
| 'shape-grid' // 图形网格
| 'color-pattern' // 颜色规律
| 'character-tracing' // 汉字描红(需 SVG 笔画数据)
| 'letter-tracing' // 字母描红
| 'pinyin-tracing' // 拼音描红
| 'static-asset' // 静态素材(不生成,直接用素材)
| 'maze' // 迷宫算法
| 'dot-connect' // 点连线
| 'clock' // 时钟
| 'custom'; // 自定义(需指定 customGeneratorId
| 'arithmetic' // 加减乘除运算题
| 'number-sequence' // 数列/排序
| 'number-decompose' // 数的分与合
| 'counting' // 数数类
| 'comparison' // 比大小
| 'shape-grid' // 图形网格
| 'color-pattern' // 颜色规律
| 'character-tracing' // 汉字描红(需 SVG 笔画数据)
| 'letter-tracing' // 字母描红
| 'pinyin-tracing' // 拼音描红
| 'static-asset' // 静态素材(不生成,直接用素材)
| 'maze' // 迷宫算法
| 'dot-connect' // 点连线
| 'clock' // 时钟
| 'custom'; // 自定义(需指定 customGeneratorId
/**
* 排版配置
*/
interface LayoutConfig {
columns?: number; // 列数
rows?: number; // 行数
fontSize?: number; // 字号
showBorder?: boolean; // 是否显示边框
showTitle?: boolean; // 是否显示标题
padding?: number; // 内边距
itemSpacing?: number; // 元素间距
showInstruction?: boolean; // 是否显示题目说明文字
instructionText?: string; // 说明文字内容
columns?: number; // 列数
rows?: number; // 行数
fontSize?: number; // 字号
showBorder?: boolean; // 是否显示边框
showTitle?: boolean; // 是否显示标题
padding?: number; // 内边距
itemSpacing?: number; // 元素间距
showInstruction?: boolean; // 是否显示题目说明文字
instructionText?: string; // 说明文字内容
}
/**
* 用户可配置的参数定义(渲染到参数设置面板)
*/
interface UserConfigField {
key: string; // 对应 generatorConfig 中的 key
label: string; // 显示名称
key: string; // 对应 generatorConfig 中的 key
label: string; // 显示名称
type: 'select' | 'slider' | 'switch';
options?: { label: string; value: any }[]; // select 类型的选项
min?: number; // slider 最小值
max?: number; // slider 最大值
options?: { label: string; value: any }[]; // select 类型的选项
min?: number; // slider 最小值
max?: number; // slider 最大值
defaultValue: any;
}
```
@@ -737,7 +751,11 @@ abstract class BaseTemplateRenderer {
this.paper = paper;
}
async render(data: any, layout: LayoutConfig, printConfig: PrintConfig): Promise<void> {
async render(
data: any,
layout: LayoutConfig,
printConfig: PrintConfig,
): Promise<void> {
this.drawBackground();
this.drawHeader(printConfig);
if (layout.showInstruction) {
@@ -747,7 +765,10 @@ abstract class BaseTemplateRenderer {
this.drawFooter(printConfig);
}
protected abstract drawContent(data: any, layout: LayoutConfig): Promise<void>;
protected abstract drawContent(
data: any,
layout: LayoutConfig,
): Promise<void>;
}
/**
@@ -804,12 +825,15 @@ class TemplateEngine {
async render(
worksheetConfig: WorksheetConfig,
userParams: Record<string, any>, // 用户在 UI 上调整的参数
userParams: Record<string, any>, // 用户在 UI 上调整的参数
ctx: CanvasRenderingContext2D,
printConfig: PrintConfig,
): Promise<void> {
// 1. 合并用户参数到 generatorConfig
const mergedConfig = { ...worksheetConfig.generatorConfig, ...userParams };
const mergedConfig = {
...worksheetConfig.generatorConfig,
...userParams,
};
// 2. 选择生成器,生成题目数据
const generator = this.generatorRegistry.get(worksheetConfig.generator);
@@ -839,7 +863,7 @@ createPage({
const config = WorksheetService.getConfig(worksheetId);
this.setData({
title: config.title,
userFields: config.userConfigurable, // 动态渲染参数面板
userFields: config.userConfigurable, // 动态渲染参数面板
userParams: getDefaultParams(config),
});
},
@@ -927,124 +951,34 @@ createPage({
---
## 六、后端(云开发)详细设计
## 六、后端概要设计
### 6.1 云数据库集合设计
> **现阶段(至 2026-09-15)**:云数据库、云存储、云函数及调用策略的**完整设计**见 → [小程序云开发方案](./小程序云开发方案.md)。
> **阶段到期后可选迁移**:NestJS 项目结构、Prisma、REST API、部署等见 → [后端部署方案](./后端部署方案.md)。
```
云数据库 Collections:
┌─────────────────────────────────────────────────┐
│ │
│ worksheets (题型配置表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string │ │
│ │ category: string │ │
│ │ subcategory: string │ │
│ │ title: string │ │
│ │ desc: string │ │
│ │ ageRange: [number, number] │ │
│ │ difficulty: 1|2|3|4 │ │
│ │ previewImage: string │ │
│ │ tags: string[] │ │
│ │ isNew: boolean │ │
│ │ isHot: boolean │ │
│ │ sortOrder: number │ │
│ │ downloadCount: number │ │
│ │ status: 'active'|'draft'|'hidden' │ │
│ │ │ │
│ │ ── 模板引擎字段 ── │ │
│ │ template: TemplateType │ ← 渲染模板│
│ │ generator: GeneratorType │ ← 生成器 │
│ │ generatorConfig: object │ ← 生成参数│
│ │ layoutConfig: object │ ← 排版参数│
│ │ userConfigurable: array │ ← 用户可调│
│ │ legacyPage: string | null │ ← 旧页面 │
│ │ │ │
│ │ createdAt: Date │ │
│ │ updatedAt: Date │ │
│ └─────────────────────────────────────┘ │
│ │
│ categories (分类表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string │ │
│ │ name: string │ │
│ │ icon: string │ │
│ │ color: string │ │
│ │ sortOrder: number │ │
│ │ parentId: string | null │ │
│ └─────────────────────────────────────┘ │
│ │
│ users (用户表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string (openid) │ │
│ │ uuid: string │ │
│ │ nickName: string │ │
│ │ avatarUrl: string │ │
│ │ totalDownloads: number │ │
│ │ createdAt: Date │ │
│ │ lastActiveAt: Date │ │
│ └─────────────────────────────────────┘ │
│ │
│ favorites (收藏表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string │ │
│ │ _openid: string │ │
│ │ worksheetId: string │ │
│ │ createdAt: Date │ │
│ └─────────────────────────────────────┘ │
│ │
│ download_logs (下载日志表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string │ │
│ │ _openid: string │ │
│ │ worksheetId: string │ │
│ │ params: object (生成参数快照) │ │
│ │ createdAt: Date │ │
│ └─────────────────────────────────────┘ │
│ │
│ feedback (用户反馈表) │
│ ┌─────────────────────────────────────┐ │
│ │ _id: string │ │
│ │ _openid: string │ │
│ │ content: string │ │
│ │ contact: string │ │
│ │ createdAt: Date │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
```
### 6.1 现阶段:微信云开发(摘要)
### 6.2 云函数设计
| 组件 | 技术 | 说明 |
| ---------- | ---------------------------- | -------------------------------------- |
| 数据 | 云数据库(文档型) | worksheets / categories / users 等集合 |
| 文件 | 云存储(COS) | 预览图、涂色线稿、字体等 |
| 逻辑 | 云函数(Node.js) | 统一入口路由、业务 action |
| 小程序接入 | `wx.cloud` + `cloud-adapter` | 与业务服务层解耦,便于日后替换为 HTTP |
```
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 ← 🆕 鉴权中间件
```
### 6.2 远期可选:自建 NestJS(摘要)
### 6.3 云函数调用策略
| 组件 | 技术 | 说明 |
| ---- | ----------------------- | --------------------------- |
| 应用 | NestJS + TypeScript | RESTful API,多端统一 |
| 数据 | MySQL 8.0 + Prisma | 关系型存储与迁移 |
| 文件 | 本地磁盘 + Nginx | 静态素材 URL 由自有域名提供 |
| 鉴权 | 微信 code2Session + JWT | 小程序与 Web 可共用逻辑 |
(接口路径、表结构、部署步骤等以 [后端部署方案](./后端部署方案.md) 为准,此处不重复。)
### 6.3 数据加载策略(两阶段一致的产品策略)
无论后端是云开发还是 HTTP API,**客户端侧优先级**保持一致:
```
┌────────────────────────────────────────────────────────────┐
@@ -1053,7 +987,7 @@ cloudfunctions/
│ 题型配置数据 (worksheets/categories) │
│ ┌────────────────────────────────────────┐ │
│ │ 优先级 1: 本地缓存(wx.Storage │ │
│ │ 优先级 2: 云数据库查询 │ │
│ │ 优先级 2: 云端拉取(云 DB 或 HTTP API │ │
│ │ 优先级 3: 前端内置兜底数据 │ ← 保证离线可用 │
│ └────────────────────────────────────────┘ │
│ │
@@ -1062,49 +996,19 @@ cloudfunctions/
│ • 后续启动:先用缓存渲染 → 后台静默更新 │
│ • 缓存有效期:24 小时 │
│ • 无网络:使用本地缓存或内置兜底 │
│ │
│ 用户数据 (favorites/history) │
│ ┌────────────────────────────────────────┐ │
│ │ 本地优先写入 → 后台同步云端 │ │
│ │ 冲突策略:以云端为准(云端时间戳更新) │ │
│ └────────────────────────────────────────┘ │
│ │
│ 统计数据 (download count) │
│ ┌────────────────────────────────────────┐ │
│ │ 批量上报:本地累计 → 退出时/定时上报 │ │
│ │ 非关键路径,允许丢失 │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
```
### 6.4 云存储规划
### 6.4 静态素材 URL(现阶段)
素材使用 **云存储 fileID / 临时 URL**,例如:
```
云存储目录结构:
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
cloud://doodle-xxx.xxxx/assets/coloring/animals/dinosaur-01.svg
```
(目录规划见 [小程序云开发方案](./小程序云开发方案.md) 第六节。迁移自建后改为 `https://你的域名/static/...`。)
---
## 七、关键技术方案
@@ -1177,7 +1081,7 @@ class WxImageAdapter implements IImageAdapter {
│ 微信聊天直接发图片,接收方零门槛查看 │
│ │
│ 4. 零服务器成本 │
│ Canvas 本地渲染,不消耗云函数算力和存储
│ Canvas 本地渲染,不消耗云函数算力与云存储(主路径)
│ 用户量增长不会带来额外成本 │
│ │
│ 5. 技术架构成熟 │
@@ -1189,7 +1093,7 @@ class WxImageAdapter implements IImageAdapter {
→ 用户打印时多选图片即可
PDF 导出方案(远期增值功能,会员专属):
小程序:客户端渲染多张 PNG → 上传云存储 → 云函数 pdfkit 合并为 PDF
小程序:客户端渲染多张 PNG → 上传云存储 → 云函数 pdfkit 合并为 PDF(迁移自建后同逻辑可落在 NestJS)
定位:会员订阅的差异化权益,非核心路径
```
@@ -1233,167 +1137,28 @@ PDF 导出方案(远期增值功能,会员专属):
└──────────────────────────────────────────────────────┘
```
---
## 八、PC Web 端技术预案(远期)
> PC Web 端属于远期规划,详细方案见独立文档:[PC-Web端技术预案](./PC-Web端技术预案.md)
>
> **当前阶段的准备**:只需确保 `core/` 层内不 import 任何 `wx.*` API,未来迁移时提取为独立 npm 包即可。
---
## 九、前端重构路线图
### Phase 1:基础重构(第 1-2 周)
```
优先级:🔴 关键
目标:不改变现有功能,优化代码结构
1. 修复技术债务
├── 统一云函数名称(callCloud 中 robotaxi → doodle
├── 清理 cloudfunctions/doodle/index.js 中无效的模块引用
├── 移除 debug 页中指向不存在页面的死链
└── 清理未使用的代码和资源
2. 建立 core/ 目录
├── 将 constants/ 迁移到 core/data/
├── 将纯工具函数迁移到 core/utils/
├── 将 DrawService 基类提取到 core/draw/
└── 定义统一的 WorksheetType 数据模型
3. 建立 platform/ 目录
├── 抽取 canvas-adapter(封装 wx Canvas API
├── 抽取 storage-adapter(封装 wx.Storage
└── 抽取 image-adapter(封装图片加载)
4. 统一 WXML 模板
└── 合并 mathPages 和 focusPages 的重复 canvas-page-template
```
### Phase 2:模板引擎 + 首页重构 + 配置化(第 3-5 周)
```
优先级:🟡 重要(模板引擎是后续扩展的基础)
1. 模板引擎核心开发
├── 实现 BaseTemplateRenderer 基类
├── 实现 TemplateEngine 入口(配置解析 + 渲染器/生成器调度)
├── 实现 RendererRegistry / GeneratorRegistry
├── 开发通用 worksheet 页面(pages/worksheet/worksheet
│ └── 根据 JSON 配置动态渲染参数面板 + Canvas 预览
├── 优先实现 3 种高复用模板渲染器:
│ ├── grid-exercise(网格计算型,覆盖 ~15 种现有数学题)
│ ├── full-page-asset(全幅素材型,覆盖涂色卡/折纸等新内容)
│ └── tracing-writing(描红书写型,覆盖练字/字母/拼音)
└── 将 2-3 种现有题型试点迁移到模板引擎(验证可行性)
2. 数据模型升级
├── WorksheetConfig 增加 template/generator/layoutConfig 等字段
├── 定义 Category 模型
├── 重构 MATH_FUNCTION_TYPES / FOCUS_FUNCTION_TYPES 为统一 JSON 格式
└── 编写存量题型的 JSON 配置映射
3. 首页重构
├── 新建 pages/home/ 替代原四个 Tab 入口页
├── 实现分类标签栏 + 搜索 + 推荐区
├── 实现年龄筛选、难度筛选
└── 使用新的 worksheet-card 组件(带预览图)
4. TabBar 重构
└── 发现 | 分龄 | 收藏 | 我的
5. 云数据库初始化
├── 建表:worksheets(含模板引擎字段)、categories
├── 编写数据初始化脚本(存量题型 JSON 导入)
└── 实现前端数据加载(缓存优先 + 云端更新)
```
### Phase 3:模板扩展 + 新内容接入(第 6-8 周)
```
优先级:🟡 重要
1. 补全剩余模板渲染器
├── match-connect(配对连线型)
├── grid-coloring(网格涂色型)
├── card-layout(卡片排列型)
├── sequence-pattern(序列/排序型)
└── special-graphic(时钟/特殊图形型)
2. 新增题型(通过 JSON 配置 + 素材上传,大部分无需写新代码)
├── 数学:时钟练习(需 special-graphic 渲染器)
├── 语文:拼音练习(复用 tracing-writing
├── 英语:字母描红(复用 tracing-writing + letter-tracing 生成器)
├── 英语:字母闪卡(复用 card-layout + static-asset
├── 益智:控笔练习(复用 tracing-writing
└── 创意:涂色卡(复用 full-page-asset,仅需上传素材)
3. 存量题型批量迁移
├── 批次 1grid-exercise 类 (~15 种)
├── 批次 2match-connect 类 (~6 种)
└── 批次 3grid-coloring 类 (~8 种)
4. 完善新分包
├── english/ 分包
├── puzzle/ 分包
└── craft/ 分包
```
### Phase 4:体验与功能升级(第 9-10 周)
```
优先级:🟢 增强
1. 用户体系
├── 微信登录
├── 收藏功能(本地 + 云端同步)
└── 下载历史
2. 打印体验
├── 打印指南页面
├── 批量生成图片(多张保存到相册)
└── 客户端渲染性能优化
3. 运营能力
├── 数据埋点完善
├── 下载统计展示(热门排行)
└── 用户反馈入口
4. 远期增值功能
└── PDF 导出(会员专属,云函数合并 PNG 为 PDF)
```
---
## 十、成本预估
### 9.1 微信云开发费用
### 10.1 后端费用
```
免费额度(基础版 1):
现阶段(微信云开发,至 2026-09-15 复盘):
┌────────────────────────────────────────┐
云数据库:2 GB 存储 / 50 万次读写/天
云存储:5 GB / 2 GB 下载/天
云函数:10 万次调用/月 / 1000 GBs/月
│ CDN5 GB/月 │
└────────────────────────────────────────┘
预估用量(DAU 1000):
┌────────────────────────────────────────┐
│ 云数据库:~100 MB(足够) │
│ 云存储:~2 GB(素材渐增) │
│ 云函数:~3 万次/月 │
│ CDN~3 GB/月 │
免费额度内:云数据库 / 云存储 / 云函数
预估早期 DAU 下可落在免费额度内
超出后按套餐计费(如 19.9 元/月起)
│ │
结论:免费额度可覆盖到 DAU 3000 左右
│ 超出后升级到 19.9 元/月即可 │
详见 → 小程序云开发方案.md 第七节
└────────────────────────────────────────┘
到期后若迁移自建(参考 后端部署方案.md):
┌────────────────────────────────────────┐
│ 已有云服务器可做到增量 ¥0(仅运维) │
│ 需在 2026-09-15 前对比两路径总成本 │
└────────────────────────────────────────┘
```
### 9.2 开发资源
### 10.2 开发资源
```
开发投入估算:
@@ -1415,44 +1180,46 @@ PDF 导出方案(远期增值功能,会员专属):
## 十一、技术风险与应对
| 风险 | 影响 | 应对方案 |
| -------------------- | ------------------ | ----------------------------------------------------------------- |
| 云开发免费额度不 | 功能受限 | ① 优化查询减少调用 ② 本地缓存减少读取 ③ 升级付费版(19.9元/月起) |
| Canvas 兼容性 | 低端机渲染异常 | ① 使用 Canvas 2D(已采用)② 控制单次绘制复杂度 ③ 降级方案 |
| 小程序包体积 | 超 2MB 限制 | ① 素材用 CDN / 云存储 ② 合理分包 ③ 图片压缩 |
| 素材制作瓶颈 | 涂色卡等需设计资源 | ① 使用开源 SVG 素材 ② AI 生成线稿 ③ 社区投稿 |
| 跨平台迁移成本 | Web 端重复开发 | ① core/ 层保持平台无关 ② 适配器模式隔离差异(详见 [PC-Web端技术预案](./PC-Web端技术预案.md) |
| 风险 | 影响 | 应对方案 |
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------- |
| 云开发免费额度不 | 功能受限或需付费 | ① 优化查询与缓存 ② 控制云函数调用 ③ **2026-09-15 前**复盘是否升级套餐或迁移 [后端部署方案](./后端部署方案.md) |
| 服务器宕机(迁移自建后) | 后端不可用 | ① PM2 自动重启 ② 前端兜底数据 ③ 定期备份数据库 |
| Canvas 兼容性 | 低端机渲染异常 | ① 使用 Canvas 2D(已采用)② 控制单次绘制复杂度 ③ 降级方案 |
| 小程序包体积 | 超 2MB 限制 | ① 素材用云存储 / CDN ② 合理分包 ③ 图片压缩 |
| 素材制作瓶颈 | 涂色卡等需设计资源 | ① 使用开源 SVG 素材 ② AI 生成线稿 ③ 社区投稿 |
| 跨平台迁移成本 | Web 端重复开发 | ① core/ 层保持平台无关 ② 适配器隔离差异(详见 [PC-Web端技术预案](./PC-Web端技术预案.md)) |
| 带宽与加载(迁移自建远期) | 素材加载慢 | ① 图片压缩 ② 长缓存 ③ 可接入 CDN |
---
## 附录 A:现有代码与目标架构映射
| 现有文件 | 目标位置 | 迁移动作 |
| -------------------------------- | -------------------------------------- | --------------------- |
| `constants/mathFunctions.ts` | `core/data/worksheets.ts` | 合并,增加字段 |
| `constants/focusFunctions.ts` | `core/data/worksheets.ts` | 合并,增加字段 |
| `constants/words.ts` | `core/data/words.ts` | 移动 |
| `constants/colors.ts` | `core/data/colors.ts` | 移动 |
| `constants/shapes.ts` | `core/data/shapes.ts` | 移动 |
| `service/baseDraw.ts` | `core/draw/base-draw.ts` | 重构,去除 wx.\* 依赖 |
| `service/wordDrawService.ts` | `core/draw/chinese-draw/word-draw.ts` | 移动,去除 wx.\* |
| `service/drawServiceFactory.ts` | `core/draw/draw-factory.ts` | 扩展 |
| `mathPages/shared/service/*.ts` | `core/draw/legacy/math-draw/*.ts` | 移入 legacy,去除 wx.\*,逐步迁移到模板引擎 |
| `focusPages/shared/service/*.ts` | `core/draw/legacy/focus-draw/*.ts` | 移入 legacy,去除 wx.\*,逐步迁移到模板引擎 |
| `base/pageMixin.ts` | `services/print-service.ts` + 页面代码 | 拆分职责 |
| `base/callCloud.ts` | `platform/cloud-adapter.ts` | 重构 |
| `utils/saveImage.ts` | `platform/canvas-adapter.ts` | 合并到适配器 |
| `utils/downloadPrint.ts` | `services/print-service.ts` | 整合 |
| `utils/tracker.ts` | `services/stats-service.ts` | 整合 |
| `config/config.ts` | `core/models/print-config.ts` | 类型化 |
| 现有文件 | 目标位置 | 迁移动作 |
| -------------------------------- | -------------------------------------- | ------------------------------------------------ |
| `constants/mathFunctions.ts` | `core/data/worksheets.ts` | 合并,增加字段 |
| `constants/focusFunctions.ts` | `core/data/worksheets.ts` | 合并,增加字段 |
| `constants/words.ts` | `core/data/words.ts` | 移动 |
| `constants/colors.ts` | `core/data/colors.ts` | 移动 |
| `constants/shapes.ts` | `core/data/shapes.ts` | 移动 |
| `service/baseDraw.ts` | `core/draw/base-draw.ts` | 重构,去除 wx.\* 依赖 |
| `service/wordDrawService.ts` | `core/draw/chinese-draw/word-draw.ts` | 移动,去除 wx.\* |
| `service/drawServiceFactory.ts` | `core/draw/draw-factory.ts` | 扩展 |
| `mathPages/shared/service/*.ts` | `core/draw/legacy/math-draw/*.ts` | 移入 legacy,去除 wx.\*,逐步迁移到模板引擎 |
| `focusPages/shared/service/*.ts` | `core/draw/legacy/focus-draw/*.ts` | 移入 legacy,去除 wx.\*,逐步迁移到模板引擎 |
| `base/pageMixin.ts` | `services/print-service.ts` + 页面代码 | 拆分职责 |
| `base/callCloud.ts` | `platform/cloud-adapter.ts` | 封装 wx.cloud;迁移 NestJS 后再考虑 http-adapter |
| `utils/saveImage.ts` | `platform/canvas-adapter.ts` | 合并到适配器 |
| `utils/downloadPrint.ts` | `services/print-service.ts` | 整合 |
| `utils/tracker.ts` | `services/stats-service.ts` | 整合 |
| `config/config.ts` | `core/models/print-config.ts` | 类型化 |
## 附录 B:技术选型决策记录
| 决策点 | 选项 | 决策 | 理由 |
| -------------- | ---------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| 跨端框架 | Taro / uni-app / 原生 | **原生** | 项目已用原生开发,迁移成本高;Taro 等框架对 Canvas 2D 支持有限;core/ 层抽取足以实现代码复用 |
| 状态管理 | MobX / 自定义 / 原生 setData | **轻量自定义** | 小程序场景简单,不需要 Redux 级方案;页面级 setData + 全局 Store 够用 |
| UI 组件库 | Vant / 自研 | **Vant + 自定义组件** | 已用 Vant,保持;业务组件自研 |
| 云开发 vs 自建 | 云开发 / 云服务器 | **云开发** | 零运维、免费额度充足、原生集成鉴权 |
| 导出方案 | PNG 保存相册 / PDF | **PNG 为主,PDF 为增值** | PNG 保存到相册是手机端最短路径(用户教育成本低、零服务器开销);PDF 作为远期会员增值功能,小程序用云端 pdfkit 合并,Web 用 jsPDF |
| Monorepo | pnpm workspace / Turborepo | **暂不采用** | 当前只有小程序,过早引入增加复杂度;core/ 作为目录组织,未来再拆包 |
| 决策点 | 选项 | 决策 | 理由 |
| --------- | ---------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 跨端框架 | Taro / uni-app / 原生 | **原生** | 项目已用原生开发,迁移成本高;Taro 等框架对 Canvas 2D 支持有限;core/ 层抽取足以实现代码复用 |
| 状态管理 | MobX / 自定义 / 原生 setData | **轻量自定义** | 小程序场景简单,不需要 Redux 级方案;页面级 setData + 全局 Store 够用 |
| UI 组件库 | Vant / 自研 | **Vant + 自定义组件** | 已用 Vant,保持;业务组件自研 |
| 后端方案 | 云开发 / 自有云服务器 | **现阶段云开发(至 2026-09-15** | 详见 [小程序云开发方案](./小程序云开发方案.md);到期后可选迁移 [后端部署方案](./后端部署方案.md)NestJS |
| 导出方案 | PNG 保存相册 / PDF | **PNG 为主,PDF 为增值** | PNG 保存到相册是手机端最短路径(用户教育成本低、零服务器开销);PDF 作为远期会员增值功能,服务端 pdfkit 合并,Web 用 jsPDF |
| Monorepo | pnpm workspace / Turborepo | **暂不采用** | 当前只有小程序,过早引入增加复杂度;core/ 作为目录组织,未来再拆包 |