做了几次“某某实时监控系统”之后你会发现这类项目看起来是个数据展示大屏拆开看其实是 Java 全栈里最典型的练习题前后端分离、接口设计、数据缓存、可视化图表、权限管理、打包部署一套流程全走通技术含金量并不比花哨的增删改查低。这篇文章我就拿“基于 Spring Boot 和 Vue 的新冠肺炎疫情实时监控系统”为例把从需求拆解到前端大屏、再到服务器部署的完整过程写出来。适合正在准备毕业设计的人、想入门 Spring Boot Vue 全栈开发的人以及想把这种“监控看板”改造成其他业务数据中台的人参考。官方统计渠道的公开数据可以拉但我不想把篇幅浪费在爬虫和合规讨论上重点只讲技术实现。1. 内容整体设计与思路拆解做这类系统最忌讳一上来就写代码。先想清楚做给谁看、有哪些模块、将来怎么扩展后面写起来会顺手很多。1.1 为什么选 Spring Boot Vue 这个组合先说后端。Spring Boot 在 Java 生态里几乎已经成了工程项目的默认起点内嵌 Tomcat、自动配置、依赖管理一步到位你不需要像 SSM 时代那样写一堆 XML 配置。对于疫情监控这种以数据查询和展示为主的系统来说Spring Boot 的 REST API 开发能力非常轻量一个 RestController 就能把数据抛给前端。前端选 Vue 的理由更直接它是渐进式框架模板语法简单组件化开发让“数据看板”这种多卡片多图表页面变得容易拆分和维护。配合 Element Plus 做后台管理界面ECharts 做图表整个前端的实现周期可以压缩得非常短。这个组合还有一个实际好处社区资料极多踩坑很容易搜到答案。无论是刚接触框架还是已经工作几年的开发者遇到问题基本都能在现有帖子里找到现成解法项目进度不容易卡住。1.2 功能模块怎么划分疫情监控系统的业务不复杂但天然分成“给普通用户看”和“给管理员维护”两个端前后端分离之后需要对功能边界有明确切分。公众端核心功能全国疫情数据看板展示累计确诊、现存确诊、治愈、死亡、境外输入等核心指标。趋势变化图表用折线图展示一段时间内全国新增确诊、新增治愈的变化曲线。地区分布地图用中国地图展示各省累计确诊数颜色深浅表示严重程度。疫情资讯列表展示官方发布的新闻、通告、防疫政策支持分页。数据更新时间提示让用户知道当前数据是什么时候刷新的。后台管理端核心功能管理员登录认证。疫情核心指标的每日数据维护。资讯的分类发布、编辑、删除。用户账号管理。模块化划分对前后端协作很重要。前端可以照着模块去建页面组件后端的 Controller 也可以按模块拆类不至于所有接口堆在一个类里。我见过不少毕设项目把新闻、统计数据、用户管理的接口全写在同一个 Controller 里工程一旦扩大就非常痛苦。1.3 几个关键技术选型的取舍首先数据库用 MySQL这个基本没有争议数据量很小不需要上 PostgreSQL 或者 Oracle。ORM 框架我推荐 MyBatis Plus而不是原生 MyBatis 或者 JPA。原因很简单它的单表 CRUD 和分页插件能省掉非常多的重复代码BaseMapper 里已经内置了 insert、updateById、selectPage 这些方法疫情数据维护这类简单业务几乎不用写 XML。如果项目里需要联表查询或者复杂统计MyBatis Plus 的 Wrapper 也够用完全不阻塞开发。缓存用 Redis 的原因是两个疫情数据是低频更新的热点数据同一个数据半小时内被请求成百上千次每次都查数据库明显浪费定时任务拉取数据时也需要一个临时存储作为中间态。如果你只是想先跑通功能Redis 不是硬性要求但生产环境下做一个监控系统缓存是必不可少的一层。实时推送用 WebSocket。数据看板如果做成前端轮询每 5 秒请求一次接口浪费资源而且不够“实时”。用 WebSocket 让服务端主动把最新数据推给所有在线的客户端才是监控类系统的常规做法。系统本身的监控用 Spring Boot Admin这个很多人容易忽略。你在做一个“监控系统”那谁来监控系统本身Spring Boot Admin 可以提供健康检查、CPU 内存指标、日志级别动态调整等能力部署到服务器上之后非常实用。2. 核心细节解析与实操要点框架选型定了后面就是每一个环节的具体实现。这个章节我把后端的数据链路、数据库设计、接口设计、缓存推送和系统自监控全部展开每一块都有我实际做过的细节。2.1 疫情数据来源与定时任务设计疫情监控系统的前提是“数据从哪里来”。正规做法是从官方公布的公开渠道获取我当时的做法是通过定时任务定时拉取公开的健康码、行程卡之外的数据源。这里不讨论如何爬取网页更推荐直接对接一些数据服务商提供的公开 JSON 接口优点是数据稳定且字段已经处理过省去解析 HTML 的麻烦。但是“实时”不等于“不停地拉”。数据源接口本身有访问频率限制我实测下来比较好的策略是每个整点和半点拉取一次通过 Scheduled 注解就能实现。Component public class DataSyncTask { Autowired private EpidemicDataService epidemicDataService; // 每 30 分钟执行一次 Scheduled(cron 0 0/30 * * * ?) public void syncData() { try { // 从第三方接口拉取全国及各省数据 String response restTemplate.getForObject(dataUrl, String.class); // 解析 JSON写入数据库并进行汇总更新 epidemicDataService.syncEpidemicData(response); // 更新 Redis 缓存让前端能够立刻感知到新数据 redisTemplate.delete(epidemic:overview); } catch (Exception e) { // 数据源异常时保留旧数据并记录错误日志 log.error(数据同步失败使用上一次数据, e); } } }很多人在这一步会把逻辑做得很啰嗦比如把同步任务做成一个复杂的调度系统。实际完全不需要Scheduled 手动兜底就够了。关键点在于数据源异常时的处理。曾经有一次第三方接口超时我一开始没做异常兜底结果前端大屏直接显示了昨天的旧数据用户第一时间就发现了。后来我在 syncData 方法里做了 try-catch数据同步失败就保留数据库里最新的一批数据同时把错误信息写入日志配合告警。这样即使第三方接口挂了一天页面数据也不会“断开”。2.2 数据库设计和实体类规划数据库表设计决定了后端的开发效率我按业务模块拆成了三张核心表第一张是疫情统计数据表。字段包含日期、省份、累计确诊、现存确诊、治愈数、死亡数、新增确诊、新增治愈等。日期加上省份必须做成联合唯一索引这样同一行数据不会重复插入。第二张是疫情资讯表。字段包含标题、摘要、正文、来源、发布时间、分类。这个表不需要复杂设计主键自增就可以了。第三张是后台用户表。字段包含用户名、密码、角色、状态。密码一定不要存明文用 BCrypt 加密后入库。建表语句核心示例如下CREATE TABLE epidemic_daily ( id bigint NOT NULL AUTO_INCREMENT, stat_date date NOT NULL COMMENT 统计日期, province varchar(50) NOT NULL COMMENT 省份, confirmed int DEFAULT 0 COMMENT 累计确诊, existing int DEFAULT 0 COMMENT 现存确诊, cured int DEFAULT 0 COMMENT 治愈, dead int DEFAULT 0 COMMENT 死亡, new_confirmed int DEFAULT 0 COMMENT 新增确诊, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stat_date_province (stat_date, province) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表结构不要设计得太花哨但是 create_time 和 update_time 建议保留。后面接定时任务和 WebSocket 推送的时候你需要知道数据什么时候更新过这两个字段能省去你很多排查问题的时间。实体类的设计对应也比较直接。Java 端用 LocalDate 对应数据库的 date 类型用 LocalDateTime 对应 datetime 类型。2.3 后端接口设计与统一返回格式接口设计遵循 RESTful 风格并且所有接口返回统一的数据结构。这一点看起来简单但很多人一开始都忽略结果前端在解析数据时要针对每个接口单独做判空处理非常散乱。统一返回结构一般包括三个字段code 表示业务状态码msg 表示提示信息data 是真正的业务数据。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg success; result.data data; return result; } public static T ResultT error(Integer code, String msg) { ResultT result new Result(); result.code code; result.msg msg; return result; } }核心接口清单如下接口路径方法功能说明/api/statistics/overviewGET获取全国累计数据概览/api/statistics/trendGET获取近一个月新增确诊趋势/api/statistics/mapGET获取各省累计数据供地图渲染/api/news/listGET分页获取疫情资讯/api/admin/loginPOST管理员登录/api/admin/epidemic/savePOST后台维护每日数据/api/admin/news/savePOST后台发布资讯Controller 层只做参数接收和结果包装业务逻辑全部放到 Service 层。比如统计概览接口Controller 里一行调用 service如果 Redis 里有缓存就直接返回缓存没有才查数据库这正好承接前面缓存设计。一次实际请求的流程可以串起来前端调用 GET /api/statistics/overview后端先查 Rediskey 是 epidemic:overview如果命中直接返回如果 miss就从数据库汇总最新一条记录重新写入 Redis 并设置 30 分钟过期时间。这样同一时间段内大量用户同时访问大屏数据库压力基本上可以忽略。2.4 Redis 缓存与 WebSocket 实时推送缓存和推送是“实时监控”体验的关键分开说。Redis 缓存的设计不用太复杂核心注意两点第一给缓存 key 设置合理的过期时间新闻类数据可以高频刷新统计数据 30 分钟左右就可以第二定时任务更新完数据后要主动删除对应的缓存 key否则用户一直看到的还是旧数据。WebSocket 的引入则让前端不需要轮询就能拿到最新数据。Spring Boot 集成 WebSocket 的依赖很小dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency核心是一个 WebSocketConfigurer 配置类和一个业务 Handler。Handler 里 onOpen 记录会话onMessage 接收前端传的订阅参数业务定时任务执行完之后调用一个广播方法把最新的统计数据推送给所有在线的会话。这里有一个容易踩的问题WebSocket 连接会因为服务器 Nginx 代理超时而被断开尤其是当页面长时间不操作时。所以前端要写一个心跳机制每隔一段时间发送 Ping服务端收到后返回 Pong一旦连接断了就自动重连。没有这个机制看板页面第二天打开可能数据就不再刷新了。2.5 系统自身的监控Spring Boot Admin做监控系统的同时一定不要忘了监控系统本身。Spring Boot Admin 可以实时查看服务的内存、线程、HTTP 请求量我是在部署到服务器之后才加的加了之后排查问题方便很多。服务端是一个独立的 Spring Boot 应用引入 spring-boot-admin-starter-server主类加 EnableAdminServer 注解。客户端就是业务系统本身引入 spring-boot-admin-starter-client配置文件中指定服务端地址然后通过 HTTP 注册上去。spring: boot: admin: client: url: http://localhost:9000 instance: name: epidemic-monitor management: endpoints: web: exposure: include: *需要留意的是管理端端口别跟业务端口混在一起我用的是 9000 端口。部署后重启业务系统登录 Admin 管理页就能看到健康状态、JVM 内存、在线线程等信息。如果服务器配置比较低这些指标能在系统卡顿之前给你预警。3. 实操过程与核心环节实现这个部分从前后端初始化开始把大屏图表、后台管理、权限控制和 Mock 联调逐步讲清楚。这些步骤我都实际跑过难度不大但容易在细节上卡住。3.1 前端初始化与技术栈配置前端我选 Vue 3 Vite而不是 Vue 2 Vue CLI。如果你是第一次做项目建议直接上 Vue 3生态现在已经非常成熟组件库和插件都是围绕 Vue 3 来发展的。npm create vitelatest epidemic-frontend -- --template vue cd epidemic-frontend npm install npm install axios element-plus echarts vue-router piniaVue Router 使用路由懒加载来拆分页面大屏页面和后台页面不会一次性打包进同一个 JS 文件。Pinia 替代 Vuex 管理全局状态比如管理员登录后的用户信息。项目的目录结构大致如下src/api所有接口请求方法的封装按模块拆成 statistics.js、news.js、admin.js。src/router路由配置文件包含前端路由守卫。src/views页面组件大屏、后台、登录等都放这里。src/components图表组件、通用卡片等。src/storePinia 的 store 文件。src/utilsaxios 实例、权限指令等工具。结构清晰是前端工程化的第一件事。组件命名统一用驼峰文件命名统一用小写加横杠不要在同一个项目里混用大驼峰和小驼峰风格。3.2 Axios 封装与跨域处理axios 不能直接在组件里到处 new一定要统一封装。封装的核心功能有三个设置 baseURL、统一请求头、响应拦截器统一处理后端返回的 Result。import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { // 业务错误统一提示 return Promise.reject(new Error(res.msg || 请求失败)); } return res; }, error { // 网络错误或超时提示 return Promise.reject(error); } ); export default request;开发环境下的跨域问题由 Vite 的 proxy 解决不需要后端开启 CORS。在 vite.config.js 里配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这里要注意 Vite 的代理只在开发环境生效。前端 npm run build 之后dist 里的文件是纯静态资源不管它proxy 不再起作用请求会直接发到当前域名下的 /api。所以生产环境要么用 Nginx 反向代理到后端服务要么把前端产物放到后端服务的静态目录里让 Spring Boot 自己托管。打包环节后面专门讲。3.3 ECharts 大屏可视化核心实现疫情数据看板是项目的门面也是 ECharts 发挥主要价值的地方。我做了三个核心图表组件全国趋势折线图、各省累计柱状图、中国地图分布图。折线图组件的数据来自 /api/statistics/trend返回近 30 天的日期和新增数字直接配置 series 即可。地图分布这块要注意的是新版 ECharts 已经不再默认内置中国地图 GeoJSON需要单独引入 china.json并从echarts的registerMap中注册。这个环节很多人踩坑页面上一大片空白控制台也没有明显报错实际上就是地图文件没引入。import * as echarts from echarts; import china from /assets/china.json; echarts.registerMap(china, china); // 地图配置核心项 myChart.setOption({ tooltip: {}, visualMap: { min: 0, max: 100000, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4] } }, series: [{ type: map, map: china, roam: true, label: { show: true }, itemStyle: { areaColor: #f0f0f0 }, data: mapData }] });图表组件一定不要在 mounted 中直接调用 ECharts 的 setOption 就完了需要处理窗口大小变化时 resize 事件同时在组件销毁前移除事件监听否则切换到后台页面再回来时图表会错乱或者报内存相关的警告。3.4 后台管理的路由、权限与 Mock 联调后台管理部分涉及登录和权限控制。页面级权限通过 Vue Router 的前置守卫来实现未登录用户访问后台路由时强制跳转到登录页。这已经算是比较基础了。按钮级别的权限控制我用了一个自定义指令。后端登录接口返回管理员角色和权限列表后前端把权限标识存到 Pinia自定义指令在元素挂载前校验权限无权限的按钮直接移除。这个设计在模板里写起来非常干净button v-permissionadmin:news:delete删除/button再补充一个 Mock 联调的问题。热搜词里很多人问 Mock 是怎么体现的我实际项目中的做法是在前端还没等到后端接口时在 src/mock 目录放置静态 JSON 数据接口封装层直接返回这些 JSON让前端开发和数据可视化工作先行。后端接口完成后把原来的 import 静态数据改成 axios 请求真实接口即可。Mock 是开发阶段的联调工具不是正式接口的替代品上线之前一定要清干净。4. 常见问题与排查技巧实录这个部分整理的是我在实际操作中真正遇到并解决的问题。我把现象、原因和解决办法都写出来方便你在复现的时候快速对照。4.1 跨域请求被拦截前后端分离联调时最常见的错误就是 CORS。现象是浏览器控制台出现类似 “Access-Control-Allow-Origin” 的错误接口数据加载不出来。处理方法有两种开发环境用 Vite 代理上面已经写过如果坚持要让后端开跨域则要写一个配置类实现 WebMvcConfigurer。但是需要注意 Spring Boot 版本差异2.4 之前的版本用 addCorsMappings 可以直接配置2.6 之后因为引入新的跨域处理机制默认情况下允许跨域的源不能是 allowedOrigins 的通配符需要改成 allowedOriginPatterns否则配置了也不生效。4.2 前端拿到的时间格式不是想要的样子后端返回 LocalDateTime 时默认序列化结果是 ISO 格式的字符串前端直接在表格里显示会非常难读。解决办法是在配置文件中统一制定格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8需要注意的是 SimpleDateFormat 只能格式化 java.util.Date如果实体类使用的是 LocalDateTime则需要引入 jackson-datatype-jsr310 模块或者直接给字段添加 JsonFormat(pattern yyyy-MM-dd HH:mm:ss) 注解。否则你会发现配置文件设置了格式接口返回还是 ISO 格式。4.3 ECharts 地图显示空白这个问题我上面提到过但值得单独强调。现象图表区域空白没有省份名称也没有地图轮廓。原因几乎都是缺少 GeoJSON 数据并且没有 registerMap。解决方法是下载 china.json 放入 src/assets 目录在组件初始化时注册地图。还有一个小细节项目打包后 GeoJSON 文件路径可能出问题不要用相对路径统一用 import 引入打包时 Vite 会正确处理资源路径。4.4 WebSocket 连接不稳定部署到服务器之后发现看板页面经常出现数据“不更新”的情况刷新页面又好了。排查后发现是 Nginx 默认的 proxy_read_timeout 设为 60 秒而 WebSocket 是长连接超过 60 秒没有数据交互就被服务端断开了。解决方案双层处理前端在 onclose 事件里延迟执行重连逻辑心跳 Ping 设置为 30 秒一次Nginx 的代理配置里增加 Upgrade 和 Connection 头提升 proxy_read_timeout 到 300 秒。两边都做了之后连接就非常稳定了。4.5 第三方数据源接口延迟或失败定时任务去第三方接口拉数据时偶尔会遇到网络抖动或者对方接口升级导致同步失败。这个问题直接决定了监控大屏的可用性。我的处理是“三级兜底”本地有一份备份数据文件数据库里始终保留最新一次成功同步的数据同步失败时日志告警并保持旧数据不变。这样即使第三方接口挂了 24 小时页面上仍然展示最近一次有效数据只是更新时间不再变动。我做了一个简单的排查速查表方便你对照问题现象可能原因处理方法接口报跨域错误CORS 配置不对或没有走代理开发环境用 Vite proxy生产用 Nginx数据请求 404前端 baseURL 与后端 context-path 不一致统一上下文路径并检查代理路径大屏图表无数据后端接口返回空数组前端未做空值处理接口层做默认值前端判空渲染地图区域颜色不显示GeoJSON 未注册或数据格式不匹配registerMap 注册后检查 series.data 字段名WebSocket 自动断开代理超时或没有心跳前端心跳 Nginx 升级协议配置定时任务重复执行数据翻倍cron 表达式写错或应用部署了多个实例校验 cron多实例下加分布式锁LocalDateTime 格式错误未配置 Jackson 时间序列化yml 统一 date-format 或字段加 JsonFormat5. 部署与联调实战前端的开发和生产环境是不同的场景这一节把从本地联调到打包部署的完整流程写出来让你直接用同样的步骤复现。5.1 开发环境准备如果你用的是 IntelliJ IDEA 社区版有一点需要提前知道社区版免费但不带 Spring Initializr 集成面板。很多新手在这个地方直接卡住以为社区版不能做 Spring Boot 项目其实不是解决办法有两个。第一种是到 start.spring.io 网页上选择依赖并且生成项目压缩包然后下载在 IDEA 中直接打开解压后的目录完全没问题。第二种是直接建一个 Maven 项目在 pom.xml 里手动引入 spring-boot-starter-parent 和需要的依赖Maven 会自动拉取。我在社区版上用一个多模块项目做过完整开发体验上没有任何障碍。后端通常需要 JDK 8 或者 JDK 11、Maven 3.6、MySQL 5.7、Redis。注意 pom.xml 里 Spring Boot 的版本一般用 2.6.x 或 2.7.x别追求最新2.x 系列的社区资料和兼容性稳定如果你选择 Spring Boot 3JDK 必须换到 17依赖可能有些 API 变化尤其是 javax 改成 jakarta 这一块新手容易踩坑。5.2 前后端联调的关键注意点联调阶段最常见的问题是字段名不一致。前端定义的是newConfirmed后端返回的是new_confirmed页面就会显示 undefined。最佳实践是后端统一使用 camelCase 风格输出数据库表字段才用 snake_case实体类上加 MyBatis Plus 的驼峰映射配置即可自动转换。联调时建议打开浏览器开发者工具的 Network 面板直接查看接口请求和返回。Vue 项目的网络请求若被代理显示的是/api/statistics/overview你可以在面板里看到代理后真正请求的地址这样能快速判断是前端路径错了还是后端接口报错了。5.3 两种部署方式合并打包与前后端分离部署部署方式有两种我都使用过。第一种方式把前端打包后的 dist 目录复制到后端项目的src/main/resources/static目录里然后直接使用 Maven 打包成一个可执行的 jar 包启动后通过同一个端口访问页面和接口。这种方式的优点是部署简单只需要跑一个 Java 进程不需要额外安装 Nginx。缺点是前后端代码物理上耦在一起以后前端更新要重新打整个 jar 包。它非常适合毕业设计演示和内部测试场景。第二种方式前后端分离部署。前端静态文件交给 Nginx 托管后端只是一个纯 API 服务。Nginx 负责拦截/api前缀的请求并反向代理到后端的 8080 端口静态页面直接由 Nginx 直接返回。这种方式更接近实际生产环境但配置成本高一些。如果你选了 Spring Boot 3要注意 Java 17 版本的 server 部署Tomcat 内嵌版本和 javax 到 jakarta 的包名变化。不过日常演示场景Spring Boot 2.x 仍然是部署最稳妥的选择。5.4 服务器部署流程参考部署到 Linux 服务器的操作流程大致如下在服务器安装 JDK、MySQL、Redis修改 MySQL 初始密码并创建数据库。在服务器执行初始化脚本建表。后端项目在本地执行mvn clean package -DskipTests打包把 target 下的 jar 包上传到服务器。前端在本地执行npm run build把 dist 目录也上传到服务器。单进程方式就直接运行 jar分离部署就先配置 Nginx 静态目录和反向代理再启动服务。启动后检查接口和数据同步是否正常curl http://localhost:8080/api/statistics/overview如果接口返回了统计数据并且定时任务日志里没有报错整个系统就算跑起来了。写在最后做这个项目之前我本来以为它只是一个“拿现成 API 数据展示”的简单系统真正做完之后才发现里面牵扯的环节非常多从数据源同步、数据库建模、统一接口设计、Redis 缓存到 WebSocket 实时推送、ECharts 地图可视化、权限控制和最后的生产部署几乎每一个模块都有值得记录的坑。最让我收获大的其实不是某个具体功能而是把前端和后端之间的“契约”想清楚字段名、返回结构、异常处理、时间格式这些约定越早统一联调阶段越省心。另外有一点个人建议如果你打算把这个项目作为毕业设计或者个人作品展示不妨在核心架构不变的前提下把“疫情数据”这个主题替换成更通用的“实时数据监控”比如设备状态监控、空气质量监控、交通流量监控等技术栈完全不需要改动只需要调整数据源和前端展示文案。这样项目的展示价值和实际意义都会更广面试时也更容易讲清楚设计思路。最后分享一个实际操作中的习惯每写一批接口就用 Postman 把请求和响应保存下来写一个简单的接口文档。这个习惯在多人和团队协作时效果极其明显关键时候真的能救命。