Kaynağa Gözat

docs: add template usage summary design

ChenYu 4 ay önce
ebeveyn
işleme
09fa9714dc

+ 383 - 0
docs/superpowers/specs/2026-06-28-template-usage-summary-design.md

@@ -0,0 +1,383 @@
+# 模板使用量与顶部统计设计
+
+## 1. 背景
+
+当前“我的发型模板”页面已经具备模板列表卡片和顶部 4 个统计卡片的 UI 骨架,但这部分数据还不是真实业务数据:
+
+- 模板列表接口 `GET /api/v1/templates` 当前返回的 `usedCount` 仍是固定值 `0`。
+- 前端 `TemplateListPage` 中顶部统计使用的是本地默认值,且请求的 `/api/v1/stylist/dashboard/template-summary` 在后端并没有真实实现。
+- 用户希望在当前模板表中新增“使用量”字段,用于支撑:
+  - 列表卡片中的“使用 x 次”
+  - 顶部统计中的“本周使用”
+  - 其余 4 个统计卡片的真实展示
+
+本轮目标是在尽量不改动现有模板列表 UI 结构的前提下,把模板使用量和顶部统计接成真实、可持续维护的数据链路。
+
+## 2. 目标
+
+本轮交付后,需要满足以下目标:
+
+1. `hair_template` 表新增累计使用量字段。
+2. 模板列表中的 `usedCount` 返回真实值,卡片展示“使用 x 次”。
+3. “我的发型模板”顶部 4 个统计卡片接入真实后端数据。
+4. “本周使用”按本周增量统计,而不是累计值。
+5. AI 发型模拟成功后,相关模板的累计使用量自动递增。
+
+## 3. 非目标
+
+以下内容不在本次范围内:
+
+1. 不补历史数据回填。当前项目没有历史数据,本轮无需做初始化聚合脚本。
+2. 不新增第五个或更多顶部统计卡片。
+3. 不调整模板列表已确认过的 UI 结构、卡片布局和交互。
+4. 不改变“使用量”的业务含义,本轮统一定义为“成功用于 AI 发型模拟的次数”。
+5. 不引入新的使用日志表。
+
+## 4. 方案选择
+
+### 方案 A:仅存累计使用量,顶部“本周使用”也直接显示累计值
+
+做法:
+
+- 只在 `hair_template` 增加累计字段。
+- 列表卡片和顶部“本周使用”都读累计值。
+
+优点:
+
+- 实现最简单。
+
+缺点:
+
+- “本周使用”语义错误,不符合用户确认的口径。
+
+### 方案 B:查询时临时聚合使用次数
+
+做法:
+
+- 不在 `hair_template` 落累计字段。
+- 列表和顶部统计都在查询时实时聚合 `ai_hair_simulation_task`。
+
+优点:
+
+- 数据完全从事实表推导。
+
+缺点:
+
+- 列表接口每次查询都要聚合,复杂度和查询成本更高。
+- 不符合用户已明确提出的“在模板表新增字段”要求。
+
+### 方案 C:模板表存累计使用量,顶部“本周使用”从任务表做周增量统计
+
+做法:
+
+- 在 `hair_template` 增加 `used_count`,保存累计使用量。
+- AI 模拟任务成功时,对对应模板 `used_count + 1`。
+- 顶部“本周使用”单独从 `ai_hair_simulation_task` 统计本周成功任务数。
+
+优点:
+
+- 列表卡片直接读累计值,查询简单。
+- 顶部“本周使用”保留正确的周增量语义。
+- 改动范围可控,符合当前系统结构。
+
+缺点:
+
+- 需要同时维护累计字段和周统计逻辑。
+
+### 结论
+
+采用 **方案 C:模板表存累计使用量,顶部“本周使用”从任务表做周增量统计**。
+
+## 5. 业务口径
+
+### 5.1 模板累计使用量
+
+- 落库字段:`hair_template.used_count`
+- 含义:该模板累计被成功用于 AI 发型模拟的次数
+- 消费位置:
+  - 模板列表卡片中的“使用 x 次”
+  - 未来如有模板详情统计,也可直接复用
+
+### 5.2 本周使用
+
+- 含义:当前自然周内,属于当前美发师模板的 AI 发型模拟成功任务总数
+- 统计来源:`ai_hair_simulation_task`
+- 判断条件:
+  - `task_status = 'SUCCESSES'`
+  - `task_finish_time` 落在当前自然周范围内
+  - `is_deleted = 0`
+  - `template_id` 能关联到当前美发师自己的 `hair_template`
+
+说明:
+
+- 统计单位是“成功任务数”,不是“去重后的模板数”。
+- 同一个模板在本周内成功使用多次,应累计多次。
+
+### 5.3 顶部 4 个统计卡片
+
+继续沿用当前页面已有的 4 个统计项,不改 UI 结构:
+
+1. `total`
+   - 含义:当前美发师未删除模板总数
+2. `published`
+   - 含义:`status = 'PUBLIC'` 的未删除模板数
+3. `weeklyUsed`
+   - 含义:本周成功使用次数
+4. `draft`
+   - 含义:`status = 'DRAFT'` 的未删除模板数
+
+## 6. 数据库设计
+
+### 6.1 表结构
+
+在 `hair_template` 新增字段:
+
+```sql
+used_count int unsigned not null default 0 comment '模板累计使用次数'
+```
+
+要求:
+
+- 默认为 `0`
+- 非空
+- 保留字段注释
+
+### 6.2 Prisma 模型
+
+在 `HairTemplate` 模型新增:
+
+```prisma
+usedCount Int @default(0) @map("used_count") @db.UnsignedInt
+```
+
+### 6.3 数据迁移策略
+
+- 本轮仅新增字段,不做历史数据回填。
+- 新建模板默认 `used_count = 0`。
+- 上线后由真实的 AI 模拟成功事件持续累积。
+
+## 7. 后端设计
+
+### 7.1 模板列表接口
+
+继续使用现有接口:
+
+```http
+GET /api/v1/templates
+```
+
+返回结构保持不变,但 `usedCount` 改为真实映射:
+
+```ts
+{
+  items: Array<{
+    id: string
+    name: string
+    category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
+    status: 'DRAFT' | 'PUBLIC'
+    description: string
+    tags: string[]
+    image: string
+    makeTaskId?: string
+    makeTaskStatus?: string
+    usedCount: number
+  }>
+  total: number
+  page: number
+  pageSize: number
+}
+```
+
+实现要求:
+
+- `usedCount` 直接来自 `hair_template.used_count`
+- 不再返回固定值 `0`
+
+### 7.2 顶部统计接口
+
+新增模板模块内的真实统计接口:
+
+```http
+GET /api/v1/templates/summary
+```
+
+返回结构:
+
+```json
+{
+  "total": 24,
+  "published": 18,
+  "weeklyUsed": 6,
+  "draft": 4
+}
+```
+
+说明:
+
+- 不再依赖不存在的 `/api/v1/stylist/dashboard/template-summary`
+- 接口归属放在模板模块内,方便和模板列表、模板编辑共用权限与领域边界
+
+### 7.3 统计实现
+
+`TemplatesService` 新增 `getSummary(stylistId)` 方法,返回:
+
+- `total`
+- `published`
+- `weeklyUsed`
+- `draft`
+
+建议实现方式:
+
+1. `total`
+   - `hair_template.count`
+   - 条件:`hairstylist_id = 当前美发师`,`is_deleted = 0`
+2. `published`
+   - `hair_template.count`
+   - 条件加上 `status = 'PUBLIC'`
+3. `draft`
+   - `hair_template.count`
+   - 条件加上 `status = 'DRAFT'`
+4. `weeklyUsed`
+   - 统计 `ai_hair_simulation_task` 中本周成功任务数
+   - 为避免把其他人的模板统计进来,需要按模板归属过滤
+
+`weeklyUsed` 的推荐查询思路:
+
+1. 先计算当前自然周的起止时间
+2. 在 `ai_hair_simulation_task` 中筛选:
+   - `taskStatus = 'SUCCESSES'`
+   - `taskFinishTime` 在本周范围内
+   - `isDeleted = 0`
+3. 通过 `templateId` 关联 `hair_template`
+4. 仅统计:
+   - `hair_template.hairstylist_id = 当前美发师`
+   - `hair_template.is_deleted = 0`
+
+如果 Prisma 直接跨表过滤不够顺手,可以采用两段式最小实现:
+
+1. 先取当前美发师所有未删除模板 ID
+2. 再用这些模板 ID 统计本周成功任务数
+
+### 7.4 累计使用量更新
+
+累计值的更新触发点放在“AI 发型模拟任务成功”的链路里。
+
+当前系统已经存在 `ai_hair_simulation_task` 的状态刷新和结果落库逻辑,因此本轮在任务状态首次转为 `SUCCESSES` 时,补充:
+
+```ts
+hair_template.used_count += 1
+```
+
+要求:
+
+- 只在任务真正从未成功状态转为 `SUCCESSES` 时递增一次
+- 避免重复轮询、重复刷新导致同一个任务重复累加
+
+推荐做法:
+
+1. 在更新任务状态前读取旧状态
+2. 只有当:
+   - 旧状态不是 `SUCCESSES`
+   - 新状态是 `SUCCESSES`
+   时,才对对应模板执行一次递增
+
+说明:
+
+- 触发位置应靠近 `SimulationsService` 中任务刷新成功的逻辑,而不是放在列表查询时临时聚合。
+- 这样可以保证模板表里的累计值始终可直接复用。
+
+## 8. 前端设计
+
+### 8.1 模板列表页
+
+`TemplateListPage` 保持现有布局与交互,数据层做以下调整:
+
+- 页面初始化时请求真实的模板 summary
+- 顶部 4 个统计卡片展示真实值
+- 列表卡片继续读 `template.usedCount`
+
+### 8.2 summary 接口调整
+
+前端 `getTemplateDashboardSummary()` 当前请求:
+
+```ts
+/stylist/dashboard/template-summary
+```
+
+本轮改为请求:
+
+```ts
+/templates/summary
+```
+
+### 8.3 初始值与失败态
+
+当前前端存在默认假数据:
+
+```ts
+{
+  total: 24,
+  published: 18,
+  weeklyUsed: 6,
+  draft: 4,
+}
+```
+
+本轮改为:
+
+- 初始值使用全 `0`
+- 请求失败时回退到全 `0`
+
+原因:
+
+- 避免页面出现和真实业务无关的假统计
+- 保持失败时的 UI 稳定,不额外扩大本轮范围
+
+## 9. 测试设计
+
+### 9.1 后端单测
+
+更新 `templates.service.spec.ts`,至少覆盖:
+
+1. 模板列表返回真实 `usedCount`
+2. `getSummary()` 返回:
+   - `total`
+   - `published`
+   - `weeklyUsed`
+   - `draft`
+3. AI 模拟任务首次成功时会触发对应模板 `used_count + 1`
+4. 同一个任务重复刷新为成功时不会重复累加
+
+### 9.2 后端 E2E
+
+更新 `templates.e2e-spec.ts`,至少覆盖:
+
+1. `GET /api/v1/templates` 返回真实 `usedCount`
+2. `GET /api/v1/templates/summary` 返回 4 个真实统计字段
+
+如现有 E2E 框架不便直接覆盖“模拟任务成功触发模板使用量递增”,可优先用单测锁住该业务规则。
+
+### 9.3 前端测试
+
+更新 `TemplateListPage.test.tsx`,至少覆盖:
+
+1. 顶部 4 个统计卡片展示真实 summary
+2. 列表卡片中的“使用 x 次”展示接口返回值
+3. 不再依赖假默认 summary
+4. summary 请求失败时展示 `0`
+
+## 10. 风险与约束
+
+1. `weeklyUsed` 依赖 `task_finish_time`,因此只有成功完成并记录完成时间的任务才会被统计。
+2. `template_id` 在 `ai_hair_simulation_task` 中是字符串,需要在统计时注意与 `hair_template.id` 的类型转换。
+3. 若未来存在“删除模板但保留历史任务”的场景,本轮设计会按当前规则忽略已删除模板对应的周统计,这与当前“只看未删除模板业务数据”的页面语义一致。
+
+## 11. 交付结果
+
+本轮完成后,系统应具备以下结果:
+
+1. 模板表具备 `used_count` 字段
+2. 模板列表返回真实使用量
+3. 顶部 4 个统计卡片展示真实数据
+4. “本周使用”按本周增量统计
+5. AI 发型模拟成功后自动累计模板使用量