diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e55bf2a --- /dev/null +++ b/CLAUDE.md @@ -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/.ts` 的函数换成 `api/.ts` 的 HTTP 调用,页面层零改动。 + +**openid 作用域隔离**(`store/keys.ts`):storage key 形如 `${scope}_${openid}`,通过 `key(scope)`(当前用户)与 `keyFor(scope, openid)`(指定用户)生成,实现多用户数据隔离。 + +类型统一放 `src/types/index.ts` 或 `src/types/.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`。 +- 每个页面都挂 ``(`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 接口未接入。 \ No newline at end of file diff --git a/docs/design-data-contract-v1.md b/docs/design-data-contract-v1.md new file mode 100644 index 0000000..63a8a9a --- /dev/null +++ b/docs/design-data-contract-v1.md @@ -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 持久 URL(https),禁止 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) | \ No newline at end of file diff --git a/docs/routes/route-r4-upload-wordcloud.md b/docs/routes/route-r4-upload-wordcloud.md index 9b4c883..f439114 100644 --- a/docs/routes/route-r4-upload-wordcloud.md +++ b/docs/routes/route-r4-upload-wordcloud.md @@ -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 + TypeScript;NestJS 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 +``` +设计清单 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 项目 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。 \ No newline at end of file