Files
zt-closet-wxmini-frontend/.codebuddy/plans/upload-process-resilience_7d735905.md
T

167 lines
13 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.
---
name: upload-process-resilience
overview: 修复"用户退出小程序后衣物处理结果无法感知、失败态误报"的前后端一致性问题:修正上传页 90s 超时误报与"额度已自动退回"文案,增加待处理衣物持久化与重进恢复查询,衣橱列表正确区分"处理中/处理失败"(后端列表与详情接口补充 image_process_status / fail_reason)。
todos:
- id: status-utils-storage
content: 新增 clothes-status 与 pending-clothes 工具,config 加 PENDING_CLOTHES,模型补可选状态字段
status: completed
- id: upload-fix
content: 改造上传页:轮询预算对齐后端、超时不误报失败、失败与额度文案仅真实失败触发、onShow 续查
status: completed
dependencies:
- status-utils-storage
- id: wardrobe-recover
content: 衣橱接入待处理解析:失败弹窗引导详情、列表区分处理中与失败、终结后静默刷新
status: completed
dependencies:
- status-utils-storage
- id: detail-align
content: 详情页用接口状态初始化并跳过终态轮询,修正处理中与失败态展示时机
status: completed
dependencies:
- status-utils-storage
- id: backend-fields
content: 后端列表与详情补处理状态字段、非属主隐藏失败原因、上传回退改用独立 context、更新接口文档
status: completed
- id: verify-audit
content: Use [subagent:code-explorer] 审计字段消费方无回归,运行前端 tsc 与后端 go build 验证
status: completed
dependencies:
- upload-fix
- wardrobe-recover
- detail-align
- backend-fields
---
## 用户需求
用户报告疑虑:"用户退出小程序之后,会导致上传的衣服处理失败?"。经排查确认:后端处理任务与客户端连接解耦,已成功上传的衣物不会因退出而处理失败;但由此暴露出多处真实的前端体验缺陷与前后端契约缺口,用户要求全面修复,包括 90s 超时误报与"额度已退回"文案不实的问题,并允许同步调整后端接口。
## 产品概述
修复「上传衣物后退出小程序」场景下的状态可感知性:处理结果在重新进入小程序后自动恢复与告知;处理中、处理失败状态如实展示;超时不再被误判为失败;额度退回文案只在真实失败时出现。
## 核心功能
- **退出后恢复处理结果**:衣物上传成功后,即使用户退出小程序或离开页面,重新进入衣橱时自动核对该衣物的最终处理结果;成功无感展示,失败给出明确提示并可直达衣物详情。
- **处理状态如实展示**:衣橱列表正确区分「处理中(排队中/抠图中/分类中)」与「处理失败」,失败项显示醒目标识,不再将失败项永久显示为"AI 处理中"。
- **超时不再误报失败**:上传页轮询时长与后端处理现实对齐;轮询结束但未收到最终结果时,展示中性提示(仍在处理中,可先返回衣橱稍后查看),不进入"识别失败"界面。
- **额度文案准确**:仅当后端确认处理失败(此时额度已自动退回)才展示"识别失败 + 额度已自动退回";处理超时、网络中断等场景不出现该文案。
- **失败原因可达**:衣物详情页进入时即可正确渲染处理中/失败状态(不再依赖轮询首次返回),失败原因与后续操作入口清晰可见。
## 技术栈
- 前端:微信小程序原生框架 + TypeScript(沿用现有工程与组件体系,无新增依赖)
- 后端:Go(Gin + GORM,仓库 `/root/zt-closet-backend`,仅做加法式接口扩展与回退加固)
- 复用既有设施:`utils/request.ts`(request/uploadFile、silentAuth/showError)、`utils/logger.ts`、`utils/outfit-status.ts`(状态语义收敛范式)、`STORAGE_KEYS` + `Ax_TASK` 式续接范式(ai-recommend 的 onShow 续轮询/onHide 停轮询/结果落库即清理)
- 组件:复用 `v2-overlay`(modal 形态)与 `clothes-card`(`statusText`/`statusType`,已有 `.tile-flag.is-ink` 醒目样式)
## 实现方案
整体策略:**前后端契约补齐 + 前端状态语义收敛 + 轻量持久化续接**,分四层落地。
1. **状态语义单一来源(前端)**:新增 `utils/clothes-status.ts`,对齐 `utils/outfit-status.ts` 范式,统一输出「处理中 / 处理失败 / 无标识」的文案与样式判定;对旧后端响应(无状态字段)保留"字段为空即处理中"的降级推断,保证向后兼容。
2. **续接机制(前端)**:新增 `utils/pending-clothes.ts`,上传成功即把 `{uuid, createdAt}` 写入本地存储(新增 `STORAGE_KEYS.PENDING_CLOTHES`);衣橱 `onShow` 时仅当有待处理记录才逐条查询 `/clothes/:uuid/status`(`showError:false` + `silentAuth:true`),终结态移出存储:成功静默、失败用 `v2-overlay` 提示并可跳详情;未完成保留待下次核对。存储上限 20 条、超 24 小时自动清理,防止膨胀。上传页自身 onHide 停轮询(对齐 ai-recommend)、onShow 恢复续查。
3. **超时策略修正(前端)**:上传页轮询预算由 60×1.5s≈90s 调整为 2s×90≈180s(与后端单任务 3 分钟下限对齐);预算耗尽只停止轮询并展示中性提示,绝不置 `failed`;`failed` 仅在收到后端 `image_process_status=5` 时进入,此时"额度已自动退回"文案才成立(后端失败路径确实回退)。
4. **后端字段补齐**:`ClothesGetByUUIDResponse` 增加 `image_process_status`、`fail_reason`(`buildClothesResponse` 填充),列表与详情共用;衣物详情页据此在 `onLoad` 即正确渲染状态并跳过终态轮询,消除"先显示正常详情/空白再回退处理中"的闪烁。非属主读取(穿搭分享 `share_uuid`、衣柜分享)屏蔽 `fail_reason`(可能含内部错误信息),保留状态字段。另加固:上传同步阶段失败回退额度改用不受请求取消影响的独立 context(沿用 `processImageAsync` 的 trace+Background 派生手法),消除"客户端中途断开导致额度已扣无法回退"窗口。
关键取舍:
- 不做全局常驻轮询/不做后台任务队列:处理本身在服务端持续进行且有恢复调度器兜底,前端只承担"进入时核对一次"的最小职责,避免耗电与请求浪费。
- 不新增接口:复用已有 status 接口(幂等只读),列表/详情字段为向后兼容的加法;旧后端可正常降级使用。
- 明确不在范围:上传请求尚未返回时退出导致该次上传中断(属"上传失败",前端无法挽回)。
性能与可靠性:pending 为空时零额外请求;解析为并发 N 条 status 查询(N 受上限 20 约束且通常为 1-2);列表刷新与解析并行、由已有 reqSeq 序号防竞态;解析完成后若存在终结态记录再做一次静默刷新,保证列表状态与服务端一致。
## 架构设计
数据流(新增/变更部分):
```mermaid
flowchart TB
subgraph 前端
U[上传页 upload] -->|uploadFile 成功写入| S[(本地存储 pending 衣物)]
U -->|180s 有界轮询| ST[/clothes/:uuid/status/]
U -->|真实失败: 弹失败态| U
W[衣橱 onShow] -->|读取 pending 逐条核对| ST
W -->|失败提示弹层| M[衣物详情]
W -->|刷新| L[列表: 处理中/处理失败]
D[衣物详情页] -->|onLoad 用详情状态初始化, 仅处理中才轮询| ST
end
subgraph 后端
ST --> P[异步处理流水线: 与连接解耦 + 定时恢复]
P --> DB[(clothes 表: 状态/失败原因)]
DB -->|列表/详情新增字段| L
end
```
沿用现有分层:页面层(upload/wardrobe/clothes-detail)→ 工具层(clothes-status、pending-clothes、request、logger)→ 存储层(wx storage);后端 controller(router) → service → dao 分层不变,仅在 DTO 与 service 映射层做加法。
## 实现说明(执行要点)
- 存储写入需按 uuid 幂等去重;读取时先做过期清理(>24h)。
- status 查询统一 `showError:false`、`silentAuth:true`:后台核对不弹错、不触发登录跳转;未登录时 request 内置直接 reject,pending 保留待下次。
- 衣物不存在(50000,用户已删除)时静默移出 pending,避免残留。
- 日志沿用 `logger` 模块名(upload/wardrobe/pending-clothes/clothes-detail),仅记录 uuid 与状态码,不打印敏感信息。
- 列表渲染保持 `_statusText/_statusType` 字段形状不变,仅替换计算逻辑,wxml 主结构不动,降低改动面。
- 后端字段为可选扩展:前端所有读取均做缺省降级,灰度期间新旧接口均可工作。
- 失败弹窗文案聚合("有 N 件衣物 AI 识别失败"),主按钮跳转最近一条失败衣物详情。
- 不改动 ai-recommend、outfit 等无关页面逻辑;`clothes-card` 组件无需改动。
## 目录结构
```
zt-closet-wxmini-frontend/
├── miniprogram/
│ ├── utils/
│ │ ├── config.ts # [MODIFY] STORAGE_KEYS 新增 PENDING_CLOTHES(待核对处理结果的衣物 uuid 列表)
│ │ ├── clothes-status.ts # [NEW] 衣物处理状态语义单一来源。导出 isProcessing/isTerminal/clothesStatusPill 等:1/2/3 归"AI 处理中"(plain),5 归"处理失败"(ink),4 为空;字段缺失时降级为原"category_main 与 processed_image 均为空则处理中"推断,保证旧后端兼容
│ │ └── pending-clothes.ts # [NEW] 待处理衣物持久化与解析器。add/remove/get(幂等、上限 20、24h 过期清理);resolvePendingClothes() 并发查询各 uuid 的 /clothes/:uuid/status,返回 {finished, failed:[{uuid,reason}], processing},终结态移出存储;50000 静默移除;network 失败保留
│ ├── types/
│ │ └── models.ts # [MODIFY] ClothesItem 增加可选 image_process_status?: number、fail_reason?: string(对齐后端新字段)
│ └── pages/
│ ├── upload/
│ │ ├── upload.ts # [MODIFY] 上传成功即 addPendingClothes;轮询改 2s×90≈180s;预算耗尽只停轮询并置 slowHint 中性提示,不再置 failed;仅后端返回 FAILED 时进入失败态(同时 removePending);onHide 停轮询、onShow 续查恢复;SUCCESS 移除 pending
│ │ └── upload.wxml # [MODIFY] 处理中区块增加 slowHint 中性文案(可先返回衣橱,结果自动更新);失败区块的"额度已自动退回"改为受数据开关控制,仅真实失败展示
│ ├── wardrobe/
│ │ ├── wardrobe.ts # [MODIFY] onShow 在有待处理记录时调用 resolvePendingClothes:失败弹窗(v2-overlay)可跳详情、存在终结态则静默重刷列表;fetchList 映射改用 clothesStatusPill
│ │ ├── wardrobe.wxml # [MODIFY] 新增 v2-overlay 节点承接失败提示
│ │ └── wardrobe.json # [MODIFY] 注册 v2-overlay 组件
│ └── clothes-detail/
│ ├── clothes-detail.ts # [MODIFY] fetchDetail 用响应状态初始化 imageStatus/step/failReason;终态(4/5)不启动轮询,仅处理中才轮询;轮询预算耗尽保持处理中视图不误报
│ └── clothes-detail.wxml # [MODIFY] 处理中提示补"排队时可能更久"中性说明(可选微调)
zt-closet-backend/
├── internal/
│ ├── dto/
│ │ └── clothes.go # [MODIFY] ClothesGetByUUIDResponse 增加 image_process_status、fail_reason(列表/详情共用)
│ └── service/
│ ├── clothes.go # [MODIFY] buildClothesResponse 填充新字段;GetByUUID 非属主(share_uuid)清空 fail_reason;上传失败回退 refundCosQuota 改用独立 context(trace+Background 派生,断开连接也能回退)
│ └── closet.go # [MODIFY] 衣柜分享响应路径同样屏蔽非属主 fail_reason,保留状态字段
└── 接口文档.md # [MODIFY] 列表/详情响应补充新字段说明,并注明 fail_reason 仅属主可见
```
## 关键代码结构
```ts
// utils/pending-clothes.ts —— 跨模块契约
export interface PendingClothes { uuid: string; createdAt: number } // createdAt: 秒级时间戳
export function addPendingClothes(uuid: string): void
export function removePendingClothes(uuid: string): void
export function getPendingClothes(): PendingClothes[] // 读取时顺带清理过期项
export function resolvePendingClothes(): Promise<{
finished: string[] // 本轮确认成功
failed: Array<{ uuid: string; reason: string }> // 本轮确认失败(含原因)
processing: string[] // 仍在处理中,保留待下次
}>
```
## Agent Extensions
### SubAgent
- **code-explorer**
- Purpose: 在改动完成后跨仓库审计 `ClothesGetByUUIDResponse`(后端 closet.go、clothes.go 等消费方)与前端 `ClothesItem` 全部使用页面(wardrobe、ai-recommend、outfit-edit、closet-share-create、outfit-detail 等)的受影响调用点,确认新增字段与非属主屏蔽逻辑无遗漏、无破坏。
- Expected outcome: 输出受影响调用点清单与回归结论(是否存在遗漏消费方或破坏性变更),作为验证环节的依据。