2026-06-28-template-make-task-design.md 19 KB

发型模板制作任务设计

背景

当前项目已经具备以下基础能力:

  • 基于 NestJS + Prisma 的后端服务
  • 基于 React + Vite 的管理端前端
  • 基于 JWT 的登录态与鉴权
  • 基于 GCS 的通用上传能力
  • 基于 hair_template / hair_template_make_task / system_config 的数据库表设计

仓库中虽然已经存在模板列表页、模板详情页和模板新建页,但这些实现仍停留在旧的静态资源和占位接口阶段,主要问题包括:

  • 前端模板新建页仍从本地静态配置读取模特、服装、背景
  • 后端 templates 模块仍是空骨架,无法真实创建模板或任务
  • 当前新建流程提交后直接跳模板详情页,不符合“先进入任务页”的目标
  • 现有模板链路没有把 hair_template_make_task 作为真实的异步任务来源
  • step1 / step2 的 AI 制作编排尚未落地

本次新 case 的目标,是围绕 数据库设计.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 为准,尤其是:

  • 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

返回结构:

{
  "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

请求体:

{
  "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"
}

响应体:

{
  "templateId": "101",
  "taskId": "88",
  "totalTaskId": "tmpl_20260628_xxx",
  "taskStatus": "PROCESSING"
}

3. 查询模板制作任务详情接口

  • GET /templates/make-tasks/:taskId

响应体建议结构:

{
  "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 请求体核心:

{
  "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 请求体核心:

{
  "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 结构保持稳定,例如:

{
  "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

本次首版可以不做:

  • 任务重试接口
  • 后台扫表补偿
  • 任务取消
  • 模板列表复杂筛选联动
  • 模板详情页的深度中间态优化