Kaynağa Gözat

chore: remove ai process docs from main

ChenYu 3 ay önce
ebeveyn
işleme
64854a5b0d

+ 0 - 234
docs/superpowers/plans/2026-06-28-prisma-v1-baseline.md

@@ -1,234 +0,0 @@
-# Prisma V1 Baseline Migration Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** 将当前稳定版数据库结构收敛为单一的 `v1 baseline migration`,删除旧增量 migration,并让 Prisma 后续从该基线继续演进。
-
-**Architecture:** 保持 `backend/prisma/schema.prisma` 不变,把 `backend/prisma/migrations` 重置为仅包含一条 `20260628_init_v1/migration.sql` 的完整建表基线;现有数据库不重建、不重新执行 baseline SQL,而是通过 Prisma 的已应用状态对齐这条基线。实现过程中同步校验 baseline SQL 与 `schema.prisma`、`数据库设计.md` 一致,并移除旧 migration 目录。
-
-**Tech Stack:** Prisma, MySQL, SQL, Node.js
-
-## Global Constraints
-
-- `schema.prisma` 继续作为当前稳定数据库结构的唯一事实来源。
-- `backend/prisma/migrations` 目录只保留一条 `v1 baseline migration`。
-- baseline SQL 必须包含当前稳定结构的完整建表、索引与注释。
-- 旧 migration 全部移除,不保留历史增量。
-- 不修改当前稳定数据库结构语义,只做基线重置。
-- 现有数据库不重新执行 baseline SQL,采用 Prisma 已应用状态对齐。
-- 后续数据库改动必须在 `v1 baseline` 之上继续新增 migration。
-
----
-
-### Task 1: 生成并校验 V1 Baseline SQL
-
-**Files:**
-- Create: `backend/prisma/migrations/20260628_init_v1/migration.sql`
-- Read: `backend/prisma/schema.prisma`
-- Read: `backend/docs/数据库设计.md`
-
-**Interfaces:**
-- Consumes: `backend/prisma/schema.prisma`
-- Consumes: `backend/docs/数据库设计.md`
-- Produces: `backend/prisma/migrations/20260628_init_v1/migration.sql`
-
-- [ ] **Step 1: 基于当前 schema 与数据库设计文档列出 baseline 必须覆盖的表**
-
-```text
-hairstylist_user
-customer
-customer_treatment_record
-ai_hair_simulation
-ai_hair_simulation_task
-hair_template
-hair_template_make_task
-system_config
-```
-
-- [ ] **Step 2: 先写 baseline SQL 文件,包含完整建表与索引定义**
-
-```sql
-CREATE TABLE `hairstylist_user` (
-  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '发型师主键ID',
-  `username` VARCHAR(50) NOT NULL COMMENT '发型师用户名',
-  `email` VARCHAR(100) NOT NULL COMMENT '登录邮箱',
-  `password` VARCHAR(100) NOT NULL COMMENT '加密后的登录密码',
-  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删除,1已删除',
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='发型师用户信息表';
-
-CREATE TABLE `hair_template` (
-  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板主键ID',
-  `template_name` VARCHAR(100) NOT NULL COMMENT '模板名称(必填)',
-  `cover_url` VARCHAR(500) DEFAULT '' COMMENT '模板封面图URL',
-  `status` ENUM('DRAFT','PUBLIC') NOT NULL DEFAULT 'DRAFT' COMMENT '模板状态:DRAFT草稿 / PUBLIC公开',
-  `category` ENUM('LONG','MEDIUM','SHORT','DYE') NOT NULL DEFAULT 'LONG' COMMENT '分类:长发/中长发/短发/染发',
-  `tags` VARCHAR(500) DEFAULT '' COMMENT '模板标签,逗号分隔存储',
-  `description` TEXT NULL COMMENT '模板详细说明',
-  `model_url` VARCHAR(500) DEFAULT '' COMMENT 'AI模特图片URL',
-  `clothes_url` VARCHAR(500) DEFAULT '' COMMENT '服装素材URL',
-  `background_url` VARCHAR(500) DEFAULT '' COMMENT '背景图片URL',
-  `ref_hair_url` VARCHAR(500) DEFAULT '' COMMENT '参考发型图片URL',
-  `hairstylist_id` BIGINT UNSIGNED NULL COMMENT '创建发型师ID,系统模板为空',
-  `template_url` VARCHAR(500) DEFAULT '' COMMENT '完整模板预览页面URL',
-  `used_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '模板累计使用次数',
-  `is_system_template` ENUM('YES','NO') NOT NULL DEFAULT 'NO' COMMENT '是否系统模板:YES系统模板,NO门店自定义模板',
-  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1删除',
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI发型模板表';
-```
-
-- [ ] **Step 3: 补齐剩余 6 张表、索引和全部注释**
-
-```sql
-CREATE INDEX `idx_category` ON `hair_template` (`category`, `is_deleted`);
-CREATE INDEX `idx_hairstylist_id` ON `hair_template` (`hairstylist_id`, `is_deleted`);
-CREATE INDEX `idx_status` ON `hair_template` (`status`, `is_deleted`);
-```
-
-- [ ] **Step 4: 自查 baseline SQL 是否包含 `used_count`、索引和注释**
-
-Run: `grep -n "used_count\\|COMMENT\\|CREATE INDEX" backend/prisma/migrations/20260628_init_v1/migration.sql | head -n 20`
-
-Expected: 输出包含 `used_count`、`COMMENT`、`CREATE INDEX`
-
-- [ ] **Step 5: 提交 baseline SQL 初稿**
-
-```bash
-git add backend/prisma/migrations/20260628_init_v1/migration.sql
-git commit -m "feat: add prisma v1 baseline migration"
-```
-
-### Task 2: 清理旧 migration 并校验目录基线
-
-**Files:**
-- Delete: `backend/prisma/migrations/20260628050000_add_customer_follow_content/migration.sql`
-- Delete: `backend/prisma/migrations/20260628120000_add_customer_table/migration.sql`
-- Delete: `backend/prisma/migrations/20260628173000_add_template_used_count/migration.sql`
-- Modify: `backend/prisma/migrations/`
-
-**Interfaces:**
-- Consumes: `backend/prisma/migrations/20260628_init_v1/migration.sql`
-- Produces: `backend/prisma/migrations/` 目录中仅保留 `20260628_init_v1`
-
-- [ ] **Step 1: 删除旧 migration 目录**
-
-```text
-backend/prisma/migrations/20260628050000_add_customer_follow_content
-backend/prisma/migrations/20260628120000_add_customer_table
-backend/prisma/migrations/20260628173000_add_template_used_count
-```
-
-- [ ] **Step 2: 列出 migration 目录,确认只剩 baseline**
-
-Run: `find backend/prisma/migrations -maxdepth 2 -type f | sort`
-
-Expected:
-
-```text
-backend/prisma/migrations/20260628_init_v1/migration.sql
-```
-
-- [ ] **Step 3: 再次校验 baseline 文件存在且旧目录已移除**
-
-Run: `git status --short backend/prisma/migrations`
-
-Expected: 只出现新 baseline 与旧目录删除记录
-
-- [ ] **Step 4: 提交 migration 目录重置**
-
-```bash
-git add backend/prisma/migrations
-git commit -m "chore: reset prisma migrations to v1 baseline"
-```
-
-### Task 3: 对齐现有数据库与 Prisma migration 状态
-
-**Files:**
-- Modify: `backend/prisma/migration_lock.toml` (if generated by Prisma tooling)
-- Read: current database state via Prisma CLI
-
-**Interfaces:**
-- Consumes: `backend/prisma/migrations/20260628_init_v1/migration.sql`
-- Produces: Prisma recognizes `20260628_init_v1` as applied for the current database
-
-- [ ] **Step 1: 先确认当前数据库不需要重新执行 baseline SQL**
-
-Run: `npx prisma db pull --print`
-
-Expected: 输出的结构与 `backend/prisma/schema.prisma` 主要一致,不出现缺失 `used_count` 等明显差异
-
-- [ ] **Step 2: 将 baseline migration 标记为已应用**
-
-Run: `npx prisma migrate resolve --applied 20260628_init_v1`
-
-Expected: 输出包含 `marked as applied` 或等价成功提示
-
-- [ ] **Step 3: 查看 Prisma migration 状态**
-
-Run: `npx prisma migrate status`
-
-Expected: 输出包含 `20260628_init_v1`,且当前数据库没有 pending migration
-
-- [ ] **Step 4: 如果 Prisma 生成了额外锁文件或状态文件,确认其已纳入版本控制策略**
-
-```text
-backend/prisma/migration_lock.toml
-```
-
-- [ ] **Step 5: 提交 migration 状态对齐改动**
-
-```bash
-git add backend/prisma
-git commit -m "chore: align prisma baseline migration state"
-```
-
-### Task 4: 最终一致性校验与文档收口
-
-**Files:**
-- Read: `backend/prisma/schema.prisma`
-- Read: `backend/prisma/migrations/20260628_init_v1/migration.sql`
-- Read: `backend/docs/数据库设计.md`
-
-**Interfaces:**
-- Consumes: `schema.prisma`, `数据库设计.md`, `20260628_init_v1/migration.sql`
-- Produces: verified v1 baseline layout ready for future migrations
-
-- [ ] **Step 1: 对照 `schema.prisma` 与 baseline SQL 逐项检查核心表和字段**
-
-```text
-重点校验:
-- customer.follow_content
-- ai_hair_simulation.template_ids
-- hair_template.used_count
-- system_config.value_type
-```
-
-- [ ] **Step 2: 运行 Prisma 客户端生成,确认 schema 与 migration 目录在当前仓库状态下可工作**
-
-Run: `npx prisma generate`
-
-Expected: PASS,输出包含 `Generated Prisma Client`
-
-- [ ] **Step 3: 运行 migration 状态检查,确认没有残留旧目录引用**
-
-Run: `npx prisma migrate status`
-
-Expected: 不再出现旧 migration 名称,只识别 `20260628_init_v1`
-
-- [ ] **Step 4: 查看最终工作区状态,只确认本次相关文件**
-
-Run: `git status --short backend/prisma backend/docs/数据库设计.md`
-
-Expected: 仅包含本次 baseline 重置相关改动,或已 clean
-
-- [ ] **Step 5: 提交最终收口**
-
-```bash
-git add backend/prisma backend/docs/数据库设计.md
-git commit -m "chore: finalize prisma v1 baseline"
-```

+ 0 - 495
docs/superpowers/plans/2026-06-28-template-form-config-assets-minimal.md

@@ -1,495 +0,0 @@
-# 模板新建页素材数据源切换 Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** 将模板新建页中的 AI 模特、服装、背景三组素材从本地静态配置切换为服务端配置接口,同时保留现有页面结构和旧提交流程。
-
-**Architecture:** 前端在模板 API 层新增素材读取方法和类型定义,`TemplateFormPage` 在挂载时请求服务端素材接口并将返回结果映射为现有卡片 UI 所需的选项结构。页面不再依赖本地 `hairstyleTemplateAssets`,接口失败或返回空组时直接显示错误并禁用提交,不做静态兜底。
-
-**Tech Stack:** React, TypeScript, React Router, Vitest, Testing Library, i18n JSON 文案, 现有 `http` API client
-
-## Global Constraints
-
-- 只替换这三个区块的数据来源。
-- 其他页面结构和旧提交流程先不改。
-- 接口失败时不回退本地静态数据。
-- 不改当前 `TemplateFormPage` 的其余表单结构。
-- 不改当前提交接口 `createTemplateAndStartGeneration()`。
-- 不在这次改动中接入模板制作任务页。
-- 不保留本地静态素材作为降级兜底。
-- 只增加与本次目标直接相关的定向前端测试。
-
----
-
-### Task 1: 定义素材接口与失败测试
-
-**Files:**
-- Create: `frontend/src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-- Modify: `frontend/src/services/api/contracts.ts`
-- Modify: `frontend/src/services/api/templates.ts`
-- Modify: `frontend/src/i18n/locales/zh/translation.json`
-- Modify: `frontend/src/i18n/locales/ja/translation.json`
-
-**Interfaces:**
-- Consumes: `http.get<T>()` from `frontend/src/services/http/client.ts`
-- Produces: `TemplateAssetOption`, `TemplateAssets`, `getTemplateAssets(): Promise<TemplateAssets>`
-
-- [ ] **Step 1: 写失败测试,锁定页面必须通过接口加载素材并在失败时禁用提交**
-
-```tsx
-import { describe, expect, it, beforeEach, vi } from 'vitest'
-import { render, screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { MemoryRouter } from 'react-router-dom'
-import { HelmetProvider } from 'react-helmet-async'
-import { TemplateFormPage } from '../TemplateFormPage'
-
-const navigateMock = vi.fn()
-
-vi.mock('../../../../shared/pageContext', async () => {
-  const actual = await vi.importActual<object>('../../../../shared/pageContext')
-  return {
-    ...actual,
-    usePreviewUrl: () => '/preview.jpg',
-    usePageContext: () => ({
-      locale: 'zh',
-      navigate: navigateMock,
-      path: (value: string) => `/zh${value}`,
-      copy: {
-        common: { cancel: '取消' },
-        templateFormPage: {
-          title: '新建发型模板',
-          description: 'desc',
-          back: '返回发型模板列表',
-          badge: '创建发型模板',
-          lead: 'lead',
-          generate: 'AI生成',
-          submitTitle: '生成AI发型模板',
-          submitLead: '提交后生成',
-          loadingAssets: '正在加载素材...',
-          assetLoadFailed: '素材加载失败,请刷新后重试',
-          assetEmpty: '素材未配置完成,请检查服务器配置',
-          reference: {
-            badge: '参考图片',
-            title: '参考发型图片',
-            previewAlt: '参考发型图片预览',
-            upload: '选择图片并预览',
-            help: 'help',
-          },
-          info: {
-            badge: '模板信息',
-            title: '发型模板基本信息',
-            name: '模板名称',
-            namePlaceholder: 'name',
-            status: '状态',
-            category: '分类',
-            tags: '标签',
-            tagsPlaceholder: 'tags',
-            description: '说明',
-            descriptionPlaceholder: 'desc',
-          },
-          model: { badge: 'AI模特', title: '选择AI模特' },
-          outfit: { badge: '服装', title: '选择服装' },
-          background: { badge: '背景', title: '选择背景' },
-          statusOptions: { Draft: '草稿', Published: '公开中' },
-          categoryOptions: { Long: '长发', Medium: '中长发', Short: '短发', Color: '染发' },
-          validation: {
-            referenceRequired: '请先上传参考图',
-            assetsRequired: '请选择模特、服装和背景',
-          },
-          errors: { saveFailed: '保存失败' },
-          submitting: { uploading: '正在上传参考图...', generating: '正在提交任务...' },
-        },
-      },
-    }),
-  }
-})
-
-const getTemplateAssetsMock = vi.fn()
-const createTemplateAndStartGenerationMock = vi.fn()
-
-vi.mock('../../../../services/api/templates', () => ({
-  getTemplateAssets: (...args: unknown[]) => getTemplateAssetsMock(...args),
-  createTemplateAndStartGeneration: (...args: unknown[]) =>
-    createTemplateAndStartGenerationMock(...args),
-}))
-
-vi.mock('../../../../services/api/media', () => ({
-  uploadMedia: vi.fn().mockResolvedValue({ url: 'https://cdn.example.com/reference.jpg' }),
-}))
-
-describe('TemplateFormPage', () => {
-  beforeEach(() => {
-    navigateMock.mockReset()
-    getTemplateAssetsMock.mockReset()
-    createTemplateAndStartGenerationMock.mockReset()
-  })
-
-  function renderPage() {
-    return render(
-      <HelmetProvider>
-        <MemoryRouter>
-          <TemplateFormPage />
-        </MemoryRouter>
-      </HelmetProvider>,
-    )
-  }
-
-  it('loads model, outfit and background options from api', async () => {
-    getTemplateAssetsMock.mockResolvedValue({
-      models: [{ id: 'm1', name: '模特A', url: 'https://img/model-a.jpg', configKey: 'model-a' }],
-      clothes: [{ id: 'c1', name: '服装A', url: 'https://img/cloth-a.jpg', configKey: 'cloth-a' }],
-      backgrounds: [{ id: 'b1', name: '背景A', url: 'https://img/bg-a.jpg', configKey: 'bg-a' }],
-    })
-
-    renderPage()
-
-    expect(await screen.findByText('模特A')).toBeInTheDocument()
-    expect(screen.getByText('服装A')).toBeInTheDocument()
-    expect(screen.getByText('背景A')).toBeInTheDocument()
-    expect(getTemplateAssetsMock).toHaveBeenCalledTimes(1)
-  })
-
-  it('shows error and disables submit when assets request fails', async () => {
-    getTemplateAssetsMock.mockRejectedValue(new Error('network error'))
-
-    renderPage()
-
-    expect(await screen.findByRole('alert')).toHaveTextContent('素材加载失败,请刷新后重试')
-    expect(screen.getAllByRole('button', { name: 'AI生成' })[0]).toBeDisabled()
-  })
-
-  it('submits api asset urls instead of local static assets', async () => {
-    const user = userEvent.setup()
-    getTemplateAssetsMock.mockResolvedValue({
-      models: [{ id: 'm1', name: '模特A', url: 'https://img/model-a.jpg', configKey: 'model-a' }],
-      clothes: [{ id: 'c1', name: '服装A', url: 'https://img/cloth-a.jpg', configKey: 'cloth-a' }],
-      backgrounds: [{ id: 'b1', name: '背景A', url: 'https://img/bg-a.jpg', configKey: 'bg-a' }],
-    })
-    createTemplateAndStartGenerationMock.mockResolvedValue({ id: '88' })
-
-    renderPage()
-
-    await screen.findByText('模特A')
-    await user.type(screen.getByLabelText('模板名称 *'), '云感长发')
-    await user.type(screen.getByLabelText('说明'), '说明内容')
-    await user.upload(
-      screen.getByLabelText('选择图片并预览'),
-      new File(['demo'], 'reference.jpg', { type: 'image/jpeg' }),
-    )
-    await user.click(screen.getAllByRole('button', { name: 'AI生成' })[0])
-
-    await waitFor(() => {
-      expect(createTemplateAndStartGenerationMock).toHaveBeenCalledWith(
-        expect.objectContaining({
-          modelImageUrl: 'https://img/model-a.jpg',
-          outfitImageUrl: 'https://img/cloth-a.jpg',
-          backgroundImageUrl: 'https://img/bg-a.jpg',
-        }),
-      )
-    })
-  })
-})
-```
-
-- [ ] **Step 2: 运行测试并确认当前失败**
-
-Run: `npm test -- src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-
-Expected:
-- FAIL,原因包括 `getTemplateAssets` 尚未导出
-- FAIL,原因包括页面仍然从 `hairstyleTemplateAssets` 读取静态素材
-- FAIL,原因包括页面尚未显示素材加载失败文案
-
-- [ ] **Step 3: 在 API contracts 中补素材类型**
-
-```ts
-export type TemplateAssetOption = {
-  id: string
-  name: string
-  url: string
-  configKey: string
-}
-
-export type TemplateAssets = {
-  models: TemplateAssetOption[]
-  clothes: TemplateAssetOption[]
-  backgrounds: TemplateAssetOption[]
-}
-```
-
-- [ ] **Step 4: 在模板 API 文件中新增素材读取方法**
-
-```ts
-import type {
-  GetTemplateByIdParams,
-  TemplateAssetOption,
-  TemplateAssets,
-  TemplateDashboardSummary,
-  TemplateDetailStats,
-  TemplateEntity,
-} from './contracts'
-
-export type {
-  GetTemplateByIdParams,
-  TemplateAssetOption,
-  TemplateAssets,
-  TemplateDashboardSummary,
-  TemplateDetailStats,
-  TemplateEntity,
-} from './contracts'
-
-export async function getTemplateAssets(): Promise<TemplateAssets> {
-  const data = await http.get<{
-    models?: TemplateAssetOption[]
-    clothes?: TemplateAssetOption[]
-    backgrounds?: TemplateAssetOption[]
-  }>('/templates/assets')
-
-  return {
-    models: data.models ?? [],
-    clothes: data.clothes ?? [],
-    backgrounds: data.backgrounds ?? [],
-  }
-}
-```
-
-- [ ] **Step 5: 在中日文文案中补充素材加载失败与空态文案**
-
-```json
-"templateFormPage": {
-  "loadingAssets": "正在加载素材...",
-  "assetLoadFailed": "素材加载失败,请刷新后重试",
-  "assetEmpty": "素材未配置完成,请检查服务器配置"
-}
-```
-
-```json
-"templateFormPage": {
-  "loadingAssets": "素材を読み込み中です...",
-  "assetLoadFailed": "素材の読み込みに失敗しました。再読み込みしてもう一度お試しください",
-  "assetEmpty": "素材設定が未完了です。サーバー設定を確認してください"
-}
-```
-
-- [ ] **Step 6: 重新运行定向测试,确认仍然只剩页面实现失败**
-
-Run: `npm test -- src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-
-Expected:
-- 仍然 FAIL
-- 错误收敛到 `TemplateFormPage` 仍未调用 `getTemplateAssets`
-
-- [ ] **Step 7: 提交**
-
-```bash
-git add frontend/src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx frontend/src/services/api/contracts.ts frontend/src/services/api/templates.ts frontend/src/i18n/locales/zh/translation.json frontend/src/i18n/locales/ja/translation.json
-git commit -m "test: lock template form asset loading contract"
-```
-
-### Task 2: 改造模板新建页为服务端素材来源
-
-**Files:**
-- Modify: `frontend/src/pages/dashboard/templates/TemplateFormPage.tsx`
-- Test: `frontend/src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-
-**Interfaces:**
-- Consumes: `getTemplateAssets(): Promise<TemplateAssets>`, `createTemplateAndStartGeneration(input: CreateTemplateAndGenerateInput): Promise<TemplateEntity>`, `uploadMedia(file: File): Promise<{ url: string }>`
-- Produces: 页面级素材加载状态 `assetsLoading: boolean`, 页面级素材错误状态 `assetsError: string`, 由 API 返回值映射出的 `modelOptions`, `outfitOptions`, `backgroundOptions`
-
-- [ ] **Step 1: 在页面测试中再补一个“空数据禁用提交”的失败用例**
-
-```tsx
-it('disables submit when any asset group is empty', async () => {
-  getTemplateAssetsMock.mockResolvedValue({
-    models: [{ id: 'm1', name: '模特A', url: 'https://img/model-a.jpg', configKey: 'model-a' }],
-    clothes: [],
-    backgrounds: [{ id: 'b1', name: '背景A', url: 'https://img/bg-a.jpg', configKey: 'bg-a' }],
-  })
-
-  renderPage()
-
-  expect(await screen.findByRole('alert')).toHaveTextContent('素材未配置完成,请检查服务器配置')
-  expect(screen.getAllByRole('button', { name: 'AI生成' })[0]).toBeDisabled()
-})
-```
-
-- [ ] **Step 2: 运行测试,确认新用例失败**
-
-Run: `npm test -- src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-
-Expected:
-- FAIL,页面还没有“空组即禁用提交”的逻辑
-
-- [ ] **Step 3: 最小实现页面加载状态、错误状态与 API 映射**
-
-```tsx
-import React, { useEffect, useMemo, useState } from 'react'
-import { createTemplateAndStartGeneration, getTemplateAssets } from '../../../services/api/templates'
-import type { TemplateAssetOption } from '../../../services/api/contracts'
-
-type TemplateOption = {
-  id: string
-  label: string
-  url: string
-  previewUrl: string
-}
-
-function toTemplateOptions(items: TemplateAssetOption[]): TemplateOption[] {
-  return items.map((item) => ({
-    id: item.id,
-    label: item.name,
-    url: item.url,
-    previewUrl: item.url,
-  }))
-}
-
-export function TemplateFormPage() {
-  const { locale, navigate, path, copy } = usePageContext()
-  const pageCopy = copy.templateFormPage
-  const [modelOptions, setModelOptions] = useState<TemplateOption[]>([])
-  const [outfitOptions, setOutfitOptions] = useState<TemplateOption[]>([])
-  const [backgroundOptions, setBackgroundOptions] = useState<TemplateOption[]>([])
-  const [assetsLoading, setAssetsLoading] = useState(true)
-  const [assetsError, setAssetsError] = useState('')
-  const [selectedModelId, setSelectedModelId] = useState('')
-  const [selectedOutfitId, setSelectedOutfitId] = useState('')
-  const [selectedBackgroundId, setSelectedBackgroundId] = useState('')
-
-  useEffect(() => {
-    let cancelled = false
-
-    async function loadAssets() {
-      setAssetsLoading(true)
-      setAssetsError('')
-      try {
-        const data = await getTemplateAssets()
-        if (cancelled) return
-
-        const nextModels = toTemplateOptions(data.models)
-        const nextClothes = toTemplateOptions(data.clothes)
-        const nextBackgrounds = toTemplateOptions(data.backgrounds)
-
-        setModelOptions(nextModels)
-        setOutfitOptions(nextClothes)
-        setBackgroundOptions(nextBackgrounds)
-
-        if (nextModels.length === 0 || nextClothes.length === 0 || nextBackgrounds.length === 0) {
-          setAssetsError(pageCopy.assetEmpty || '素材未配置完成,请检查服务器配置')
-          return
-        }
-
-        setSelectedModelId((current) => current || nextModels[0].id)
-        setSelectedOutfitId((current) => current || nextClothes[0].id)
-        setSelectedBackgroundId((current) => current || nextBackgrounds[0].id)
-      } catch {
-        if (!cancelled) {
-          setAssetsError(pageCopy.assetLoadFailed || '素材加载失败,请刷新后重试')
-        }
-      } finally {
-        if (!cancelled) {
-          setAssetsLoading(false)
-        }
-      }
-    }
-
-    void loadAssets()
-
-    return () => {
-      cancelled = true
-    }
-  }, [pageCopy.assetEmpty, pageCopy.assetLoadFailed])
-
-  const hasReadyAssets = useMemo(
-    () =>
-      !assetsLoading &&
-      !assetsError &&
-      modelOptions.length > 0 &&
-      outfitOptions.length > 0 &&
-      backgroundOptions.length > 0,
-    [assetsLoading, assetsError, modelOptions, outfitOptions, backgroundOptions],
-  )
-```
-
-- [ ] **Step 4: 删除页面对本地静态素材的依赖,并把状态展示改为加载/失败/空态**
-
-```tsx
-- import {
--   getHairstyleTemplateAssetsByCategory,
-- } from '../../../config/hairstyleTemplateAssets'
-
-{assetsLoading ? (
-  <p className="fs-14 text-medium-gray mb-20px" role="status">
-    {pageCopy.loadingAssets || '正在加载素材...'}
-  </p>
-) : null}
-
-{!assetsLoading && assetsError ? (
-  <p className="fs-14 text-red mb-20px" role="alert">
-    {assetsError}
-  </p>
-) : null}
-```
-
-- [ ] **Step 5: 保持原提交流程不变,只确保提交使用接口返回的 URL**
-
-```tsx
-const selectedModel = modelOptions.find((item) => item.id === selectedModelId)
-const selectedOutfit = outfitOptions.find((item) => item.id === selectedOutfitId)
-const selectedBackground = backgroundOptions.find((item) => item.id === selectedBackgroundId)
-
-if (!selectedModel || !selectedOutfit || !selectedBackground) {
-  throw new Error(pageCopy.validation?.assetsRequired || '请选择模特、服装和背景')
-}
-
-const template = await createTemplateAndStartGeneration({
-  name,
-  category,
-  status,
-  description,
-  tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
-  referenceImage,
-  modelImageUrl: selectedModel.url,
-  outfitImageUrl: selectedOutfit.url,
-  backgroundImageUrl: selectedBackground.url,
-})
-```
-
-- [ ] **Step 6: 运行定向测试确认通过**
-
-Run: `npm test -- src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx`
-
-Expected:
-- PASS
-- 覆盖素材加载成功、接口失败、空组禁用提交、提交使用服务端 URL
-
-- [ ] **Step 7: 运行类型检查**
-
-Run: `npm run typecheck`
-
-Expected:
-- PASS
-- 无新增 TypeScript 错误
-
-- [ ] **Step 8: 检查最近编辑文件诊断**
-
-Use: `GetDiagnostics` for:
-- `frontend/src/pages/dashboard/templates/TemplateFormPage.tsx`
-- `frontend/src/services/api/templates.ts`
-- `frontend/src/services/api/contracts.ts`
-
-Expected:
-- 无新增诊断;若有简单错误,先修复再继续
-
-- [ ] **Step 9: 提交**
-
-```bash
-git add frontend/src/pages/dashboard/templates/TemplateFormPage.tsx frontend/src/pages/dashboard/templates/__tests__/TemplateFormPage.test.tsx frontend/src/services/api/templates.ts frontend/src/services/api/contracts.ts frontend/src/i18n/locales/zh/translation.json frontend/src/i18n/locales/ja/translation.json
-git commit -m "feat: load template form assets from server config"
-```
-
-## Self-Review
-
-- **Spec coverage:** 已覆盖三组素材改为服务端接口、失败不回退静态数据、空组禁用提交、保持现有页面结构和旧提交接口不变、补充必要文案与定向测试。
-- **Placeholder scan:** 计划中没有 `TODO`、`TBD` 或“稍后实现”类占位语句;每个任务都给了具体文件、命令和代码片段。
-- **Type consistency:** `TemplateAssetOption` / `TemplateAssets` / `getTemplateAssets()` 在 Task 1 中定义,并在 Task 2 中按相同命名消费;页面内部映射结构 `TemplateOption` 只在 `TemplateFormPage` 内部使用,不向外暴露。

+ 0 - 225
docs/superpowers/specs/2026-06-28-prisma-v1-baseline-design.md

@@ -1,225 +0,0 @@
-# Prisma V1 Baseline Migration 设计
-
-## 1. 背景
-
-当前项目的 `backend/prisma/schema.prisma` 已经基本代表了当前数据库的稳定结构,可以视为第一版数据库模型。但仓库中的 `backend/prisma/migrations` 目录存在以下问题:
-
-- 历史 migration 数量很少,但依赖顺序已经不健康。
-- `20260628050000_add_customer_follow_content` 依赖 `customer` 表存在,却早于 `20260628120000_add_customer_table`。
-- `20260628173000_add_template_used_count` 是后补增量,而用户已经手动执行过对应 SQL。
-- 当前 migration 历史更像是开发过程痕迹,而不是可长期维护的基线。
-
-用户希望把现在这一版数据库正式收敛为“第一个稳定版本”,后续新功能再继续追加新的 migration。
-
-## 2. 目标
-
-本次调整后,需要满足以下目标:
-
-1. `schema.prisma` 继续作为当前稳定数据库结构的唯一事实来源。
-2. `backend/prisma/migrations` 目录只保留一条 `v1 baseline migration`。
-3. 这条 baseline migration 能用于新环境从零初始化数据库。
-4. 现有旧增量 migration 全部移除,不再保留开发阶段的历史痕迹。
-5. 后续数据库改动从 `v1 baseline` 继续追加新的增量 migration。
-
-## 3. 非目标
-
-以下内容不在本次范围内:
-
-1. 不保留旧 migration 的增量演进历史。
-2. 不再尝试修复旧 migration 的依赖顺序。
-3. 不修改当前数据库表结构语义,只做基线重置。
-4. 不做业务字段增删改设计。
-5. 不把 `schema.prisma` 回退到旧版本状态。
-
-## 4. 方案选择
-
-### 方案 A:保留旧 migration,只新增一个 baseline
-
-做法:
-
-- 保留当前 3 条旧 migration。
-- 再新增一条 `init_v1` 作为“以后认这个版本”的基线。
-
-优点:
-
-- 旧历史还在。
-
-缺点:
-
-- 目录仍然不干净。
-- 新成员会被旧历史误导。
-- 无法真正达到“第一版稳定基线”的目标。
-
-### 方案 B:删除全部旧 migration,只保留一个新的 baseline
-
-做法:
-
-- 删除当前所有旧 migration。
-- 基于当前稳定 `schema.prisma` 与数据库设计,新增一条 `init_v1`。
-- 后续所有数据库变更从该基线继续新增。
-
-优点:
-
-- 目录结构最干净。
-- 与“当前稳定结构就是 v1”完全一致。
-- 未来维护成本最低。
-
-缺点:
-
-- 旧增量历史不再保留。
-
-### 结论
-
-采用 **方案 B:删除全部旧 migration,只保留一个新的 baseline**。
-
-## 5. 目标目录结构
-
-调整后,`backend/prisma` 的目标结构如下:
-
-```text
-backend/prisma/
-  schema.prisma
-  migrations/
-    20260628_init_v1/
-      migration.sql
-```
-
-要求:
-
-- 不再保留:
-  - `20260628050000_add_customer_follow_content`
-  - `20260628120000_add_customer_table`
-  - `20260628173000_add_template_used_count`
-- `migrations` 目录内只保留一个 `init_v1`
-
-## 6. Baseline 定义
-
-### 6.1 基线语义
-
-`20260628_init_v1` 表示当前项目第一版稳定数据库结构。
-
-它不是增量修改脚本,而是一个完整初始化脚本,语义是:
-
-- 在一个空数据库中,直接创建当前版本所需的全部表结构、字段、索引和约束。
-
-### 6.2 基线内容来源
-
-baseline migration 的 SQL 内容必须与以下两处保持一致:
-
-1. `backend/prisma/schema.prisma`
-2. `backend/docs/数据库设计.md`
-
-其中:
-
-- `schema.prisma` 是代码层的事实来源。
-- `数据库设计.md` 用于校验字段注释、表注释和索引注释是否完整。
-
-### 6.3 基线必须包含的结构
-
-至少包含当前稳定版本中的全部核心表:
-
-- `hairstylist_user`
-- `customer`
-- `customer_treatment_record`
-- `ai_hair_simulation`
-- `ai_hair_simulation_task`
-- `hair_template`
-- `hair_template_make_task`
-- `system_config`
-
-并包含:
-
-- 当前稳定字段定义
-- 当前稳定索引
-- 当前稳定注释
-- `hair_template.used_count`
-
-## 7. 现有环境的处理策略
-
-### 7.1 已存在数据库的前提
-
-当前开发环境数据库已经存在,且用户已手动执行过部分 SQL。此时不能把新的 `init_v1` 当成一条要“重新执行建表”的 migration 直接运行到现有数据库上,否则会和已有表冲突。
-
-### 7.2 仓库层与数据库层分开处理
-
-本次设计把处理分成两层:
-
-- 仓库层:
-  - 清理旧 migration
-  - 新增 `20260628_init_v1/migration.sql`
-- 数据库层:
-  - 将当前数据库视为已经处于 `v1 baseline` 状态
-  - 用 Prisma 的 migration 状态对齐手段把这条 baseline 标记为已应用
-
-### 7.3 对齐原则
-
-原则上,现有开发库不需要重建,也不需要重新执行 baseline SQL。
-
-需要做的是:
-
-- 确保当前数据库结构与 `schema.prisma` 一致
-- 确保 Prisma 侧知道 `20260628_init_v1` 已经是“已应用状态”
-
-## 8. 实施约束
-
-### 8.1 不修改稳定结构
-
-本次重置 migration 历史时,不允许顺手修改当前数据库结构定义。`schema.prisma` 中现有的稳定结构应保持不变。
-
-### 8.2 保留注释完整性
-
-因为用户对 SQL 质量有明确要求,所以 baseline SQL 必须保留:
-
-- 表注释
-- 字段注释
-- 索引注释
-
-不能只生成“无注释的最小 SQL”。
-
-### 8.3 baseline 只作为起点
-
-一旦 `init_v1` 建立完成,后续新增字段或表时:
-
-- 不能回头修改 `init_v1`
-- 必须新增新的 migration
-
-除非未来再次明确决定整体重置数据库基线。
-
-## 9. 风险与规避
-
-### 风险 1:仓库中的 baseline 与当前数据库不一致
-
-如果 `migration.sql` 漏了字段或索引,新环境初始化会和当前开发环境不一致。
-
-规避方式:
-
-- 生成 baseline 后,对照 `schema.prisma` 和 `数据库设计.md` 做逐项校验。
-
-### 风险 2:现有环境误执行 baseline SQL
-
-如果在已有数据库上直接执行 `init_v1`,会因为表已存在而失败。
-
-规避方式:
-
-- 本次只把 baseline 作为仓库中的初始化基线。
-- 对现有环境采用“标记为已应用”的方式对齐,而不是重新执行完整建表 SQL。
-
-### 风险 3:后续开发者继续改 baseline
-
-如果未来继续把字段直接改进 `init_v1`,新的迁移历史会再次失真。
-
-规避方式:
-
-- 明确规定 `init_v1` 只代表第一版稳定结构。
-- 后续所有变更必须新增新的 migration。
-
-## 10. 验收标准
-
-本次完成后,应满足以下验收标准:
-
-1. `backend/prisma/migrations` 中只剩一个 `20260628_init_v1` 目录。
-2. `migration.sql` 能完整表达当前稳定数据库结构。
-3. `migration.sql` 包含 `used_count` 等当前稳定字段。
-4. `schema.prisma` 与 baseline SQL 一致。
-5. 旧 3 条 migration 不再保留。
-6. 后续数据库开发可以在 `v1 baseline` 之上继续新增 migration。

+ 0 - 135
docs/superpowers/specs/2026-06-28-template-form-config-assets-minimal-design.md

@@ -1,135 +0,0 @@
-# 模板新建页素材数据源切换最小设计
-
-## 背景
-
-当前主仓库 `main` 分支上的 [TemplateFormPage.tsx](file:///Users/yuchen/Documents/hairstylist/frontend/src/pages/dashboard/templates/TemplateFormPage.tsx) 仍然使用本地静态素材:
-
-- `选择AI模特`
-- `选择服装`
-- `选择背景`
-
-这三块目前通过 `getHairstyleTemplateAssetsByCategory()` 从本地 `hairstyleTemplateAssets` 读取,而不是从服务端的 `config` 表读取。
-
-用户本次要求只做最小改动:
-
-- 只替换这三个区块的数据来源
-- 其他页面结构和旧提交流程先不改
-- 接口失败时不回退本地静态数据
-
-## 目标
-
-- 让模板新建页中的模特、服装、背景三组素材改为从服务端配置接口读取
-- 前端展示仍保持现有卡片式 UI,不做结构重写
-- 如果配置接口失败或返回为空,页面明确显示异常状态,并阻止继续提交
-
-## 非目标
-
-- 不改当前 `TemplateFormPage` 的其余表单结构
-- 不改当前提交接口 `createTemplateAndStartGeneration()`
-- 不在这次改动中接入模板制作任务页
-- 不保留本地静态素材作为降级兜底
-
-## 推荐方案
-
-采用最小替换方案:
-
-1. 在前端模板 API 层新增一个读取模板素材的方法
-2. 页面初始化时请求服务端素材接口
-3. 将返回结果映射为当前页面已有卡片 UI 所需的数据结构
-4. 删除页面对本地 `hairstyleTemplateAssets` 的依赖
-5. 当素材未加载成功时,显示错误提示并禁用提交按钮
-
-这样可以只清理用户明确指出的旧逻辑,不扩大到整条模板创建链路。
-
-## 接口与数据约定
-
-前端读取服务端模板素材接口,返回三组数据:
-
-- `models`
-- `clothes`
-- `backgrounds`
-
-每个选项至少包含:
-
-- `id`
-- `name`
-- `url`
-- `configKey`
-
-服务端数据来源约定如下:
-
-- `config_group = 'ai-model'` -> `models`
-- `config_group = 'clothing'` -> `clothes`
-- `config_group = 'background'` -> `backgrounds`
-
-前端页面映射规则如下:
-
-- `id` 直接作为选中值
-- `name` 映射为卡片标题
-- `url` 映射为当前卡片图片地址
-
-## 页面行为
-
-### 1. 初始化加载
-
-页面挂载时请求一次模板素材接口。
-
-成功时:
-
-- 将三组接口结果写入本地状态
-- 默认选中每组的第一项
-
-失败时:
-
-- 不回退到本地静态素材
-- 显示“素材加载失败”类提示
-- 禁用顶部和底部的提交按钮
-
-### 2. 空数据处理
-
-如果接口返回成功但某一组为空,也视为“资源未准备完成”:
-
-- 显示提示文案
-- 页面不崩溃
-- 禁用提交按钮
-
-### 3. 提交流程
-
-本次不改旧提交流程,只替换素材来源。
-
-因此提交时仍然:
-
-- 上传参考图
-- 读取当前选中的模特、服装、背景 URL
-- 调用现有 `createTemplateAndStartGeneration()`
-
-也就是说,提交 payload 中使用的三组 URL 仍来自当前选中的服务端配置素材,而不是本地静态素材。
-
-## 影响文件
-
-- 修改 [TemplateFormPage.tsx](file:///Users/yuchen/Documents/hairstylist/frontend/src/pages/dashboard/templates/TemplateFormPage.tsx)
-  - 删除对 `hairstyleTemplateAssets` 的依赖
-  - 新增页面级素材加载状态、错误状态和接口映射
-- 修改 [templates.ts](file:///Users/yuchen/Documents/hairstylist/frontend/src/services/api/templates.ts)
-  - 新增读取模板素材的 API 方法
-- 修改 [contracts.ts](file:///Users/yuchen/Documents/hairstylist/frontend/src/services/api/contracts.ts)
-  - 新增模板素材类型定义
-- 可选修改 [translation.json](file:///Users/yuchen/Documents/hairstylist/frontend/src/i18n/locales/zh/translation.json) / [translation.json](file:///Users/yuchen/Documents/hairstylist/frontend/src/i18n/locales/ja/translation.json)
-  - 如果现有提示文案不够准确,则补充“素材加载失败”相关文案
-
-## 测试策略
-
-本次测试只覆盖真实回归风险:
-
-1. 页面能从接口读取三组素材并渲染
-2. 页面不再依赖本地 `hairstyleTemplateAssets`
-3. 接口失败时显示错误提示并禁用提交
-4. 选择素材后提交时使用接口返回的 URL
-
-不增加与本次目标无关的重构测试。
-
-## 风险与边界
-
-- 因为当前页面仍保留旧提交接口,所以这次只是“清理三组素材来源”,不是整页真实链路切换
-- 如果后续要切到模板制作任务流,应在下一次改动中再统一收敛 `TemplateFormPage`
-- 本次明确不做静态兜底,因此如果服务端配置没准备好,页面将无法提交,这是符合用户当前要求的

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

@@ -1,383 +0,0 @@
-# 模板使用量与顶部统计设计
-
-## 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 发型模拟成功后自动累计模板使用量