# R4 上传 + 词云生成 + 线稿 + 下单后 WCD 生产任务(feat/r4-upload-wordcloud) **Goal:** 打通用户图片上传、词云任务生成、DIY 贴纸线稿处理、异步任务状态轮询, 并在**下单后把订单对应的设计以 WCD 格式投递到词云平台**形成生产任务; 这是四条路线中唯一复用 `wordcloud` 项目的路线,必须通过冻结接口契约的方式隔离它的大量变更。 **Architecture:** 小程序只与 `wxmp_backend` 通信;wxmp_backend 实现 COS 上传凭证、词云适配器、 sketch 接口、任务记录/轮询,以及**由设计数据构造 `.wcd` 包并投递给词云平台**的派单服务; `wordcloud` FastAPI 服务作为外部引擎,只暴露并冻结最小契约。 小程序页面对词云使用轮询进度,不使用 SSE/EventSource。 **Tech Stack:** Taro 3.6 + React 18 + TypeScript;NestJS 11 + Prisma/PostgreSQL + Redis/BullMQ; 腾讯云 COS;wordcloud FastAPI + EfficientWordCloud。 ## 1. 本分支要开发的内容 ### 前端 `wechat_wc` | 文件 | 职责 | |---|---| | `src/utils/api/upload.ts` | `getUploadCredentials/uploadToCos/sketchImage/createWordCloudJob/getWordCloudJob/getWordCloudResult` | | `src/pages/wordcloud/index.tsx` | 真实上传底图、提交名字清单、创建任务、轮询进度、展示结果图并保存相册 | | `src/pages/diy/stickerEdit/index.tsx` | 把占位域名 `https://your-api-domain.com/api/sketch` 换成 `BASE_URL/api/sketch`;保留前端灰度降级 | | `src/types/index.ts` 或 `src/types/upload.ts` | `WordCloudJob/WordCloudStatus/SketchResult/CosCredentials` | | `src/utils/request.ts` 或新增 `upload.ts` | 上传文件统一带 token 与业务 header | | `src/types/index.ts`(designData) | 按 `docs/design-data-contract-v1.md` 冻结结构补充字段(详见下文 R2 依赖) | > 下单后的 WCD 投递完全发生在后端,**前端不新增页面与接口**;前端只需保证 > `designData` 按冻结契约携带持久图片 URL、布局与名单快照。 ### 后端 `wxmp_backend` | 模块 | 内容 | |---|---| | `upload` | `GET /api/upload/credentials?key=...` 返回 STS 临时凭证或 COS 预签名 URL;`Upload` 表记录 | | `wordcloud`(新增) | `POST /api/wordcloud/generate`、`GET /api/wordcloud/jobs/:id`、`GET /api/wordcloud/jobs/:id/result` | | `wordcloud` 适配器 | 把小程序传入的底图+名字列表转成 wordcloud 契约请求;任务状态入库,用户归属校验 | | **`wordcloud` 派单服务(新增)** | `buildWcdPackage(designData)` 构造 `.wcd` → `dispatchToWordcloud(orderId)` 投递 → 落 `CustomizationTask` → 轮询产物并转存 COS(见 §3) | | `sketch`(新增) | `POST /api/sketch` 接收图片,返回处理后图片 URL;内部可先做基础处理或接外部 AI | | `queue` | BullMQ 消费者把 wordcloud/sketch/生产派单任务状态同步到 `CustomizationTask`/`WordCloudJob` | | `docs/wordcloud-contract.md` | 冻结 wordcloud 接口,含**可选的 WCD 任务输入**,含版本号 | | `config` | `.env` 增加 `WORDCLOUD_API_URL`(词云平台地址),见 §3.4 | | `prisma` | `CustomizationTask` 增加 `orderId`/`wordcloudJobId`(migration) | ### 词云项目 `wordcloud` - 需要冻结的接口:`POST /api/jobs`(现有:`name_list(.xlsx)` 必填;**新增可选 `wcd_file`**)、 `GET /api/jobs/{id}`、`GET /api/jobs/{id}/result`、`GET /api/jobs/{id}/files/{kind}`。 - 其余 canvas、assets、projects、templates 等接口继续按它自己的节奏演进,小程序后端不依赖。 - 若契约不变,wordcloud 内部重构无需通知 R4;契约变更时先升版本号,R4 单独出适配器更新。 ## 2. 词云任务模型 小程序侧采用异步任务模型: ```ts interface WordCloudJob { id: string status: 'queued' | 'running' | 'success' | 'failed' progress: number imageUrl?: string error?: string } ``` 流程: 1. 前端上传底图,拿到 COS URL 或临时文件。 2. `POST /api/wordcloud/generate` 提交底图 + 名字文本,后端创建任务并返回 `jobId`。 3. 前端每 1-2 秒 `GET /api/wordcloud/jobs/:id` 轮询,`success` 后取 `imageUrl`。 4. 结果图建议由 wxmp_backend 转存 COS 后返回微信可下载域名链接。 ## 3. 下单后生产任务:WCD 投递(本分支新增功能) > 场景:用户在 DIY 过程中生成的词云图、贴纸布局最终落在一条设计清单(`design-list`)里。 > 下单之后,订单对应的整套设计必须能**原样恢复到词云平台**,形成可追溯、可复用的 > **生产任务**(用于后续加工/激光雕刻)。传输格式采用词云平台已定义的 `.wcd` > 画布导入导出包(Zip:`manifest.json` + `document.json` + `assets/`)。 ### 3.1 端到端链路 ``` 设计清单 designData(R2 按 design-data-contract-v1.md 冻结结构保存) → R3 POST /api/orders 创建订单 → 订单进入生产(PENDING → PROCESSING;支付未配置期可走幂等触发,见 3.3) → queue 入队 customization 生产任务 → WordCloudService.dispatchToWordcloud(orderId) 1. 读订单关联 design-list 的 designData 2. 贴纸图片持久化(R4 职责,契约束 #1):本地 `wxfile://`/`tmp` 图 → 上传 COS → 回写 `stickers[].src` 为持久 URL 3. buildWcdPackage(designData) → 内存构造 .wcd(布局 → document.json,图片字节 → assets/) 4. POST {WORDCLOUD_API_URL}/api/jobs multipart wcd_file + params 5. 落 CustomizationTask{ orderId, wordcloudJobId, status },开轮询 6. 轮询 GET /api/jobs/{id}/result → 产物(PNG/SVG)转存 COS → 更新 CustomizationTask.status / resultUrl ``` 小程序前端永不接触 wordcloud;WCD 完全由 wxmp_backend 构造与投递。 ### 3.2 WCD 包结构(wxmp_backend 构造,wordcloud 侧已能消费) 后端按 `designData` 构造以下 Zip: ``` {orderNo}.wcd ├── manifest.json // format: "wordcloud-canvas", version: 1 │ // + canvas{width,height,background} + assets[ id/name/type/mimeType/sha256/size ] ├── document.json // CanvasDocument:width/height/background/layers/elements └── assets/. // 每个 sticker/底图的图片字节(从持久 URL 下载) ``` 映射规则(以词云平台 `/api/design-templates/import` 现有实现为准): - `document.canvas` ← 商品 `category.mask` 尺寸 + 背景色。 - `elements[]` ← 底图(`background.src`)作为一个 element;每个贴纸 `{ type:'sticker', assetId: asset-N, x, y, width, height, rotation?, opacity? }`, `assetId` 用包内临时 ID。 - `assets/` ← 每个贴纸图片 + 底图的字节,文件名以包内 `assetId` 开头 (wordcloud 解包根据 `assets/*` 匹配)。 - 名单快照(`designData.wordcloud.names`)写入 `manifest` 的自定义 `meta` 字段, 仅作记录,不影响导入还原。 ### 3.3 触发与幂等 - 推荐:订单进入 `PROCESSING` 时(支付回调确认后)入队 `customization` 任务, 由处理器调用派单服务。 - 支付未配置期:提供 `POST /api/orders/:id/dispatch`(幂等)作为联调/运营触发手段, 同一订单只投递一次(以 `CustomizationTask.orderId` 唯一或状态机约束)。 ### 3.4 环境变量(wxmp_backend) ```dotenv # ── 词云平台(WCD 生产任务)──────────────────────────── # 词云服务(FastAPI)地址;未配置时下单后的 WCD 派单返回“未配置”,不做假成功 WORDCLOUD_API_URL= # 请求 wordcloud 超时(毫秒);可选 WORDCLOUD_TIMEOUT_MS=30000 ``` `config/configuration.ts` 增加 `wordcloud: { apiUrl, timeoutMs }`; `config/validation.schema.ts` 增加 `WORDCLOUD_API_URL: Joi.string().allow('').default('')` (允许为空,与"支付密钥未配置是合法状态"一致)。 ### 3.5 词云平台需新增的能力 - `POST /api/jobs` 支持可选 `wcd_file`(multipart `.wcd`):存在时跳过 `.xlsx` 名单 模式,进入"还原设计 → 生成生产任务"模式;状态机与产物管线复用现有 jobs。 - wordcloud 平台改动点(供其 R4 实现参考,契约先行冻结): 1. `name_list` 与 `wcd_file` 二选一(`wcd_file` 对已有调用向后兼容,纯新增)。 2. 解包 `.wcd` 校验 `format/version`,`assets` 按 SHA-256 去重(复用现有导入逻辑)。 3. `document.json` 归一化后落 `design_documents`,`sticker.assetId` 重映射为真实素材 ID。 4. 创建生产 job,产物沿用 `GET /api/jobs/{id}/result` + `/files/{kind}`。 ### 3.6 设计注意事项 #### DO - DO 由 wxmp_backend 构造并投递 WCD,wordcloud 地址、密钥、内部参数对小程序完全不可见。 - DO `wordcloudJobId → orderId/userId` 关联入库,轮询与结果查询带归属校验。 - DO `WORDCLOUD_API_URL` 未配置时返回结构化“未配置”错误(状态字面量 `not_configured`, 见 `api-contract-v1.md` §8),前端给出可理解提示。 - DO 派单幂等:同一订单重复触发只产生一次投递。 - DO 产物由 wxmp_backend 下载转存 COS 后返回公网 URL,不向小程序暴露 wordcloud 内部地址。 - DO 名单/布局等设计快照写入 `designData`(冻结契约),保证下单后可重建 WCD。 - DO WCD 内部的贴纸图片必须来自持久 URL(COS),打包前先下载字节。 - DO 包大小与单账号投递频率限制(wordcloud 为 CPU 密集任务)。 #### DON'T - DON'T 在小程序前端硬编码 wordcloud 基础地址或直接调用它。 - DON'T 让用户通过猜 `jobId` 读取他人任务结果,所有查询都带 userId 条件。 - DON'T 在密钥/`WORDCLOUD_API_URL` 未配置时静默跳过或假装成功。 - DON'T 用假的 `setInterval` 进度伪装下单后的任务结果;同样不模拟生产派单成功。 - DON'T 把 `.wcd` 文件当最终产物直接返回小程序,它只是 wordcloud 的交换容器。 - DON'T 接受本地临时路径(`wxfile://`/`tmp`)作为 `designData` 中的贴纸图,R4 打包拿不到字节。 ### 3.7 验收标准 - 订单进入生产后,wordcloud 平台出现对应 job:`queued → running → success`, 产物 PNG 可下载并被 wxmp_backend 转存 COS。 - 同一订单重复触发只投递一次。 - `WORDCLOUD_API_URL` 未配置(留空)时,派单返回结构化“未配置”,不落假成功记录。 - 由 `designData` 构造的 `.wcd` 能在 wordcloud `/api/design-templates/import` 同套逻辑下原样还原(素材去重、布局一致)。 - 后端 `npm run build`、前端 `npm run build:weapp` 通过;Swagger 与契约文档一致。 ### 3.8 风险 - wordcloud 侧 `POST /api/jobs` 现强制 `name_list`;支持 `wcd_file` 二选一需要 wordcloud 排期改动。契约先冻结,wordcloud 实现可后置,wxmp_backend 按契约先写适配层。 - `designData` 中贴纸图片若仍为本地/临时 URL,WCD 打包会失败——**依赖 R2 冻结持久 URL 规则**。 - 支付未接入期没有自然触发点,需要一个幂等 `dispatch` 接口用于联调。 - `CustomizationTask` 缺 `orderId`/`wordcloudJobId` 字段,需一次 migration。 - 贴纸 `edits`(亮度/色相/对比度/线稿)暂无法由 WCD `document.json` 表达,本期先不随包携带, 在 `manifest.meta` 记录并在契约中标注边界。 ## 4. 对 R2 的依赖(接口先冻结) R4 的 WCD 打包依赖 R2 设计清单里保存的 `designData` 结构。**接口需在本分支开工前冻结**, 相关契约见新文档 `docs/design-data-contract-v1.md`(跨路线共享,R2 为数据生产者,R4 为消费者)。 需 R2 配合冻结的要点: | 项 | 要求 | |---|---| | `designData` 结构 | 按 `docs/design-data-contract-v1.md` 补 `version/background/wordcloud` 字段;`category.mask` 必须保存 | | 贴纸图片持久化 | **R4 负责**:下单/派单前把贴纸本地路径上传为 COS 持久 URL 并回写 `src`;R2 允许保存时 `src` 暂为本地路径 | | 名单快照 | `designData.wordcloud.names` **为 optional,小程序可不带**;R4 生成词云后按实际写入,R2 保存/更新清单时保留该组字段,后端白名单放行 | | 布局完整性 | 贴纸 `rotation`/`zIndex` 本期持久化(现有 `StickerItem` 缺 rotation,需补) | | `design-list` 白名单 | 后端 items JSON 校验放行上述新字段,单条 ≤1MB 上限按新结构复核 | 决策记录与细则:见 `docs/design-data-contract-v1.md` §6。 ## 5. 补充内容 ### 合并与依赖 - 分支名:前端与后端均为 `feat/r4-upload-wordcloud`。 - 与 R1-R3 并行,不依赖商品/订单链路;后端上传基础设施可为后续 R2 的贴纸图片持久化提供能力。 - 下单后 WCD 派单依赖 R2 冻结的 designData 结构与 R3 的订单状态机。 - wordcloud 契约若未冻结,R4 先完成“契约文档 + 后端适配层”,联调阶段再补真实 job。 ### 测试要求 - 后端:上传凭证、大小/类型校验、wordcloud 适配器正常/失败/超时、**WCD 打包与派单、任务归属权限**、sketch 接口。 - 前端:词云上传、轮询进度、fail 重试、结果保存相册;线稿接口失败降级;**下单后派单状态展示**;无 COS/无 WORDCLOUD 配置提示。 - 集成:用固定 wordcloud 部署版本跑一次真实生成端到端 smoke + 一次下单派单 smoke。