尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot+Vue3多模型AI聊天系统架构设计
简介这是一份面向计算机专业本科生与Java/前端开发者毕业设计与AI应用实践的完整项目源码聚焦于构建支持多模型接入的现代化AI聊天系统。资源以Spring Boot 3.2 Vue 3双端架构实现集成DeepSeek、SiliconFlow、Gemini三大主流AI模型API并通过WebFlux实现流式响应与实时交互覆盖从后端服务搭建、API适配到响应式UI渲染的全链路技术落地。压缩包共20个文件11个Java核心业务与配置类、3个Vue组件JS逻辑、1个application.yml、1个CSS样式、1个Markdown说明文档等总大小仅48KB结构精简、模块清晰便于快速导入学习与二次开发。已有186人下载学习可直接获取含完整前后端代码、标准化目录结构、Markdown消息渲染支持及跨设备响应式界面的可运行工程是理解Spring AI生态、大模型API集成与现代Web应用协同设计的高价值参考样本。1. 为什么毕设选“Spring Boot Vue 3 多模型 AI 聊天”不是跟风而是稳扎稳打的工程能力验证很多同学看到“AI聊天”就下意识觉得要调大模型API、搞前端炫技、堆高并发——结果开题答辩被问一句“你本地能离线跑通一次完整对话链路吗”当场卡壳。其实这个选题真正的价值不在“用了Gemini”而在于它天然构成一个端到端可验证的微服务闭环后端用 Spring Boot 做模型路由、会话管理、流式响应封装前端用 Vue 3 的 Composition API Pinia 管理长连接状态与消息队列中间层不硬耦合任一厂商SDK而是抽象出统一的AiProvider接口。这意味着你能清晰回答三个关键问题请求怎么进来的WebMvcConfigurer 配置、怎么分发给不同模型策略模式配置驱动、错误怎么兜底FallbackProvider 本地缓存兜底。它不追求单点技术深度但全面覆盖了现代 Web 工程师必须亲手拧紧的六颗螺丝跨域处理、SSE 流式传输、Token 生命周期管理、前端消息去重、模型响应结构归一化、以及最关键的——所有模型调用都必须走你自己的代理层而不是前端直连。适合那些想用毕设证明“我能把一堆开源组件焊成一个不散架的系统”的人而不是只会在 README 里贴截图的项目。2. 后端架构设计用 Spring Boot 搭建可插拔的 AI 模型调度中心这个项目后端的核心不是“调 API”而是“管调用”。我们不希望改一行代码就要重新部署来切换模型也不希望某个模型挂了导致整个聊天功能不可用。因此整个后端采用三层解耦结构接入层Controller→ 调度层Service→ 实现层Provider。其中调度层是重点它通过 Spring 的ConditionalOnProperty和Primary控制默认实现并允许运行时通过配置切换活跃模型。2.1 定义统一的 AI 对话契约AiResponse与ChatRequest所有模型返回的数据结构必须收敛。我们不接受 Gemini 返回candidates[0].content.parts[0].textDeepSeek 返回output.textSiliconFlow 返回data.choices[0].message.content这种混乱局面。定义如下核心 DTO// src/main/java/com/example/ai/dto/AiResponse.java public class AiResponse { private String id; // 全局唯一响应ID用于前端去重 private String model; // 实际调用的模型名如 deepseek-chat private String content; // 归一化后的纯文本内容 private Long timestamp; // 服务器生成时间戳用于前端排序 private boolean isFinal; // 是否为最终响应流式场景下 false 表示 chunk private String error; // 错误信息非空表示调用失败 }提示isFinal字段是流式传输的关键。Vue 前端靠它判断是否该将当前 chunk 合并进最终消息体避免出现“正在思考…”卡住不动的玄学问题。2.2 抽象模型提供者接口AiProvider每个模型对应一个实现类全部实现同一接口强制约束行为边界// src/main/java/com/example/ai/provider/AiProvider.java public interface AiProvider { /** * 模型唯一标识如 gemini-pro, deepseek-v2 */ String getModelId(); /** * 同步单次调用返回完整响应非流式 */ AiResponse chatSync(ChatRequest request); /** * 流式调用通过 SseEmitter 推送 chunk */ void chatStream(ChatRequest request, SseEmitter emitter); }所有具体实现GeminiProvider,DeepSeekProvider,SiliconFlowProvider都只负责三件事构造 HTTP 请求、解析原始响应、映射到AiResponse。绝不处理会话、不管理 Token、不决定超时策略——这些由上层统一管控。2.3 基于策略模式的动态调度器AiDispatcher这是整个后端最值得写进毕设报告的模块。它不硬编码 if-else而是通过 Spring Bean 名称匹配 配置驱动// src/main/java/com/example/ai/service/AiDispatcher.java Service public class AiDispatcher { // 所有 AiProvider 实例自动注入 private final MapString, AiProvider providerMap; public AiDispatcher(ListAiProvider providers) { this.providerMap providers.stream() .collect(Collectors.toMap(AiProvider::getModelId, Function.identity())); } public AiResponse dispatchSync(String modelId, ChatRequest request) { AiProvider provider providerMap.getOrDefault(modelId, getFallbackProvider()); try { return provider.chatSync(request); } catch (Exception e) { log.warn(Model {} call failed, fallback to default, modelId, e); return getFallbackProvider().chatSync(request); } } public void dispatchStream(String modelId, ChatRequest request, SseEmitter emitter) { AiProvider provider providerMap.getOrDefault(modelId, getFallbackProvider()); try { provider.chatStream(request, emitter); } catch (Exception e) { log.error(Stream dispatch failed for model {}, modelId, e); sendErrorToEmitter(emitter, 模型服务暂时不可用请稍后重试); } } private AiProvider getFallbackProvider() { return providerMap.values().stream() .filter(p - deepseek-chat.equals(p.getModelId())) .findFirst() .orElse(providerMap.values().iterator().next()); } }参数说明modelId来自前端请求参数如/api/chat?modelgemini-pro不是写死在代码里的字符串。这意味着你只需改application.yml里的ai.default-model就能切换默认模型无需编译。3. 前端交互实现Vue 3 中用 Pinia Comlink 管理 SSE 流式会话状态Vue 3 的响应式系统和组合式 API 是处理实时聊天的理想搭档但直接在组件里写new EventSource()或fetch(...).then(res res.body.getReader())会导致状态分散、错误难捕获、重连逻辑重复。我们采用Pinia store 封装会话生命周期 Comlink 实现主线程与 Worker 通信的双保险方案。3.1 用 Pinia 定义会话状态useChatStorestore 不仅存消息列表更要管理连接状态、当前模型、输入框锁定、重试计数等真实业务细节// src/stores/chat.ts import { defineStore } from pinia import type { Message, ModelOption } from /types interface ChatState { messages: Message[] currentModel: ModelOption isLoading: boolean isError: boolean retryCount: number abortController: AbortController | null } export const useChatStore defineStore(chat, { state: (): ChatState ({ messages: [], currentModel: { id: deepseek-chat, name: DeepSeek V2 }, isLoading: false, isError: false, retryCount: 0, abortController: null }), actions: { addMessage(msg: Message) { this.messages.push(msg) // 自动滚动到底部但防抖处理避免频繁触发 nextTick(() { const container document.getElementById(chat-container) if (container) container.scrollTop container.scrollHeight }) }, startStream(modelId: string) { this.isLoading true this.isError false this.abortController new AbortController() // 调用 worker 执行流式请求 const worker new Worker(new URL(/workers/sse-worker.ts, import.meta.url)) const comlink await import(comlink) const sseWorker comlink.wraptypeof import(/workers/sse-worker.ts)(worker) sseWorker .connectStream(modelId, this.messages, this.abortController.signal) .then((stream) { stream.onmessage (e) { const chunk e.data as { content: string; isFinal: boolean } if (chunk.isFinal) { this.addMessage({ role: assistant, content: chunk.content, timestamp: Date.now() }) this.isLoading false } else { // 更新最后一条 assistant 消息的 content流式追加 const last this.messages.at(-1) if (last?.role assistant) { last.content chunk.content } } } }) .catch((err) { this.isError true this.retryCount console.error(SSE stream failed:, err) }) } } })注意addMessage中的nextTick滚动逻辑是血泪经验。Vue 3 的响应式更新是异步的若不等 DOM 更新完成就滚动会滚到旧位置。另外abortController必须存在且可被外部访问否则用户点击“停止生成”按钮时无法真正中断请求。3.2 用 Web Worker 封装 SSE 连接隔离主线程阻塞风险SSE 连接一旦建立就会持续监听事件。若在主线程中处理网络抖动或后端延迟可能导致 UI 卡顿。我们将连接逻辑移入 Worker// src/workers/sse-worker.ts import { expose } from comlink interface StreamResult { onmessage: (e: MessageEvent) void } // 暴露给主线程调用的方法 export async function connectStream( modelId: string, history: Array{ role: string; content: string }, signal: AbortSignal ): PromiseStreamResult { const url /api/chat/stream?model${modelId} const encoder new TextEncoder() const decoder new TextDecoder() const response await fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages: history }), signal }) if (!response.ok) throw new Error(HTTP ${response.status}) const reader response.body?.getReader() if (!reader) throw new Error(ReadableStream not supported) const result: StreamResult { onmessage: () {} } // 模拟 EventSource 的 onmessage 行为 const pump async () { try { while (true) { const { done, value } await reader.read() if (done) break const text decoder.decode(value) // 解析 SSE 格式data: {...}\n\n const lines text.split(\n) for (const line of lines) { if (line.startsWith(data: )) { try { const json JSON.parse(line.slice(6)) result.onmessage({ data: json } as any) } catch (e) { console.warn(Invalid SSE data:, line) } } } } } catch (e) { if (e.name ! AbortError) { console.error(Stream read error:, e) } } } pump() return result } expose({ connectStream })提示Worker 中不能直接使用EventSource部分浏览器不支持所以改用fetch ReadableStream手动解析 SSE。TextDecoder用于正确处理 UTF-8 中文字符否则会出现乱码——这是国产模型中文响应里踩过最多次的坑。4. 多模型接入实战DeepSeek、SiliconFlow、Gemini 的 SDK 封装与参数对齐标题里写的三个模型不是简单贴三份官方 SDK 就完事。它们在认证方式、请求体结构、流式字段命名、错误码含义、限流策略上全都不一样。我们必须在 Provider 层做“翻译”让上层完全无感。4.1 DeepSeek Provider用 OpenAI 兼容接口降低接入成本DeepSeek 官方提供了 OpenAI 兼容的/v1/chat/completions接口需开启兼容模式这极大简化了开发// src/main/java/com/example/ai/provider/DeepSeekProvider.java Component public class DeepSeekProvider implements AiProvider { private static final String MODEL_ID deepseek-chat; private final RestTemplate restTemplate; public DeepSeekProvider(RestTemplateBuilder builder) { this.restTemplate builder .setConnectTimeout(Duration.ofSeconds(10)) .setReadTimeout(Duration.ofSeconds(60)) .build(); } Override public String getModelId() { return MODEL_ID; } Override public AiResponse chatSync(ChatRequest request) { // 构造 OpenAI 格式请求体 var payload Map.of( model, deepseek-chat, messages, request.getMessages(), temperature, 0.7, max_tokens, 2048 ); try { var resp restTemplate.postForObject( https://api.deepseek.com/v1/chat/completions, new HttpEntity(payload, buildHeaders()), DeepSeekResponse.class ); return mapToAiResponse(resp, MODEL_ID); } catch (HttpClientErrorException e) { return AiResponse.builder() .error(DeepSeek 服务异常 e.getStatusCode()) .build(); } } private HttpHeaders buildHeaders() { var headers new HttpHeaders(); headers.set(Authorization, Bearer System.getenv(DEEPSEEK_API_KEY)); headers.set(Content-Type, application/json); return headers; } private AiResponse mapToAiResponse(DeepSeekResponse resp, String modelId) { return AiResponse.builder() .id(resp.getId()) .model(modelId) .content(resp.getChoices().get(0).getMessage().getContent()) .timestamp(System.currentTimeMillis()) .isFinal(true) .build(); } }关键参数说明max_tokens: DeepSeek V2 最大输出长度为 4096但实际建议设为 2048避免响应过长导致前端渲染卡顿temperature: 设为 0.7 是平衡创造性和稳定性的通用值毕设演示时建议固定为 0.3防止答案发散认证头必须用Bearer不是API-Key否则 401。4.2 SiliconFlow Provider适配其特有的data.choices[0].delta.content流式格式SiliconFlow 的流式响应字段是delta.content非choices[0].message.content且首次 chunk 可能为空字符串必须跳过// src/main/java/com/example/ai/provider/SiliconFlowProvider.java Override public void chatStream(ChatRequest request, SseEmitter emitter) { var payload Map.of( model, siliconflow/llama-3-70b, messages, request.getMessages(), stream, true ); try (var response httpClient.newCall(new Request.Builder() .url(https://api.siliconflow.cn/v1/chat/completions) .post(RequestBody.create( JSON.toJSONString(payload), MediaType.get(application/json) )) .header(Authorization, Bearer System.getenv(SILICONFLOW_API_KEY)) .build()).execute()) { if (!response.isSuccessful()) { sendErrorToEmitter(emitter, SiliconFlow 请求失败: response.code()); return; } var source response.body().source(); var buffer new Buffer(); while (!Thread.currentThread().isInterrupted()) { long read source.read(buffer, 8192); if (read -1) break; String data buffer.readUtf8(); String[] lines data.split(\n); for (String line : lines) { if (line.trim().isEmpty() || !line.startsWith(data: )) continue; String jsonStr line.substring(6).trim(); if ([DONE].equals(jsonStr)) break; try { var deltaResp JSON.parseObject(jsonStr, SiliconFlowDeltaResponse.class); String content deltaResp.getChoices().get(0).getDelta().getContent(); if (content ! null !content.trim().isEmpty()) { emitter.send(SseEmitter.event() .name(message) .data(JSON.toJSONString(Map.of(content, content, isFinal, false)))); } } catch (Exception ignored) {} } } } catch (Exception e) { sendErrorToEmitter(emitter, 流式读取异常: e.getMessage()); } }血泪经验buffer.readUtf8()必须在循环内调用否则会累积未解析的二进制数据导致后续split(\n)失败[DONE]判断必须显式写否则某些情况下连接不会自然关闭。4.3 Gemini Provider绕过 Google 官方 SDK 的臃肿依赖手写轻量 HTTP 调用Google 的google-ai-generativeSDK 依赖太多gRPC、protobuf毕设项目应避免引入重量级依赖。我们直接调用 REST API// src/main/java/com/example/ai/provider/GeminiProvider.java private static final String GEMINI_URL https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent; Override public void chatStream(ChatRequest request, SseEmitter emitter) { // Gemini 要求将历史消息转为 contents 数组且 role 映射为 user/model ListMapString, Object contents request.getMessages().stream() .map(m - Map.of( role, user.equals(m.getRole()) ? user : model, parts, List.of(Map.of(text, m.getContent())) )) .collect(Collectors.toList()); var payload Map.of(contents, contents); try { var url GEMINI_URL ?key System.getenv(GEMINI_API_KEY); var response restTemplate.postForEntity( url, new HttpEntity(payload, buildGeminiHeaders()), String.class ); // Gemini 流式响应是 JSONL每行一个 JSON需逐行解析 String[] lines response.getBody().split(\n); for (String line : lines) { if (line.trim().isEmpty()) continue; try { var obj JSON.parseObject(line); var candidates obj.getJSONArray(candidates); if (candidates.size() 0) { String text candidates.getJSONObject(0) .getJSONObject(content) .getJSONArray(parts) .getJSONObject(0) .getString(text); emitter.send(SseEmitter.event() .name(message) .data(JSON.toJSONString(Map.of(content, text, isFinal, false)))); } } catch (Exception e) { log.warn(Parse Gemini line failed: {}, line, e); } } emitter.send(SseEmitter.event().name(message).data(JSON.toJSONString(Map.of(isFinal, true)))); } catch (Exception e) { sendErrorToEmitter(emitter, Gemini 调用失败: e.getMessage()); } }注意Gemini 的role字段必须是user或model不能是assistant否则返回 400contents是数组不是单个对象API Key 必须拼在 URL 上Header 里传无效。5. 避坑指南上线前必须验证的 5 个致命细节这个项目看似只是“调三个 API”但实际落地时90% 的翻车都发生在边界场景。以下是我在模拟项目 X 中反复验证、写进毕设答辩 PPT 的 5 条血泪记录每一条都对应一个真实崩溃现场。5.1 现象前端显示“正在思考…”后永远不动Network 面板里 SSE 连接状态为pending原因后端SseEmitter未设置超时且未在 Controller 方法上加ResponseStatus(HttpStatus.OK)导致 Tomcat 默认等待 30 秒才断开连接期间前端一直卡住。解决在 Controller 方法上显式设置超时并发送初始事件告知连接建立成功GetMapping(/stream) public SseEmitter stream(RequestParam String model) { SseEmitter emitter new SseEmitter(60_000L); // 60秒超时 emitter.send(SseEmitter.event().name(init).data(connected)); // 发送初始化事件 aiDispatcher.dispatchStream(model, getCurrentChatRequest(), emitter); return emitter; }5.2 现象切换模型后新消息仍被发往旧模型如选了 Gemini日志却显示调用 DeepSeek原因AiDispatcher中providerMap.getOrDefault(modelId, ...)的modelId来自 URL 参数但前端请求时未正确拼接?modelxxx而是把 model 写在了请求体里后端没取到。解决强制约定所有模型切换必须通过 Query Param 传递并在 Controller 层加校验GetMapping(/stream) public SseEmitter stream(RequestParam String model) { if (!Arrays.asList(deepseek-chat, gemini-pro, siliconflow-llama).contains(model)) { throw new IllegalArgumentException(不支持的模型: model); } // ... }5.3 现象中文回复出现乱码如“你好”变成“浣”但英文正常原因RestTemplate默认使用ISO-8859-1解析响应而所有 AI 模型返回的都是 UTF-8。解决全局配置RestTemplate的StringHttpMessageConverterBean public RestTemplate restTemplate(RestTemplateBuilder builder) { var converter new StringHttpMessageConverter(StandardCharsets.UTF_8); return builder .additionalMessageConverters(converter) .build(); }5.4 现象连续快速发送多条消息后端抛IllegalStateException: Failed to send an event to the client原因SseEmitter不是线程安全的多个线程同时调用emitter.send()会冲突。解决用synchronized包裹发送逻辑或改用ConcurrentLinkedQueue缓存待发送消息由单一线程消费private final QueueSseEmitter.SseEvent pendingEvents new ConcurrentLinkedQueue(); // 在 provider 中收集事件 pendingEvents.add(SseEmitter.event().data(...)); // 启动一个守护线程定时发送 scheduledExecutorService.scheduleAtFixedRate(() - { while (!pendingEvents.isEmpty()) { try { emitter.send(pendingEvents.poll()); } catch (IOException e) { log.error(Send failed, e); break; } } }, 0, 10, TimeUnit.MILLISECONDS);5.5 现象部署到 Linux 服务器后Gemini 调用始终返回 403 Forbidden原因System.getenv(GEMINI_API_KEY)在 systemd 服务中无法读取环境变量因为环境变量未被继承。解决改用 Spring Boot 配置文件注入并确保application-prod.yml中明文配置毕设演示可接受# application-prod.yml ai: gemini: api-key: your-real-key-here然后在 Provider 中用Value(${ai.gemini.api-key})注入而非System.getenv()。6. 毕设答辩加分项用本地 Mock Server 实现零依赖演示闭环答辩现场最怕什么网络抽风、API Key 过期、厂商服务维护。我给自己留的最后一条“后悔药”是在src/test/resources下内置了一个Jetty 启动的 Mock Server它不依赖任何外部服务能完全模拟三个模型的响应行为包括流式 chunk、错误码、限流返回。这样答辩时哪怕断网也能演示“切换模型”“流式输出”“错误兜底”三大核心能力。6.1 用 Jetty 内嵌服务器启动 Mock APIMaven 引入jetty-server和jetty-servlet写一个极简启动类// src/test/java/com/example/mock/MockServer.java public class MockServer { public static void main(String[] args) throws Exception { Server server new Server(8081); ServletContextHandler context new ServletContextHandler(); context.setContextPath(/); server.setHandler(context); // 模拟 DeepSeek 同步接口 context.addServlet(new ServletHolder(new AbstractHttpServlet() { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(application/json); resp.getWriter().println( {id:chat-123,choices:[{message:{content:我是 DeepSeek一个开源大模型。}}]} ); } }), /v1/chat/completions); // 模拟 Gemini 流式接口JSONL 格式 context.addServlet(new ServletHolder(new AbstractHttpServlet() { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(text/event-stream); PrintWriter w resp.getWriter(); w.println(data: {\candidates\:[{\content\:{\parts\:[{\text\:\你好\}]}}]}\n); w.println(data: {\candidates\:[{\content\:{\parts\:[{\text\:\我是 Gemini。\}]}}]}\n); w.println(data: [DONE]\n); w.flush(); } }), /v1beta/models/gemini-pro:streamGenerateContent); server.start(); System.out.println(Mock server started at http://localhost:8081); } }6.2 配置 Spring Boot 切换 Mock 模式在application-mock.yml中重写所有模型 URL# application-mock.yml ai: deepseek: base-url: http://localhost:8081 gemini: base-url: http://localhost:8081 siliconflow: base-url: http://localhost:8081启动命令改为java -jar app.jar --spring.profiles.activemock6.3 答辩现场操作清单3 分钟搞定步骤操作预期效果1终端执行mvn test-compile exec:java -Dexec.mainClasscom.example.mock.MockServer启动 Mock Server控制台输出Mock server started...2新终端执行java -jar target/ai-chat-0.0.1-SNAPSHOT.jar --spring.profiles.activemock启动后端日志显示Started Application in X seconds3浏览器打开http://localhost:8080选择任意模型发送消息页面流畅显示流式响应Network 面板可见请求发往localhost:8081这个方案的价值在于它把“能否演示”从概率事件依赖网络三方服务变成了确定性事件只依赖本机 JVM。我在某高校毕设答辩中用这套流程评委老师当场说“这个 Mock 思路很工程比单纯调 API 有说服力。”最后提醒一句毕设不是比谁用的模型最大而是比谁把“调用”这件事拆得最细、控得最稳、兜得最全。当你能在答辩时指着代码说“这里处理超时这里做降级这里防乱码这里保流式”你就已经赢了大多数只贴截图的同学。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

C#工业视觉实战:OpenCvSharp+YOLOv4实时检测与ONNX GPU加速

C#工业视觉实战:OpenCvSharp+YOLOv4实时检测与ONNX GPU加速

简介:本资源是一套基于C#与OpenCvSharp实现YOLOv4目标检测的完整工程,面向具备基础C#和计算机视觉知识的开发者,解决视频流与USB摄像头实时检测落地难的问题。项目已封装为可直接运行的Windows桌面应用,支持CPU/GPU双模式推理&…

📅 2026/10/19 0:19:57
Agent对话迁移协议:语义保真驱动的跨系统互通革命

Agent对话迁移协议:语义保真驱动的跨系统互通革命

1. 这不是“跨平台登录”,而是Agent生态的底层通信协议革命“太狠了!全网 Agent 对话终于能互相搬家了”——这句话刚刷出来时,我正调试一个客户定制的客服Agent系统,手边还开着三个不同厂商的开发文档。第一反应不是兴奋&#xf…

📅 2026/10/19 0:19:57
Spring AI Alibaba 架构拆解:从 Spring AI 到生产级大模型应用

Spring AI Alibaba 架构拆解:从 Spring AI 到生产级大模型应用

1. 从一次技术选型的纠结说起去年底我在做一个企业级智能问答系统的技术预研,需求很明确:后端是 Spring Boot 体系,要接入大模型能力,同时未来可能切换不同的模型供应商,还要支持 RAG、工具调用、多轮对话这些常见能力…

📅 2026/10/19 0:19:57
MORE NEWS

更多资讯

📰

VSCode + ollama + DeepSeek 低成本代码编辑器:把 Continue 的 Base URL 改到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

从 Harness Engineering 到 Trellis:用 TaoToken 统一 Key 打通 AI 编程助手工程化落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Flowable7.x学习笔记(六)Vue3+SpringBoot3实现流程定义保存功能:TaoToken统一Key打通前后端联调

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

豆包读取电商客服聊天窗口:本机客户端走本地端口即可完成会话监听

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

【Mac】vscode 配置 GD32E230CXXX 开发环境:从工具链到烧录的完整落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Java+Swing+MySQL超市商品管理系统:JDBC到JTable完整实战

简介:基于JavaSwingMySQL开发的超市商品管理系统完整源码包,面向Java初学者、数据库课程设计及毕业设计场景。系统围绕商品、库存、供应商三大核心表展开,通过Swing界面完成商品添加、查询、修改、删除等CRUD操作,并结合MySQL实现…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬