Files
wechat_wc/docs/r2-workflow.md
T
lhmin0604andClaude 954cddfc22 feat(r2): 阶段4 store 降级为离线兜底缓存层
- designList/address 两页缓存优先渲染(冷启动不空屏),联网后以服务端为准覆盖
- store/design.ts、store/address.ts 文件头注明缓存层语义(回写/兜底/旧数据不合并)
- 流程文档勾选阶段 1-4 进度

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-12 03:58:55 +08:00

238 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# R2 地址 + 设计清单上链路工作流程
> 对应路线文档:`docs/routes/route-r2-address-design.md`
> 契约文档:后端 `wxmp_backend/docs/api-contract-v1.md` §4/§5、前端 `docs/design-data-contract-v1.md`
> 分支:前端 `wechat_wc` / 后端 `wxmp_backend` 均为 `feat/r2-address-design`
---
## 一、这条链路在做什么
把「收货地址」和「设计清单」两组数据从**小程序本地 Storage**openid 隔离的离线数据)
迁到**后端 PostgreSQL**(真实账号数据)。做完之后:
- 换手机、清缓存、卸载重装,地址和设计清单不丢;
- 多端(未来 web/管理后台)能看到同一份数据;
- R3 的下单闭环能直接消费服务端的地址 id 和设计清单 id(下单接口要传 `addressId``designListId`);
- R4 的 WCD 生产打包能从服务端读到完整的 `designData` JSON。
一句话:这是把"玩具数据"换成"账本数据"的一步,R3 整条交易链路都压在它上面。
---
### 阶段 0:契约确认 ✅(2026-09-11 已确认,零契约改动)
以下结论为 R2 实现的唯一依据,实现阶段不再讨论:
1. **清单映射:一条前端 DesignItem = 一条后端 DesignList 记录**`items` 数组固定 1 个元素。
依据:状态枚举是清单级实体字段(逐条映射的前提)、批量删除是 `{ids: string[]}`
`DesignList.orders` 逐条关联、R4 `wordcloud.service.ts` 已按 `items[0]` 消费。
POST /api/design-list 请求体 `{ title, items: [单个条目] }`
2. **designData.category 裁剪为 `{ id, mask, tone? }`**DIY 保存时只取这三个字段提交
(当前 `diy/index.tsx` 提交完整 ProductCategory,需改为裁剪),其余展示字段由客户端
按 productId 从 productConfig 推导。与冻结契约 §2 一致,后端白名单不放宽。
3. **productIcon 不入契约**:服务端不存图标,设计清单页继续用现有
`PRODUCT_ICON_MAP[productId]` 兜底推导(`designList/index.tsx:168` 逻辑已存在)。
4. **title 前端传 productName**:后端 `title` 保持必填不动,创建时填商品名。
5. **GET 两个列表接口不分页**:返回纯数组(地址、清单均为小数据量)。
6. **状态迁移触发点**:创建默认 `DRAFT`product 加入清单);DIY 保存设计 → `SUBMITTED`
`PROCESSING`/`DONE` 由 R3 订单流程驱动,前端不提交;`ordered``orderId != null` 派生,
前端只读。状态机单向推进,禁止回退。
7. **其他默认**:批量删除用单接口 `POST /api/design-list/batch-delete`
`productId` R2 不做存在性校验(R1 商品表可能未 seed);手机号只做非空字符串校验;
越权 403 / 不存在 404(契约既定)。
### WCD 格式硬性规定(R2 全程适用,依据 `docs/design-data-contract-v1.md` 冻结版 2026-08-12
> R2 是 `designData` 的**生产者**R4 下单后把它打成 `.wcd` 包(Zip`manifest.json` +
> `document.json` + `assets/`)投递给词云平台还原整套画布。还原依赖三样东西全部在
> designData 里:① 画布尺寸 ← `category.mask`;② 贴纸布局 ← `stickers[]`
> x/y/scale/width/height/rotation/zIndex);③ 贴纸图片字节 ← 每个贴纸的**持久 URL**。
> R2 保存的每一份 designData 都要为这三样负责,否则 R4 打包直接失败或画布走样。
**结构规定(后端白名单已按此实现):**
1. designData 顶层只允许 **7 个键**`version` / `category` / `background` / `wordcloud` /
`stickers` / `imageSrc` / `imagePos`(后两个为旧版兼容)。多余顶层键 → 后端 400
forbidNonWhitelisted + service 白名单双重拦截)。
2. `category` 只提交 `{ id, mask, tone? }`(阶段0 决策#2)。`mask` 是 WCD 画布尺寸来源,
**禁止裁掉**——缺 mask 的数据 WCD 只能按产品默认尺寸兜底,画布会走样(契约约束#3)。
3. `stickers[]` 内字段后端不深校验、原样透传。其中 `rotation` / `zIndex` 本期必须随 DIY
保存持久化(契约决策#2 冻结,WCD 按 zIndex 排图层,约束#4;前端类型已具备);
`edits`brightness/hue/contrast/sketchSrc)同样透传,虽然本期不进 WCD document.json。
4. 贴纸 `src` 允许暂存 `wxfile://` / `tmp` 本地路径——契约约束#1 明确允许
(R4 在下单/派单前上传 COS 并回写 src)。**R2 任何环节禁止把它当脏数据清洗掉**。
5. `wordcloud` 分组由 R4 词云生成后写入,R2 任何写入路径**不得丢弃**(约束#2):
后端 PATCH 已做部分更新语义(不传 items 不清 designData);但前端 `updateDesign`
是 items 全量替换——页面必须基于服务端最新 designData 合并改动后再整包提交。
6. 状态机不受本契约影响:`ordered` 仍由 `orderId != null` 派生,前端只读(约束#6)。
**边界(本期不做,契约 §4):** 贴纸 edits/线稿不进 WCD `document.json`(仅记 `manifest.meta`);
字体不随包携带;`.wcd` 只做「订单 → 词云平台」单向投递。
**涉及阶段:** 阶段 3(三个写入点)、阶段 4(缓存回写),注意事项已插入下文对应位置。
## 二、工作流程(按顺序)
### 原阶段 0 任务清单(存档)
1. 通读后端 `docs/api-contract-v1.md` §4(地址)、§5(设计清单)。
2. 确认状态映射表已定死(不再讨论):
| 前端显示 | 前端状态码 | 后端 DesignListStatus |
|---|---|---|
| 待设计 | `undesigned` | `DRAFT` |
| 设计中 | `designing` | `SUBMITTED` |
| 生产中 | `processing` | `PROCESSING` |
| 已下单 | `ordered`(派生) | `DONE` |
`ordered` **不落库**,由 `orderId != null` 派生;前端不提交这个状态。
3. 确认 Prisma `Address` / `DesignList` 模型字段与契约一致,缺字段先出迁移。
### 阶段 1:后端补齐 CRUD(先做,前端等它)✅(2026-09-12,后端提交 0a3c8e8
改动范围:`src/addresses/``src/design-list/`,两个模块均已注册,骨架已存在。
**addresses(当前只有 GET/POST/PATCH :id/default,且 service 全是 TODO):**
1. `PATCH /api/addresses/:id` —— 缺失,新增。
2. `DELETE /api/addresses/:id` —— 缺失,新增;若删的是默认地址,事务内把最新一条设为默认。
3. `POST` / `PATCH :id` / `PATCH :id/default` 的**默认地址唯一性事务**
`prisma.$transaction` 内先 `updateMany` 清掉该用户所有 `isDefault`,再设新默认。
4. `setDefault` 补 userId 归属校验(现在谁都能改任何人的地址,这是越权洞)。
5. 所有按 id 的路由统一:非本人 → 403,不存在 → 404。
**design-list(当前只有 GET/GET :id/POST):**
1. `PATCH /api/design-list/:id` —— 更新 `title`/`items`/状态迁移(状态机后端校验,
只允许 `DRAFT → SUBMITTED → PROCESSING → DONE` 单向推进)。
2. `DELETE /api/design-list/:id` + `POST /api/design-list/batch-delete`body `{ ids: string[] }`
一次事务删,返回实际删除数——不要让前端循环单删)。
3. `items` JSON 白名单校验:放行 `version/background/wordcloud/rotation/zIndex`
**原样透传不修改**`wordcloud` 分组是 R4 写入的,丢了 WCD 打包就失败);
单条 designData ≤ 1MB,超限 400。
4. `findOne` 补归属校验(当前 TODO)。
5. Swagger 补全以上全部接口。
### 阶段 2:前端 API 层(页面不动)✅(2026-09-12,前端提交 8438fea
1. `src/utils/api/address.ts`(现为空占位):实现
`fetchAddresses / createAddress / updateAddress / deleteAddress / setDefaultAddress`
**唯一一处**做 `region: [province, city, district]``province/city/district` 双向转换,
页面和 store 不允许出现第二次转换。
2. `src/utils/api/design.ts`:实现
`fetchDesignList / createDesign / updateDesign / deleteDesign(s)`
做后端 `DRAFT/SUBMITTED/...` ↔ 前端 `undesigned/designing/...` 的状态码映射。
3. `src/types/index.ts``AddressItem` 增加服务端字段(服务端 id、`createdAt` 等);
`DesignItem.status``'processing'``DesignItem.orderId` 派生 `ordered` 展示。
### 阶段 3:切换页面(两个页面 + 三个写入点)✅(2026-09-12,前端提交 e557dbcWCD 红线已按上文落实)
| 文件 | 改动 |
|---|---|
| `pages/address/index.tsx` | 增删改查、设默认全部走 API;401 由 request 层统一抛出并引导登录;API 失败降级读本地缓存并提示 |
| `pages/designList/index.tsx` | 列表/筛选/批量删除走 API;状态筛选基于服务端返回的 `status` 计算,消灭魔法字符串 |
| `pages/product/index.tsx`(加入清单) | `addDesign` 写 API(需登录),失败降级本地。此时尚无 designData,创建后保持 DRAFT |
| `pages/diy/index.tsx`(保存 designData | `updateDesign` 走 API`designData.category` 裁剪为 `{id, mask, tone}`(决策#2);贴纸本地图路径允许暂存(R4 派单前才持久化,契约约束 #1)。**WCD 红线**`category.mask` 必须保留;`stickers[].rotation/zIndex` 随保存持久化;items 是全量替换——提交前必须基于服务端最新 designData(含 R4 写入的 `wordcloud` 分组)合并改动后整包提交,不得只传改动片段(见「WCD 格式硬性规定」) |
| `pages/diy/stickerEdit/index.tsx` | 同上,保存贴纸改动走 API;同样受 WCD 红线约束(合并后整包提交,rotation/zIndex 带全) |
### 阶段 4:本地 store 降级改造 ✅(2026-09-12
`utils/store/address.ts``design.ts` 保留,但语义变为"离线兜底缓存"(文件头已注明):
- API 成功 → 把服务端数据写回本地缓存(下次冷启动先展示缓存再刷新,designList/address
两页已实现缓存优先渲染);
**designData 原样存储**:不得清洗 `wxfile://` 等本地贴纸路径、不得裁剪任何字段
WCD 硬性规定 #4/#5);
- API 失败/断网 → 页面读缓存并可正常浏览,写操作提示失败;
- 登录后首次进入:只读服务端,**不自动合并**本地旧数据(联网刷新以服务端为准覆盖缓存,
`smart_design_list_<openid>` 与服务端并存的问题按路线文档风险节处理:忽略或提供
一次性导入,默认忽略)。
### 阶段 5:联调与验收
按路线文档验收标准逐条过:
- 登录态下地址/清单全流程走真实后端;
- 越权访问返回 403、不存在返回 404(用两个账号互测);
- 默认地址永远唯一;删默认地址后自动产生新默认;
- 断网降级提示、401 引导登录;
- 前端 `npm run build:weapp` 通过、后端 `nest build` 通过;
- Swagger 涵盖全部接口;状态映射落进契约文档。
---
## 三、需要注意的部分(坑位清单)
### 后端
1. **越权是本分支最大风险**。每个按 id 操作的 service 必须带 `where: { id, userId }`
查不到时区分 404/403:先查存在性再查归属,或统一 404(避免枚举他人资源 id 时,
推荐统一 404,路线文档要求两者区分则按 403 处理——按契约文档走)。
2. **默认地址唯一性只能由后端事务保证**。前端传 `isDefault: true` 只是一个"请求"
不是规则;不要在 controller 之外有任何直接 update `isDefault` 的路径。
3. **删除默认地址的补偿**必须和删除在同一个事务里,否则会出现"全员无默认"的中间态。
4. **items JSON 是白名单透传,不是深校验**。后端只验结构和大小,不改内容;
特别是 PATCH 时要做**合并语义**确认:不传 `designData` 不得清掉已有值
(否则 R4 写入的 `wordcloud` 会被一次普通数量修改冲掉)。
5. **状态机校验**:拒绝 `DONE → DRAFT` 这类回退;`ordered` 不接受前端提交。
6. **批量删除**要么后端一个接口,要么明确部分失败语义——不要默认前端循环单删。
### 前端
1. **region 转换只写在 `api/address.ts` 一处**。散落到页面就会出现两套转换,
后续排查字段错位会花双倍时间。
2. **不要改 `checkout/index.tsx`**(红线:checkout 归 R3)。R2 只保证接口稳定,
checkout 里对本地 store 的读取暂不动,R3 接入时一并切换。
3. **不要把 `wxfile://` 临时路径当脏数据清洗掉**。DIY 保存时贴纸 src 暂为本地路径是
契约允许的(R4 在下单/派单前才持久化),后端白名单放行即可。
4. **`DesignItem.status` 增加 `processing` 后**`designList` 页的 `STATUS_STYLE`
筛选 tabs 要同步扩展,漏了会出现"生产中"条目渲染不出徽标。
5. **401 处理已有全局机制**`request.ts` 自动续登 + `onUnauthorized`),
页面里不要自己再写跳登录逻辑,重复处理会出现双弹窗。
6. **旧本地数据不迁移**。首次切换后以服务端为准,本地旧清单直接忽略;
提前和需求方确认这一点(用户可能反馈"我的设计没了"——是预期行为,需要文案兜底)。
### 跨路线影响
| 受影响方 | 影响 | 需要做的 |
|---|---|---|
| **R3 订单** | 下单接口依赖服务端 `addressId` / `designListId` | R2 保证 id 稳定、可查询;R3 接入时 checkout 改传服务端 id |
| **R4 词云/派单** | `designData` 结构和保留字段 | R2 PATCH 不得丢 `wordcloud` / `category.mask` / 贴纸本地路径 |
| **profile / settings / orderDetail 页** | 现在从本地 store 读地址和清单数 | R2 期间 API 成功会回写本地缓存,这些页面**暂不改也能读到底数据**(读的是缓存);R3/R4 后续各自切换 |
| **userDatabase 账号管理** | 靠 `getDesignListFor(openid)` 跨账号读本地清单 | 服务端化后该功能语义变化(读不到别人云端数据),属于已知降级,无需处理 |
---
## 四、影响面(改动文件总览)
**后端 `wxmp_backend`(分支 feat/r2-address-design):**
- `src/addresses/*`:补 PATCH :id、DELETE :id、默认事务、归属校验、DTO、Swagger
- `src/design-list/*`:补 PATCH/DELETE/批量删除、items 白名单与 1MB 校验、状态机、归属校验
- `prisma/`:仅当模型字段与契约不一致时才出迁移(预计不需要)
- `docs/api-contract-v1.md`:落地状态映射表与错误码(如尚未完整落地)
**前端 `wechat_wc`(分支 feat/r2-address-design):**
- `src/utils/api/address.ts``design.ts`:从空占位到完整实现(核心新增)
- `src/types/index.ts`AddressItem/DesignItem 对齐服务端结构
- `src/pages/address/index.tsx``designList/index.tsx`:数据源切 API
- `src/pages/product/index.tsx``diy/index.tsx``diy/stickerEdit/index.tsx`:写入点切 API
- `src/utils/store/address.ts``design.ts`:降级为缓存层,接口签名尽量不变以减少页面改动
**明确不动:** `checkout/index.tsx``orders`/`orderDetail` 页、`utils/store/order.ts`R3 所有)。
---
## 五、里程碑建议
| 步骤 | 产出 | 验证 |
|---|---|---|
| 1 | 后端 addresses 完整 CRUD + 事务 | Swagger 手测 + 双账号越权测试 |
| 2 | 后端 design-list 完整 CRUD + 校验 | 同上 + 超 1MB JSON 拒绝测试 |
| 3 | 前端 API 层 + 类型对齐 | `build:weapp` 通过 |
| 4 | 两个页面 + 三个写入点切换 | 真机全流程 |
| 5 | store 降级缓存 + 断网/401 场景 | 关服务端模拟断网 |
| 6 | 契约文档收口 + 双端构建 | 验收标准逐条打勾 |