当前项目已经具备以下基础能力:
仓库中虽然已经存在 simulations、templates 以及若干模拟相关前端页面,但这些实现仍停留在旧的业务假设中,主要问题包括:
hairstyle / aftercare 两条旧链路job/results 语义simulations、templates 模块仍是未完成骨架本次新 case 的目标,是围绕 数据库设计.md 中的以下三张表,完成“AI 发型模拟”最小真实闭环:
ai_hair_simulationai_hair_simulation_taskhair_template用户已明确约束:
微调 页面不要删除,但只能保留为未开发状态ai-user-origin-imagehair_template 中查询“发型师模板”和“系统模板”。aftercare / 剪后预测真实能力。本次模拟链路的字段语义完全以 数据库设计.md 为准,尤其是:
ai_hair_simulation.origin_image 存原图 URLai_hair_simulation_task.task_status 存子任务状态hair_template.template_url 作为 AI 调用时的模板地址hair_template.is_system_template 作为系统模板与发型师模板的来源区分字段不再沿用旧前端页面中的:
simulationType = hairstyle | aftercarejob/results 数据模型用户明确要求“微调页面不要删除”,因此页面壳和路由可以保留;但旧业务逻辑、旧 API 契约和旧状态模型需要删除或改写,避免两套模拟模型并存。
前端只请求本系统详情接口,后端统一负责:
这样数据库状态始终是唯一事实源,前端不会出现“页面看到的状态”和数据库状态不一致的问题。
当前仓库没有消息队列或定时任务基础设施,因此本次采用:
这是当前复杂度下最稳妥、最可验证的闭环方案。
当前后端已经具备:
POST /api/v1/storage/uploadsimulations 模块空骨架templates 模块空骨架但当前缺失:
hair_template 的模板列表接口ai_hair_simulation 与 ai_hair_simulation_task 的真实读写当前前端已经具备:
StylistSimulationPageSimulationResultPageSimulationEditPageAftercareResultPage但当前存在明显不一致:
selected/guest + hairstyle/aftercare 建模jobId 和旧 results 接口simulationSession 状态模型不适合真实任务详情页根据最新表结构,hair_template 现已具备本次查询所需关键字段:
template_namecover_urlstatuscategorytagsdescriptiontemplate_urlhairstylist_idis_system_template这意味着本次无需额外拼接模板来源,也无需依赖前端静态模板配置。
本次采用“后端主导任务编排 + 详情页触发按规则刷新”的方案。
POST /executeai_hair_simulationai_hair_simulation_taskPROCESSING 的子任务建议前端相关路由收口为:
/:locale/simulation
/:locale/simulation/result/:simulationId
/:locale/simulation/edit
/:locale/simulation/aftercare
其中:
/:locale/simulation?customerId={id} 进入创建页/:locale/simulation 进入创建页创建页继续使用 StylistSimulationPage,但需整体重写为本次业务语义。
交互规则:
customerId页面需支持两种模式:
绑定顾客不绑定顾客当选择 绑定顾客 时:
不绑定顾客不再使用旧的:
selectedguest而直接使用更贴近业务语义的文案。
图片上传沿用通用上传能力,规则固定为:
uploadMedia(file, 'ai-user-origin-image')上传成功后前端保留:
最终创建模拟时传原图 URL,而不是直接传 File。
模板选择区固定拆分为两个来源 Tab:
我的模板系统模板底层数据都来自 hair_template,区别只在筛选条件。
可选筛选项:
categoryqpagepageSize模板卡片展示:
cover_urltemplate_namecategorytags本次允许多选模板,且至少选择 1 个。
SimulationResultPage 重写为真正的任务详情页,不再沿用旧的 job + results 模型。
customerId绑定顾客时展示:
匿名模拟时展示:
右侧展示模拟单汇总信息:
左侧信息区:
右侧子任务区:
当前 CustomerDetailPage 中“开始模拟”按钮仍为禁用态。
本次调整为:
/:locale/simulation?customerId={customer.id}顾客详情页中的其他未开发模块保持原样,不在本次扩展。
SimulationEditPage 保留页面和路由,但统一改为未开发态:
AftercareResultPage 保留页面和路由,但统一改为未开发态:
这满足用户提出的:
aftercare 不做微调 页面不要删除,但明确标记未开发建议新增:
GET /api/v1/simulations/template-options请求参数建议:
type ListSimulationTemplateOptionsQuery = {
source: 'STYLIST' | 'SYSTEM'
category?: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
q?: string
page?: number
pageSize?: number
}
查询规则:
公共条件:
is_deleted = 0status = 'PUBLIC'template_url 非空发型师模板:
is_system_template = 'NO'hairstylist_id = 当前登录发型师系统模板:
is_system_template = 'YES'返回字段建议:
type SimulationTemplateOption = {
id: string
templateName: string
coverUrl: string
category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
tags: string[]
description: string | null
templateUrl: string
isSystemTemplate: boolean
}
接口:
POST /api/v1/simulations请求体:
type CreateAiHairSimulationRequest = {
customerId?: string
originImage: string
templateIds: string[]
simulateRemark?: string
}
规则:
originImage 必填templateIds 至少 1 个customerId,必须属于当前登录发型师PUBLIC,且属于:
服务端处理顺序:
POST /executesimulationId推荐响应结构:
type CreateAiHairSimulationResponse = {
simulationId: string
}
接口:
GET /api/v1/simulations/:simulationId职责:
PROCESSING 子任务查询 AI 结果返回结构建议:
type AiHairSimulationDetail = {
id: string
customer: {
id: string
name: string
avatar: string
phone: string
expectContact: 'LINE' | 'PHONE' | 'EMAIL' | 'NONE'
} | null
originImage: string
simulateRemark: string | null
totalTaskCount: number
processingTaskCount: number
successTaskCount: number
failedTaskCount: number
overallStatus: 'PROCESSING' | 'PARTIAL_SUCCESS' | 'SUCCESSES' | 'FAILED'
tasks: Array<{
id: string
templateId: string
templateName: string
templateCoverUrl: string
templateCategory: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
templateSource: 'STYLIST' | 'SYSTEM'
taskId: string
taskStatus: 'PROCESSING' | 'SUCCESSES' | 'FAILED'
resultImages: string[]
errorMsg: string | null
taskStartTime: string
taskFinishTime: string | null
}>
}
聚合状态规则:
PROCESSING -> PROCESSINGSUCCESSESFAILEDPARTIAL_SUCCESS这个聚合状态不落库,只在详情查询时动态计算。
ai_hair_simulation每次创建模拟只生成 1 条主记录:
customer_idhairstylist_idorigin_imagesimulate_remarkai_hair_simulation_task每个模板生成 1 条子任务记录:
simulation_idtemplate_idcustomer_idtask_idtask_statusresult_imagestask_start_timetask_finish_timeerror_msg初始值:
task_status = PROCESSINGresult_images = NULLtask_finish_time = NULLerror_msg = NULLAI POST /execute 请求体映射规则:
{
"template": "hair_template.template_url",
"inputs": {
"images": ["ai_hair_simulation.origin_image"]
}
}
也就是说:
cover_url 仅用于前端展示template_url 才是真正发给 AI 服务的模板地址新增配置:
AI_SIMULATION_BASE_URLAI_SIMULATION_REFRESH_INTERVAL_MSAI_SIMULATION_HTTP_TIMEOUT_MS建议默认值:
AI_SIMULATION_BASE_URL=http://localhost:8084AI_SIMULATION_REFRESH_INTERVAL_MS=10000AI_SIMULATION_HTTP_TIMEOUT_MS=15000建议新增独立客户端,例如:
AiSimulationClient.execute(templateUrl, imageUrl)AiSimulationClient.getResult(taskId)职责:
simulations 模块复用不负责:
这是本次方案的核心。
当 GET /simulations/:simulationId 被调用时:
task_status = PROCESSING 的子任务GET /result建议规则:
AI_SIMULATION_REFRESH_INTERVAL_MS 时,才查询 AI当前阶段可直接利用 update_time 作为“最近刷新时间”的判断基准,不额外新增字段。
AI GET /result 返回 status 与数据库状态映射如下:
processing -> PROCESSINGsuccesses -> SUCCESSESfailed -> FAILED成功时回写:
task_status = SUCCESSESresult_imagestask_finish_time失败时回写:
task_status = FAILEDerror_msgtask_finish_time处理中时:
PROCESSINGtask_finish_time本次不采用“先写数据库,再慢慢补 taskId”的方案。
推荐顺序:
POST /execute 获取所有 taskIdai_hair_simulationai_hair_simulation_task优点:
当前阶段不做:
建议将 frontend/src/services/api/simulations.ts 收口为只保留本次 case 所需能力:
listSimulationTemplates(params)createSimulation(input)getSimulationById(simulationId)建议新增或更新的数据类型:
SimulationTemplateOptionAiHairSimulationDetailAiHairSimulationTaskDetail建议删除旧接口:
getSimulationJobgetSimulationResultswaitForSimulationJobsaveSimulationResultadjustSimulationResultsaveAftercareFollow旧的 simulationSession 若仍只服务旧链路,应一并清理或重构为仅保留必要导航上下文。
前端详情页只轮询:
GET /api/v1/simulations/:simulationId建议策略:
overallStatus = PROCESSING
这样前端与后端配合节奏为:
以下情况返回错误:
execute 调用失败当前策略:
若模拟单不存在或不属于当前发型师:
404若 AI 查询结果失败:
这可以避免因为一次瞬时网络抖动就把原本仍在运行的任务错误标成失败。
至少覆盖:
source 正确筛选系统模板与发型师模板overallStatusPROCESSING 子任务processing / successes / failed 时正确映射数据库状态至少覆盖:
401404至少覆盖:
ai-user-origin-image/:locale/simulation/result/:simulationIdai_hair_simulation_task.template_id 为字符串而 hair_template.id 是 BIGINT。本次实现需统一转换策略,建议前后端都以字符串承接,后端查询时再转成 BigInt。
template_url 是 AI 调用强依赖字段即使系统模板数据后续补充,本次也必须对 template_url 非空做校验,否则会造成 AI 创建失败。
这是一种为当前阶段做的折中。随着后续任务系统变复杂,未来可能需要拆为:
但当前阶段不必提前引入更重的基础设施。
实施时要警惕只“叠加新逻辑”而不删除旧逻辑,避免:
ai-user-origin-image。hair_template,AI 调用使用 template_url。ai_hair_simulation 和多条 ai_hair_simulation_task。/:locale/simulation/result/:simulationId。aftercare 和旧模拟结果链路不再作为真实业务实现存在。本次 case 的正确实现方式是:
hair_template 的真实字段直接支撑模板查询与 AI 调用。ai_hair_simulation + ai_hair_simulation_task 作为任务事实源。