Stable-Diffusion-V1-5 应用开发:基于SpringBoot和Vue的全栈AI绘画平台

📅 发布时间:2026/7/17 4:37:04 👁️ 浏览次数:
Stable-Diffusion-V1-5 应用开发:基于SpringBoot和Vue的全栈AI绘画平台
Stable-Diffusion-V1-5 应用开发基于SpringBoot和Vue的全栈AI绘画平台最近几年AI绘画从实验室里的新奇玩具变成了很多创意工作者和开发者的日常工具。但说实话直接把模型跑起来和把它变成一个稳定、好用、能服务多人的产品中间隔着一道不小的鸿沟。你可能遇到过这些问题自己用脚本跑图还行但怎么让团队其他成员也能方便地使用生成任务一多就卡住怎么管理排队和调度生成的图片东一张西一张怎么统一管理和展示今天我们就来聊聊怎么用SpringBoot和Vue亲手搭建一个属于自己的全栈AI绘画平台。这不仅仅是一个调用API的Demo而是一个考虑了用户管理、任务调度、实时反馈和作品管理的完整应用。我会把我们在实际项目中趟过的坑、总结的经验用最直白的方式分享给你让你也能从零开始构建一个既稳定又好用的AI绘画工具。1. 项目全景我们要做一个什么样的平台在动手写代码之前我们先得想清楚这个平台到底要解决什么问题长什么样。简单来说它就像一个内部的“AI绘画工作室”核心目标就两个让生成图片变得简单让管理图片变得高效。从用户的角度看他只需要打开一个网页在输入框里写下“一只戴着宇航员头盔的猫星空背景赛博朋克风格”调整几个滑块选择图片尺寸和生成步数点击“生成”然后就能在任务列表里看到进度完成后图片自动出现在画廊里。整个过程不需要知道模型在哪里不需要敲命令更不需要关心服务器状态。从我们开发者的角度看这个平台背后要处理的事情就多了用户与权限谁可以用能不能区分不同用户的作品任务调度很多人同时点“生成”先处理谁的服务器扛不住怎么办实时通信用户点了生成不能让他干等着刷新页面吧得告诉他“正在排队”、“生成中”、“完成了”。资源管理生成的图片存哪里怎么方便用户下次找到元数据比如用的提示词怎么保存稳定与扩展一个生成任务跑崩了不能影响其他任务。以后用户量大了系统能不能方便地扩容基于这些考虑我们的技术栈就清晰了后端 (SpringBoot)负责核心业务逻辑。提供REST API处理用户请求集成Stable Diffusion模型进行推理管理任务队列和状态处理用户认证和图片存储。前端 (Vue.js)负责用户交互。一个响应式的单页面应用提供友好的操作界面实时展示任务状态优雅地展示作品画廊。关键通信 (WebSocket)连接前后端的“实时消息通道”用于推送任务状态更新。整个系统的核心数据流你可以想象成这样用户在前端提交任务 - SpringBoot接收放入队列 - 调度器分配资源调用Python模型服务 - 生成过程中通过WebSocket向前端推送进度 - 生成完成图片保存任务状态更新 - 前端收到通知更新界面并展示图片。接下来我们就从搭建后端核心开始。2. 构建后端引擎SpringBoot如何驱动Stable Diffusion后端是整个平台的大脑和心脏。我们选择SpringBoot就是看中了它快速构建、易于集成和生态丰富的特点。这一部分我们重点解决三个核心问题怎么调用模型、怎么管理任务、怎么通知前端。2.1 模型调用层与Python推理服务通信Stable Diffusion模型通常用Python来加载和运行。我们的SpringBoot应用不需要也不建议直接去操作Python进程更通用的做法是让Python跑一个HTTP服务比如用FastAPI然后SpringBoot通过HTTP客户端去调用它。首先我们定义一个数据模型用来封装生成图片所需的所有参数。// 提示词增强类可以加入权重、负面提示词等 Data public class PromptEnhancement { private String positivePrompt; // 正面提示词 private String negativePrompt ; // 负面提示词默认为空 private MapString, Float keywordWeights; // 关键词权重如 {cat: 1.2, sunset: 0.8} } // 核心的图片生成请求体 Data public class ImageGenerationRequest { NotBlank(message 提示词不能为空) private String prompt; // 基础提示词 private PromptEnhancement enhancement; // 增强参数 Min(value 1, message 生成数量至少为1) private Integer batchSize 1; Min(value 256) Max(value 1024) private Integer width 512; Min(value 256) Max(value 1024) private Integer height 512; Min(value 1) Max(value 150) private Integer steps 30; private Float guidanceScale 7.5f; // 引导尺度 private Long seed; // 随机种子用于复现结果 }接着我们创建一个服务专门负责和Python推理服务“对话”。Service Slf4j public class StableDiffusionService { Value(${ai.sd.api.base-url}) private String sdApiBaseUrl; // 例如: http://localhost:7860 private final RestTemplate restTemplate; // 发送生成请求 public String generateImage(ImageGenerationRequest request) { String url sdApiBaseUrl /sdapi/v1/txt2img; // 将我们的请求对象转换为Python服务所需的格式 MapString, Object payload buildSdPayload(request); try { // 这里假设Python服务同步返回一个包含图片base64或任务ID的响应 Map response restTemplate.postForObject(url, payload, Map.class); // 处理响应返回任务ID或图片URL return extractTaskIdOrImageUrl(response); } catch (Exception e) { log.error(调用Stable Diffusion API失败, e); throw new RuntimeException(图片生成服务暂时不可用, e); } } // 查询任务状态 public GenerationTaskStatus queryTaskStatus(String taskId) { String url sdApiBaseUrl /sdapi/v1/task/ taskId; // ... 调用查询接口 } private MapString, Object buildSdPayload(ImageGenerationRequest request) { MapString, Object payload new HashMap(); payload.put(prompt, request.getPrompt()); payload.put(negative_prompt, request.getEnhancement() ! null ? request.getEnhancement().getNegativePrompt() : ); payload.put(width, request.getWidth()); payload.put(height, request.getHeight()); payload.put(steps, request.getSteps()); payload.put(cfg_scale, request.getGuidanceScale()); payload.put(batch_size, request.getBatchSize()); if (request.getSeed() ! null) { payload.put(seed, request.getSeed()); } return payload; } }2.2 任务调度与管理应对高并发请求用户点一下“生成”很简单但后台可能同时收到几十个请求。如果直接让每个请求都去调模型服务器瞬间就会崩溃。所以我们必须引入一个任务队列。我们这里采用一个简单的内存队列加线程池的方案对于中小规模的应用足够了。核心思想是接收请求立即返回一个任务ID然后把耗时的生成工作丢到后台队列慢慢处理。Service public class TaskQueueService { private final ExecutorService taskExecutor Executors.newFixedThreadPool(3); // 控制并发数 private final MapString, GenerationTask taskStore new ConcurrentHashMap(); private final BlockingQueueGenerationTask pendingQueue new LinkedBlockingQueue(); Autowired private StableDiffusionService sdService; Autowired private WebSocketService wsService; PostConstruct public void init() { // 启动一个消费者线程不断从队列取任务执行 new Thread(this::consumeTask).start(); } // 提交一个新任务 public GenerationTask submitTask(ImageGenerationRequest request, String userId) { String taskId UUID.randomUUID().toString(); GenerationTask task new GenerationTask(taskId, request, userId); task.setStatus(TaskStatus.PENDING); taskStore.put(taskId, task); pendingQueue.offer(task); // 放入等待队列 log.info(任务已提交ID: {}, 当前队列长度: {}, taskId, pendingQueue.size()); // 立即通过WebSocket通知前端任务已接收正在排队 wsService.notifyTaskStatus(userId, taskId, TaskStatus.PENDING, 任务已进入队列等待执行); return task; } // 消费者线程处理队列中的任务 private void consumeTask() { while (true) { try { GenerationTask task pendingQueue.take(); // 阻塞直到有任务 taskExecutor.submit(() - executeTask(task)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } // 实际执行生成任务 private void executeTask(GenerationTask task) { try { task.setStatus(TaskStatus.RUNNING); wsService.notifyTaskStatus(task.getUserId(), task.getTaskId(), TaskStatus.RUNNING, 开始生成图片); // 调用真正的生成服务 String imageUrl sdService.generateImage(task.getRequest()); task.setResultImageUrl(imageUrl); task.setStatus(TaskStatus.SUCCESS); task.setFinishTime(new Date()); wsService.notifyTaskStatus(task.getUserId(), task.getTaskId(), TaskStatus.SUCCESS, 图片生成完成, imageUrl); log.info(任务执行成功: {}, task.getTaskId()); } catch (Exception e) { task.setStatus(TaskStatus.FAILED); task.setErrorMessage(e.getMessage()); wsService.notifyTaskStatus(task.getUserId(), task.getTaskId(), TaskStatus.FAILED, 生成失败: e.getMessage()); log.error(任务执行失败: {}, task.getTaskId(), e); } } // 根据ID查询任务 public GenerationTask getTask(String taskId) { return taskStore.get(taskId); } }这里的GenerationTask是一个实体类记录了任务的所有信息ID、状态、请求参数、提交用户、结果、创建和完成时间等。TaskStatus是一个枚举定义了PENDING,RUNNING,SUCCESS,FAILED等状态。2.3 状态实时推送WebSocket让界面“活”起来任务在后台运行可能耗时几十秒。用户不可能一直刷新页面。这时候WebSocket就派上用场了。它能建立一个持久化的双向连接服务器可以主动给浏览器“推送”消息。我们用Spring Boot提供的STOMP over WebSocket来实现这样前后端通信会更规范一些。首先配置WebSocketConfiguration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 客户端连接的服务端点 registry.addEndpoint(/ws).setAllowedOriginPatterns(*).withSockJS(); } Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 客户端订阅消息的前缀 registry.enableSimpleBroker(/topic, /queue); // 客户端发送消息到服务器的前缀 registry.setApplicationDestinationPrefixes(/app); } }然后创建一个服务来发送状态更新Service public class WebSocketService { Autowired private SimpMessagingTemplate messagingTemplate; /** * 向特定用户发送任务状态更新 * param userId 用户ID * param taskId 任务ID * param status 状态 * param message 附加信息 */ public void notifyTaskStatus(String userId, String taskId, TaskStatus status, String message) { notifyTaskStatus(userId, taskId, status, message, null); } public void notifyTaskStatus(String userId, String taskId, TaskStatus status, String message, String imageUrl) { MapString, Object payload new HashMap(); payload.put(taskId, taskId); payload.put(status, status.toString()); payload.put(message, message); payload.put(timestamp, System.currentTimeMillis()); if (imageUrl ! null) { payload.put(imageUrl, imageUrl); } // 发送到用户专属的队列格式如/queue/task-status/{userId} String destination /queue/task-status/ userId; messagingTemplate.convertAndSend(destination, payload); log.debug(WebSocket消息已发送: destination{}, payload{}, destination, payload); } }这样后端在任务状态变化的关键节点进入队列、开始执行、成功、失败都会通过WebSocketService给对应用户发一条消息。前端只要订阅了这个消息通道就能实时收到更新。3. 打造前端界面Vue.js构建响应式管理台后端准备好了我们需要一个界面让用户能方便地操作。Vue.js的响应式特性和丰富的生态让我们能快速构建一个体验良好的单页面应用。这里我们重点实现四个核心功能模块。3.1 核心交互提示词输入与参数面板这是用户最常接触的地方设计要直观。我们用几个组件来搭建。template div classgeneration-panel el-card template #header span生成参数/span /template !-- 提示词输入 -- div classparam-row label正面提示词:/label el-input v-modelform.positivePrompt typetextarea :rows3 placeholder详细描述你想要的画面例如A beautiful sunset over a mountain lake, digital art / /div div classparam-row label负面提示词 (可选):/label el-input v-modelform.negativePrompt typetextarea :rows2 placeholder描述你不想要的内容例如blurry, ugly, deformed / /div !-- 基础参数滑块 -- div classparam-grid div classparam-item label图片尺寸: {{ form.width }} x {{ form.height }}/label div classslider-group el-slider v-modelform.width :min256 :max1024 :step64 / el-slider v-modelform.height :min256 :max1024 :step64 / /div /div div classparam-item label生成步数: {{ form.steps }}/label el-slider v-modelform.steps :min10 :max100 :step5 / /div div classparam-item label引导尺度: {{ form.guidanceScale.toFixed(1) }}/label el-slider v-modelform.guidanceScale :min1 :max20 :step0.5 / small值越高越遵循提示词值越低越有创意。/small /div div classparam-item label生成数量: {{ form.batchSize }}/label el-input-number v-modelform.batchSize :min1 :max4 / /div /div !-- 生成按钮 -- div classaction-row el-button typeprimary :loadingisSubmitting clickhandleSubmit {{ isSubmitting ? 生成中... : 开始生成 }} /el-button el-button clickhandleReset重置参数/el-button el-button typetext clickshowAdvanced !showAdvanced {{ showAdvanced ? 隐藏 : 显示 }}高级选项 /el-button /div /el-card /div /template script setup import { ref, reactive } from vue import { ElMessage } from element-plus import { submitGenerationTask } from /api/task const emit defineEmits([task-created]) const isSubmitting ref(false) const showAdvanced ref(false) const form reactive({ positivePrompt: , negativePrompt: , width: 512, height: 512, steps: 30, guidanceScale: 7.5, batchSize: 1, seed: null }) const handleSubmit async () { if (!form.positivePrompt.trim()) { ElMessage.warning(请输入提示词) return } isSubmitting.value true try { const task await submitGenerationTask(form) ElMessage.success(任务已提交ID: ${task.taskId}) emit(task-created, task) // 通知父组件有新任务了 // 可以在这里清空表单或保留 } catch (error) { ElMessage.error(提交失败: error.message) } finally { isSubmitting.value false } } const handleReset () { Object.assign(form, { positivePrompt: , negativePrompt: , width: 512, height: 512, steps: 30, guidanceScale: 7.5, batchSize: 1, seed: null }) } /script3.2 实时任务队列WebSocket连接与状态展示这是体现平台“实时性”的关键。我们需要建立WebSocket连接并监听特定用户的任务状态主题。template div classtask-queue h3任务队列 ({{ tasks.length }})/h3 el-table :datafilteredTasks sizesmall stylewidth: 100% el-table-column proptaskId label任务ID width180 template #default{ row } el-tooltip :contentrow.taskId placementtop span classtask-id{{ row.taskId.substring(0, 8) }}.../span /el-tooltip /template /el-table-column el-table-column propstatus label状态 width100 template #default{ row } el-tag :typegetStatusTagType(row.status) sizesmall {{ row.status }} /el-tag /template /el-table-column el-table-column propprompt label提示词 show-overflow-tooltip / el-table-column propcreatedAt label提交时间 width160 template #default{ row } {{ formatTime(row.createdAt) }} /template /el-table-column el-table-column label操作 width120 template #default{ row } el-button v-ifrow.status SUCCESS sizesmall clickviewImage(row) 查看 /el-button el-button v-ifrow.status FAILED sizesmall typetext clickshowError(row) 详情 /el-button /template /el-table-column /el-table /div /template script setup import { ref, onMounted, onUnmounted, computed } from vue import { Client } from stomp/stompjs import SockJS from sockjs-client import { ElMessage } from element-plus import { getCurrentUser } from /utils/auth const tasks ref([]) // 存储所有任务 let stompClient null // 连接WebSocket const connectWebSocket () { const user getCurrentUser() if (!user) return const socket new SockJS(http://localhost:8080/ws) stompClient new Client({ webSocketFactory: () socket, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, }) stompClient.onConnect () { console.log(WebSocket连接成功) // 订阅用户专属的任务状态队列 const subscription /queue/task-status/${user.id} stompClient.subscribe(subscription, (message) { const update JSON.parse(message.body) handleTaskUpdate(update) }) } stompClient.onStompError (frame) { console.error(WebSocket错误:, frame.headers[message]) } stompClient.activate() } // 处理服务器推送的任务更新 const handleTaskUpdate (update) { const index tasks.value.findIndex(t t.taskId update.taskId) if (index 0) { // 更新已有任务 tasks.value[index] { ...tasks.value[index], ...update } ElMessage.info(任务 ${update.taskId.substring(0, 8)}... 状态更新: ${update.status}) } else { // 或者从服务器拉取一次完整任务列表这里简化处理 fetchTaskList() } // 如果任务完成可以触发一些额外操作比如播放提示音 if (update.status SUCCESS) { playCompletionSound() } } // 获取任务列表 const fetchTaskList async () { try { const response await fetch(/api/tasks/my) tasks.value await response.json() } catch (error) { console.error(获取任务列表失败:, error) } } // 根据状态返回标签类型 const getStatusTagType (status) { const map { PENDING: info, RUNNING: , SUCCESS: success, FAILED: danger } return map[status] || info } onMounted(() { fetchTaskList() connectWebSocket() }) onUnmounted(() { if (stompClient) { stompClient.deactivate() } }) /script3.3 作品画廊图片展示与管理生成好的图片需要有一个家。画廊组件负责以美观的方式展示图片并提供基本的浏览和管理功能。template div classgallery div classgallery-header h3我的作品 ({{ images.length }})/h3 div classheader-actions el-input v-modelsearchKeyword placeholder搜索提示词... clearable stylewidth: 200px; margin-right: 10px; / el-select v-modelsortBy placeholder排序方式 el-option label最新优先 valuenewest / el-option label最旧优先 valueoldest / /el-select /div /div !-- 图片网格 -- div v-iffilteredImages.length 0 classimage-grid div v-forimg in filteredImages :keyimg.id classimage-card el-image :srcimg.url :preview-src-list[img.url] fitcover classgallery-image lazy / div classimage-info p classprompt :titleimg.prompt{{ truncatePrompt(img.prompt) }}/p div classmeta span{{ formatTime(img.createdAt) }}/span span尺寸: {{ img.width }}x{{ img.height }}/span /div div classactions el-button sizesmall clickdownloadImage(img)下载/el-button el-button sizesmall typetext clickshowDetail(img)详情/el-button /div /div /div /div el-empty v-else description暂无作品 / /div /template script setup import { ref, computed, onMounted } from vue import { fetchMyImages } from /api/gallery const images ref([]) const searchKeyword ref() const sortBy ref(newest) // 获取图片列表 const loadImages async () { try { images.value await fetchMyImages() } catch (error) { console.error(加载作品失败:, error) } } // 过滤和排序 const filteredImages computed(() { let result [...images.value] // 搜索过滤 if (searchKeyword.value) { const keyword searchKeyword.value.toLowerCase() result result.filter(img img.prompt.toLowerCase().includes(keyword) ) } // 排序 if (sortBy.value newest) { result.sort((a, b) new Date(b.createdAt) - new Date(a.createdAt)) } else if (sortBy.value oldest) { result.sort((a, b) new Date(a.createdAt) - new Date(b.createdAt)) } return result }) // 截断过长的提示词 const truncatePrompt (prompt) { if (prompt.length 80) { return prompt.substring(0, 80) ... } return prompt } // 下载图片 const downloadImage (img) { const link document.createElement(a) link.href img.url link.download ai_image_${img.id}.png document.body.appendChild(link) link.click() document.body.removeChild(link) } onMounted(() { loadImages() }) /script style scoped .image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; } .image-card { border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; transition: box-shadow 0.3s; } .image-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .gallery-image { width: 100%; height: 200px; object-fit: cover; } .image-info { padding: 12px; } .prompt { font-size: 14px; line-height: 1.4; margin-bottom: 8px; color: #333; } .meta { display: flex; justify-content: space-between; font-size: 12px; color: #909399; margin-bottom: 10px; } /style4. 项目总结与展望把这个全栈AI绘画平台跑起来之后最大的感受是技术选型和架构设计真的决定了项目的上限和开发体验。用SpringBoot做后端各种功能模块集成起来非常顺畅像任务队列、WebSocket、安全认证这些都有成熟的解决方案不用自己重复造轮子。Vue.js的前端开发体验也很好响应式数据绑定让实时更新任务状态变得特别简单用户看到任务进度条在动体验感一下子就上来了。实际用下来这套架构在应对几十个并发用户、每天几百个生成任务时表现挺稳定的。任务队列把突发请求“削峰填谷”WebSocket让用户不用傻等画廊功能也让生成的作品不至于散落各处。当然过程中也踩过一些坑比如初期WebSocket断线重连没做好导致用户偶尔收不到通知还有图片存储直接用了本地磁盘后来用户多了才迁移到对象存储。如果你也想自己搭一个我的建议是先从核心功能开始把模型调通做个最简单的提交和生成。然后再一步步加上任务队列、状态推送、用户管理和画廊。遇到问题很正常尤其是模型服务的内存管理和任务调度的公平性需要根据你的实际硬件和用户量慢慢调整。这个项目就像一个乐高底座上面可以搭很多东西。比如可以加入“风格模板”让用户一键应用不同的绘画风格可以做“提示词市场”让大家分享好用的提示词甚至可以集成多个不同的AI绘画模型让用户自己选。方向很多关键是想清楚你最想解决的那个痛点是什么。希望这些分享能给你带来一些启发动手试试看把想法变成代码的过程其实挺有意思的。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。