# Doodle Mini — Debug 模式内容发布方案 > 版本:v1.0 > 最后更新:2026-04-23 > 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md) --- ## 一、方案定位与动机 ### 1.1 当前痛点 每新增一种可打印资料,从开发完成到上线需要经历以下**手动步骤**: ``` 开发页面 + Draw 逻辑 → ① 在预览中截图或导出入口图原图 → ② 放入 node-tools/entranceInput/<分类>/ → ③ 运行 processEntrancePicture.js 裁剪缩放 → ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/ → ⑤ 手动编辑 category.data.ts 添加元数据(title、subtitle、path、img 等) → ⑥ 提交代码 → 审核 → 发版 ``` 其中 ①~⑤ 是重复性机械劳动,且容易出错(路径拼错、忘记更新数据文件、图片尺寸不一致等)。 ### 1.2 核心洞察 **当一个页面开发调试完成时,所有需要的信息已经就绪**: - **页面路径** — 就是当前正在调试的页面 URL - **标题 / 副标题** — 已写在绘制逻辑或配置中 - **入口图片** — 预览 Canvas 上正在渲染的就是 - **分类 / 难度 / 年龄段 / 标签** — 已在页面配置中定义 在这个时刻一键上传,是**成本最低、数据最准确**的做法。 ### 1.3 方案目标 在 **develop / devtools 环境**的预览页中增加「发布到云端」按钮,点击后自动: 1. 从 Canvas 导出入口图 → 裁剪 → 压缩 2. 上传图片到云存储 3. 组装页面元数据 → 写入云数据库 `worksheets` 集合 4. 线上小程序通过缓存策略拉取云端数据,新内容**无需发版即可上线** **不需要开发独立后台系统,不需要手动录入数据。** --- ## 二、整体流程 ``` ┌──────────────────────────────────────────────────────────────────┐ │ Debug 模式发布流程 │ │ │ │ ① 开发者在 develop / devtools 环境下完成页面开发 │ │ (绘制逻辑调试完毕,预览效果满意) │ │ │ │ ② 预览区底部出现 [📤 发布到云端] 按钮 │ │ (仅 develop / devtools 环境可见,线上版本不显示) │ │ │ │ ③ 点击后弹出确认面板,预览即将上传的数据: │ │ ┌─────────────────────────────────────────┐ │ │ │ ID: letter-tracing-daily-checkin │ │ │ │ 标题: 每日打卡 │ │ │ │ 副标题: 四宫格每日字母打卡练习 │ │ │ │ 分类: english │ │ │ │ 路径: /englishPages/letterTracing/... │ │ │ │ 难度: beginner │ │ │ │ 年龄: 4-7岁 │ │ │ │ 标签: [字母, 描红, 打卡] │ │ │ │ [预览图缩略图] │ │ │ │ │ │ │ │ ─ 可编辑字段(允许微调) ─ │ │ │ │ │ │ │ │ 状态: ○ draft(默认) ○ active │ │ │ │ │ │ │ │ [取消] [确认发布] │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ④ 确认发布后,自动执行: │ │ a. Canvas 导出 → 裁剪页眉页脚 → 压缩至 ≤200KB │ │ b. 上传图片到云存储 │ │ cloud://xxx/assets/previews//.jpg │ │ c. 组装 WorksheetConfig → upsert 到云数据库 worksheets 集合 │ │ d. 显示「发布成功 ✅」 │ │ │ │ ⑤ 线上小程序按缓存策略拉取新数据,内容自动出现 │ │ (或在 debug 管理页手动将 status 从 draft 切为 active) │ └──────────────────────────────────────────────────────────────────┘ ``` --- ## 三、技术设计 ### 3.1 环境门控 发布功能**仅在开发环境**可见,**绝不暴露给普通用户**。 ```typescript /** * 判断当前是否为开发环境,可执行 debug 发布操作。 * 复用 downloadPrint.ts 中 isDevBypassLimits 的判断模式。 */ function isDebugPublishEnabled(): boolean { const accountInfo = wx.getAccountInfoSync(); const { envVersion } = accountInfo.miniProgram; // develop: 开发版;trial: 体验版;release: 正式版 return envVersion === 'develop'; } ``` WXML 中条件渲染: ```xml ``` ### 3.2 页面元数据约定 每个 draw 页面需提供 `getPublishMeta()` 方法,返回标准化的发布元数据。 ```typescript /** * 发布元数据接口 * 各 draw 页面实现此接口,提供自身的配置信息 */ interface PublishMeta { // ─── 必填字段 ─── id: string; // 唯一标识,如 'letter-tracing-daily-checkin' title: string; // 显示标题 subtitle: string; // 显示副标题 category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft'; subcategory: string; // 子分类 path: string; // 页面完整路径(含参数) // ─── 选填字段(有默认值)─── icon?: string; // Emoji 图标 ageRange?: [number, number]; // 适用年龄,默认 [3, 8] difficulty?: 'beginner' | 'basic' | 'intermediate' | 'advanced'; tags?: string[]; // 搜索标签 sortOrder?: number; // 排序权重 status?: 'draft' | 'active' | 'hidden'; // 默认 'draft' // ─── 模板引擎相关(可选,未来扩展)─── template?: string; generator?: string; generatorConfig?: Record; layoutConfig?: Record; } ``` 在 `pageMixin` 中约定调用方式: ```typescript // pageMixin 中新增 debugPublishMixin: { getPublishMeta(): PublishMeta { // 子类覆写此方法 throw new Error('页面未实现 getPublishMeta()'); } } ``` 各页面实现示例(letterTracing): ```typescript getPublishMeta(): PublishMeta { const drawService = this.drawService; return { id: this.data.currentId, title: drawService.title, subtitle: drawService.subtitle, category: 'english', subcategory: 'letter-tracing', path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`, icon: '🔠', ageRange: [4, 7], difficulty: 'beginner', tags: ['字母', '描红', '英语'], }; } ``` ### 3.3 入口图导出与处理 复用已有的 `preview-card` 组件的 `exportToTempFile` 能力,再做裁剪处理。 ```typescript /** * 从预览 Canvas 导出入口图 * * 处理流程: * 1. 从 preview-card 导出完整 A4 临时文件 * 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐) * 3. 缩放至入口图标准宽度(600px) * 4. 导出为 JPEG(quality: 0.85) */ async function exportEntranceImage( canvas: WechatMiniprogram.Canvas, ctx: CanvasRenderingContext2D, paperConfig: { headerHeight: number; footerHeight: number; width: number; height: number } ): Promise { // 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致 const cropTop = paperConfig.headerHeight; const cropBottom = paperConfig.footerHeight; const sourceW = paperConfig.width; const sourceH = paperConfig.height - cropTop - cropBottom; const ENTRANCE_WIDTH = 600; const scale = ENTRANCE_WIDTH / sourceW; const targetH = Math.round(sourceH * scale); // 调整 Canvas 尺寸用于裁剪输出 canvas.width = ENTRANCE_WIDTH; canvas.height = targetH; ctx.drawImage( canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转) 0, cropTop, sourceW, sourceH, // 源区域:去掉页眉页脚 0, 0, ENTRANCE_WIDTH, targetH // 目标区域:缩放到标准宽度 ); const tempPath = await canvasToTempFilePath(canvas, { fileType: 'jpg', quality: 0.85, destWidth: ENTRANCE_WIDTH, destHeight: targetH, }); return tempPath; } ``` > **实际实现说明**:小程序 Canvas 不能直接自引用 `drawImage`,需要先导出为临时文件(`canvasToTempFilePath`),再用 `canvas.createImage()` 加载临时文件,然后在清空的 Canvas 上绘制裁剪区域。具体实现参考 `preview-card` 已有的 `exportToTempFile` 方法。 ### 3.4 云端上传 #### 3.4.1 图片上传到云存储 ```typescript async function uploadEntranceImage( tempFilePath: string, category: string, id: string ): Promise { const cloudPath = `assets/previews/${category}/${id}.jpg`; const res = await wx.cloud.uploadFile({ cloudPath, filePath: tempFilePath, }); return res.fileID; // cloud://doodle-xxx/assets/previews/english/letter-tracing-daily-checkin.jpg } ``` #### 3.4.2 元数据写入云数据库 ```typescript async function publishWorksheet(meta: PublishMeta, imageFileID: string): Promise { const db = wx.cloud.database(); const collection = db.collection('worksheets'); const doc = { ...meta, previewImage: imageFileID, status: meta.status || 'draft', publishedAt: db.serverDate(), updatedAt: db.serverDate(), version: 1, publishedBy: 'debug', // 标记为 debug 发布 }; // upsert:如果已存在则更新,不存在则创建 const existing = await collection.where({ id: meta.id }).get(); if (existing.data.length > 0) { const oldDoc = existing.data[0]; await collection.doc(oldDoc._id).update({ data: { ...doc, version: (oldDoc.version || 0) + 1, updatedAt: db.serverDate(), }, }); } else { await collection.add({ data: doc }); } } ``` ### 3.5 发布流程整合 ```typescript /** * Debug 发布入口 — 挂载在 pageMixin 上 * 由预览页的「发布到云端」按钮触发 */ async function onDebugPublish(this: any): Promise { if (!isDebugPublishEnabled()) return; // 1. 获取页面元数据 const meta: PublishMeta = this.getPublishMeta(); // 2. 弹出确认面板(展示即将发布的数据,允许微调) const confirmed = await showPublishConfirmDialog(meta); if (!confirmed) return; wx.showLoading({ title: '发布中...' }); try { // 3. 导出入口图 const tempPath = await exportEntranceImage( this.canvas, this.ctx, this.paperConfig ); // 4. 上传图片到云存储 const fileID = await uploadEntranceImage( tempPath, meta.category, meta.id ); // 5. 写入云数据库 await publishWorksheet(meta, fileID); wx.hideLoading(); wx.showToast({ title: '发布成功 ✅', icon: 'success' }); } catch (err) { wx.hideLoading(); wx.showModal({ title: '发布失败', content: JSON.stringify(err), showCancel: false, }); } } ``` --- ## 四、云端数据与现有架构的衔接 ### 4.1 数据加载优先级(保持不变) 与 [技术架构设计文档](./技术架构设计文档.md) 第 6.3 节、[小程序云开发方案](./小程序云开发方案.md) 第五节一致: ``` 优先级 1: 本地缓存(wx.Storage) 优先级 2: 云端拉取(worksheets 集合) ← debug 发布的数据在此 优先级 3: 前端内置兜底(category.data.ts)← 保持稳定兜底 ``` ### 4.2 category.data.ts 的角色变化 | 阶段 | category.data.ts 的作用 | |------|------------------------| | **当前** | 唯一数据源(硬编码所有题型信息) | | **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) | | **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 | ### 4.3 worksheets 集合字段映射 debug 发布写入的字段,与 [小程序云开发方案](./小程序云开发方案.md) §3.3 `worksheets` 集合的字段**完全对齐**: | PublishMeta 字段 | worksheets 集合字段 | 说明 | |-----------------|-------------------|------| | `id` | `id` | 业务唯一标识 | | `title` | `title` | 显示标题 | | `subtitle` | `desc` | 显示描述 | | `category` | `category` | 所属大类 | | `subcategory` | `subcategory` | 子分类 | | `path` | 新增字段 `pagePath` | 小程序页面路径 | | `icon` | 新增字段 `icon` | Emoji 图标 | | `ageRange` | `ageRange` | 适用年龄段 | | `difficulty` | `difficulty` | 难度级别 | | `tags` | `tags` | 搜索标签 | | `sortOrder` | `sortOrder` | 排列顺序 | | `status` | `status` | 上架状态 | | — | `previewImage` | 云存储 fileID | | — | `publishedAt` | 发布时间 | | — | `version` | 版本号(递增) | --- ## 五、云存储目录规划 与 [小程序云开发方案](./小程序云开发方案.md) §6 一致: ``` cloud://doodle-xxx/ ├── assets/ │ └── previews/ ← debug 发布的入口图存放于此 │ ├── math/ │ │ ├── number-find.jpg │ │ ├── addition-10.jpg │ │ └── ... │ ├── english/ │ │ ├── letter-tracing-single.jpg │ │ ├── letter-tracing-daily-checkin.jpg │ │ └── ... │ ├── puzzle/ │ ├── chinese/ │ └── craft/ ``` 命名规则:`.jpg`,与 `PublishMeta.id` 一致,便于查找和管理。 --- ## 六、安全与权限控制 ### 6.1 客户端门控 ```typescript // 三重保障 const canPublish = isDebugPublishEnabled() // ① envVersion === 'develop' && isDevBypassLimits() // ② 与下载绕过逻辑一致 && wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启 ``` ### 6.2 云端安全规则 在云数据库安全规则中,`worksheets` 集合限制写入权限: ```json { "worksheets": { ".write": false, ".read": true } } ``` debug 发布通过**云函数**中转,云函数内校验 `openId` 白名单: ```javascript // 云函数 debugPublish exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext(); const ADMIN_OPENIDS = ['开发者的openid']; if (!ADMIN_OPENIDS.includes(OPENID)) { throw new Error('无权限执行此操作'); } // 执行 upsert 操作... }; ``` ### 6.3 数据保护 | 措施 | 说明 | |------|------| | 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` | | 版本递增 | 每次更新 `version + 1`,可追踪变更历史 | | publishedBy 标记 | `publishedBy: 'debug'` 区分来源 | | 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 | --- ## 七、辅助工具 ### 7.1 syncFromCloud 脚本 在 `node-tools/` 中新增脚本,发版前从云数据库拉取最新数据,同步回 `category.data.ts` 作为兜底数据。 ``` Debug 发布 → 云端数据 → 线上可见 ↓ syncFromCloud.js ← 发版前运行 ↓ category.data.ts 更新 ← 兜底数据同步 ↓ 下次发版包含 ``` ```javascript // node-tools/src/syncFromCloud.js(伪代码) // 通过云开发 HTTP API 或管理端 SDK 拉取 worksheets 集合 // 按 category 分组 → 生成 TypeScript 代码 → 写入 category.data.ts ``` ### 7.2 Debug 管理页扩展 在已有的 `supportPages/debug/debug` 页面中增加 tab,提供简易内容管理: ``` ┌─────────────────────────────────────────────┐ │ Debug 工具页 │ │ │ │ [调试配置] [已发布内容] [系统信息] │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ 已发布内容列表 │ │ │ │ │ │ │ │ ● letter-tracing-single │ │ │ │ 状态: active 版本: 3 英语 │ │ │ │ [编辑] [下架] │ │ │ │ │ │ │ │ ● letter-tracing-daily-checkin │ │ │ │ 状态: draft 版本: 1 英语 │ │ │ │ [激活] [编辑] [删除] │ │ │ │ │ │ │ │ ● addition-10 │ │ │ │ 状态: active 版本: 5 数学 │ │ │ │ [编辑] [下架] │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ``` --- ## 八、实施计划 ### Phase 1:基础发布能力(约 0.5~1 天) - [ ] `pageMixin` 中新增 `getPublishMeta()` 接口约定 - [ ] 实现 `isDebugPublishEnabled()` 环境判断 - [ ] 实现入口图导出 + 裁剪逻辑 - [ ] 实现云存储上传 + 云数据库写入 - [ ] 在 `preview-card` 或 `preview-footer-actions` 中添加发布按钮(条件渲染) ### Phase 2:确认面板与安全(约 0.5 天) - [ ] 发布前确认弹窗(数据预览 + 可编辑字段) - [ ] 云函数安全校验(openId 白名单) - [ ] 默认 draft 状态 + 版本管理 ### Phase 3:数据消费侧适配(约 0.5~1 天) - [ ] `category.ts`(或 `worksheet-service`)增加从云 DB 拉取逻辑 - [ ] 实现缓存策略(本地缓存 → 云端 → 内置兜底) - [ ] 入口图从云存储 fileID 获取临时 URL 展示 ### Phase 4:辅助工具(约 0.5 天) - [ ] `syncFromCloud.js` 脚本 - [ ] Debug 管理页扩展(列表 + 状态管理) **总计预估:2~3 天** --- ## 九、与模板引擎的衔接(远期) 当 [技术架构设计文档](./技术架构设计文档.md) 第五章所述模板引擎落地后,debug 发布方案可进一步升级: ``` ┌─────────────────────────────────────────────────────────────────┐ │ 模板引擎 + Debug 发布(远期形态) │ │ │ │ ① 在通用 worksheet 页面中: │ │ 选择模板 + 配置生成器参数 → 实时预览 │ │ │ │ ② 效果满意后点击「发布」: │ │ 自动上传: │ │ • JSON 配置(template + generator + generatorConfig + layout)│ │ • 入口预览图 │ │ • 元数据(title、tags、ageRange 等) │ │ │ │ ③ 新题型无需任何前端代码改动即可上线 │ │ (前提:使用已有的 TemplateRenderer + Generator 组合) │ │ │ │ ④ 配合 80% 新题型可动态上线的目标 │ │ debug 发布成为主要的内容上线方式 │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 十、风险与应对 | 风险 | 影响 | 应对 | |------|------|------| | **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 | | **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` | | **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 | | **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 | | **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 | | **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 | --- ## 附录 A:与现有代码的关系 | 现有模块 | 本方案的关联 | |---------|------------| | `preview-card` 组件 | 复用 `exportToTempFile`,新增裁剪逻辑 | | `pageMixin.ts` | 新增 `getPublishMeta()` 约定和 `onDebugPublish()` 方法 | | `downloadPrint.ts` / `isDevBypassLimits()` | 复用环境判断模式 | | `category.data.ts` | 角色从「唯一数据源」变为「兜底数据源」 | | `supportPages/debug/debug` | 扩展内容管理 tab | | `node-tools/processEntrancePicture.js` | 裁剪参数对齐;流程被 debug 发布替代 | | 云数据库 `worksheets` 集合 | 写入发布数据(字段与云开发方案对齐) | | 云存储 `assets/previews/` | 存放入口图 |