|
|
@@ -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`
|
|
|
+
|
|
|
+本次首版可以不做:
|
|
|
+
|
|
|
+- 任务重试接口
|
|
|
+- 后台扫表补偿
|
|
|
+- 任务取消
|
|
|
+- 模板列表复杂筛选联动
|
|
|
+- 模板详情页的深度中间态优化
|