feat: worksheet发布功能开发

This commit is contained in:
R524809
2026-04-24 17:49:11 +08:00
parent 5106dbbd7c
commit bb69d5c49d
24 changed files with 2540 additions and 1014 deletions
+95 -83
View File
@@ -18,7 +18,7 @@
→ ② 放入 node-tools/entranceInput/<分类>/
→ ③ 运行 processEntrancePicture.js 裁剪缩放
→ ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/
→ ⑤ 手动编辑 category.data.ts 添加元数据(title、subtitle、path、img 等)
→ ⑤ 手动编辑 category.data.ts 添加元数据(title、desc、path、previewImg 等)
→ ⑥ 提交代码 → 审核 → 发版
```
@@ -67,10 +67,11 @@
│ │ 副标题: 四宫格每日字母打卡练习 │ │
│ │ 分类: english │ │
│ │ 路径: /englishPages/letterTracing/... │ │
│ │ 难度: beginner │ │
│ │ 难度: 1 │ │
│ │ 年龄: 4-7岁 │ │
│ │ 标签: [字母, 描红, 打卡] │ │
│ │ [预览图缩略图] │ │
│ │ 裁剪图标:头部和底部、仅头部、不裁剪 │ │
│ │ │ │
│ │ ─ 可编辑字段(允许微调) ─ │ │
│ │ │ │
@@ -132,26 +133,20 @@ WXML 中条件渲染:
*/
interface PublishMeta {
// ─── 必填字段 ───
id: string; // 唯一标识,如 'letter-tracing-daily-checkin'
title: string; // 显示标题
subtitle: string; // 显示副标题
id: string; // 唯一标识,如 'letter-tracing-daily-checkin'
title: string; // 显示标题
desc: string; // 显示描述
category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft';
subcategory: string; // 子分类
path: string; // 页面完整路径(含参数)
subcategory: string; // 子分类
path: string; // 页面完整路径(含参数)
ageMin: number; // 适龄最小值
ageMax: number; // 适龄最大值
difficulty: 1 | 2 | 3 | 4; // 1-4:入门、基础、进阶、挑战
// ─── 选填字段(有默认值)───
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<string, any>;
layoutConfig?: Record<string, any>;
previewImg?: string; // 上传入口图后回填的云存储 fileID
tags?: string[]; // 搜索标签
sortOrder?: number; // 排序权重
}
```
@@ -175,13 +170,13 @@ getPublishMeta(): PublishMeta {
return {
id: this.data.currentId,
title: drawService.title,
subtitle: drawService.subtitle,
desc: drawService.desc,
category: 'english',
subcategory: 'letter-tracing',
path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`,
icon: '🔠',
ageRange: [4, 7],
difficulty: 'beginner',
ageMin: 4,
ageMax: 7,
difficulty: 1,
tags: ['字母', '描红', '英语'],
};
}
@@ -194,7 +189,7 @@ getPublishMeta(): PublishMeta {
```typescript
/**
* 从预览 Canvas 导出入口图
*
*
* 处理流程:
* 1. 从 preview-card 导出完整 A4 临时文件
* 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐)
@@ -204,7 +199,12 @@ getPublishMeta(): PublishMeta {
async function exportEntranceImage(
canvas: WechatMiniprogram.Canvas,
ctx: CanvasRenderingContext2D,
paperConfig: { headerHeight: number; footerHeight: number; width: number; height: number }
paperConfig: {
headerHeight: number;
footerHeight: number;
width: number;
height: number;
},
): Promise<string> {
// 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致
const cropTop = paperConfig.headerHeight;
@@ -220,9 +220,15 @@ async function exportEntranceImage(
canvas.width = ENTRANCE_WIDTH;
canvas.height = targetH;
ctx.drawImage(
canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转)
0, cropTop, sourceW, sourceH, // 源区域:去掉页眉页脚
0, 0, ENTRANCE_WIDTH, targetH // 目标区域:缩放到标准宽度
canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转)
0,
cropTop,
sourceW,
sourceH, // 源区域:去掉页眉页脚
0,
0,
ENTRANCE_WIDTH,
targetH, // 目标区域:缩放到标准宽度
);
const tempPath = await canvasToTempFilePath(canvas, {
@@ -246,21 +252,24 @@ async function exportEntranceImage(
async function uploadEntranceImage(
tempFilePath: string,
category: string,
id: string
id: string,
): Promise<string> {
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
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<void> {
async function publishWorksheet(
meta: PublishMeta,
imageFileID: string,
): Promise<void> {
const db = wx.cloud.database();
const collection = db.collection('worksheets');
@@ -271,7 +280,7 @@ async function publishWorksheet(meta: PublishMeta, imageFileID: string): Promise
publishedAt: db.serverDate(),
updatedAt: db.serverDate(),
version: 1,
publishedBy: 'debug', // 标记为 debug 发布
publishedBy: 'debug', // 标记为 debug 发布
};
// upsert:如果已存在则更新,不存在则创建
@@ -312,12 +321,16 @@ async function onDebugPublish(this: any): Promise<void> {
try {
// 3. 导出入口图
const tempPath = await exportEntranceImage(
this.canvas, this.ctx, this.paperConfig
this.canvas,
this.ctx,
this.paperConfig,
);
// 4. 上传图片到云存储
const fileID = await uploadEntranceImage(
tempPath, meta.category, meta.id
tempPath,
meta.category,
meta.id,
);
// 5. 写入云数据库
@@ -352,33 +365,32 @@ async function onDebugPublish(this: any): Promise<void> {
### 4.2 category.data.ts 的角色变化
| 阶段 | category.data.ts 的作用 |
|------|------------------------|
| **当前** | 唯一数据源(硬编码所有题型信息) |
| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) |
| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 |
| 阶段 | 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` | 版本号(递增) |
| PublishMeta 字段 | worksheets 集合字段 | 说明 |
| ---------------- | ------------------- | ------------------------ |
| `id` | `_id` | 业务唯一标识 |
| `title` | `title` | 显示标题 |
| `desc` | `desc` | 副标题 |
| `category` | `category` | 所属大类 |
| `subcategory` | `subcategory` | 子分类,当前默认为空 |
| `path` | `path` | 小程序页面路径 |
| `previewImg` | `previewImg` | 云存储 fileID |
| `ageMin` | `ageMin` | 适龄最小值 |
| `ageMax` | `ageMax` | 适龄最大值 |
| `difficulty` | `difficulty` | 难度 1–4(入门、基础、) |
| `tags` | `tags` | 搜索标签 |
| `sortOrder` | `sortOrder` | 排列顺序 |
| — | `createdAt` | 创建时间 |
| — | `updatedAt` | 更新时间 |
---
@@ -414,9 +426,9 @@ cloud://doodle-xxx/
```typescript
// 三重保障
const canPublish =
isDebugPublishEnabled() // ① envVersion === 'develop'
&& isDevBypassLimits() // ② 与下载绕过逻辑一致
&& wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启
isDebugPublishEnabled() && // ① envVersion === 'develop'
isDevBypassLimits() && // ② 与下载绕过逻辑一致
wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启
```
### 6.2 云端安全规则
@@ -450,12 +462,12 @@ exports.main = async (event, context) => {
### 6.3 数据保护
| 措施 | 说明 |
|------|------|
| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` |
| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 |
| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 |
| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 |
| 措施 | 说明 |
| ---------------- | --------------------------------------------------- |
| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` |
| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 |
| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 |
| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 |
---
@@ -571,26 +583,26 @@ Debug 发布 → 云端数据 → 线上可见
## 十、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 |
| **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` |
| **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 |
| **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 |
| **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 |
| **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 |
| 风险 | 影响 | 应对 |
| ---------------------- | ----------------------- | --------------------------------------------------------- |
| **误操作发布测试数据** | 污染线上列表 | 默认 `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/` | 存放入口图 |
| 现有模块 | 本方案的关联 |
| ------------------------------------------ | ------------------------------------------------------ |
| `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/` | 存放入口图 |