2026-06-28-template-usage-summary-design.md 10 KB

模板使用量与顶部统计设计

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 新增字段:

used_count int unsigned not null default 0 comment '模板累计使用次数'

要求:

  • 默认为 0
  • 非空
  • 保留字段注释

6.2 Prisma 模型

在 HairTemplate 模型新增:

usedCount Int @default(0) @map("used_count") @db.UnsignedInt

6.3 数据迁移策略

  • 本轮仅新增字段,不做历史数据回填。
  • 新建模板默认 used_count = 0。
  • 上线后由真实的 AI 模拟成功事件持续累积。

7. 后端设计

7.1 模板列表接口

继续使用现有接口:

GET /api/v1/templates

返回结构保持不变,但 usedCount 改为真实映射:

{
  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 顶部统计接口

新增模板模块内的真实统计接口:

GET /api/v1/templates/summary

返回结构:

{
  "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 时,补充:

hair_template.used_count += 1

要求:

  • 只在任务真正从未成功状态转为 SUCCESSES 时递增一次
  • 避免重复轮询、重复刷新导致同一个任务重复累加

推荐做法:

  1. 在更新任务状态前读取旧状态
  2. 只有当:
    • 旧状态不是 SUCCESSES
    • 新状态是 SUCCESSES 时,才对对应模板执行一次递增

说明:

  • 触发位置应靠近 SimulationsService 中任务刷新成功的逻辑,而不是放在列表查询时临时聚合。
  • 这样可以保证模板表里的累计值始终可直接复用。

8. 前端设计

8.1 模板列表页

TemplateListPage 保持现有布局与交互,数据层做以下调整:

  • 页面初始化时请求真实的模板 summary
  • 顶部 4 个统计卡片展示真实值
  • 列表卡片继续读 template.usedCount

8.2 summary 接口调整

前端 getTemplateDashboardSummary() 当前请求:

/stylist/dashboard/template-summary

本轮改为请求:

/templates/summary

8.3 初始值与失败态

当前前端存在默认假数据:

{
  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 发型模拟成功后自动累计模板使用量