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

15 KiB
Raw Blame History

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


一、这条链路在做什么

把「收货地址」和「设计清单」两组数据从小程序本地 Storageopenid 隔离的离线数据) 迁到后端 PostgreSQL(真实账号数据)。做完之后:

  • 换手机、清缓存、卸载重装,地址和设计清单不丢;
  • 多端(未来 web/管理后台)能看到同一份数据;
  • R3 的下单闭环能直接消费服务端的地址 id 和设计清单 id(下单接口要传 addressIddesignListId);
  • 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. 状态迁移触发点:创建默认 DRAFTproduct 加入清单);DIY 保存设计 → SUBMITTED PROCESSING/DONE 由 R3 订单流程驱动,前端不提交;orderedorderId != 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 包(Zipmanifest.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;前端类型已具备); editsbrightness/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-deletebody { 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.tsAddressItem 增加服务端字段(服务端 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 走 APIdesignData.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.tsdesign.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 增加 processingdesignList 页的 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.tsdesign.ts:从空占位到完整实现(核心新增)
  • src/types/index.tsAddressItem/DesignItem 对齐服务端结构
  • src/pages/address/index.tsxdesignList/index.tsx:数据源切 API
  • src/pages/product/index.tsxdiy/index.tsxdiy/stickerEdit/index.tsx:写入点切 API
  • src/utils/store/address.tsdesign.ts:降级为缓存层,接口签名尽量不变以减少页面改动

明确不动: checkout/index.tsxorders/orderDetail 页、utils/store/order.tsR3 所有)。


五、里程碑建议

步骤 产出 验证
1 后端 addresses 完整 CRUD + 事务 Swagger 手测 + 双账号越权测试
2 后端 design-list 完整 CRUD + 校验 同上 + 超 1MB JSON 拒绝测试
3 前端 API 层 + 类型对齐 build:weapp 通过
4 两个页面 + 三个写入点切换 真机全流程
5 store 降级缓存 + 断网/401 场景 关服务端模拟断网
6 契约文档收口 + 双端构建 验收标准逐条打勾