- r2-workflow:阶段5 验证记录、验收期修复(PATCH /users/me、.env OSS) - routes/R2/README:R3(checkout 切换/删除守卫/状态触发权)、R4(designData 整包合并回写/贴纸 COS 硬前置)、R1(productId 校验/iconImg 下发)接入须知 - routes/README 索引更新 Co-Authored-By: Claude <noreply@anthropic.com>
259 lines
17 KiB
Markdown
259 lines
17 KiB
Markdown
# 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,前端提交 e557dbc;WCD 红线已按上文落实)
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `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:联调与验收 ✅(2026-09-12 核心收口)
|
||
|
||
**已验证:**
|
||
|
||
- 后端 35 项 API 测试全过(CRUD/默认地址事务/403/404/状态机/白名单/1MB/401);
|
||
- 前端 API 层 13 项断言全过(复跑于阶段 3/4 之后,确认无回归);
|
||
- 真机验收步骤 1-9、11-13 全过(清单链路/地址链路/登录态边界);
|
||
- **数据库直查验证 WCD 红线**:`category` 裁剪为 `{id, mask, tone}` 落库、`mask` 完整、
|
||
贴纸 `wxfile://` 路径按契约保留(`persistDesignMedia` 因 upload 占位静默降级);
|
||
- `build:weapp` / `nest build` 双端构建通过;
|
||
- 越权 403/404 由双账号 API 测试覆盖(真机步骤 #14 免测)。
|
||
|
||
**验收期间发现并修复的问题(非 R2 引入):**
|
||
|
||
1. 后端缺 `PATCH /api/users/me` 路由(契约 §2 遗漏)→ 已补(后端提交 3fc65e5);
|
||
2. 本机缺 `.env`(`OSS_BASE_URL`)导致首页大图空白 → 环境问题,已记录到
|
||
`docs/routes/R2/README.md` 公共注意第 6 条。
|
||
|
||
**未覆盖(低风险,可后补):** 断网降级真机步骤 #10(代码路径已实现,逻辑简单)。
|
||
|
||
**交接收口:** 跨线路注意事项见 `docs/routes/R2/README.md`(R3 的 checkout 切换与
|
||
删除守卫、R4 的整包合并回写、R1 的 productId 校验等)。
|
||
|
||
按路线文档验收标准逐条过:
|
||
|
||
- 登录态下地址/清单全流程走真实后端;
|
||
- 越权访问返回 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 | 契约文档收口 + 双端构建 | 验收标准逐条打勾 |
|