从前端 Prompt 编辑到后端异步任务队列,再到双引擎出图落盘------完整拆解管理端 AI 生图模块的设计思路与实现细节。
目录
- [背景:为什么管理端需要 AI 生图能力](#背景:为什么管理端需要 AI 生图能力 "#1-%E8%83%8C%E6%99%AF%E4%B8%BA%E4%BB%80%E4%B9%88%E7%AE%A1%E7%90%86%E7%AB%AF%E9%9C%80%E8%A6%81-ai-%E7%94%9F%E5%9B%BE%E8%83%BD%E5%8A%9B")
- [整体架构:双引擎 + 异步任务](#整体架构:双引擎 + 异步任务 "#2-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E5%8F%8C%E5%BC%95%E6%93%8E--%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1")
- [第一阶段:Prompt 管理与大模型文案生成](#第一阶段:Prompt 管理与大模型文案生成 "#3-%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5prompt-%E7%AE%A1%E7%90%86%E4%B8%8E%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%87%E6%A1%88%E7%94%9F%E6%88%90")
- 第二阶段:图片生成核心流程
- [双引擎详解:ComfyUI vs DashScope](#双引擎详解:ComfyUI vs DashScope "#5-%E5%8F%8C%E5%BC%95%E6%93%8E%E8%AF%A6%E8%A7%A3comfyui-vs-dashscope")
- 异步任务机制与状态机
- 一键批量生图:栏目级素材自动化
- 前端管理界面设计
- 经验总结与踩坑记录
1 背景:为什么管理端需要 AI 生图能力
AI 海报素材站是一个面向用户的移动端资讯与工具平台,核心内容之一是海报素材库------各类资源的宣传海报、推荐图片需要持续更新,以保持用户新鲜感。传统做法是由设计师手工出图,周期长、成本高,且难以快速响应运营活动。
引入 AI 生图后,运营人员只需在管理端维护一批结构化 Prompt 模板 ,点击"生成"按钮,后台即可调用图像模型自动产出海报,全程无需设计师介入。更进一步,系统支持一键为整个栏目批量重刷封面,数十张图片几分钟内全部到位。
| 痛点 | 描述 |
|---|---|
| 出图效率低 | 设计师手工制作一张海报需 1~2 小时,远不能满足运营频次 |
| 素材库难维护 | 数百条资源素材,封面更新后需逐条替换,工作量巨大 |
| 解法:AI + 异步任务 | Prompt 模板 + 异步队列,运营直接操作,出图全自动 |
2 整体架构:双引擎 + 异步任务
整个 AI 生图模块由三层组成:前端管理界面 (Vue 3 + Element Plus)、后端 Spring Boot 服务 (异步任务 + Service 层 + Client 层),以及两个可切换的图像引擎(本地 ComfyUI 或阿里云 DashScope)。
bash
管理员(前端)
│
│ ① 选 Prompt / 提交生成
▼
AdminAiController /api/admin/ai/**
│
▼
AiMaterialService 业务编排层
├──②a 单条/批量──► AiTaskExecutor @Async 异步线程池
└──②b 同步出图───► AiImageClient 统一出图入口
│
┌───────────┴────────────┐
│ provider=comfyui │ provider=dashscope
▼ ▼
ComfyUiImageClient DashScopeImageClient
HTTP Polling REST API
ComfyUI :8188 DashScope aliyuncs.com
│ │
└───────────┬────────────┘
▼
FileUploadUtil UUID 落盘 uploads/ai/
│
③ 回写 URL
▼
MySQL: ai_generate_task / material
④ 前端轮询 batchTaskStatus
核心设计决策是将出图引擎与业务逻辑完全解耦 ------AiImageClient 作为唯一出图入口,通过 ai.image-provider 配置项在运行时动态切换 ComfyUI 或 DashScope,上层业务代码完全无感知。
3 第一阶段:Prompt 管理与大模型文案生成
3.1 Prompt 模板数据结构
每条 Prompt 模板(AiPromptTemplate)包含标题、分类、资源 ID、排序权重、提示词正文和启用状态。管理员可在前端 CRUD 维护这批模板,不同资源类型、不同类型(号码推荐 / 活动海报等)各自有对应的模板。
3.2 大模型辅助生成 Prompt 文案
很多运营人员对"文生图 Prompt 怎么写"并不熟悉。系统内置了一个大模型辅助生成 Prompt 的接口(POST /api/admin/ai/generatePrompt),运营只需填写素材标题、分类和补充要求,系统自动调用阿里云 DashScope Chat API 生成专业提示词。
java
// AiMaterialServiceImpl.java --- generatePromptText()
public String generatePromptText(String title, String category, String hint) {
StringBuilder sb = new StringBuilder();
sb.append("你是资源平台宣传海报提示词专家。请直接输出一段可用于文生图模型的中文提示词,不要解释、不要编号。");
sb.append("要求:竖版海报、红金喜庆、商业宣传风格,底部预留合规区「推荐仅供参考,请理性选购」。");
if (StringUtils.hasText(title)) sb.append("素材标题:").append(title).append("。");
if (StringUtils.hasText(category)) sb.append("分类:").append(category).append("。");
if (StringUtils.hasText(hint)) sb.append("补充要求:").append(hint).append("。");
sb.append("请生成完整提示词。");
return dashScopeChatClient.generatePrompt(sb.toString());
}
3.3 DashScope Chat 客户端:流式 SSE 解析
Chat 模型返回的是 Server-Sent Events(SSE) 流式响应,需要逐行读取并聚合 delta.content。实现上用原生 HttpURLConnection 打开长连接,开启 "enable_thinking": true 让模型先思考再输出。
java
// DashScopeChatClient.java --- 流式读取并聚合 content
private String readSseContent(InputStream is) throws Exception {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || !line.startsWith("data:")) continue;
String data = line.substring(5).trim();
if ("[DONE]".equals(data)) break;
JsonNode delta = objectMapper.readTree(data)
.path("choices").path(0).path("delta");
if (delta.has("content"))
content.append(delta.get("content").asText(""));
}
}
return content.toString();
}
注意: 请求头需设置
Accept: text/event-stream,并将超时设置到 180 秒,否则大模型 thinking 阶段可能导致连接超时。
4 第二阶段:图片生成核心流程
文案 Prompt 准备好后,正式进入出图环节。根据业务场景,系统支持三种触发方式:
步骤 1 --- 单条异步生成(基于 Prompt 模板)
管理员在「提示词管理」页勾选模板,点击批量生成,后台为每条模板创建一个 AiGenerateTask(状态 pending),立即丢入异步线程池。
步骤 2 --- 同步出图(素材行内生成)
管理员在素材管理页直接输入 Prompt,点击「立即出图」,调用 generateImageSync,同步等待图片返回后刷新预览。适合快速验证 Prompt 效果。
步骤 3 --- 一键批量生图(栏目级)
按资源类型批量为当前栏目下所有图片素材重新出图,所有任务一次性提交异步队列,前端轮询进度直至全部完成。
4.1 任务创建与状态流转
异步生成路径中,任务对象(AiGenerateTask)的状态机遵循严格的单向流转:pending → running → success / fail。失败的任务支持手动重试,会将状态重置为 pending 后重新投入队列。
css
[创建] ──► pending ──► running ──► success ──► [结束]
└──► fail ──► pending(重试)
5 双引擎详解:ComfyUI vs DashScope
系统通过 AiImageClient 提供统一的 generateImage(promptText) 方法,内部根据配置选择实际引擎。两套引擎在接入方式上差异显著:
| 维度 | ComfyUI(本地) | DashScope(云端) |
|---|---|---|
| 部署方式 | 本地/私有服务器,127.0.0.1:8188 | 阿里云 SaaS API |
| 通信模式 | 提交 → HTTP 轮询 history API | 一次请求,等待响应 |
| 工作流 | JSON 工作流文件(z-image-turbo) | 多模态消息体 |
| 图片返回 | PNG 文件名,需再下载字节流 | URL 或 Base64 |
| 随机种子 | 每次随机注入 seed 节点 | API 自动随机 |
| 适用场景 | 开发/本地测试,自有 GPU | 生产环境,稳定 SLA |
| 超时配置 | 默认 300 秒(可配置) | readTimeout 180 秒 |
5.1 ComfyUI 引擎:工作流注入 + 轮询
ComfyUI 使用节点工作流(z-image-turbo-workflow.json)驱动出图。客户端在启动时通过 @PostConstruct 将工作流 JSON 加载到内存,出图时做两件事:① 把 Prompt 文本注入 57:27 节点 ;② 随机注入 seed 到 57:3 节点,保证每次出图不重复。
java
// ComfyUiImageClient.java --- 工作流节点注入
ObjectNode workflow = workflowTemplate.deepCopy();
((ObjectNode) workflow.path("57:27").path("inputs")).put("text", promptText);
long seed = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE);
((ObjectNode) workflow.path("57:3").path("inputs")).put("seed", seed);
// 提交后轮询 history/{promptId},800ms 一次
while (System.currentTimeMillis() < deadline) {
JsonNode history = getJson("/history/" + promptId);
if (history.has(promptId)) {
JsonNode images = history.path(promptId)
.path("outputs").path("9").path("images");
return downloadAndSave(images.get(0));
}
Thread.sleep(800L);
}
5.2 DashScope 引擎:多结构兼容解析
DashScope 图像 API 的响应结构在不同模型版本下字段路径可能不同 (output.choices[].message.content[].image、output.results[].url、data[].url 等均有出现)。为了应对版本差异,extractImage 方法实现了多路径优先级解析 ,最终兜底用递归深度搜索 image/url 字段。
拿到图片数据后,无论是 HTTP URL 还是 Base64,统一调用 saveImage() 落盘到 uploads/ai/{UUID}.png,返回相对访问路径。
配置切换: 在
application.yml中设置ai.image-provider: comfyui或ai.image-provider: dashscope即可切换,无需改代码。
6 异步任务机制与状态机
6.1 Spring @Async 线程池
AiTaskExecutor 标注了 @Component,其 execute() 和 executeMaterialBatch() 方法均标注 @Async,依赖 AsyncConfig 中配置的专用线程池执行,不阻塞 HTTP 请求线程。
Controller 层在接收到生成请求后,立即返回 taskId 给前端 ,前端再通过轮询 /api/admin/ai/batchTaskStatus 拿到任务的成功/失败统计。这样避免了出图时间过长(30~300 秒)导致 HTTP 请求超时。
java
// AiMaterialServiceImpl.java --- 提交任务后立即返回
AiGenerateTask task = new AiGenerateTask();
task.setPromptId(promptId);
task.setAdminId(adminId);
task.setStatus("pending");
taskMapper.insert(task); // ① 先入库
aiTaskExecutor.execute(task.getId()); // ② 异步执行,立即返回
return task.getId(); // ③ taskId 同步返回给调用方
6.2 异步执行器内部流程
AiTaskExecutor.execute() 是真正干活的地方,步骤如下:
- 标记 running --- 任务开始时立即将状态更新为 running,前端可感知任务已被处理。
- 读取 Prompt 模板 --- 从
ai_prompt_template表查出 promptText,若不存在则直接 fail。 - 调用出图引擎 --- 通过
AiImageClient.generateImage(promptText)出图,获取本地文件相对路径。 - 写入 Material 并更新任务 --- 新建一条
material(类型 poster,状态上架),回写result_url和material_id,状态设为 success。 - 异常处理 --- 任何异常(含 API 限流、超时)均 catch,将错误信息截取前 480 字符写入
error_msg,状态设为 fail。
6.3 前端轮询进度
任务管理页(task.vue)在 onMounted 时启动一个 5 秒定时器 自动刷新任务列表,并在 onUnmounted 时清除,避免内存泄漏。批量任务提交后,前端拿到 taskIds 数组,通过 batchTaskStatus 接口获得 pending/running/success/fail 四个计数,当 pending + running === 0 时判定全部完成。
7 一键批量生图:栏目级素材自动化
「一键生图」是整个 AI 生图模块最有业务价值的功能。其逻辑入口是 POST /api/admin/ai/batchGenerateMaterials,参数 resourceId(资源 ID)、type(素材类型,仅 poster)、onlyEmpty(是否仅为无封面的素材生成)。
7.1 Prompt 自动构建
批量模式下没有人工 Prompt,系统在 AiTaskExecutor.buildMaterialPosterPrompt() 中根据素材的标题、分类和资源名称自动拼接一段标准化 Prompt:
java
// AiTaskExecutor.java --- 批量模式自动构建 Prompt
static String buildMaterialPosterPrompt(Material material, String resourceName) {
String title = hasText(material.getTitle()) ? material.getTitle() : "资源宣传海报";
String category = hasText(material.getCategory()) ? material.getCategory() : "号码推荐";
String lot = hasText(resourceName) ? resourceName : "资源";
return "竖版 9:16 手机海报," + lot + "宣传海报,主题标题「" + title
+ "」,分类「" + category
+ "」,热情红主色搭配琥珀金点缀,喜庆现代电商活动海报质感,明亮高饱和,"
+ "干净排版,扁平化商业 UI,高清,8k。底部合规区文字:推荐仅供参考,请理性选购。";
}
这段 Prompt 具有很强的工程实践性:竖版比例、颜色风格(红+琥珀金)、质感描述(电商海报)、分辨率和合规文字全部内置,保证批量产出的图片风格统一、满足监管要求。
7.2 批量任务提交与 onlyEmpty 优化
batchGenerateMaterials 先查出当前资源类型下最多 500 条素材,若 onlyEmpty=true 则跳过已有封面的素材,仅为空封面素材创建任务,避免重复消耗 AI 算力配额 。任务列表批量插入后,调用 executeMaterialBatch(taskIds) 统一异步执行。
8 前端管理界面设计
8.1 Prompt 管理页(prompt.vue)
Prompt 管理页的核心交互非常简洁:表格展示所有模板,支持多选后点击「批量异步生成」一键提交,勾选数量实时显示在按钮上。编辑对话框支持 6 行 Textarea 直接修改提示词正文。
| 功能模块 | 说明 |
|---|---|
| 提示词 CRUD | 新增、编辑、删除,支持标题 / 分类 / 资源 ID / 状态管理 |
| 批量异步生成 | 勾选多条模板一键提交,后台异步处理,不卡前端页面 |
| 跳转任务监控 | 提交后可一键跳转到「生成任务」页查看实时进度 |
8.2 生成任务页(task.vue)
任务监控页展示所有任务的状态、结果缩略图、错误信息,并支持对失败任务单条重试 。页面在 mounted 时自动启动 5 秒轮询,让运营实时看到出图进度,无需手动刷新。
javascript
// task.vue --- 自动轮询 5 秒一次
onMounted(() => {
loadData()
timer = setInterval(loadData, 5000) // 5 秒刷新
})
onUnmounted(() => clearInterval(timer)) // 组件销毁时清除
9 经验总结与踩坑记录
| 问题 | 原因 | 解决方案 | 严重度 |
|---|---|---|---|
| ComfyUI 出图超时 | 默认超时 60s 不够 | 将 comfyui.timeout-seconds 调到 300s |
高 |
| DashScope 响应结构变化 | 不同版本 API 字段路径不同 | 多路径优先级解析 + 递归兜底 | 中 |
| SSE 流式请求超时 | readTimeout 默认太短 | 设置 readTimeout=180000ms + Accept: text/event-stream |
高 |
| 批量任务重复生成 | 未过滤已有封面的素材 | 新增 onlyEmpty 参数过滤 |
中 |
| error_msg 存储溢出 | 异常信息过长超出字段长度 | 截取前 480 字符 | 低 |
| 前端轮询内存泄漏 | timer 未在组件销毁时清除 | 在 onUnmounted 中 clearInterval |
中 |
| 随机种子固定导致重复图 | 工作流默认 seed 不变 | 每次出图随机注入新 seed | 中 |
9.1 整体设计感悟
整个 AI 生图模块最核心的设计是关注点分离:Controller 负责鉴权和参数解析,Service 负责业务编排(任务创建、进度汇总),TaskExecutor 负责异步执行,Client 层负责与外部 AI 服务通信,每一层职责清晰,便于独立测试和替换。
双引擎设计让系统既能在开发阶段使用本地 ComfyUI 快速迭代 Prompt 效果,又能在生产环境无缝切换到云端 DashScope,无需修改任何业务代码,只需改一行配置项。这种策略模式的应用,在实际项目中非常实用。