当前项目已经具备以下基础能力:
hair_template / hair_template_make_task / system_config 的数据库表设计仓库中虽然已经存在模板列表页、模板详情页和模板新建页,但这些实现仍停留在旧的静态资源和占位接口阶段,主要问题包括:
templates 模块仍是空骨架,无法真实创建模板或任务hair_template_make_task 作为真实的异步任务来源本次新 case 的目标,是围绕 数据库设计.md 中的以下三张表,完成“发型模板制作”最小真实闭环:
hair_templatehair_template_make_tasksystem_config用户已明确约束:
ai-template-origin-imagesystem_config 动态读取config_group = 'ai-model' 提供模特config_group = 'clothing' 提供服装config_group = 'background' 提供背景config_name 作为展示名称,config_value 作为对应图片 URLhair_template,再写入 hair_template_make_tasksystem_config 中 config_group = 'template-step1' 的配置值作为 AI 模板 URLbackend/src/step2/config.json 与 backend/src/step2/content.json,加上运行时素材生成 zip 并上传system_config 查询 AI 模特、服装、背景预设资源。hair_template 记录和 1 条 hair_template_make_task 记录。step1 -> step2 顺序调用外部 AI 服务。hair_template.cover_url 和 hair_template.template_url。本次模板制作链路的字段语义完全以 数据库设计.md 为准,尤其是:
hair_template 表承载最终可使用的模板实体hair_template_make_task 表承载模板制作过程和阶段状态system_config 表承载模板制作所需的动态素材配置与 step1 模板配置不新增第二套任务模型,不在前端保留旧的静态 mock 资源来源。
hair_template 与 hair_template_make_task 职责必须分开:
hair_template 存业务上可识别的模板基本信息和最终产物地址hair_template_make_task 存异步制作过程中的任务 ID、阶段资源、阶段结果和状态这样模板列表和模板详情都可围绕模板实体展开,而任务页则围绕制作过程展开。
前端只请求本系统任务详情接口,后端统一负责:
当前仓库没有消息队列或任务调度基础设施,因此本次采用:
这是当前复杂度下最稳妥、最可验证的闭环方案。
backend/src/step2 只用于存放静态模版文件:
config.jsoncontent.json运行期下载的 step1 图片、服装图、背景图、zip 包等中间文件必须放入系统临时目录,任务完成后尽量清理,避免把运行时产物写回源码目录。
当前后端已经具备:
POST /api/v1/storage/uploadsimulations 模块中的 AI 调用与“详情查询时刷新状态”的范式hair_template 与 hair_template_make_task 的模型映射但当前缺失:
system_config 的 Prisma 模型和查询能力当前前端已经具备:
TemplateFormPage.tsx 的基础 UI 骨架但当前缺失:
hair_template创建模板任务时,先写入 hair_template 一条草稿记录,字段来源如下:
template_name:前端表单提交status:前端表单提交,默认 DRAFTcategory:前端表单提交tags:前端表单提交,逗号拼接description:前端表单提交model_url:预设或上传后的最终 URLclothes_url:预设或上传后的最终 URLbackground_url:预设或上传后的最终 URLref_hair_url:参考发型图 URLhairstylist_id:当前登录发型师 IDis_system_template:固定为 NO创建瞬间不要求已有:
cover_urltemplate_url只有当 step2 成功后,这两个字段才会被回写完整。
hair_template_make_task任务表以“每次创建模板对应一条制作任务”为原则,主要字段职责如下:
total_task_id:本地生成的总任务唯一标识step1_task_id:step1 外部 AI 任务 IDstep2_task_id:step2 外部 AI 任务 IDtask_status:本地阶段状态template_id:关联模板 IDstep1_resource:step1 阶段资源 JSONstep2_resource:step2 阶段资源 JSONref_hair_url / model_url / clothes_url / background_url:本次任务使用的素材快照step1_result_img:step1 结果图 URLstep2_result_img:step2 最终封面图 URLsystem_config本次新增的模板素材来源与 step1 模板来源都来自 system_config:
config_group = 'ai-model':模特资源config_group = 'clothing':服装资源config_group = 'background':背景资源config_group = 'template-step1':step1 模板地址字段映射规则:
config_name -> 前端展示名称config_value -> 图片 URL 或模板 URLid -> 前端选项 ID前端在 TemplateFormPage 完成以下动作:
refHairUrl服务端接到创建请求后:
template-step1 配置存在hair_templatehair_template_make_taskstep1_task_idPROCESSING若 step1 无法发起,则任务直接标记为 FAILED。
前端收到创建成功响应后,跳转至新的模板制作任务页,例如:
/:locale/templates/tasks/:taskId任务页在以下状态下持续轮询:
WAITPROCESSINGSTEP1_SUCCESS后端在详情接口中:
当 step2 成功时:
hair_template.cover_url = step2 最终结果图hair_template.template_url = 上传后的 zip URLhair_template_make_task.step2_result_img = step2 最终结果图hair_template_make_task.task_status = STEP2_SUCCESS此前模板记录虽然存在,但不应视为可用于 AI 发型模拟的可用模板。
本次直接沿用 hair_template_make_task.task_status 的既有枚举:
WAITPROCESSINGSTEP1_SUCCESSSTEP2_SUCCESSFAILEDWAIT表示本地任务已创建,但还没有成功发起 step1。
PROCESSING表示至少有一个外部阶段任务已发起,且整体制作未结束。
该状态覆盖以下两种场景:
STEP1_SUCCESS表示 step1 已成功,且 step1 结果图已经落地并保存到任务记录。
该状态是进入 step2 之前的中间确认态。
STEP2_SUCCESS表示 step2 已成功,模板包和封面图都已生成,并且 hair_template 已完成最终回写。
FAILED表示任一阶段失败,或关键配置缺失、素材下载失败、zip 上传失败等不可恢复错误发生。
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。
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"
}
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"
}
}
接口职责:
本次不作为首版必做接口。
若后续补充,建议形态为:
POST /templates/make-tasks/:taskId/retry保留现有 TemplateFormPage.tsx 布局骨架,主要替换以下行为:
hairstyleTemplateAssetsGET /templates/assets每一组素材提供两种来源:
界面上允许用户切换来源类型,当前生效来源决定最终提交 URL。
新增页面,例如:
TemplateMakeTaskPage.tsx路由建议:
/:locale/templates/tasks/:taskId页面结构:
展示内容:
主按钮规则:
分两步展示:
Step 1 发型抽取Step 2 模板合成每一步展示:
展示四张素材卡片:
TemplateListPage 中的模板卡片点击逻辑建议调整为:
TemplateDetailPage 仍作为制作完成后的模板详情页使用。
现有 AiSimulationClient 的 execute(templateUrl, imageUrl) 只适用于发型模拟的固定输入结构。
本次建议抽象为更通用的方法,例如:
executeTemplate(template: string, inputs: Record<string, unknown>)这样:
inputs.images = [originImage]inputs.image = refHairUrlinputs.image = modelUrl结果查询方法可继续复用现有的 getResult(taskId) 形式。
step1 的模板来源:
system_config 读取 config_group = 'template-step1'config_value 作为 AI templatestep1 请求体核心:
{
"template": "system_config.config_value",
"inputs": {
"image": "refHairUrl"
}
}
step1 成功后:
hair_template_make_task.step1_result_imgstep1_resourceSTEP1_SUCCESSstep2 的目标是基于静态模版文件和运行时素材生成 zip 包,并将 zip 的上传 URL 作为 AI 模板地址。
运行流程:
os.tmpdir()/template-make/{totalTaskId}backend/src/step2/config.json 和 backend/src/step2/content.json 到工作目录hairstyle.jpginput-outfit.jpginput-background.jpgStorageService.uploadBuffer 上传 ziptemplatemodelUrl 作为 step2 AI 请求中的 inputs.imagestep2 请求体核心:
{
"template": "uploaded-zip-url",
"inputs": {
"image": "modelUrl"
}
}
step2 成功后:
hair_template_make_task.step2_result_imgstep2_resourcehair_template.cover_url = step2_result_imghair_template.template_url = uploaded-zip-urlhair_template_make_task.task_status = STEP2_SUCCESS任务推进完全参考 AI 发型模拟当前的“查询详情时刷新状态”模式。
WAITPROCESSINGPROCESSINGstep1_task_id 已存在且 step1_result_img 为空,则查询 step1 结果STEP1_SUCCESSstep2_task_id 已存在,则查询 step2 结果STEP2_SUCCESSSTEP1_SUCCESSstep2_task_id 为空,则开始组装 zip 并发起 step2PROCESSINGFAILED本次首版尽量不改数据库表结构。
运行态附加信息统一放入:
step1_resourcestep2_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.tssrc/modules/templates/templates.service.tssrc/modules/templates/templates.module.tssrc/modules/templates/dto/*src/modules/templates/template-ai.client.ts 或复用/扩展现有 AI clientsrc/modules/templates/template-task-runner.ts 或同类服务src/pages/dashboard/templates/TemplateFormPage.tsxsrc/pages/dashboard/templates/TemplateMakeTaskPage.tsxsrc/pages/dashboard/templates/TemplateListPage.tsxsrc/services/api/templates.tssrc/services/api/contracts.tssrc/config/routes.ts建议补充有价值的最小测试:
GET /templates/assets 能按配置分组返回素材POST /templates/make-tasks 能正确创建模板与任务记录FAILED建议补充有价值的最小测试:
本次首版必须完成:
hair_template + hair_template_make_task 双写入hair_template.cover_url 与 hair_template.template_url本次首版可以不做: