1226 lines
69 KiB
Markdown
1226 lines
69 KiB
Markdown
# 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-adapter(wx.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 种)
|
||
├── 批次 4:tracing-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/ 作为目录组织,未来再拆包 |
|