Explorar el Código

docs: add template make task design spec

ChenYu hace 4 meses
padre
commit
e47ead1d7d
Se han modificado 1 ficheros con 694 adiciones y 0 borrados
  1. 694 0
      docs/superpowers/specs/2026-06-28-template-make-task-design.md

+ 694 - 0
docs/superpowers/specs/2026-06-28-template-make-task-design.md

@@ -0,0 +1,694 @@
+# 发型模板制作任务设计
+
+## 背景
+
+当前项目已经具备以下基础能力:
+
+- 基于 NestJS + Prisma 的后端服务
+- 基于 React + Vite 的管理端前端
+- 基于 JWT 的登录态与鉴权
+- 基于 GCS 的通用上传能力
+- 基于 `hair_template` / `hair_template_make_task` / `system_config` 的数据库表设计
+
+仓库中虽然已经存在模板列表页、模板详情页和模板新建页,但这些实现仍停留在旧的静态资源和占位接口阶段,主要问题包括:
+
+- 前端模板新建页仍从本地静态配置读取模特、服装、背景
+- 后端 `templates` 模块仍是空骨架,无法真实创建模板或任务
+- 当前新建流程提交后直接跳模板详情页,不符合“先进入任务页”的目标
+- 现有模板链路没有把 `hair_template_make_task` 作为真实的异步任务来源
+- step1 / step2 的 AI 制作编排尚未落地
+
+本次新 case 的目标,是围绕 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md) 中的以下三张表,完成“发型模板制作”最小真实闭环:
+
+- `hair_template`
+- `hair_template_make_task`
+- `system_config`
+
+用户已明确约束:
+
+- 模板新建页沿用当前前端 UI 骨架,不做大改版
+- 参考发型图片上传类型固定为 `ai-template-origin-image`
+- AI 模特、服装、背景都从 `system_config` 动态读取
+- `config_group = 'ai-model'` 提供模特
+- `config_group = 'clothing'` 提供服装
+- `config_group = 'background'` 提供背景
+- `config_name` 作为展示名称,`config_value` 作为对应图片 URL
+- 新建时模特、服装、背景都采用“预设或上传二选一”,最终统一提交 URL
+- 提交成功后先进入新的模板制作任务页,不直接进入模板详情页
+- 服务端先写入 `hair_template`,再写入 `hair_template_make_task`
+- step1 使用 `system_config` 中 `config_group = 'template-step1'` 的配置值作为 AI 模板 URL
+- step2 需要基于 `backend/src/step2/config.json` 与 `backend/src/step2/content.json`,加上运行时素材生成 zip 并上传
+- 服务端和前端都采用轮询详情接口推进任务,参考现有 AI 发型模拟逻辑
+
+## 目标
+
+- 实现一个真实可用的发型模板新建页。
+- 支持上传参考发型图,并将图片存储为可访问 URL。
+- 支持从 `system_config` 查询 AI 模特、服装、背景预设资源。
+- 支持模特、服装、背景改用用户自定义上传资源。
+- 提交模板后创建 1 条 `hair_template` 记录和 1 条 `hair_template_make_task` 记录。
+- 后端按 `step1 -> step2` 顺序调用外部 AI 服务。
+- 新增模板制作任务页,按阶段展示处理中、成功、失败三种状态。
+- 服务端在任务详情查询时按节流规则刷新任务状态并推进下一阶段。
+- step2 成功后回写 `hair_template.cover_url` 和 `hair_template.template_url`。
+
+## 非目标
+
+- 本次不实现后台常驻 worker、消息队列或定时补偿任务。
+- 本次不实现任务批量管理页。
+- 本次不实现任务取消。
+- 本次不实现复杂失败重试策略。
+- 本次不实现模板列表页的完整筛选重构。
+- 本次不改动数据库设计文档中的表结构定义。
+
+## 设计原则
+
+### 1. 数据库表结构为唯一事实来源
+
+本次模板制作链路的字段语义完全以 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md) 为准,尤其是:
+
+- `hair_template` 表承载最终可使用的模板实体
+- `hair_template_make_task` 表承载模板制作过程和阶段状态
+- `system_config` 表承载模板制作所需的动态素材配置与 step1 模板配置
+
+不新增第二套任务模型,不在前端保留旧的静态 mock 资源来源。
+
+### 2. 模板实体与制作任务分层
+
+`hair_template` 与 `hair_template_make_task` 职责必须分开:
+
+- `hair_template` 存业务上可识别的模板基本信息和最终产物地址
+- `hair_template_make_task` 存异步制作过程中的任务 ID、阶段资源、阶段结果和状态
+
+这样模板列表和模板详情都可围绕模板实体展开,而任务页则围绕制作过程展开。
+
+### 3. 前端只轮询自家后端,不直连 AI
+
+前端只请求本系统任务详情接口,后端统一负责:
+
+- 是否需要查询 step1 / step2 外部 AI 状态
+- 何时推进下一阶段
+- 何时回写数据库
+- 如何把阶段状态和结果映射为前端 DTO
+
+### 4. 在没有队列系统的前提下保持最小闭环
+
+当前仓库没有消息队列或任务调度基础设施,因此本次采用:
+
+- 创建任务时同步发起 step1
+- 任务页轮询详情接口
+- 后端在详情查询时按规则刷新外部任务状态并推进 step2
+
+这是当前复杂度下最稳妥、最可验证的闭环方案。
+
+### 5. 运行时文件不得污染源码目录
+
+`backend/src/step2` 只用于存放静态模版文件:
+
+- `config.json`
+- `content.json`
+
+运行期下载的 step1 图片、服装图、背景图、zip 包等中间文件必须放入系统临时目录,任务完成后尽量清理,避免把运行时产物写回源码目录。
+
+## 现状分析
+
+### 1. 后端现状
+
+当前后端已经具备:
+
+- 鉴权体系和发型师登录态
+- 通用上传能力 `POST /api/v1/storage/upload`
+- `simulations` 模块中的 AI 调用与“详情查询时刷新状态”的范式
+- Prisma 中对 `hair_template` 与 `hair_template_make_task` 的模型映射
+
+但当前缺失:
+
+- 真实可用的模板创建接口
+- 真实可用的模板任务详情接口
+- `system_config` 的 Prisma 模型和查询能力
+- step1 / step2 的模板制作 AI 编排
+- zip 打包与上传逻辑
+
+### 2. 前端现状
+
+当前前端已经具备:
+
+- `TemplateFormPage.tsx` 的基础 UI 骨架
+- 参考图上传能力
+- 模板列表页与模板详情页路由
+
+但当前缺失:
+
+- 模板新建页对动态素材接口的接入
+- 模板新建页的“预设或上传二选一”交互
+- 新的模板制作任务页
+- 模板创建后跳转任务页的流程
+- 模板列表在“制作中/失败/完成”之间的跳转分流
+
+## 数据模型与职责
+
+### 1. `hair_template`
+
+创建模板任务时,先写入 `hair_template` 一条草稿记录,字段来源如下:
+
+- `template_name`:前端表单提交
+- `status`:前端表单提交,默认 `DRAFT`
+- `category`:前端表单提交
+- `tags`:前端表单提交,逗号拼接
+- `description`:前端表单提交
+- `model_url`:预设或上传后的最终 URL
+- `clothes_url`:预设或上传后的最终 URL
+- `background_url`:预设或上传后的最终 URL
+- `ref_hair_url`:参考发型图 URL
+- `hairstylist_id`:当前登录发型师 ID
+- `is_system_template`:固定为 `NO`
+
+创建瞬间不要求已有:
+
+- `cover_url`
+- `template_url`
+
+只有当 step2 成功后,这两个字段才会被回写完整。
+
+### 2. `hair_template_make_task`
+
+任务表以“每次创建模板对应一条制作任务”为原则,主要字段职责如下:
+
+- `total_task_id`:本地生成的总任务唯一标识
+- `step1_task_id`:step1 外部 AI 任务 ID
+- `step2_task_id`:step2 外部 AI 任务 ID
+- `task_status`:本地阶段状态
+- `template_id`:关联模板 ID
+- `step1_resource`:step1 阶段资源 JSON
+- `step2_resource`:step2 阶段资源 JSON
+- `ref_hair_url` / `model_url` / `clothes_url` / `background_url`:本次任务使用的素材快照
+- `step1_result_img`:step1 结果图 URL
+- `step2_result_img`:step2 最终封面图 URL
+
+### 3. `system_config`
+
+本次新增的模板素材来源与 step1 模板来源都来自 `system_config`:
+
+- `config_group = 'ai-model'`:模特资源
+- `config_group = 'clothing'`:服装资源
+- `config_group = 'background'`:背景资源
+- `config_group = 'template-step1'`:step1 模板地址
+
+字段映射规则:
+
+- `config_name` -> 前端展示名称
+- `config_value` -> 图片 URL 或模板 URL
+- `id` -> 前端选项 ID
+
+## 总体流程
+
+### 1. 新建模板
+
+前端在 `TemplateFormPage` 完成以下动作:
+
+- 上传参考发型图,获取 `refHairUrl`
+- 读取动态素材接口,展示模特、服装、背景预设
+- 允许用户为模特、服装、背景切换为自定义上传
+- 整理最终生效的三个素材 URL
+- 调用“创建模板制作任务”接口
+
+### 2. 服务端创建记录并发起 step1
+
+服务端接到创建请求后:
+
+- 校验当前发型师身份
+- 校验 `template-step1` 配置存在
+- 创建 `hair_template`
+- 创建 `hair_template_make_task`
+- 发起 step1 外部 AI 任务
+- 保存 `step1_task_id`
+- 将任务状态更新为 `PROCESSING`
+
+若 step1 无法发起,则任务直接标记为 `FAILED`。
+
+### 3. 跳转任务页
+
+前端收到创建成功响应后,跳转至新的模板制作任务页,例如:
+
+- `/:locale/templates/tasks/:taskId`
+
+### 4. 轮询任务详情并推进阶段
+
+任务页在以下状态下持续轮询:
+
+- `WAIT`
+- `PROCESSING`
+- `STEP1_SUCCESS`
+
+后端在详情接口中:
+
+- 查询本地任务状态
+- 按节流规则判断是否需要刷新 step1 / step2
+- 在 step1 成功且 step2 尚未发起时自动启动 step2
+- 在 step2 成功时回写模板表
+
+### 5. 任务完成
+
+当 step2 成功时:
+
+- `hair_template.cover_url = step2 最终结果图`
+- `hair_template.template_url = 上传后的 zip URL`
+- `hair_template_make_task.step2_result_img = step2 最终结果图`
+- `hair_template_make_task.task_status = STEP2_SUCCESS`
+
+此前模板记录虽然存在,但不应视为可用于 AI 发型模拟的可用模板。
+
+## 状态流转设计
+
+本次直接沿用 `hair_template_make_task.task_status` 的既有枚举:
+
+- `WAIT`
+- `PROCESSING`
+- `STEP1_SUCCESS`
+- `STEP2_SUCCESS`
+- `FAILED`
+
+### 1. `WAIT`
+
+表示本地任务已创建,但还没有成功发起 step1。
+
+### 2. `PROCESSING`
+
+表示至少有一个外部阶段任务已发起,且整体制作未结束。
+
+该状态覆盖以下两种场景:
+
+- step1 已发起,等待 step1 完成
+- step2 已发起,等待 step2 完成
+
+### 3. `STEP1_SUCCESS`
+
+表示 step1 已成功,且 step1 结果图已经落地并保存到任务记录。
+
+该状态是进入 step2 之前的中间确认态。
+
+### 4. `STEP2_SUCCESS`
+
+表示 step2 已成功,模板包和封面图都已生成,并且 `hair_template` 已完成最终回写。
+
+### 5. `FAILED`
+
+表示任一阶段失败,或关键配置缺失、素材下载失败、zip 上传失败等不可恢复错误发生。
+
+## 接口设计
+
+### 1. 获取模板素材接口
+
+- `GET /templates/assets`
+
+返回结构:
+
+```json
+{
+  "models": [
+    {
+      "id": "12",
+      "name": "自然女模",
+      "url": "https://example.com/model.jpg",
+      "configKey": "ai-model-natural"
+    }
+  ],
+  "clothes": [],
+  "backgrounds": []
+}
+```
+
+查询规则:
+
+- `models` <- `config_group = 'ai-model'`
+- `clothes` <- `config_group = 'clothing'`
+- `backgrounds` <- `config_group = 'background'`
+
+返回顺序遵循 `sort asc, id asc`。
+
+### 2. 创建模板制作任务接口
+
+- `POST /templates/make-tasks`
+
+请求体:
+
+```json
+{
+  "templateName": "柔和层次长发",
+  "category": "LONG",
+  "status": "DRAFT",
+  "description": "适合脸周柔和修饰",
+  "tags": ["首次建议", "层次"],
+  "refHairUrl": "https://example.com/ref.jpg",
+  "modelUrl": "https://example.com/model.jpg",
+  "clothesUrl": "https://example.com/clothes.jpg",
+  "backgroundUrl": "https://example.com/background.jpg"
+}
+```
+
+响应体:
+
+```json
+{
+  "templateId": "101",
+  "taskId": "88",
+  "totalTaskId": "tmpl_20260628_xxx",
+  "taskStatus": "PROCESSING"
+}
+```
+
+### 3. 查询模板制作任务详情接口
+
+- `GET /templates/make-tasks/:taskId`
+
+响应体建议结构:
+
+```json
+{
+  "id": "88",
+  "templateId": "101",
+  "templateName": "柔和层次长发",
+  "taskStatus": "PROCESSING",
+  "errorMessage": null,
+  "createTime": "2026-06-28T12:00:00.000Z",
+  "refHairUrl": "https://example.com/ref.jpg",
+  "modelUrl": "https://example.com/model.jpg",
+  "clothesUrl": "https://example.com/clothes.jpg",
+  "backgroundUrl": "https://example.com/background.jpg",
+  "step1": {
+    "taskId": "ai_step1_xxx",
+    "status": "PROCESSING",
+    "templateUrl": "https://example.com/step1-template",
+    "resultImage": null
+  },
+  "step2": {
+    "taskId": "",
+    "status": "WAIT",
+    "bundleUrl": null,
+    "resultImage": null
+  },
+  "template": {
+    "coverUrl": "",
+    "templateUrl": "",
+    "status": "DRAFT"
+  }
+}
+```
+
+接口职责:
+
+- 返回任务详情
+- 在返回前推进 step1 / step2 状态
+- 在成功时返回最新模板结果
+- 在失败时返回可展示的错误文案
+
+### 4. 重试接口
+
+本次不作为首版必做接口。
+
+若后续补充,建议形态为:
+
+- `POST /templates/make-tasks/:taskId/retry`
+
+## 前端页面设计
+
+### 1. 模板新建页
+
+保留现有 `TemplateFormPage.tsx` 布局骨架,主要替换以下行为:
+
+- 不再读取本地静态 `hairstyleTemplateAssets`
+- 改为请求 `GET /templates/assets`
+- 模特、服装、背景都支持“预设或上传二选一”
+- 三项最终都统一提交 URL
+- 提交成功后跳任务页,不跳模板详情页
+
+#### 模特、服装、背景交互
+
+每一组素材提供两种来源:
+
+- 预设列表
+- 自定义上传
+
+界面上允许用户切换来源类型,当前生效来源决定最终提交 URL。
+
+### 2. 模板制作任务页
+
+新增页面,例如:
+
+- `TemplateMakeTaskPage.tsx`
+
+路由建议:
+
+- `/:locale/templates/tasks/:taskId`
+
+页面结构:
+
+- 左侧任务概览卡
+- 右侧阶段进度卡
+- 下方素材确认区
+
+#### 左侧任务概览卡
+
+展示内容:
+
+- 模板名称
+- 当前总状态
+- 分类
+- 标签
+- 创建时间
+- 失败原因
+
+主按钮规则:
+
+- 处理中:按钮禁用,仅展示处理中提示
+- 成功:展示“查看模板详情”
+- 失败:首版仅展示失败文案,不强制实现重试按钮
+
+#### 右侧阶段进度卡
+
+分两步展示:
+
+- `Step 1 发型抽取`
+- `Step 2 模板合成`
+
+每一步展示:
+
+- 当前状态
+- 外部任务 ID
+- 结果图预览
+- 当前说明文案
+
+#### 素材确认区
+
+展示四张素材卡片:
+
+- 参考发型图
+- AI 模特
+- 服装
+- 背景
+
+### 3. 模板列表与模板详情衔接
+
+`TemplateListPage` 中的模板卡片点击逻辑建议调整为:
+
+- 若模板存在未完成或失败的制作任务,优先跳任务页
+- 若模板已完成制作,跳模板详情页
+
+`TemplateDetailPage` 仍作为制作完成后的模板详情页使用。
+
+## 服务端编排设计
+
+### 1. AI 客户端抽象
+
+现有 `AiSimulationClient` 的 `execute(templateUrl, imageUrl)` 只适用于发型模拟的固定输入结构。
+
+本次建议抽象为更通用的方法,例如:
+
+- `executeTemplate(template: string, inputs: Record<string, unknown>)`
+
+这样:
+
+- AI 发型模拟仍可使用 `inputs.images = [originImage]`
+- 模板制作 step1 可使用 `inputs.image = refHairUrl`
+- 模板制作 step2 可使用 `inputs.image = modelUrl`
+
+结果查询方法可继续复用现有的 `getResult(taskId)` 形式。
+
+### 2. step1 编排
+
+step1 的模板来源:
+
+- 从 `system_config` 读取 `config_group = 'template-step1'`
+- 取其中的 `config_value` 作为 AI `template`
+
+step1 请求体核心:
+
+```json
+{
+  "template": "system_config.config_value",
+  "inputs": {
+    "image": "refHairUrl"
+  }
+}
+```
+
+step1 成功后:
+
+- 拿到 step1 结果图 URL
+- 下载该图片到系统临时目录
+- 回写 `hair_template_make_task.step1_result_img`
+- 把模板 URL、结果图 URL、本地文件路径等写入 `step1_resource`
+- 将任务状态改为 `STEP1_SUCCESS`
+
+### 3. step2 编排
+
+step2 的目标是基于静态模版文件和运行时素材生成 zip 包,并将 zip 的上传 URL 作为 AI 模板地址。
+
+运行流程:
+
+1. 创建任务工作目录,例如 `os.tmpdir()/template-make/{totalTaskId}`
+2. 复制 `backend/src/step2/config.json` 和 `backend/src/step2/content.json` 到工作目录
+3. 下载并落地以下文件:
+   - step1 结果图 -> `hairstyle.jpg`
+   - 服装图 -> `input-outfit.jpg`
+   - 背景图 -> `input-background.jpg`
+4. 将工作目录内容打成 zip
+5. 使用 `StorageService.uploadBuffer` 上传 zip
+6. 得到 zip URL,作为 step2 AI 请求中的 `template`
+7. 使用 `modelUrl` 作为 step2 AI 请求中的 `inputs.image`
+
+step2 请求体核心:
+
+```json
+{
+  "template": "uploaded-zip-url",
+  "inputs": {
+    "image": "modelUrl"
+  }
+}
+```
+
+step2 成功后:
+
+- 结果图 URL 回写 `hair_template_make_task.step2_result_img`
+- zip URL 和结果图 URL 写入 `step2_resource`
+- `hair_template.cover_url = step2_result_img`
+- `hair_template.template_url = uploaded-zip-url`
+- `hair_template_make_task.task_status = STEP2_SUCCESS`
+
+### 4. 任务推进逻辑
+
+任务推进完全参考 AI 发型模拟当前的“查询详情时刷新状态”模式。
+
+#### `WAIT`
+
+- 若尚未成功发起 step1,则尝试发起 step1
+- 发起成功后更新为 `PROCESSING`
+
+#### `PROCESSING`
+
+- 若 `step1_task_id` 已存在且 `step1_result_img` 为空,则查询 step1 结果
+- 若 step1 已成功,则下载结果图、更新 `STEP1_SUCCESS`
+- 若 `step2_task_id` 已存在,则查询 step2 结果
+- 若 step2 已成功,则回写模板并更新 `STEP2_SUCCESS`
+
+#### `STEP1_SUCCESS`
+
+- 若 `step2_task_id` 为空,则开始组装 zip 并发起 step2
+- 发起成功后将任务状态设回 `PROCESSING`
+
+#### `FAILED`
+
+- 不自动推进
+- 由后续重试接口或人工处理
+
+## 资源与错误信息存储
+
+本次首版尽量不改数据库表结构。
+
+运行态附加信息统一放入:
+
+- `step1_resource`
+- `step2_resource`
+
+建议 JSON 结构保持稳定,例如:
+
+```json
+{
+  "templateUrl": "https://example.com/template",
+  "uploadedZipUrl": "https://example.com/template.zip",
+  "localFiles": {
+    "step1Image": "/tmp/task-1/hairstyle.jpg",
+    "clothes": "/tmp/task-1/input-outfit.jpg",
+    "background": "/tmp/task-1/input-background.jpg",
+    "zip": "/tmp/task-1/template.zip"
+  },
+  "resultImage": "https://example.com/result.jpg",
+  "lastError": "step2 upload failed"
+}
+```
+
+任务页展示失败文案时,优先从阶段 resource 中提取 `lastError`,若无则返回通用错误提示。
+
+## 文件与模块拆分建议
+
+### 后端
+
+- `src/modules/templates/templates.controller.ts`
+- `src/modules/templates/templates.service.ts`
+- `src/modules/templates/templates.module.ts`
+- `src/modules/templates/dto/*`
+- `src/modules/templates/template-ai.client.ts` 或复用/扩展现有 AI client
+- `src/modules/templates/template-task-runner.ts` 或同类服务
+
+### 前端
+
+- `src/pages/dashboard/templates/TemplateFormPage.tsx`
+- `src/pages/dashboard/templates/TemplateMakeTaskPage.tsx`
+- `src/pages/dashboard/templates/TemplateListPage.tsx`
+- `src/services/api/templates.ts`
+- `src/services/api/contracts.ts`
+- `src/config/routes.ts`
+- i18n 文案与样式文件
+
+## 验证与测试
+
+### 后端
+
+建议补充有价值的最小测试:
+
+- `GET /templates/assets` 能按配置分组返回素材
+- `POST /templates/make-tasks` 能正确创建模板与任务记录
+- 任务详情查询能推进 step1 成功
+- 任务详情查询能推进 step2 成功并回写模板
+- 任一阶段失败时任务进入 `FAILED`
+
+### 前端
+
+建议补充有价值的最小测试:
+
+- 模板新建页能加载动态素材
+- 模特、服装、背景切换来源后能提交最终生效 URL
+- 提交成功后跳转任务页
+- 任务页在处理中时持续轮询,在成功或失败后停止轮询
+- 任务页能正确展示步骤状态与成功 CTA
+
+## 最小交付边界
+
+本次首版必须完成:
+
+- 动态素材接口
+- 模板新建真实提交
+- `hair_template` + `hair_template_make_task` 双写入
+- step1 / step2 真实跑通
+- 模板制作任务页
+- 任务状态推进
+- 成功后回写 `hair_template.cover_url` 与 `hair_template.template_url`
+
+本次首版可以不做:
+
+- 任务重试接口
+- 后台扫表补偿
+- 任务取消
+- 模板列表复杂筛选联动
+- 模板详情页的深度中间态优化