浏览代码

docs: add customer detail simulation gallery spec

ChenYu 4 月之前
父节点
当前提交
00147b93bd
共有 1 个文件被更改,包括 198 次插入 和 0 次删除
  1. 198 0
      docs/superpowers/specs/2026-06-28-customer-detail-simulation-gallery-design.md

+ 198 - 0
docs/superpowers/specs/2026-06-28-customer-detail-simulation-gallery-design.md

@@ -0,0 +1,198 @@
+# 顾客详情页 AI 模拟结果卡片流设计
+
+## 目标
+
+在顾客详情页的“过往照片和AI发型模拟”区块中,替换当前未开发占位卡片,展示该顾客历史 AI 发型模拟生成结果。每一张生成图对应一张卡片,按创建时间倒序展示,点击卡片进入对应任务详情页。
+
+## 背景
+
+- 当前顾客详情页中,“过往照片和AI发型模拟”区块仍是静态占位,不承载真实业务结果。
+- AI 发型模拟主流程已经具备创建任务、查询任务详情、展示子任务结果图的能力。
+- 用户要求在顾客详情页直接看到“刚刚任务的结果”,并明确要求:
+  - 一个图片就是一个任务卡片
+  - 按时间顺序全部展示
+  - 点击图片卡片进入对应任务详情页
+  - 排序采用最新在前
+
+## 范围
+
+### 本次包含
+
+- 新增顾客维度的 AI 模拟结果卡片列表接口
+- 顾客详情页接入真实结果卡片流
+- 每张生成图展开为一张独立卡片
+- 点击卡片跳转到对应模拟任务详情页
+- 空状态展示
+
+### 本次不包含
+
+- 分页
+- 筛选
+- 失败任务和处理中任务的卡片展示
+- 在顾客详情页直接轮询任务状态
+- 任务详情页结构变更
+
+## 交互设计
+
+### 区块位置
+
+继续使用顾客详情页现有“过往照片和AI发型模拟”区块,不改变整体页面骨架、标题和右上角“AI发型模拟”入口按钮位置。
+
+### 卡片规则
+
+- 一张生成图展开为一张卡片
+- 一个任务下如果有多张生成图,则展开成多张卡片
+- 同一个 `simulationId` 下出现多张卡片属于预期行为
+- 卡片至少展示:
+  - 生成图
+  - 模板名称
+  - 生成时间
+
+### 点击行为
+
+- 点击任意卡片,跳转至 `/:locale/simulation/result/:simulationId`
+- 不跳到子任务维度页面,继续复用现有任务详情页
+
+### 排序
+
+- 按模拟任务创建时间倒序排列
+- 最新的结果卡片排在最前面
+
+### 空状态
+
+- 当顾客还没有任何成功生成的 AI 模拟结果时,不显示假卡片
+- 使用清晰的空状态文案提示当前暂无 AI 模拟结果
+
+## 后端设计
+
+### 新接口
+
+- 路由:`GET /api/v1/simulations/customer/:customerId/gallery`
+- 权限:仅允许当前登录发型师查看自己名下顾客的数据
+
+### 返回结构
+
+返回扁平化卡片列表,而不是主任务嵌套结构。每条记录代表一张生成图卡片。
+
+建议 DTO:
+
+```ts
+export type CustomerSimulationGalleryItemDto = {
+  simulationId: string;
+  taskId: string;
+  imageUrl: string;
+  templateName: string;
+  createdAt: string;
+  taskStatus: 'SUCCESSES';
+};
+```
+
+响应示例:
+
+```json
+{
+  "items": [
+    {
+      "simulationId": "91",
+      "taskId": "task-11",
+      "imageUrl": "https://cdn.example.com/result-1.png",
+      "templateName": "Layer Bob",
+      "createdAt": "2026-06-28T09:00:00.000Z",
+      "taskStatus": "SUCCESSES"
+    }
+  ]
+}
+```
+
+### 查询规则
+
+- 只查询当前发型师名下的顾客
+- 只查询该顾客关联的 `ai_hair_simulation`
+- 只展开 `ai_hair_simulation_task` 中 `task_status = 'SUCCESSES'` 的任务
+- 从 `result_images` 中展开真实图片列表
+- 每张图片映射为一条扁平记录
+- 按所属模拟任务 `create_time` 倒序输出
+
+### 数据来源
+
+- 主表:`ai_hair_simulation`
+- 子表:`ai_hair_simulation_task`
+- 模板名来自 `hair_template.template_name`
+
+### 安全约束
+
+- 必须校验 `customerId` 属于当前发型师
+- 不允许跨发型师读取顾客结果
+
+## 前端设计
+
+### API 层
+
+在 `frontend/src/services/api/simulations.ts` 中新增顾客详情页所需的 gallery 查询方法,并在 `contracts.ts` 中补齐对应类型。
+
+建议签名:
+
+```ts
+export async function listCustomerSimulationGallery(
+  customerId: string,
+): Promise<{ items: CustomerSimulationGalleryItem[] }>
+```
+
+### 顾客详情页
+
+在 `CustomerDetailPage.tsx` 中替换原有 `photoPlaceholders` 占位卡片渲染逻辑:
+
+- 页面加载后并行请求:
+  - 顾客详情
+  - 顾客 AI 模拟结果卡片列表
+- 卡片区改为真实图片网格
+- 每张卡片使用 `Link`
+- 跳转目标为 `path(\`/simulation/result/${simulationId}\`)`
+
+### 展示字段
+
+每张卡片展示:
+
+- 主图:`imageUrl`
+- 标题:`templateName`
+- 辅助时间:`createdAt` 的格式化日期时间
+
+## 边界与异常
+
+- 如果接口失败:
+  - 顾客详情主体仍按现有策略处理
+  - AI 模拟图片区块显示错误或空状态,而不是整页崩溃
+- 如果某个成功任务的 `result_images` 为空数组:
+  - 该任务不生成卡片
+- 如果模板被删除:
+  - 模板名称允许回退为默认文案,但不阻塞图片展示
+
+## 测试策略
+
+### 后端
+
+- service 单测:
+  - 顾客归属校验
+  - 仅展开成功任务
+  - 一个任务多图展开为多条记录
+  - 倒序输出
+- e2e:
+  - 返回当前顾客 gallery
+  - 非本人顾客不可访问
+
+### 前端
+
+- API 测试:
+  - 请求路径正确
+- 页面测试:
+  - 成功渲染真实卡片
+  - 空状态渲染
+  - 点击卡片跳转到结果详情页
+
+## 验收标准
+
+- 顾客详情页“过往照片和AI发型模拟”区块展示真实 AI 模拟结果卡片
+- 一张生成图对应一张卡片
+- 最新结果排在最前
+- 点击卡片进入对应任务详情页
+- 页面整体 UI 骨架不被破坏