Files
doodle-mini/docs/技术架构设计文档.md
T

1243 lines
68 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 - 技术架构设计文档
> 版本: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 适配层。
---
## 一、技术决策总览
### 1.1 核心原则
| 原则 | 说明 |
| -------- | ------------------------------------------------------- |
| **成本优先** | 现阶段用云开发免费额度控制成本;**2026-09-15** 前复盘是否续用或迁移自建;核心逻辑仍保持前端执行 |
| **渐进增强** | 先小程序跑通,后续再扩展 PC Web,不为未来过度设计 |
| **前端为主** | Canvas 渲染、随机生成、PNG 导出等核心逻辑保持前端执行,零服务器成本;后端仅做必要的数据存储和服务 |
| **可迁移** | 核心绘制逻辑与平台 API 解耦,为将来迁移 Web 做准备 |
### 1.2 后端方案决策
#### 需要后端的场景 vs 不需要的场景
```
┌─────────────────────────────────────────────────────────┐
│ 不需要后端(前端完成) │
├─────────────────────────────────────────────────────────┤
│ ✅ Canvas 绘制所有题型 │
│ ✅ 随机题目生成算法 │
│ ✅ A4 排版与打印预览 │
│ ✅ PNG 导出 → 保存相册 │
│ ✅ 题型配置数据(可前端内置 JSON) │
│ ✅ 分享功能 │
│ ✅ 本地收藏/历史(wx.Storage
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 需要后端(轻量级) │
├─────────────────────────────────────────────────────────┤
│ 📦 内容配置远程下发(热更新题型/分类,无需发版) │
│ 📦 静态素材存储与分发(涂色卡线稿、折纸模板等) │
│ 📦 用户数据同步(收藏、历史记录跨设备) │
│ 📦 下载统计 / 热度排名 │
│ 📦 用户标识 / 鉴权(云开发侧 OpenID 等) │
│ 📦 PDF 多页合并导出(远期增值功能,非核心路径) │
│ 📦 未来 PC Web 端的 API 服务 │
└─────────────────────────────────────────────────────────┘
```
#### 方案对比与选择
| 维度 | 微信云开发 | 自有云服务器(NestJS) | 现阶段决策(至 2026-09-15 |
| --------- | ------------------ | ------------------ | ------------------- |
| 接入成本 | ⭐ 极低(原生集成) | ⭐⭐ 需搭建部署(~3-5天) | **云开发 ✅** |
| 运维成本 | ⭐ 免运维 | ⭐⭐ 需简单维护(PM2 守护) | **云开发 ✅** |
| 费用 | 免费额度内 ¥0;超出按套餐计费 | 已有服务器可零增量 | **云开发(阶段内)✅** |
| 小程序集成 | ⭐ 鉴权零成本 | ⭐⭐ 需对接微信登录+JWT | **云开发 ✅** |
| PC Web 支持 | ⭐⭐ 需通过 HTTP API 桥接 | ⭐ RESTful API 天然支持 | 远期再评估 NestJS |
| 灵活性 | ⭐⭐ 受限于云开发 SDK | ⭐ 完全自由 | 到期后按需评估 |
| 数据迁移 | ⭐⭐ 可导出 | ⭐ 标准 MySQL | 到期后按需评估 |
**最终决策(分阶段)**
```
现阶段(至 2026-09-15):微信小程序云开发
└── 云数据库 + 云存储 + 云函数;详情见 → 小程序云开发方案.md
阶段到期后:在 2026-09-15 前复盘,可选路径包括
① 续用云开发(含升级套餐)
② 迁移至 NestJS + 自有服务器 → 后端部署方案.md
③ 混合方案(按模块拆分)
平台适配层保持抽象:当前 cloud-adapterwx.cloud),迁移时可替换为 http-adapter
业务服务层接口尽量不变(见 platform/ 设计)。
```
---
## 二、系统架构总图
```
┌────────────────────────────────────────────────────────────────┐
│ 微信小程序(当前重心) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ UI 层 (WXML + WXSS) │ │
│ │ pages/ + components/ — Skyline 渲染 │ │
│ └────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────┐ │
│ │ 业务服务层 (services/ + store/) │ │
│ │ worksheet-service / print-service / user-service │ │
│ └────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────┐ │
│ │ 平台适配层 (platform/) │ │
│ │ canvas-adapter / storage-adapter / cloud-adapter │ │
│ └────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────┐ │
│ │ 共享核心层 (core/) — 纯 TypeScript,零平台依赖 │ │
│ │ ┌─────────────┐ ┌────────────┐ ┌────────────────┐ │ │
│ │ │ 模板引擎 │ │ 生成器 │ │ 数据模型/工具 │ │ │
│ │ │ 8种渲染器 │ │ 15种生成器 │ │ WorksheetConfig│ │ │
│ │ └─────────────┘ └────────────┘ └────────────────┘ │ │
│ │ 🔑 此层可直接在未来 PC Web 项目中 import 复用 │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────┬────────────────────────────────────┘
│ wx.cloud / callFunction
┌────────────────────────────────────────────────────────────────┐
│ 后端服务层(现阶段:微信云开发) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 微信云开发 │ │
│ │ │ │
│ │ ┌────────────┐ ┌──────────┐ ┌─────────────┐ │ │
│ │ │ 云数据库 │ │ 云存储 │ │ 云函数 │ │ │
│ │ │ (MongoDB) │ │ (COS) │ │ (Node.js) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • 题型配置 │ │ • 涂色卡 │ │ • getOpenId │ │ │
│ │ │ • 用户数据 │ │ • 折纸图 │ │ • syncData │ │ │
│ │ │ • 收藏记录 │ │ • 手工图 │ │ • stats │ │ │
│ │ │ • 下载统计 │ │ • 预览图 │ │ • genPDF │ │ │
│ │ │ • 反馈数据 │ │ • 字体 │ │ (增值) │ │ │
│ │ └────────────┘ └──────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CDN(云开发资源) │ │
│ │ 素材图片 / 字体文件 / 预览图 / 分享图 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 云开发详细设计 → 小程序云开发方案.md │
│ 阶段到期后可选迁移 → 后端部署方案.md │
│ PC Web 远期方案 → PC-Web端技术预案.md │
└────────────────────────────────────────────────────────────────┘
```
---
## 三、前端架构详细设计
### 3.1 现有架构问题
```
当前代码组织:
miniprogram/
├── pages/ ← 主包页面,各自直接引用 mixin
├── mathPages/ ← 数学分包,每个页面有自己的 Draw 服务
│ └── shared/ ← 数学共用(service、templates、assets
├── focusPages/ ← 专注力分包,结构同上
│ └── shared/
├── service/ ← 识字/练字的 Draw 服务
├── base/ ← pageMixin、callCloud 等基础层
├── components/ ← UI 组件
├── constants/ ← 静态配置数据
└── utils/ ← 工具函数
```
**问题分析:**
| 问题 | 详情 |
| ---------- | ------------------------------------------------------------- |
| Mixin 模式笨重 | `pageMixin` 通过对象展开合并,无类型安全,难以追踪数据流 |
| Draw 服务分散 | 每个分包下各有 `shared/service/`,大量重复的绘制工具方法 |
| 平台耦合严重 | Draw 服务直接使用 `wx.createImage``canvas.createImage` 等 API |
| 模板重复 | `mathPages``focusPages``canvas-page-template.wxml` 内容相同 |
| 无共享核心 | 数据模型、生成算法与 UI 逻辑混在一起,无法跨平台复用 |
| 状态管理原始 | 纯 `setData`,无统一的状态管理方案 |
### 3.2 目标架构
```
miniprogram/
├── core/ ← 🆕 共享核心层(平台无关,可被主包/分包复用)
│ ├── models/ ← 数据模型定义
│ │ ├── worksheet.ts ← WorksheetType 统一模型
│ │ ├── category.ts ← 分类模型
│ │ ├── user.ts ← 用户数据模型
│ │ └── print-config.ts ← 打印配置模型
│ ├── generators/ ← 题目生成算法(仅保留跨分包通用部分)
│ │ ├── common/ ← 公共算法(如随机、序列、通用算术)
│ │ └── registry.ts ← 生成器注册与装配
│ ├── draw/ ← 绘制服务(仅保留跨分包通用部分)
│ │ ├── base-draw.ts ← 基础绘制(纸面、页眉、网格等)
│ │ ├── template-engine.ts ← 🆕 模板引擎入口
│ │ ├── renderer-registry.ts ← 🆕 渲染器注册表
│ │ ├── base-template-renderer.ts ← 🆕 渲染器基类
│ │ ├── renderers/ ← 🆕 各排版模板渲染器(6-8 种)
│ │ │ ├── grid-exercise.ts
│ │ │ ├── match-connect.ts
│ │ │ ├── grid-coloring.ts
│ │ │ ├── card-layout.ts
│ │ │ ├── tracing-writing.ts
│ │ │ ├── full-page-asset.ts
│ │ │ ├── sequence-pattern.ts
│ │ │ └── special-graphic.ts
│ │ └── legacy/ ← 存量中仍可复用的通用兼容层(逐步收敛)
│ ├── data/ ← 内容配置数据
│ │ ├── worksheets.ts ← 所有题型定义(本地兜底)
│ │ ├── categories.ts ← 分类定义
│ │ ├── words.ts ← 词库
│ │ └── difficulty.ts ← 难度与年龄映射
│ └── utils/ ← 纯工具函数(无平台依赖)
│ ├── random.ts
│ ├── math-utils.ts
│ └── format.ts
├── platform/ ← 🆕 平台适配层
│ ├── canvas-adapter.ts ← Canvas API 适配(wx ↔ Web
│ ├── storage-adapter.ts ← 存储适配(wxStorage ↔ localStorage
│ ├── cloud-adapter.ts ← 云服务适配(wx.cloud;迁移 NestJS 后可换 http-adapter
│ ├── image-adapter.ts ← 图片加载适配
│ └── share-adapter.ts ← 分享能力适配
├── services/ ← 🆕 业务服务层
│ ├── worksheet-service.ts ← 题型数据加载(本地 + 远程兜底)
│ ├── user-service.ts ← 用户数据管理
│ ├── favorite-service.ts ← 收藏管理
│ ├── history-service.ts ← 下载历史管理
│ ├── print-service.ts ← 打印/导出统一入口
│ ├── stats-service.ts ← 统计上报
│ └── ad-service.ts ← 广告管理
├── store/ ← 🆕 状态管理
│ ├── app-store.ts ← 全局状态(用户/配置/主题)
│ └── page-store.ts ← 页面级状态工具
├── pages/ ← 主包(受 2MB 限制,只放高频入口与通用页)
│ ├── home/ ← 发现首页
│ ├── category/ ← 分类详情页(仅列表,不承载重绘制逻辑)
│ ├── profile/ ← 我的
│ └── guide/ ← 打印指南
├── 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/ ← 通用组件
│ │ ├── worksheet-card/ ← 题型卡片(带预览图)
│ │ ├── category-tabs/ ← 分类标签栏
│ │ ├── age-filter/ ← 年龄筛选器
│ │ ├── difficulty-badge/ ← 难度标签
│ │ ├── canvas-preview/ ← Canvas 预览区
│ │ ├── action-bar/ ← 底部操作栏(保存/分享/换一批)
│ │ ├── print-header/ ← 打印页眉配置
│ │ └── empty-state/ ← 空状态
│ └── business/ ← 业务组件
│ ├── worksheet-list/ ← 题型列表
│ ├── param-panel/ ← 参数设置面板
│ └── ...
├── config/ ← 配置
├── assets/ ← 静态资源
└── 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 分层职责
```
┌──────────────────────────────────────────────────────┐
│ UI 层 │
│ pages/ + components/ (WXML + WXSS + 页面 TS) │
│ 职责:界面渲染、用户交互、调用 services │
├──────────────────────────────────────────────────────┤
│ 业务服务层 │
│ services/ + store/ │
│ 职责:组合 core 能力,管理状态,对接平台 API │
├──────────────────────────────────────────────────────┤
│ 平台适配层 │
│ platform/ │
│ 职责:抹平 wx.* 和 Web API 差异 │
├──────────────────────────────────────────────────────┤
│ 共享核心层 │
│ core/ (纯 TypeScript,零平台依赖) │
│ 职责:绘制服务、生成算法、数据模型 │
│ 🔑 可直接在 Web 项目中 import 使用 │
└──────────────────────────────────────────────────────┘
```
---
## 四、共享核心层设计(@doodle/core
这是未来跨平台复用的关键。所有与 `wx.*` 无关的逻辑都应该放在这一层。
### 4.1 Canvas 适配器接口
```typescript
/**
* 平台无关的 Canvas 上下文接口
* 微信小程序和 Web 的 Canvas 2D API 基本一致,
* 但图片加载、字体加载等需要适配
*/
interface ICanvasAdapter {
getContext(): CanvasRenderingContext2D;
setSize(width: number, height: number): void;
loadImage(src: string): Promise<CanvasImageSource>;
toDataURL(type?: string, quality?: number): Promise<string>;
toBlob(): Promise<Blob>; // Web 专用
toTempFilePath(): Promise<string>; // 小程序专用
}
```
### 4.2 绘制服务基类(平台无关)
```typescript
abstract class BaseDrawService {
protected ctx: CanvasRenderingContext2D;
protected paperWidth: number;
protected paperHeight: number;
// 这些方法仅使用标准 Canvas 2D API,不依赖任何平台
protected drawGrid(rows: number, cols: number): void {
/* ... */
}
protected drawText(
text: string,
x: number,
y: number,
options: TextOptions,
): void {
/* ... */
}
protected drawLine(x1: number, y1: number, x2: number, y2: number): void {
/* ... */
}
protected drawHeader(config: PrintConfig): void {
/* ... */
}
protected drawRoundedRect(
x: number,
y: number,
w: number,
h: number,
r: number,
): void {
/* ... */
}
abstract draw(params: DrawParams): Promise<void>;
}
```
### 4.3 题目生成器模式
```typescript
/**
* 所有题目生成器实现此接口
* 输入配置参数,输出与平台无关的题目数据
* 绘制服务消费这些数据来渲染 Canvas
*/
interface IWorksheetGenerator<TConfig, TData> {
generate(config: TConfig): TData;
getDefaultConfig(): TConfig;
}
// 示例:加法题生成器
class AdditionGenerator
implements IWorksheetGenerator<AdditionConfig, AdditionData>
{
generate(config: AdditionConfig): AdditionData {
const questions = [];
for (let i = 0; i < config.count; i++) {
const a = randomInt(0, config.maxNumber);
const b = randomInt(0, config.maxNumber - a);
questions.push({ a, b, answer: a + b });
}
return { questions, config };
}
}
```
---
## 五、模板引擎架构(核心设计)
> 本章解决一个根本性问题:**每新增一种可打印资料,都需要新写前端页面和绘制代码,如何实现内容的动态扩展?**
### 5.1 问题分析
现有模式下,新增一种题型的完整链路是:
```
新增「时钟练习」题型:
① 新建 clockPractice/ 页面目录(.ts + .wxml + .wxss + .json
② 新写 ClockPracticeDraw 绘制服务(~200-400 行 TS
③ 新写 ClockGenerator 生成器
④ 在 constants 注册题型元数据
⑤ 提交代码 → 审核 → 发版
─────────────────────────
成本:~1-3 天开发 + 1-3 天审核
```
如果要达到产品目标的 150+ 种可打印资料,按这种模式需要写 150+ 个页面和绘制服务,**不可持续**。
### 5.2 核心洞察:排版模式是收敛的
虽然题型看起来有几十上百种,但**排版模式(Layout Pattern)只有有限的几种**
```
┌──────────────────────────────────────────────────────────────────┐
│ │
│ 模式 A:网格计算型 │
│ ┌────────────────────────┐ │
│ │ 3 + 5 = ___ │ 适用:加减法、混合运算、凑十/破十、 │
│ │ 7 - 2 = ___ │ 乘法口诀、数字填空、比大小等 │
│ │ 9 + 1 = ___ │ │
│ │ ... │ 变化点:题目数据、运算符、行列数、 │
│ └────────────────────────┘ 是否带图示辅助 │
│ │
│ 模式 B:配对连线型 │
│ ┌────────────────────────┐ │
│ │ 3 ╌╌╌╌ 🍎🍎🍎 │ 适用:数物连线、连连看、数字点连线、│
│ │ 5 ╌╌╌╌ 🍎🍎 │ 译码连线、配对连线等 │
│ │ 2 ╌╌╌╌ 🍎🍎🍎🍎 │ │
│ └────────────────────────┘ 变化点:左右两列内容、连线方式 │
│ │
│ 模式 C:网格涂色型 │
│ ┌────────────────────────┐ │
│ │ ┌─┬─┬─┬─┬─┐ │ 适用:找数字涂色、按数涂色、方位涂 │
│ │ │3│7│3│5│3│ │ 色、格子仿画、图形符号配对等 │
│ │ ├─┼─┼─┼─┼─┤ │ │
│ │ │2│3│8│3│1│ │ 变化点:网格大小、单元格内容/颜色、 │
│ └────────────────────────┘ 涂色规则说明 │
│ │
│ 模式 D:卡片排列型 │
│ ┌────────────────────────┐ │
│ │ [图片] [图片] │ 适用:涂色卡、闪卡、识字卡、字母卡、│
│ │ 苹果 香蕉 │ 贴纸、翻翻卡等 │
│ │ [图片] [图片] │ │
│ └────────────────────────┘ 变化点:图片源、文字、布局方式 │
│ │
│ 模式 E:描红书写型 │
│ ┌────────────────────────┐ │
│ │ ┌田┐┌田┐┌田┐ │ 适用:练字、笔画练习、字母描红、 │
│ │ │大││大││ │ │ 数字描红、拼音描红等 │
│ │ └──┘└──┘└──┘ │ │
│ └────────────────────────┘ 变化点:字符数据、格子类型、淡化规则│
│ │
│ 模式 F:全幅素材型 │
│ ┌────────────────────────┐ │
│ │ │ 适用:涂色卡(整页)、折纸展开图、 │
│ │ [整页图片/SVG] │ 手工模板、迷宫底图等 │
│ │ │ │
│ └────────────────────────┘ 变化点:素材文件本身 │
│ │
│ 模式 G:序列/排序型 │
│ ┌────────────────────────┐ │
│ │ 2 → __ → 4 → __ → 6 │ 适用:数字排序、缺失数字、颜色找 │
│ │ │ 规律、图形规律等 │
│ └────────────────────────┘ 变化点:序列数据、空位位置 │
│ │
│ 模式 H:时钟/特殊图形型 │
│ ┌────────────────────────┐ │
│ │ 🕐 → 3:00 │ 适用:时钟练习、形状分类、货币认知 │
│ │ 🕑 → ___ │ 等需要特殊图形绘制的题型 │
│ └────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
**结论:只需要实现 6-8 种 TemplateRenderer,就能覆盖绝大多数题型。**
### 5.3 模板引擎架构
```
┌──────────────────────────────────────────────────────────────┐
│ 模板引擎工作流 │
│ │
│ ┌─────────────┐ │
│ │ 题型 JSON │ ← 本地内置 / 云端下发 │
│ │ 配置数据 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌───────────┐ ┌──────────┐ │
│ │ template │ │ generator │ │ layout │ │
│ │ 选择渲染器 │ │ 选择生成器│ │ 排版参数 │ │
│ └─────┬──────┘ └─────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌───────────┐ ┌──────────┐ │ │
│ │ Template │ │Generator │ │ │
│ │ Renderer │ │ 生成数据 │ │ │
│ │ (6-8种) │ │ │ │ │
│ └─────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Canvas 2D 渲染 │ │
│ │ TemplateRenderer.render(data, layout) │
│ └───────────────────┬────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ A4 打印图片 │ │
│ └─────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### 5.4 JSON 配置 Schema
每个题型由一条 JSON 配置定义,包含三个核心部分:「用哪个模板渲染」「用哪个生成器产出数据」「排版参数」。
```typescript
/**
* 题型配置(可云端下发,可前端内置)
*/
interface WorksheetConfig {
// ─── 元数据(展示用)───
id: string;
title: string;
desc: string;
category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft';
subcategory: string;
ageRange: [number, number];
difficulty: 1 | 2 | 3 | 4;
previewImage: string;
tags: string[];
isNew: boolean;
sortOrder: number;
status: 'active' | 'draft' | 'hidden';
// ─── 模板引擎配置(渲染用)───
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:时钟/特殊图形型
/**
* 数据生成器枚举(前端代码实现,新增需发版)
*/
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
/**
* 排版配置
*/
interface LayoutConfig {
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; // 显示名称
type: 'select' | 'slider' | 'switch';
options?: { label: string; value: any }[]; // select 类型的选项
min?: number; // slider 最小值
max?: number; // slider 最大值
defaultValue: any;
}
```
### 5.5 配置示例
**示例 1:算法生成型 — 10 以内加法**
```json
{
"id": "addition-10",
"title": "10以内加法",
"desc": "练习10以内的加法运算,图形化展示",
"category": "math",
"subcategory": "basic-arithmetic",
"ageRange": [4, 6],
"difficulty": 2,
"template": "grid-exercise",
"generator": "arithmetic",
"generatorConfig": {
"operators": ["+"],
"maxNumber": 10,
"count": 20,
"showDots": true,
"showAnswer": false
},
"layoutConfig": {
"columns": 2,
"showBorder": true,
"fontSize": 18,
"showInstruction": true,
"instructionText": "算一算,填上答案"
},
"userConfigurable": [
{
"key": "count",
"label": "题目数量",
"type": "select",
"options": [
{ "label": "10 题", "value": 10 },
{ "label": "20 题", "value": 20 },
{ "label": "30 题", "value": 30 }
],
"defaultValue": 20
},
{
"key": "showDots",
"label": "显示圆点辅助",
"type": "switch",
"defaultValue": true
}
]
}
```
**示例 2:素材型 — 恐龙涂色卡**
```json
{
"id": "coloring-dinosaur-01",
"title": "恐龙涂色卡",
"desc": "可爱恐龙线稿,涂出你喜欢的颜色",
"category": "craft",
"subcategory": "coloring",
"ageRange": [3, 6],
"difficulty": 1,
"template": "full-page-asset",
"generator": "static-asset",
"generatorConfig": {
"assetUrl": "cloud://doodle-xxx/assets/coloring/dinosaur-01.svg",
"assetType": "svg"
},
"layoutConfig": {
"padding": 20,
"showTitle": true
},
"userConfigurable": []
}
```
**示例 3:混合型 — 字母描红**
```json
{
"id": "letter-tracing-uppercase",
"title": "大写字母描红",
"desc": "A-Z 大写字母描红练习",
"category": "english",
"subcategory": "letter-tracing",
"ageRange": [4, 7],
"difficulty": 1,
"template": "tracing-writing",
"generator": "letter-tracing",
"generatorConfig": {
"letters": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"case": "upper",
"lettersPerPage": 4,
"repetitions": 6,
"gridType": "four-line"
},
"layoutConfig": {
"showInstruction": true,
"instructionText": "沿虚线描写字母,注意笔画顺序"
},
"userConfigurable": [
{
"key": "lettersPerPage",
"label": "每页字母数",
"type": "select",
"options": [
{ "label": "2 个", "value": 2 },
{ "label": "4 个", "value": 4 },
{ "label": "6 个", "value": 6 }
],
"defaultValue": 4
}
]
}
```
### 5.6 TemplateRenderer 实现结构
```typescript
/**
* 所有模板渲染器的基类
*/
abstract class BaseTemplateRenderer {
protected ctx: CanvasRenderingContext2D;
protected paper: PaperConfig;
constructor(ctx: CanvasRenderingContext2D, paper: PaperConfig) {
this.ctx = ctx;
this.paper = paper;
}
async render(
data: any,
layout: LayoutConfig,
printConfig: PrintConfig,
): Promise<void> {
this.drawBackground();
this.drawHeader(printConfig);
if (layout.showInstruction) {
this.drawInstruction(layout.instructionText!);
}
await this.drawContent(data, layout);
this.drawFooter(printConfig);
}
protected abstract drawContent(
data: any,
layout: LayoutConfig,
): Promise<void>;
}
/**
* 模式 A:网格计算型渲染器
* 可渲染所有「一行一题」或「多列排布」的练习题
*/
class GridExerciseRenderer extends BaseTemplateRenderer {
protected async drawContent(data: ArithmeticData, layout: LayoutConfig) {
const { columns = 2, fontSize = 16, showBorder = true } = layout;
const { questions } = data;
// 按 columns 分列排布,每题绘制:操作数 + 运算符 + 等号 + 空位
// 如果 showDots,绘制圆点图示辅助
}
}
/**
* 模式 F:全幅素材型渲染器
* 加载图片/SVG → 居中适配到 A4 页面
*/
class FullPageAssetRenderer extends BaseTemplateRenderer {
protected async drawContent(data: AssetData, layout: LayoutConfig) {
const image = await this.loadImage(data.assetUrl);
// 计算等比缩放,居中绘制到 A4 内容区
}
}
```
```
core/draw/
├── base-template-renderer.ts ← 渲染器基类
├── renderers/
│ ├── grid-exercise.ts ← 模式 A:网格计算型
│ ├── match-connect.ts ← 模式 B:配对连线型
│ ├── grid-coloring.ts ← 模式 C:网格涂色型
│ ├── card-layout.ts ← 模式 D:卡片排列型
│ ├── tracing-writing.ts ← 模式 E:描红书写型
│ ├── full-page-asset.ts ← 模式 F:全幅素材型
│ ├── sequence-pattern.ts ← 模式 G:序列/排序型
│ └── special-graphic.ts ← 模式 H:时钟/特殊图形型
├── renderer-registry.ts ← 渲染器注册表
└── template-engine.ts ← 模板引擎入口
```
### 5.7 模板引擎入口
```typescript
/**
* 模板引擎:根据 JSON 配置,选择渲染器和生成器,完成绘制
* 这是通用 worksheet 页面的核心调用逻辑
*/
class TemplateEngine {
private rendererRegistry: Map<TemplateType, BaseTemplateRenderer>;
private generatorRegistry: Map<GeneratorType, IWorksheetGenerator>;
async render(
worksheetConfig: WorksheetConfig,
userParams: Record<string, any>, // 用户在 UI 上调整的参数
ctx: CanvasRenderingContext2D,
printConfig: PrintConfig,
): Promise<void> {
// 1. 合并用户参数到 generatorConfig
const mergedConfig = {
...worksheetConfig.generatorConfig,
...userParams,
};
// 2. 选择生成器,生成题目数据
const generator = this.generatorRegistry.get(worksheetConfig.generator);
const data = generator.generate(mergedConfig);
// 3. 选择渲染器,绘制到 Canvas
const renderer = this.rendererRegistry.get(worksheetConfig.template);
await renderer.render(data, worksheetConfig.layoutConfig, printConfig);
}
}
```
### 5.8 通用 Worksheet 页面
有了模板引擎后,**不再需要为每种题型创建独立页面**。只需一个通用页面:
```
pages/worksheet/worksheet ← 通用题型生成页
```
```typescript
// pages/worksheet/worksheet.ts(伪代码)
createPage({
onLoad(options) {
const worksheetId = options.id;
// 从本地缓存或云端获取该题型的 JSON 配置
const config = WorksheetService.getConfig(worksheetId);
this.setData({
title: config.title,
userFields: config.userConfigurable, // 动态渲染参数面板
userParams: getDefaultParams(config),
});
},
onCanvasReady() {
this.drawCanvas();
},
async drawCanvas() {
await TemplateEngine.render(
this.config,
this.data.userParams,
this.ctx,
getApp().globalData.printConfig,
);
},
onParamChange(e) {
// 用户调整参数 → 重新绘制
this.setData({ userParams: { ...this.data.userParams, ...e.detail } });
this.drawCanvas();
},
onRefresh() {
// 换一批 → 重新生成+绘制
this.drawCanvas();
},
});
```
### 5.9 什么能动态更新 vs 什么需要发版
```
┌────────────────────────────────────────────────────────────────┐
│ │
│ ✅ 可动态更新(修改 JSON 配置 / 上传素材,无需发版) │
│ ────────────────────────────────────────────────── │
│ • 已有模板+已有生成器 组合出的新题型 │
│ (例:用 grid-exercise + arithmetic 配出「50以内减法」) │
│ • 修改题型参数(数量范围、难度、显示选项等) │
│ • 上下架/排序/分类调整/标签修改 │
│ • 新增素材型内容(涂色卡、折纸模板、闪卡图片等) │
│ • 修改页眉/页脚样式配置 │
│ • 新增分类/子分类 │
│ │
│ ❌ 需要发版(新增前端代码) │
│ ────────────────────────────────────────────────── │
│ • 全新的排版模板(TemplateRenderer
│ • 全新的生成算法(Generator) │
│ • 新的用户交互模式 │
│ • 新的 UI 组件 │
│ │
│ 📊 预估覆盖率 │
│ ────────────────────────────────────────────────── │
│ • 实现 8 种模板 + 15 种生成器后 │
│ • 约 80% 的新题型可通过 JSON 配置动态上线 │
│ • 剩余 20% 需要新模板/生成器,但复用已有基础代码 │
│ │
└────────────────────────────────────────────────────────────────┘
```
### 5.10 存量题型迁移策略
现有 40+ 种题型已经稳定运行,**不建议一次性全部推翻重写**。推荐渐进式迁移:
```
阶段 1(与模板引擎同步开发):
├── 新题型全部走模板引擎
└── 旧题型保持原有专用 DrawService,正常运行
阶段 2(按模板类型逐批迁移):
├── 批次 1:所有 grid-exercise 类
│ └── addition, subtraction, calculationPractice,
│ missingNumber, compare, makeTen, breakTen... (~15 种)
├── 批次 2:所有 match-connect 类
│ └── countMatch, numberObjectMatch, matchConnect,
│ codeConnect, dotConnect... (~6 种)
├── 批次 3:所有 grid-coloring 类
│ └── numberFind, positionColoring, gridDrawing,
│ shapeSymbol, colorPattern... (~8 种)
├── 批次 4tracing-writing 类
│ └── copyBook, numberWrite... (~2 种)
└── 批次 5:其余类型
路由兼容:
旧页面路径不变,内部逻辑替换为 TemplateEngine.render()
或:旧路径 redirect 到通用 worksheet 页面
```
---
## 六、后端概要设计
> **现阶段(至 2026-09-15)**:云数据库、云存储、云函数及调用策略的**完整设计**见 → [小程序云开发方案](./小程序云开发方案.md)。
> **阶段到期后可选迁移**:NestJS 项目结构、Prisma、REST API、部署等见 → [后端部署方案](./后端部署方案.md)。
### 6.1 现阶段:微信云开发(摘要)
| 组件 | 技术 | 说明 |
| ----- | ---------------------------- | ----------------------------------- |
| 数据 | 云数据库(文档型) | worksheets / categories / users 等集合 |
| 文件 | 云存储(COS) | 预览图、涂色线稿、字体等 |
| 逻辑 | 云函数(Node.js | 统一入口路由、业务 action |
| 小程序接入 | `wx.cloud` + `cloud-adapter` | 与业务服务层解耦,便于日后替换为 HTTP |
### 6.2 远期可选:自建 NestJS(摘要)
| 组件 | 技术 | 说明 |
| --- | --------------------- | ---------------- |
| 应用 | NestJS + TypeScript | RESTful API,多端统一 |
| 数据 | MySQL 8.0 + Prisma | 关系型存储与迁移 |
| 文件 | 本地磁盘 + Nginx | 静态素材 URL 由自有域名提供 |
| 鉴权 | 微信 code2Session + JWT | 小程序与 Web 可共用逻辑 |
(接口路径、表结构、部署步骤等以 [后端部署方案](./后端部署方案.md) 为准,此处不重复。)
### 6.3 数据加载策略(两阶段一致的产品策略)
无论后端是云开发还是 HTTP API,**客户端侧优先级**保持一致:
```
┌────────────────────────────────────────────────────────────┐
│ 数据加载策略 │
│ │
│ 题型配置数据 (worksheets/categories) │
│ ┌────────────────────────────────────────┐ │
│ │ 优先级 1: 本地缓存(wx.Storage │ │
│ │ 优先级 2: 云端拉取(云 DB 或 HTTP API) │ │
│ │ 优先级 3: 前端内置兜底数据 │ ← 保证离线可用 │
│ └────────────────────────────────────────┘ │
│ │
│ 缓存策略: │
│ • 首次启动:云端拉取 → 写入本地缓存 │
│ • 后续启动:先用缓存渲染 → 后台静默更新 │
│ • 缓存有效期:24 小时 │
│ • 无网络:使用本地缓存或内置兜底 │
└────────────────────────────────────────────────────────────┘
```
### 6.4 静态素材 URL(现阶段)
素材使用 **云存储 fileID / 临时 URL**,例如:
```
cloud://doodle-xxx.xxxx/assets/coloring/animals/dinosaur-01.svg
```
(目录规划见 [小程序云开发方案](./小程序云开发方案.md) 第六节。迁移自建后改为 `https://你的域名/static/...`。)
---
## 七、关键技术方案
### 7.1 Canvas 渲染架构(改进)
**现有问题**:预览区尺寸初始化和 A4 导出尺寸是两阶段,`setPaper()` 会重设 Canvas 尺寸。
**改进方案**
```
┌─────────────────────────────────────────────────────┐
│ Canvas 渲染管线 │
│ │
│ ① 逻辑坐标系统(所有 Draw 服务统一使用) │
│ 固定使用 A4 逻辑坐标: 595 × 842 │
│ 所有绘制操作都在这个坐标系下 │
│ │
│ ② 预览渲染 │
│ Canvas 物理尺寸 = 容器宽度 × dpr │
│ ctx.scale(容器宽度/595 × dpr, ...) │
│ → 绘制服务使用 A4 坐标 → 自动缩放到预览区 │
│ │
│ ③ 导出渲染 │
│ Canvas 物理尺寸 = 1240 × 1754 (150DPI A4) │
│ ctx.scale(1240/595, 1754/842) │
│ → 同一套绘制代码 → 导出高清图 │
│ │
│ 好处: │
│ • Draw 服务完全不感知预览/导出差异 │
│ • 同一套代码在小程序和 Web 上直接可用 │
│ • 预览和导出结果 100% 一致 │
└─────────────────────────────────────────────────────┘
```
### 7.2 图片加载适配
```typescript
// platform/image-adapter.ts — 小程序实现
class WxImageAdapter implements IImageAdapter {
async loadImage(canvas: WechatMiniprogram.Canvas, src: string) {
const img = canvas.createImage();
return new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
}
// Web 端适配见 PC-Web端技术预案.md
```
### 7.3 导出方案
```
核心路径:客户端 Canvas 渲染 → PNG 图片 → 保存到相册
┌─────────────────────────────────────────────────────┐
│ 为什么选择 PNG 保存到相册作为主路径: │
│ │
│ 1. 用户认知成本最低 │
│ 相册是手机用户最熟悉的文件存储位置 │
│ PDF 保存后,大量用户找不到文件在哪里 │
│ │
│ 2. 打印路径最短 │
│ 相册 → 手机连打印机 → 直接打印 │
│ 无需额外 App 打开 PDF │
│ │
│ 3. 分享最便捷 │
│ 微信聊天直接发图片,接收方零门槛查看 │
│ │
│ 4. 零服务器成本 │
│ Canvas 本地渲染,不消耗云函数算力与云存储(主路径) │
│ 用户量增长不会带来额外成本 │
│ │
│ 5. 技术架构成熟 │
│ 现有 Canvas → PNG 链路已完善且稳定 │
└─────────────────────────────────────────────────────┘
批量生成方案(P4):
→ 一次渲染多张不同题目 → 批量保存到相册
→ 用户打印时多选图片即可
PDF 导出方案(远期增值功能,会员专属):
小程序:客户端渲染多张 PNG → 上传云存储 → 云函数 pdfkit 合并为 PDF(迁移自建后同逻辑可落在 NestJS)
定位:会员订阅的差异化权益,非核心路径
```
### 7.4 涂色卡/迷宫等素材型内容方案
```
┌──────────────────────────────────────────────────────┐
│ 两类内容的技术方案差异 │
│ │
│ 类型 A:算法生成型(现有主力) │
│ ┌──────────────────────────────────────┐ │
│ │ 前端算法随机生成题目数据 │ │
│ │ → DrawService 渲染到 Canvas │ │
│ │ → 导出图片 │ │
│ │ │ │
│ │ 适用:数学题、专注力题、字母练习等 │ │
│ │ 优点:无限变化、零存储成本 │ │
│ │ 开发方式:写 Generator + DrawService │ │
│ └──────────────────────────────────────┘ │
│ │
│ 类型 B:素材模板型(需扩展) │
│ ┌──────────────────────────────────────┐ │
│ │ 设计师制作 SVG/PNG 线稿 │ │
│ │ → 上传到云存储 │ │
│ │ → 前端加载 + Canvas 排版到 A4 │ │
│ │ → 导出图片 │ │
│ │ │ │
│ │ 适用:涂色卡、折纸、手工、贴纸等 │ │
│ │ 优点:品质高、风格可控 │ │
│ │ 开发方式:素材管理 + 通用排版渲染器 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 类型 C:混合型 │
│ ┌──────────────────────────────────────┐ │
│ │ 算法生成结构 + 素材填充 │ │
│ │ │ │
│ │ 适用:迷宫(算法生路径+主题皮肤) │ │
│ │ 找不同(模板+随机差异点) │ │
│ │ 闪卡(模板+词库数据) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
## 十、成本预估
### 10.1 后端费用
```
现阶段(微信云开发,至 2026-09-15 复盘):
┌────────────────────────────────────────┐
│ 免费额度内:云数据库 / 云存储 / 云函数 │
│ 预估早期 DAU 下可落在免费额度内 │
│ 超出后按套餐计费(如 19.9 元/月起) │
│ │
│ 详见 → 小程序云开发方案.md 第七节 │
└────────────────────────────────────────┘
到期后若迁移自建(参考 后端部署方案.md):
┌────────────────────────────────────────┐
│ 已有云服务器可做到增量 ¥0(仅运维) │
│ 需在 2026-09-15 前对比两路径总成本 │
└────────────────────────────────────────┘
```
### 10.2 开发资源
```
开发投入估算:
┌──────────────────────────────────────────────────┐
│ Phase 1: ~20 人时(基础重构 + 修复技术债) │
│ Phase 2: ~60 人时(模板引擎 + 首页重构 + 配置化) │
│ Phase 3: ~40 人时(模板扩展 + 新内容 + 存量迁移) │
│ Phase 4: ~30 人时(用户体系 + 体验升级) │
│ ────────────────────── │
│ 总计: ~150 人时 │
│ (1 人兼职 ~10 周可完成) │
│ │
│ 注:模板引擎上线后,新增内容仅需 JSON 配置 │
│ + 素材上传,边际成本趋近于零 │
└──────────────────────────────────────────────────┘
```
---
## 十一、技术风险与应对
| 风险 | 影响 | 应对方案 |
| ------------- | --------- | ----------------------------------------------------------------------- |
| 云开发免费额度不足 | 功能受限或需付费 | ① 优化查询与缓存 ② 控制云函数调用 ③ **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` | 封装 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,保持;业务组件自研 |
| 后端方案 | 云开发 / 自有云服务器 | **现阶段云开发(至 2026-09-15** | 详见 [小程序云开发方案](./小程序云开发方案.md);到期后可选迁移 [后端部署方案](./后端部署方案.md)NestJS |
| 导出方案 | PNG 保存相册 / PDF | **PNG 为主,PDF 为增值** | PNG 保存到相册是手机端最短路径(用户教育成本低、零服务器开销);PDF 作为远期会员增值功能,服务端 pdfkit 合并,Web 用 jsPDF |
| Monorepo | pnpm workspace / Turborepo | **暂不采用** | 当前只有小程序,过早引入增加复杂度;core/ 作为目录组织,未来再拆包 |