ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+DeepSeek+Vue3构建AI智慧健康管理系统实战

SpringBoot+DeepSeek+Vue3构建AI智慧健康管理系统实战 做这个项目之前我已经被纯“数据记录型”健康管理App折磨过很多轮那种“每天提醒你喝水、晚上问你睡得好不好、最后给你一张看不出门道的折线图”的产品套路用户很难在这里面得到真正的帮助。恰好DeepSeek系列大模型开放了兼容性很好的API接口我就动了把手以SpringBoot做后端底座Vue3搭前端界面通过大模型把“数据记录”升级成“有问有答、有分析有建议”的AI智慧健康管理系统。这还是一套非常标准的前后端分离架构前端Vue只负责交互与展示后端SpringBoot统一管理鉴权、业务、数据和大模型调用最终还能用一条部署脚本把整个服务推到远程服务器上跑起来。这篇文章就把这套系统从设计、编码到全套部署的完整过程拆给你尤其适合正在用Java后端做AI应用、想低成本接入大模型能力的开发者。你要的DeepSeek API调用方式、Vue流式对话渲染、SSE推送、jar包打包、Nginx反向代理后面全部有实操记录。1. 整体架构拆解为什么值得用这套组合1.1 前后端分离到底在拆什么很多人一说“前后端分离”就默认是把页面和接口拆开部署这个理解没错但拆完之后要多解决三个问题跨域怎么处理、鉴权信息怎么传、部署时怎么让两个服务协同工作。这套系统里前端跑在Vite开发的Nginx静态服务上后端是SpringBoot的独立jar包两边唯一联系就是HTTP接口。这么做最直接的好处是“各自能独立演进”。Vue端可以随时换UI框架、增加新的图表组件不影响到后端的接口逻辑后端增加一个分析引擎只要接口契约不变前端也不用跟着改。对AI类型应用来说这一点比传统单体应用重要得多因为大模型的能力边界一直在变后端要频繁调prompt、换模型版本、加流式接口前端不能每次都被迫发布。另一层考虑是安全。DeepSeek的API密钥绝不能出现在前端代码里浏览器打包出来的JS谁都能看密钥一旦混进去就等于公开了。前后端分离之后密钥只保存在SpringBoot的配置文件和服务器环境变量中前端所有对话请求都走后端转发相当于在用户和大模型之间加了一道自己的网关。1.2 三个核心部件的职责边界SpringBoot在这套系统里不只做一个CRUD后端它同时承担四件事用户体系与健康档案管理、数据库读写、对DeepSeek接口的封装与转发、以及报告生成等业务逻辑。Vue端则专心解决“用户感知”的问题健康档案表单、对话聊天窗口、流式打字机效果、历史记录列表、身体指标可视化图表。它不关心大模型是怎么被调用的只负责把用户的提问送出去再把后端返回的文字一段接一段显示出来。DeepSeek大模型在这里扮演的是“智慧大脑”的决策角色。系统里所有需要理解和生成的内容比如“根据我的睡眠和饮食情况给出下周调整建议”“帮我把这份体检指标解读成人话”都由它来完成。我这里用的主要是对话补全接口它兼容OpenAI的调用格式所以SpringBoot集成起来非常顺手只换baseUrl和模型名称就能用。1.3 功能模块与用户流程完整的功能链可以概括成用户在Vue页面输入健康数据或直接打字提问前端通过axios把数据提交给SpringBoot接口后端把用户信息、历史上下文组装好再发给DeepSeekDeepSeek返回结果后SpringBoot把结果写入数据库同时把内容推给前端展示。系统里我划分了四个大模块健康档案管理、AI智能问诊助手健康咨询对话、健康趋势分析、报告导出。其中AI智能问诊是核心用户可以直接问“我最近压力大、总失眠应该怎么调整”也可以基于档案数据问“我的BMI是27结合我每天坐办公室的情况帮我定个运动计划”。这种自然语言交互方式把原来只能靠表单选项触达的功能全部解放了。2. SpringBoot接入DeepSeek的完整实现2.1 配置与密钥管理实操DeepSeek的API风格我直接沿用了最稳定的chat/completions方式SpringBoot里通过RestTemplate发起POST请求就能调通。前提是先把配置独立出来不要写死在业务代码里。在application.yml里我这样拆配置deepseek: api-key: ${DEEPSEEK_API_KEY:} base-url: https://api.deepseek.com model: deepseek-chat max-tokens: 2048 temperature: 0.7密钥从环境变量DEEPSEEK_API_KEY读取本地开发时把密钥配到IDEA的环境变量里打包时完全不落盘。之前我见过太多人把API密钥写在application.yml里然后传到Git仓库等着电脑上的所有脚本去爬它这事真不能干。SpringBoot里用ConfigurationProperties做一个配置类再封装一个Client组件后面不管换模型还是换baseUrl只改配置不动代码。Component ConfigurationProperties(prefix deepseek) public class DeepSeekProperties { private String apiKey; private String baseUrl https://api.deepseek.com; private String model deepseek-chat; // getter/setter 省略 }2.2 请求体封装与接口调用一个请求包含三件事模型名称、消息列表、生成参数。消息列表里又分system、user、assistant三种角色system用来设定行为约束user是用户输入assistant是历史回答。在SpringBoot里我定义两个DTOChatMessage和ChatCompletionRequest结构尽量保持和接口对齐不要为了“漂亮”做过度字段映射。public ChatCompletionResponse chatCompletion(ListChatMessage messages) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(props.getApiKey()); MapString, Object body new HashMap(); body.put(model, props.getModel()); body.put(messages, messages); body.put(temperature, props.getTemperature()); body.put(stream, false); HttpEntityMapString, Object entity new HttpEntity(body, headers); ResponseEntityDeepSeekResponse resp restTemplate.exchange( props.getBaseUrl() /chat/completions, HttpMethod.POST, entity, DeepSeekResponse.class ); return resp.getBody(); }这里有个容易被忽略的细节RestTemplate默认对超时时间不敏感慢接口能挂住线程很久。我后来显式配了连接超时和读取超时连接设5秒、读取设60秒等模型生成长文本的时候不会把连接池占满。2.3 流式响应怎么做更顺滑非流式方案最大的问题是首字延迟太高用户点完发送盯着转圈圈等好几秒才看到整段话直觉就是“卡住了”。所以我把健康咨询接口改成了SSE流式推送也就是服务端把DeepSeek返回的内容切成小片段通过SSE通道持续推给浏览器。SpringBoot里用SseEmitter实现最轻量。后端开启独立线程去调DeepSeek的流式接口接口每返回一小段就通过emitter.send推到前端。设置超时时间60秒结束后主动调用emitter.complete。GetMapping(value /api/chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(RequestParam String question, RequestParam Long userId) { SseEmitter emitter new SseEmitter(60_000L); healthChatService.streamAnswer(question, userId, emitter); return emitter; }推给SSE的格式如果用自定义事件需要包一层JSON简单场景就直接用默认message事件数据体保持纯文本。但要注意DeepSeek流式接口返回的是“data: {json}\n\n”这种格式后端不能把原始流直接透传要把JSON字段里的content单独解析出来再推送否则前端拿到的全是JSON片段体验直接崩。2.4 后端封装超时、重试、限流与内容边界调大模型和调普通数据库完全不一样模型接口可能会因为服务繁忙返回429或500响应时间波动也很大。我给这块代码加了三层防护重试机制、线程隔离、内容边界。重试只对“可重试”的异常生效比如网络抖动、HTTP 5xx连续重试最多3次中间隔1秒。业务参数类错误400、401这种直接抛给前端不要浪费重试次数。如果用RestTemplate重试要小心同一请求体能不能安全复用流式请求每次都重新构造Request避免把用过的流再发一遍。线程隔离就是用线程池跑调用大模型的任务Tomcat的线程不能一直阻塞等模型输出。我建了一个核心线程数10的ThreadPoolTaskExecutor专门处理大模型IO这样就算DeepSeek响应慢后端处理登录、查询等普通请求也不受影响。内容边界必须要在system消息里提前框定。健康领域最怕胡说八道我在system提示词里明确要求不做疾病诊断、不谈处方药、不承诺疗效、对紧急症状输出必须建议就医。这个不是道德宣讲是产品底线。用户咨询时会拿着这条约束去过滤生成内容SpringBoot后端在把用户输入发出去之前也做一遍简单的关键词校验防止敏感文本被拼进prompt。3. Vue3前端搭建与AI对话交互实现3.1 前端环境配置与联调代理Vue前端我直接用VitVue3脚手架初始化npm install的时候容易报版本冲突锁定依赖版本很关键。这个项目里用Vue3.4.x、Vite5.x、axios1.6.x一组稳定搭配不用天天追新版本。开发阶段的跨域是最大拦路虎。vite默认跑在5173端口SpringBoot接口跑在8080如果前端直接请求http://localhost:8080/api/xxx浏览器会触发CORS预检麻烦不说cookie和认证头还得特殊处理。我在vite.config.js里配置了代理让前端“欺骗”自己接口请求全部走相对路径/api由Vite开发服务器转发到8080。server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境则用Nginx做同样的事情把/api前缀的请求反向代理到SpringBoot的jar端口。这样前后端代码里都只写相对路径省掉了一整类环境切换的麻烦。3.2 对话流式渲染打字机效果是怎么来的对话页面是这个系统交互的核心。用户点击发送后前端不等待完整回复而是通过fetch发起SSE请求用异步迭代器逐段读取后端推送的数据。async function sendMessage() { const resp await fetch(/api/chat/stream?question${encodeURIComponent(text)}userId1); const reader resp.body.getReader(); const decoder new TextDecoder(); let buffer ; let answer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const chunks buffer.split(\n\n); buffer chunks.pop(); for (const chunk of chunks) { const json parseSSEChunk(chunk); if (json?.choices?.[0]?.delta?.content) { answer json.choices[0].delta.content; currentMessage.content answer; } } } }这里有个血泪教训SSE数据包可能被TCP拆分可能多个事件一起到达也可能一个JSON被切成两半。必须维护一个buffer按“\n\n”切分事件之后再把残留的半截留到下一次循环继续处理。收到内容后不要直接替换整段DOMVue3的响应式array会自动更新列表配合CSS的white-space: pre-wrap就能保持换行打字机效果就这么实现了不需要任何第三方库。3.3 健康档案与趋势页展示健康档案页包含基础信息、身体指标、生活方式三个分组。身体指标里身高、体重、BMI、血压、心率是固定字段生活方式里睡眠时长、运动频率、饮食偏好是选择题自由文本的组合。趋势页我用ECharts展示体重和睡眠趋势图表数据从SpringBoot接口拿到后前端做简单的数据清洗。DeepSeek在其中的作用体现为“解读”而不是“画图”后端把趋势数据丢给模型模型生成一段人性化的文字总结比如“过去两周你的入睡时间推迟了1小时左右深睡时长却在提高说明虽然睡得晚但睡眠质量并不差建议保持规律作息节奏”。这种图文配合比单纯一条折线给用户的感知强太多了。4. 业务逻辑落地上下文、存储与文档导出4.1 多轮对话上下文组装大模型是无状态的每个请求都是独立推理想让AI记住“你刚才说的失眠问题”就必须把历史消息作为上下文一起传过去。但上下文不是越全越好Token数量直接关联接口费用和响应速度。我设计了一个滑动窗口策略保存最近两轮完整问答再加上当前问题如果这些内容超过4000个Token就优先丢弃最早的消息。实现上比较简单从数据库查历史消息时按create_time倒序取最近N条然后再反转成时间正序拼接到messages里。ListChatMessage buildContext(Long userId, String question) { ListChatMessage history chatHistoryMapper.selectLatest(userId, 6); ListChatMessage messages new ArrayList(); messages.add(ChatMessage.system(SYSTEM_PROMPT)); messages.addAll(history); // 已包含 user/assistant 角色 messages.add(ChatMessage.user(question)); return messages; }system提示词维护在配置中心或单独常量类里不要散落在业务代码各处。大模型效果不好时优先调它而不是改代码。4.2 数据库设计与一致性处理整个系统只有六张核心表user、health_profile、health_metric、chat_session、chat_message、health_report。用户信息与健康档案是一对一健康档案与指标是一对多会话与消息是一对多。写数据库时最容易出问题的点是“先调大模型再写数据库”的顺序。如果先写库再调模型模型调用失败用户看到提问没回复但库里多了一条没人回答的残记录如果先调模型再写库回复超时但库已经拿来用了用户反复点击发送会重复扣费。我的方案是提问先落库标记为已提交调用模型成功后再补齐回复用异步任务更新状态。万一失败前端可以展示“生成失败点击重试”重试时用原问题重新调用不产生重复数据。为防接口重入问题前端发送按钮做了防抖后端根据session_id和消息序号做唯一约束。这两个配合下来才能保证AI场景下的数据一致性和不重复扣费。4.3 生成健康报告并导出Word报告导出模块是大模型能力的另一个出口。后端把某个时间段的指标数据、用户基本信息、对话摘要组装成结构化文本让DeepSeek生成一份结论、建议、风险提示分开的健康报告再通过Java的Word模板技术渲染成文件下载。普通的小工具做Word表格很痛苦我把报告内容封装成自定义对象然后用一个只保留了少量占位符的docx模板配合POI输出。这里说的生成图表我还没完全自动化目前是靠着模型生成文字总结配合一个基础的身体指标表格后续我会把ECharts生成的图片直接嵌入到docx里做到真正的图文报告。整个流程实测下来也就1-2秒用户的感知是“点击导出报告立刻生成一份文档”这个体验比纯网页截图好很多。5. 部署体系从jar到一键远程部署5.1 Maven多环境打包与版本控制部署第一步是打出可运行的jar包。需要注意SpringBoot版本不能随意升级有一个很常见的坑Spring Boot 3.x要求Java 17如果你服务器装的是Java 8直接over。我这次服务器是Java 17所以pom里锁定Spring Boot 3.2.xMaven打包时用profile区分测试环境和生产环境。mvn clean package -DskipTests -P prod生产环境打包完拿到target/health-ai.jar这个包里已经包含了内置Tomcat只需要java -jar就能启动。前端则是npm run build生成dist目录里面是纯静态文件。5.2 systemd守护进程与Nginx反向代理后端jar在服务器上不能裸跑断开SSH进程就没了。我写了一个systemd服务文件让Linux把它当成系统服务管理开机自启、崩溃自动重启。[Unit] DescriptionHealth AI Backend Afternetwork.target [Service] Userroot WorkingDirectory/opt/health EnvironmentFile/opt/health/env ExecStart/usr/bin/java -jar /opt/health/health-ai.jar Restartalways RestartSec5 [Install] WantedBymulti-user.targetEnvironmentFile用于读取数据库密码、DeepSeek API密钥等环境变量这些内容不进代码仓库。Nginx则负责托管前端dist目录同时把/api开头的请求转发到localhost:8080。server { listen 80; server_name your-domain.com; root /opt/health/frontend; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }Nginx这一段里proxy_pass后面如果带了路径转发规则容易出偏差我建议只写到端口为止让后端自己去解析完整路径省得踩前缀丢失的坑。SSE接口在Nginx下还需要把缓冲关掉否则流式内容会被Nginx攒起来一次性吐给前端打字机效果就废了。5.3 一键远程部署脚本的编写思路所谓“一键部署”本质上是把本地的打包、上传、重启三条命令固化成一个脚本。我写的deploy.sh接受服务器IP或SSH别名为参数maven打包成功后用scp把jar传到服务器再通过ssh执行systemctl restart。#!/bin/bash set -e SERVERrootyour-server-ip echo 1/4 后端打包 mvn clean package -DskipTests -P prod echo 2/4 上传jar包 scp target/health-ai.jar $SERVER:/opt/health/ echo 3/4 重启后端服务 ssh $SERVER systemctl restart health-ai echo 4/4 前端构建与同步 cd frontend npm run build ssh $SERVER rm -rf /opt/health/frontend/* scp -r dist/* $SERVER:/opt/health/frontend/这个脚本已经跑过了很多次但有一个建议生产环境用rsync而不是scp传前端静态文件只同步有变化的文件几百个静态资源一次scp要好几秒rsync秒级完成。我这里为了依赖最小化用了scp你公司有rsync环境的话直接换掉。5.4 数据库初始化与迁移注意事项后端首次启动时用Spring Boot的Flyway机制管理数据库结构变更。所有建表SQL放在classpath:db/migration目录下启动时自动检查版本并执行增量迁移避免了“服务器上忘记建表导致反复500”这种低级失误。一键部署脚本里不要自动执行Flyway重跑旧SQLFlyway是靠版本号递增控制重复执行相同版本会直接报错。我遇到过几次因为手改了已执行的迁移脚本导致后面所有启动都卡在校验失败上最后手动改了flyway_schema_history表才恢复。教训就是已经提交的迁移脚本只允许增加、不允许修改。6. 高频问题排查与避坑记录6.1 前端依赖安装与控制台报错npm install这个阶段就能卡掉三分之一的人。常见的报错是vite相关依赖的版本冲突尤其是esbuild安装时平台二进制下载失败。我有一台Windows机器装不上后来清掉node_modules和package-lock.json重装才正常。你可以换pnpm或者cnpm核心原则是保持node版本稳定我用的Node 18 LTS配Vite5没有任何问题。还有一个Vue新手高频问题路由刷新页面就404。这是因为前端是SPA路由由JS控制Nginx没有把未知路径回退到index.html。我只配置了location /的try_files就解决了如果你用history路由这个必须配。6.2 后端版本与依赖冲突“SpringBoot版本太高”这个问题我被问了很多次。高版本适合新项目但很多老库还在用javax命名空间Spring Boot 3.x已经整体迁移到jakarta命名空间直接引入老版本MyBatis或连接池就会启动失败。如果你从Spring Boot 2.x项目升级不要只改版本号要把所有javax.的import换成jakarta.并且确认使用的starter有没有对应的3.x版本。这次项目里我最稳的组合是Spring Boot 3.2.5 MyBatis Plus 3.5.5 MySQL 8这几个配套都是发版比较久的版本API稳定文档也多。为了一个“最新版”去踩坑在AI应用开发里实在不值得。6.3 跨域与代理疑难杂症有时候前端代理配置没问题但请求还是405或403。排查步骤我建议按顺序来先看浏览器Network面板里请求落到了哪个URL如果请求直接打到了8080说明代理没生效再看后端日志有没有收到请求如果收到但返回403说明CORS配置拦截了预检请求。开发环境最省事的方式不是在后端写CORS过滤器而是用Vite代理把同源问题消灭在萌芽状态。后端只需要处理生产环境Nginx转发后可能出现的X-Forwarded-For头以及SSE场景下禁用Nginx缓冲。如果你非要在后端开CORS记住allowCredentials(true)的时候allowedOrigins不能写*要写具体域名。6.4 AI接口异常与数据问题DeepSeek接口时延高是常态不是偶发。用户点击发送后3秒没反应一定要端到端排查前端有没有收到SSE连接建立事件、后端线程池是否被占满、DeepSeek接口自身的排队时间是多少。我之前在一次压测后把线程池调小了结果用户一多所有AI请求排队几十秒前端全在转圈。后来线程池和连接池都放大并加了异步队列才缓解。对话内容出现重复回复时检查上下文是否传重复了前端发送时把错误消息也存了库导致下一次请求把“正在生成中的回复”也带进上下文模型就会把同一段话再生成一遍。解决办法很纯粹只有状态为“完成”的消息才允许进入上下文这个过滤条件写死在SQL里不让模型端背锅。写在最后的实践建议如果重新做一遍这个项目我会把更多的精力放在“提示词工程”和“评估集”上而不是只接API调通就完事。同一个健康问题换个问法DeepSeek生成的回答质量能差出一大截想要稳定输出必须先把system提示词打磨到位再准备好20条典型用户问题作为回归测试集。部署脚本我再建议加一个回滚逻辑备份旧jar新版本启动后自动做健康检查发现接口连续失败就自动切回旧包。这套系统的数据安全边界也得提前想清楚用户基因检测、体检报告这类属于高度敏感数据健康领域一旦出现数据泄露后果不只是口碑问题。不要让健康信息流经任何不该存的地方。这套SpringBoot DeepSeek Vue的项目走到这里已经是完整的AI应用落地闭环。你顺着这篇文档把代码跑起来就是一套能对话、能分析、能导出报告、能一键上线的智慧健康服务。剩下的功能方向比如接入更多体征监测设备、增加语音交互、做多租户健康档案都留好扩展位了。
返回列表