Files
wechat_wc/docs/r2-workflow.md
T
lhmin0604andClaude 8438fea72f feat(r2): 前端 API 层——地址/设计清单切换到后端接口(阶段2)
- api/address.ts: fetchAddresses/createAddress/updateAddress/deleteAddress/
  setDefaultAddress;region[] ↔ province/city/district 双向转换唯一落点
- api/design.ts: fetchDesignList/createDesign/updateDesign/deleteDesign/
  deleteDesigns;状态映射 DRAFT↔undesigned、SUBMITTED↔designing、
  PROCESSING↔processing、DONE↔ordered;一条设计=一条清单(items 固定 1 元素,
  title=productName,productIcon 不上传由页面按 productId 兜底)
- types/index.ts: DesignItem.status 补 'processing'、productIcon 改 optional、
  AddressItem 补 createdAt?;全部消费点已确认兼容(均有兜底)
- docs/r2-workflow.md: R2 工作流程与阶段0 契约确认记录

验证:build:weapp 通过;Node 打桩 Taro.request 转发本地 3091 后端实测
13 项全过(region 往返/状态映射/designData 保留/越权 403/无效 token 401)

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

204 lines
12 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(契约既定)。
## 二、工作流程(按顺序)
### 原阶段 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(先做,前端等它)
改动范围:`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 层(页面不动)
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:切换页面(两个页面 + 三个写入点)
| 文件 | 改动 |
|---|---|
| `pages/address/index.tsx` | 增删改查、设默认全部走 API;401 由 request 层统一抛出并引导登录;API 失败降级读本地缓存并提示 |
| `pages/designList/index.tsx` | 列表/筛选/批量删除走 API;状态筛选基于服务端返回的 `status` 计算,消灭魔法字符串 |
| `pages/product/index.tsx`(加入清单) | `addDesign` 写 API(需登录),失败降级本地 |
| `pages/diy/index.tsx`(保存 designData | `updateDesign` 走 API`designData.category` 裁剪为 `{id, mask, tone}`(决策#2);贴纸本地图路径允许暂存(R4 派单前才持久化,契约约束 #1 |
| `pages/diy/stickerEdit/index.tsx` | 同上,保存贴纸改动走 API |
### 阶段 4:本地 store 降级改造
`utils/store/address.ts``design.ts` 保留,但语义变为"离线兜底缓存":
- API 成功 → 把服务端数据写回本地缓存(下次冷启动先展示缓存再刷新);
- 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 | 契约文档收口 + 双端构建 | 验收标准逐条打勾 |