docs: R4 WCD 生产任务契约(designData→词云)与路线文档

This commit is contained in:
2026-08-12 19:10:41 +08:00
parent 3e0346ad35
commit 85356e7a39
3 changed files with 359 additions and 42 deletions
+87
View File
@@ -0,0 +1,87 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概览
智绘微刻(Smart-Engraving)微信小程序:词云生成 + 个性化激光雕刻定制。基于 **Taro 3.6.31 + React 18 + TypeScript**,SCSS 变量驱动主题,构建产物运行在微信小程序(webpack5 编译到 `dist/`)。
## 常用命令
```bash
npm install # 安装依赖
npm run dev:weapp # 微信小程序开发模式(watch,配合微信开发者工具)
npm run build:weapp # 生产构建;也是提交前 / 验收的标准验证命令
npm run dev:h5 # H5 预览调试(H5 下事件用 onClick,见下文)
node --test tools/homeTopMask.test.mjs # 运行 node:test 单测(目前唯一测试,无需 jest)
```
没有配置 lint/test 脚本。提交前验证 = `npm run build:weapp` 通过(见 `docs/parallel-development-guide.md` §6)。
## 数据访问层(核心架构,务必先读)
页面**不得**直接读写 Storage 或直接发请求。所有数据操作取道两个聚合入口,二者都只 re-export、不堆业务:
- `../../utils/store`(DA 层,本地 Storage,按域拆文件):页面从 `src/utils/store/index.ts` 导入 `getDesignList()``addAddress()``designToOrder()` 等。
- `../../utils/api`HTTP 层,按域拆文件):页面从 `src/utils/api/index.ts` 导入 `login()``fetchProducts()` 等。
每个域文件(`store/keys|user|design|address|order|theme.ts``api/auth|user|product|address|design|order|upload.ts`)按领域职责划分,新增逻辑加到对应域文件里(不要新建 `xxx2.ts`),新文件需在聚合入口显式 re-export。后端替换 = 把 `store/<domain>.ts` 的函数换成 `api/<domain>.ts` 的 HTTP 调用,页面层零改动。
**openid 作用域隔离**`store/keys.ts`):storage key 形如 `${scope}_${openid}`,通过 `key(scope)`(当前用户)与 `keyFor(scope, openid)`(指定用户)生成,实现多用户数据隔离。
类型统一放 `src/types/index.ts``src/types/<domain>.ts`,不要散落在页面里。
## 协作约定(R0–R4 并行路线)
仓库按四人并行开发组织,主干为 `master`,禁止直接推主干,合入走 PR。每人一条端到端路线(前端 + 后端、同名分支):
| 路线 | 分支 | 页面归属 | 数据域 |
|---|---|---|---|
| R1 商品目录 | `feat/r1-catalog` | `index``shop``shop/detail``product` | `api/product` |
| R2 地址+设计清单 | `feat/r2-address-design` | `address``designList` | `store/design``store/address``api/address``api/design` |
| R3 订单+支付占位 | `feat/r3-order-pay` | `checkout``orders``orderDetail` | `store/order``api/order` |
| R4 上传+词云+线稿 | `feat/r4-upload-wordcloud` | `wordcloud``diy/stickerEdit` | `api/upload` |
`store/index.ts``api/index.ts` 由 R0(本分支 `feat/r0-dev-foundation`)维护,**只加不删、不堆业务**。完整规则见 `docs/team-collaboration-guide.md``docs/parallel-development-guide.md`(含合并顺序:R1 → R2/R4 → R3)。
## 主题系统
`ThemeMode``light` / `dark` / `auto``store/theme.ts`)。`ThemeContext``src/context/ThemeContext.tsx`)产出最终生效的 `resolvedTheme: 'light' | 'dark'`
- 页面根节点 `className={theme-${resolvedTheme}}`;所有颜色/字号来自 `app.scss` 里的 CSS 变量(浅色 `:root` + `.theme-dark`),**禁止在页面里硬编码颜色/字号/组件形态**,token 语义见 `DESIGN.md`,落地规则见 `设计变更文档.md`
- 每个页面都挂 `<ThemedPageMeta />``PageMeta + NavigationBar`),按 `resolvedTheme` 强制覆盖顶部/状态栏颜色;`useStatusBar(resolvedTheme)` 在页面显示时再同步一次。`app.config.ts` 里的 `darkmode: true` 只作为系统主题探测通道(`onThemeChange`/`getAppBaseInfo`),窗口颜色不依赖它。
- 自定义 tabBar 无法继承 React Context,通过 `Taro.eventCenter` 订阅 `themeChange` 事件 (`THEME_CHANGE_EVENT`) 同步主题。
- 图标一律用 `src/icon/` 下的 PNG/SVG**禁止用 emoji 当图标**。
## 事件与生命周期(重要兼容坑)
- **一律用 `onTap`,不要用 `onClick`**。微信基础库 3.17.0 + Taro React 下动态更新 handler 会触发 `TaroElement.removeEventListener(undefined)`,报 `Cannot read properties of undefined (reading '_num')`。仅在 H5 构建里可用 `onClick`
- 页面显示刷新用 `useDidShow``@tarojs/taro`),不要手动覆盖 `page.onShow`(热更新会重复叠加导致异常)。
## 请求与登录
- `src/utils/request.ts``BASE_URL = https://wxbackend.tokenleaping.com`;后端统一响应 `{ code, message, data }``code === 0` 成功;token 存 `smart_access_token`,走 `Authorization: Bearer`。HTTP 401 会自动触发一次会话续登(`app.tsx` 注册的 `refreshSession`),失败则清 token 并调 `onUnauthorized`
- `session.ts``refreshSession``wx.login` 换新 code → `/api/auth/login` 续签 → `/api/users/me` 校验 openid 与本地账号一致才放行(防串号)。`mock_*` 开头的 openid 是历史残留,`isMockOpenid` 会识别并拒绝。
- 登录态校验有缓存(`src/utils/authState.ts`),`LoginGuard` 监听 `authStateChanged`/`tabBarChange`/`themeChange` 事件自动刷新。
## 静态资源与 OSS
- 产品大图已迁到阿里云 OSS(bucket `wordcloudwechat`,杭州),包体积需保持在微信 2MB 限制内(当前 `dist/` 约 1MB)。
- 基址在根目录 `.env``OSS_BASE_URL`git 忽略),由 `config/index.js` 注入为 `__OSS_BASE_URL__` 常量。
- 所有资源路径经 `assetUrl()``src/utils/asset.ts`)处理:`/img/*` 大图拼 OSS 远程地址,`/icon/*` 小图标与 `src/icon``theme.json` 通过 `config/index.js` 的 copy patterns 打包进 `dist/`
- 商品/品类配置集中在 `src/utils/productConfig.ts``PRODUCTS` 数组 + icon 映射 + `tone` 主题色),遮罩尺寸说明见 `docs/mask-config-guide.md`
## 页面结构说明
路由与 tabBar 见 `src/app.config.ts`(全局 `navigationStyle: custom`5 个 tab 的自定义 tabBar)。几个非平凡的页面:
- `pages/shop/index`:单页双容器——2 列商品网格 + 固定 overlay 沉浸式详情(滚动视差、产品 `tone` 主题色联动),点击卡片在当前页内过渡,不跳转;底部 CTA 跳 `/pages/product/index?id=xxx` 衔接购买链路。
- `pages/diy/index`:DIY 工作台(贴纸拖拽/缩放/碰撞检测),后续 `diy/stickerEdit` 为贴纸编辑画板。
- 共享组件在 `src/components/``ThemedPageMeta``ScrollTopMask``TopBarGradient``BottomActionBar``LoginGuard``LoginModal` 等)。
## 已知遗留问题
README「已知遗留问题」表记载(改动相关页面时注意):
- checkout 页在设计中新增地址后无法及时同步(design 数据写入与读取生命周期不一致)。
- `diy/stickerEdit` 页面无法运行:Canvas 2D `createImage` 在部分基础库版本返回 `undefined`,且线稿 AI 接口未接入。
+120
View File
@@ -0,0 +1,120 @@
# 设计数据 → WCD 生产任务契约 v1
> 跨路线契约:**R2(数据生产者)与本路线 R4(WCD 打包消费者)共同遵守**。
> 冻结目的:R4 下单后要把设计清单以 `.wcd` 包投递到词云平台形成生产任务
> (见 `docs/routes/route-r4-upload-wordcloud.md` §3)。R4 能打包,取决于 R2
> 保存的 `designData` 携带哪些字段。本文档把这些字段**先定死**。
>
> 版本规则:字段只能新增 optional 字段;改名/改语义/改类型必须升版本并同步本文件。
## 1. 为什么需要本契约
词云平台用 `.wcd`Zip`manifest.json` + `document.json` + `assets/`)还原整套画布。
还原需要三样东西全部可用:
1. **画布尺寸 / 背景** ← 来自 `category.mask`
2. **贴纸布局**(位置、大小、旋转、层级)← 来自 `stickers[]`
3. **贴纸图片字节** ← 来自每个贴纸的**持久 URL**(COS)。
第 3 点是关键约束:如果贴纸图是 `wxfile://`/`tmp` 临时路径,后端打包时下载不到字节,
WCD 生产任务直接失败。因此本契约把"图片必须持久化"从 R2 的已知限制升级为**冻结约束**。
## 2. 冻结的 `designData` 结构(v1
与现有前端 `DesignItem.designData` 对齐,保留 `imageSrc/imagePos` 兼容旧版,**新增**如下字段:
```ts
/** 设计数据 v1 —— 支持 R4 WCD 生产任务打包 */
interface DesignDataV1 {
/** 结构版本,缺省按 v1 处理;旧数据可由后端/前端按版本迁移 */
version?: 1
/** 商品品类(已有)。mask 是画布尺寸来源,保存清单时必须保留 */
category?: {
id: string
mask: { shape: 'rect' | 'circle'; width: number; height: number; borderRadius?: number }
tone?: [number, number, number]
}
/** 底图(已有 imageSrc 语义的规范化):src 必须是持久 URL */
background?: {
src: string // COS 持久 URL;禁止 wxfile:// / tmp 临时路径
color?: string // 画布底色(缺省由 mask/主题决定)
pos?: { x: number; y: number; scale: number } // 兼容旧 imagePos
}
/**
* 词云信息(R4 在词云生成后写入;R2 保存/更新清单时原样保留这组字段,
* 后端 items JSON 白名单放行)。
*/
wordcloud?: {
jobId?: string // 生成该词云的 wxmp_backend 侧任务 id
imageUrl: string // 词云结果图持久 URL(COS)
names: string[] // 名单快照(≤200),生产/重建用,可为空
}
/** 贴纸列表(已有)。每个贴纸的 src 升级为持久 URL 约束 */
stickers?: Array<{
id: string
src: string // ★ 必须为 COS 持久 URLhttps),禁止 wxfile:// / tmp / 空
x: number
y: number
scale: number
width: number
height: number
rotation?: number // 已冻结(2026-08-12 决策#2):本期 DIY 持久化;无旋转则省略
zIndex?: number // 已冻结(2026-08-12 决策#2):图层顺序;WCD 按 zIndex 排列元素
isOverlapping?: boolean
edits?: {
brightness?: number // -100~100
hue?: number // -180~180
contrast?: number // -100~100
sketchSrc?: string
}
}>
}
```
关系说明:
- `DesignItem.designData`(前端类型文件 `src/types/index.ts`)与后端 `design-list.items[].designData`
(服务端 JSON)为**同一份**结构;前端类型以本契约为准,后端只做白名单校验不改业务 JSON。
- `imageSrc/imagePos` 保留为兼容旧版字段;新写入统一走 `background.src/pos`
WCD 打包器同时兜底读旧字段。
## 3. 冻结约束(R2 需落地)
| # | 约束 | 说明 |
|---|---|---|
| 1 | **贴纸图持久化(R4 负责)** | 决策#4:**贴纸持久化由 R4 完成**。R2 允许保存/更新清单时贴纸 `src` 暂为本地路径(`wxfile://`/`tmp`);**R4 在下单/派单前**把本地图上传为 COS 持久 URL 并回写 `designData.stickers[].src`。WCD 打包只消费持久 URL。R2 需保证该字段可被 R4 回写 |
| 2 | **词云字段保留** | `designData.wordcloud` 由 R4 写入后,R2 的 PATCH/列表接口不得丢弃该分组 |
| 3 | **mask 必须存在** | `designData.category.mask` 是画布尺寸来源;缺 mask 的旧数据 WCD 打包按产品默认尺寸兜底 |
| 4 | **布局字段持久化** | 决策#2`rotation`/`zIndex` 本期由 DIY 随贴纸持久化(当前 `StickerItem` 缺 rotation,需补) |
| 5 | **白名单放行** | 后端 `design-list` 的 items JSON 白名单放行 `version/background/wordcloud/rotation/zIndex` 字段;单条 ≤1MB 上限以新结构复核(图片在远端 URL,JSON 本体仍应很小) |
| 6 | **状态映射不变** | `ordered` 继续由 `orderId != null` 派生,本契约不改变 R2 状态机 |
## 4. 边界(本期不做)
- 贴纸 `edits`(亮度/色相/对比度/`sketchSrc` 线稿)无法由词云平台 `.wcd`
`document.json` 表达,本期不随 WCD 携带;仅记录在 `manifest.meta`。若后续生产需要
线稿还原,需单独升 WCD 契约(wordcloud 侧支持后再谈)。
- 字体不随包携带(wordcloud 第一阶段不做字体打包)。
- `.wcd` 只做"订单 → wordcloud"单向投递;暂不做"词云平台 → 小程序"的反向导出。
## 5. 落地位置
| 文件 | 内容 |
|---|---|
| 前端类型 | `wechat_wc/src/types/index.ts``DesignItem.designData` 按本契约补齐字段 |
| 前端页面 | `diy`(保存 designData)、`checkout`(下单前确认 designData 完整) |
| 后端白名单 | `wxmp_backend/src/design-list/*` 的 items 校验放行新字段 |
| 契约发行 | `wxmp_backend/docs/api-contract-v1.md` §5 引用本文档 |
## 6. 决策记录(2026-08-12 冻结)
| 评审点 | 决策 |
|---|---|
| 1. `background`/`wordcloud` 命名 | **接受**,按 §2 结构冻结 |
| 2. `rotation`/`zIndex` 是否本期持久化 | **本期持久化**DIY 随贴纸保存(见约束 #4 |
| 3. 名单快照 `wordcloud.names` 是否必须 | **小程序可不必须**`names` 保持 optional,小程序可不带;R4 生成词云后按实际写入,生产侧不强依赖 |
| 4. 贴纸图持久化由谁触发 | **R4 负责**:下单/派单前把本地贴纸图上传为 COS 持久 URL 并回写 `src`(见约束 #1 |
+152 -42
View File
@@ -1,10 +1,12 @@
# R4 上传 + 词云生成 + 线稿(feat/r4-upload-wordcloud
# R4 上传 + 词云生成 + 线稿 + 下单后 WCD 生产任务feat/r4-upload-wordcloud
**Goal:** 打通用户图片上传、词云任务生成、DIY 贴纸线稿处理异步任务状态轮询
**Goal:** 打通用户图片上传、词云任务生成、DIY 贴纸线稿处理异步任务状态轮询
并在**下单后把订单对应的设计以 WCD 格式投递到词云平台**形成生产任务;
这是四条路线中唯一复用 `wordcloud` 项目的路线,必须通过冻结接口契约的方式隔离它的大量变更。
**Architecture:** 小程序只与 `wxmp_backend` 通信;wxmp_backend 实现 COS 上传凭证、词云适配器、
sketch 接口任务记录/轮询`wordcloud` FastAPI 服务作为外部引擎,只暴露并冻结最小契约。
sketch 接口任务记录/轮询,以及**由设计数据构造 `.wcd` 包并投递给词云平台**的派单服务;
`wordcloud` FastAPI 服务作为外部引擎,只暴露并冻结最小契约。
小程序页面对词云使用轮询进度,不使用 SSE/EventSource。
**Tech Stack:** Taro 3.6 + React 18 + TypeScriptNestJS 11 + Prisma/PostgreSQL + Redis/BullMQ
@@ -21,6 +23,10 @@ sketch 接口和任务记录/轮询;`wordcloud` FastAPI 服务作为外部引
| `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`
@@ -29,13 +35,17 @@ sketch 接口和任务记录/轮询;`wordcloud` FastAPI 服务作为外部引
| `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 三/四个端点、字段、状态与错误语义,含版本号 |
| `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`建议增加直接传名字列表/JSON 的方式)、`GET /api/jobs/{id}``GET /api/jobs/{id}/files/png`
- 需要冻结的接口:`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 单独出适配器更新。
@@ -60,57 +70,157 @@ interface WordCloudJob {
3. 前端每 1-2 秒 `GET /api/wordcloud/jobs/:id` 轮询,`success` 后取 `imageUrl`
4. 结果图建议由 wxmp_backend 转存 COS 后返回微信可下载域名链接。
## 3. 设计注意事项
## 3. 下单后生产任务:WCD 投递(本分支新增功能)
### DO
> 场景:用户在 DIY 过程中生成的词云图、贴纸布局最终落在一条设计清单(`design-list`)里。
> 下单之后,订单对应的整套设计必须能**原样恢复到词云平台**,形成可追溯、可复用的
> **生产任务**(用于后续加工/激光雕刻)。传输格式采用词云平台已定义的 `.wcd`
> 画布导入导出包(Zip`manifest.json` + `document.json` + `assets/`)。
- DO 小程序只调用 `wxmp_backend``wordcloud` 的地址、密钥、内部参数对小程序完全不可见。
- DO 上传走临时凭证或预签名 URL,前端拿不到主账号 SecretKey。
- DO 文件名与 `key` 由服务端生成或校验(如 `uploads/{userId}/{uuid}.jpg`),杜绝用户传路径穿越。
- DO 限制文件类型与大小(底图建议 png/jpg ≤ 10MB、名单 ≤ 200 个名字),前后端双重校验。
- DO 词云任务在数据库建记录并按用户隔离,用户只能查询自己的 job。
- DO 轮询采用普通 HTTP GET,不做 SSE;小程序端 EventSource 支持不稳定。
- DO 进度字段以 wordcloud 返回为准,前端只负责展示,不再用随机数模拟进度。
- DO sketch 请求带登录态和文件大小校验,失败时前端降级本地灰度,并明确提示“已使用本地线稿”。
- DO 长期运行的任务设置超时与失败清理,避免 COS 对象和任务记录无限堆积。
- DO 在 `docs/wordcloud-contract.md` 记录契约版本,并在后端适配器代码里注释依赖版本。
### 3.1 端到端链路
### DON'T
```
设计清单 designDataR2 按 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
```
小程序前端永不接触 wordcloudWCD 完全由 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 // CanvasDocumentwidth/height/background/layers/elements
└── assets/<assetId>.<ext> // 每个 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/<assetId>*` 匹配)。
- 名单快照(`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 构造并投递 WCDwordcloud 地址、密钥、内部参数对小程序完全不可见。
- 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 项目 fork 进 wxmp_backend,也不要把它的内部 Python/C++ 代码搬进 NestJS。
- DON'T 在小程序前端硬编码 wordcloud 基础地址或直接调用它。
- DON'T 上传逻辑使用完整云厂商密钥;密钥只存在于服务端环境变量。
- DON'T 接受本地临时路径作为最终结果地址,wordcloud 返回的内部 `/api/jobs/...` 必须转成 COS 公网 URL。
- DON'T 让用户通过猜 `jobId` 读取他人任务结果,所有查询都带 userId 条件。
- DON'T 在密钥未配置时静默跳过上传/词云;返回结构化“未配置”错误,前端给出可理解提示
- DON'T 用假的 `setInterval` 进度假装生成完成,R4 结束前必须移除当前 wordcloud 页的模拟逻辑
- DON'T 在密钥/`WORDCLOUD_API_URL` 未配置时静默跳过或假装成功
- DON'T 用假的 `setInterval` 进度伪装下单后的任务结果;同样不模拟生产派单成功
- DON'T 把 `.wcd` 文件当最终产物直接返回小程序,它只是 wordcloud 的交换容器。
- DON'T 接受本地临时路径(`wxfile://`/`tmp`)作为 `designData` 中的贴纸图,R4 打包拿不到字节。
## 4. 补充内容
### 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 与契约文档一致。
- 小程序端上传图片可拿到 COS 可访问 URL,`Upload` 表有记录且归属当前用户。
- `POST /api/wordcloud/generate` 可在真实 wordcloud 服务上产出 PNG,前端轮询到结果并保存相册。
- `POST /api/sketch` 返回有效图片 URL;接口失败时前端走降级且不白屏。
- 词云任务非本人不可访问;上传文件类型/大小校验生效
- `docs/wordcloud-contract.md` 已冻结并带版本号;前端 `wordcloud` 页不再有随机进度
### 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 正在大量变更,任务状态/产物路径随时可能变化;这正是契约冻结要解决的问题,联调时先锁定一个部署版本。
- COS 与微信 `downloadFile`/`uploadFile` 合法域名必须提前配置,否则真机上传下载会失败。
- 词云生成是 CPU 密集型任务,需要限制并发与单账号频率,防止被刷爆资源。
- `designData` 中的贴纸图片若在 R4 接入上传,需要 R2 的设计 JSON 结构配合增加 `uploadedUrl` 字段,两分支交接时注意。
### 测试要求
- 后端:上传凭证、大小/类型校验、wordcloud 适配器正常/失败/超时、任务归属权限、sketch 接口。
- 前端:词云上传、轮询进度、fail 重试、结果保存相册;线稿接口失败降级;无 COS 配置提示。
- 集成:用固定 wordcloud 部署版本跑一次真实生成端到端 smoke。
- 后端:上传凭证、大小/类型校验、wordcloud 适配器正常/失败/超时、**WCD 打包与派单、任务归属权限**、sketch 接口。
- 前端:词云上传、轮询进度、fail 重试、结果保存相册;线稿接口失败降级;**下单后派单状态展示**;无 COS/无 WORDCLOUD 配置提示。
- 集成:用固定 wordcloud 部署版本跑一次真实生成端到端 smoke + 一次下单派单 smoke