拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue疾病防控管理系统开发实战与部署指南

SpringBoot+Vue疾病防控管理系统开发实战与部署指南

做疾病防控类的管理系统,这几年需求一直很稳定。医院、疾控中心、社区卫生服务站、学校校医室,甚至一些企业的健康管理部门,都需要一套能管人员信息、能记录防控动态、能出统计报表的工具。用SpringBoot+Vue这套组合来做,后端跟前端分离,职责清晰,后期维护和二次开发都方便,Java+MySQL+MyBatis这条技术链路也足够稳。这篇文章我就基于自己做过的这类项目,把设计思路、核心实现、踩过的坑完整梳理一遍,给要做类似系统或者正在准备相关毕设的朋友一个可参考的完整方案。

1. 项目背景与整体设计思路

1.1 疾病防控系统的核心需求到底有哪些

别一上来就想着“做一个大而全的疾控平台”,那是机构级别的事。个人开发或者中小型项目,核心盯住几个真实场景就够了:人员信息建档、健康状态跟踪、异常情况上报、防控物资管理、统计报表展示。把这五件事做扎实,系统就能真正用起来。

我做这个项目之前,专门跟社区卫生服务中心的工作人员聊过。他们最头疼的不是“有没有系统”,而是“系统能不能减少Excel操作”。比如每天要统计辖区内发热门诊人数、隔离人员健康情况、物资库存余量,这些数据分散在不同表格里,汇总靠手工。所以系统的核心价值就一条:让数据录入更简单、查询更快、报表自动生成。

基于这个需求定位,功能模块我拆成了这几个:

  • 用户管理:管理员、医护人员、普通用户分级权限,支持账号启停用
  • 人员信息管理:姓名、身份证号、联系方式、住址、健康状态、风险等级
  • 健康上报:每日体温、症状、行程轨迹、接触史记录
  • 异常预警:体温超标或风险等级变化时自动提醒
  • 物资管理:口罩、消毒液、防护服等物资的入库、出库、库存预警
  • 数据统计:按区域、按时间、按人群维度的图表统计

这套模块划分,既有业务闭环,又不会把开发量撑爆。每个模块对应若干张表,表之间通过外键关联,逻辑清晰,写代码的时候也不容易乱。

1.2 为什么选SpringBoot+Vue这套技术组合

技术选型这事,很多人喜欢追新,但我一直坚持一个原则:稳定、熟悉、生态成熟。SpringBoot+Vue+MySQL+MyBatis这个组合,在JavaWeb领域属于“标准答案”级别的搭配,无论找工作面试说项目经验,还是毕设答辩讲技术选型,都能站得住脚。

后端用SpringBoot,最直接的好处是省掉了大量XML配置。我最早做SSM项目的时候,光是Spring和MyBatis的配置文件就要写上百行,还要处理各种版本兼容问题。SpringBoot的自动配置机制把这些都封装了,开箱即用,内置Tomcat,打jar包直接跑。对于业务开发来说,可以把精力集中在Controller和Service层的逻辑上,踩坑少,产出快。

MyBatis这块,我觉得它的优势是SQL可控。我自己属于那种“看得见SQL才安心”的开发者,MyBatis把SQL写在XML里,调优的时候直接看语句就行,不像JPA那样自动生成SQL,查起来跟黑盒似的。半自动化的特性在复杂查询场景下特别适用,像疾病防控这种需要大量多表关联统计的系统,手写SQL更灵活、效率也更高。

前端选Vue,核心理由是组件化开发带来的维护性提升。页面虽然多,但很多UI块是复用的,比如表格组件、表单弹窗、状态标签。Vue的单文件组件把这些都拆开了,改一处全局生效。配合Vue Router做页面跳转、Vuex做全局状态管理(比如用户登录信息)、Axios做HTTP请求,一整套下来非常顺。

MySQL 8.0作为数据库,性能足够,而且8.0版本对窗口函数、公共表表达式的支持更好,做统计报表的SQL时能省不少事。比如要算“最近7天每日新增异常人员数”,用递归CTE辅助日期序列,一条SQL就搞定。

2. 数据库设计与后端核心实现

2.1 数据库表结构设计的关键细节

表结构设计是整个系统的地基。我的习惯是先用PowerDesigner或者Navicat把ER图画出来,确认好关系再落库。这个项目最主要的几张表,我列出核心字段,给后面要做的人做个参考。

用户表(sys_user)

字段名类型说明
idbigint主键,自增
usernamevarchar(50)登录账号,唯一索引
passwordvarchar(100)BCrypt加密后的密码
real_namevarchar(50)真实姓名
phonevarchar(20)手机号
role_idbigint关联角色表
statustinyint1启用 0禁用
create_timedatetime创建时间

密码一定不要明文存储。我用的Spring Security自带的BCryptPasswordEncoder,加密后的密文长度60位,同一密码每次加密结果都不同,安全性有保障。网上流传的MD5加盐方案也可以,但BCrypt更省事,不用自己实现加盐逻辑。

人员信息表(person_info)

字段名类型说明
idbigint主键
namevarchar(50)姓名
id_cardvarchar(18)身份证号,唯一索引
gendertinyint1男 2女
ageint年龄
addressvarchar(200)居住地址
health_statustinyint1正常 2发热 3隔离 4确诊 5康复
risk_leveltinyint1低风险 2中风险 3高风险
user_idbigint关联系统用户ID

身份证号设唯一索引很重要,实际场景里一个人只能有一条健康档案。但要注意,身份证号涉及个人隐私,数据库里存储时建议先用AES加密,展示给前端时做脱敏处理,只显示前6位和后4位。这个细节在真实项目里会被反复问到,也是评审老师关注的点。

健康上报记录表(health_report)

字段名类型说明
idbigint主键
person_idbigint关联人员信息表
report_datedate上报日期
temperaturedecimal(4,1)体温
symptomvarchar(255)症状描述
is_contacttinyint是否接触史,1是 0否
locationvarchar(200)当前所在位置
create_timedatetime提交时间

这张表的数据量会增长很快,建议针对report_date和person_id建联合索引。查询“某人某段时间的上报记录”时,走索引和不走索引的性能差距在数据量大时非常明显。虽然个人项目数据量有限,但养成建索引的习惯是专业素养。

防控物资表(material_info)和物资出入库记录表(material_record)

物资表字段相对简单:名称、规格、库存总量、预警阈值、单位、更新时间。出入库记录表则要记录操作类型(1入库 2出库)、操作数量、操作人、操作时间。出入库操作必须放在事务里执行,一次操作同时更新记录表和库存总量,保证数据一致性。我在Service层加了@Transactional注解,并指定rollbackFor = Exception.class,确保任何异常都回滚。

2.2 SpringBoot整合MyBatis的配置细节

SpringBoot整合MyBatis其实很机械化,引入依赖、配置数据源、写Mapper接口和XML,三步走。

pom.xml里核心依赖就这几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

这里要提醒一句,mybatis-spring-boot-starter的版本和SpringBoot版本要匹配。我用的SpringBoot 2.7.x配合MyBatis starter 2.3.x,工作正常。如果你用的是SpringBoot 3.x,那就要换用mybatis-spring-boot-starter 3.0+版本,同时JDK版本也得到17以上,不然启动会报错。这个坑我见不少人踩过,一启动就报ClassNotFound,查半天发现是版本兼容问题。

application.yml里的关键配置:

spring: datasource: url: jdbc:mysql://localhost:3306/disease_control?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

mapper-locations指定XML文件的位置,type-aliases-package让实体类可以不写全限类名,map-underscore-to-camel-case把数据库下划线字段自动映射到驼峰属性,这个配置必须开启,否则你在XML里写resultMap时会被字段名映射搞得头大。log-impl在开发阶段配成StdOutImpl,可以在控制台直接打印SQL和执行参数,调试效率翻倍。生产环境记得去掉,不然日志文件会爆炸。

Mapper接口和XML的配合,我习惯用MyBatis-Plus还是纯MyBatis?这个项目我用的纯MyBatis。说实话,MyBatis-Plus的单表CRUD确实省事,但自定义多表关联查询还是要手写SQL,而且它的部分魔法操作(比如QueryWrapper)在复杂场景下反而不直观。纯MyBatis虽然样板代码多一点,但每个SQL都明明白白,出了问题好排查。对于学习阶段或者想要技术水平成长的人来说,纯MyBatis更有价值。

2.3 核心业务接口的设计思路

后端接口设计遵循RESTful风格,统一返回结果封装为Result对象,包含code、msg、data三个字段:

@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } }

接口统一返回Result,前后端对接的时候不用对着一堆乱七八糟的数据结构猜测含义。Controller层只做参数接收和结果封装,业务逻辑放在Service层,数据访问在Mapper层,三层架构职责分明。

以健康上报接口为例,前端传来的JSON格式长这样:

{ "personId": 12, "temperature": 36.5, "symptom": "无异常", "isContact": 0, "location": "北京市朝阳区XX街道" }

Controller接收后调用Service,Service里要做这几件事:

  1. 根据personId查询人员信息,确认档案存在
  2. 判断体温是否超过37.3,超过则自动更新person_info表中的health_status为2(发热)
  3. 如果人员risk_level为高风险且体温异常,自动生成一条预警记录
  4. 插入health_report表

这个逻辑用事务包起来,任何一步失败都整体回滚。体温判断的阈值我建议不要写死在代码里,放在配置项里(比如ThresholdProperties),因为不同时期、不同地区的标准可能调整,写死的话改个阈值还得重新发版。

疫苗接种记录、密接管理这些扩展模块,也沿用同样的设计模式:实体类→Mapper接口→XML→Service→Controller→Vue页面。把第一个模块跑通,后面的都是体力活。

3. 前端Vue实现与交互细节

3.1 Vue项目初始化与工程结构组织

前端我用Vue CLI创建的项目,Vue 2.6版本配合Element UI组件库。后台管理系统的界面,Element UI的表格、表单、弹窗、消息提示组件都成熟稳定,不用自己造轮子。项目装好之后,我习惯先把目录结构理清楚,避免写到后面文件到处乱放。

src/ ├── api/ # 封装所有后端接口请求 │ ├── user.js │ ├── person.js │ ├── report.js │ └── material.js ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── Pagination.vue │ └── StatusTag.vue ├── router/ # 路由配置 │ └── index.js ├── store/ # Vuex状态管理 │ └── modules/user.js ├── utils/ # 工具函数 │ ├── request.js # Axios封装 │ └── auth.js ├── views/ # 页面组件 │ ├── Login.vue │ ├── dashboard/ │ ├── person/ │ ├── report/ │ └── material/ └── App.vue

api目录把每个模块的接口请求集中管理,组件里不直接写axios调用。这样做的直接好处是,后端接口变了路径或者参数,只需要改api目录里对应的文件,不用满项目搜索哪里调了这个接口。

utils/request.js这个文件是Axios的封装层,我在里面做了三件事:请求拦截器自动附加token、响应拦截器统一处理错误码、401状态时自动跳转登录页。代码不复杂,但是能省掉大量重复劳动。

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } Message.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

Vuex里我用user模块存储登录用户的token、用户信息、权限标识。刷新页面后Vuex数据会丢,所以初始化时从localStorage读取一份作为默认值。这个细节处理好了,就不会出现“登录后一切正常,一刷新就跳回登录页”的尴尬问题。

3.2 路由权限控制与动态路由实现

后台管理系统必须有权限控制,不然每个用户登录进来都能看到所有菜单、访问所有页面,那还区分管理员和普通用户做什么。

路由权限这块我采用的方式是:静态路由登录注册页和首页,动态路由根据用户roleId从后端拉取可见模块,然后router.addRoutes动态添加。

后端在用户登录成功后返回用户的角色信息,前端根据角色从路由表里过滤。我预先定义好一个完整路由表,每个路由的meta字段标记需要的权限标识。

export const constantRoutes = [ { path: '/login', component: Login, hidden: true }, { path: '/', redirect: '/dashboard' } ] export const asyncRoutes = [ { path: '/person', component: Layout, meta: { title: '人员管理', icon: 'el-icon-user', roles: ['admin', 'doctor'] }, children: [ { path: 'list', component: PersonList, meta: { title: '人员列表' } }, { path: 'detail/:id', component: PersonDetail, meta: { title: '详情', hidden: true } } ] }, { path: '/report', component: Layout, meta: { title: '健康上报', icon: 'el-icon-document', roles: ['admin', 'doctor', 'user'] }, children: [{ path: 'index', component: ReportIndex, meta: { title: '每日上报' } }] } ]

登录后根据用户角色过滤这个asyncRoutes数组,再router.addRoutes,这是Vue 2.8版本后官方推荐的做法,2.6版本也能用。菜单栏则根据过滤后的路由配置递归生成。

按钮级别的权限控制,我用Vue自定义指令v-permission实现。指令里判断当前用户权限列表是否包含需要的权限码,不包含就直接移除DOM元素。比如“删除”按钮只有admin角色能看到,其他角色登录后页面就不渲染这个按钮。

Vue.directive('permission', { inserted(el, binding) { const role = store.getters.role const requiredRoles = binding.value if (requiredRoles && !requiredRoles.includes(role)) { el.parentNode && el.parentNode.removeChild(el) } } })

3.3 核心页面功能逐个拆解

人员管理页面是整个系统使用频率最高的页面,我做成搜索列表的形式。顶部是查询条件:姓名、身份证号、健康状态、风险等级,支持组合查询。底部表格用el-table展示,操作列放“查看”“编辑”“上报记录”三个按钮。分页用el-pagination组件,每页10条,支持页码跳转。

这个页面的搜索逻辑有一个细节:搜索条件变化后,必须把当前页码重置为1。不然你在第5页搜索“张三”,接口按第5页去查,出来一个空列表,用户还以为系统出bug了。我踩过这个坑,后来在watch搜索条件变化时手动把pageNum重置为1才解决。

健康上报页面,我设计了两种入口。一种是普通用户每天给自己上报,表单简单,就是体温、症状、位置。另一种是医护工作者代他人上报,需要先选人员再填上报数据。两种入口共用同一个后端接口,只是参数里是否传personId的区别。前端用不同的组件封装,看起来像两个功能,代码上复用了很多逻辑。

异常预警页面用列表加红色标点的方式展示。列表显示预警人员的姓名、风险等级、体温、症状、上报时间,风险等级高的行背景色标红。点击操作列的“处理”按钮,弹出对话框填写处理意见,处理后记录标记为已处理。这个页面我加了一个轮询刷新,每30秒请求一次接口拉最新数据,不用手动刷新页面就能看到新增预警。医疗场景的时效性要求高,这个功能大家反馈很实用。

统计报表板块,我用ECharts做图表。柱状图展示每日上报人数趋势,饼图展示健康状态分布(正常、发热、隔离、确诊、康复各占多少比例),折线图展示近7天发热人数变化。ECharts的配置项比较繁琐,我封装了一个ChartCard公共组件,传入option配置对象即可渲染图表。

<template> <div class="chart-card"> <h4>{{ title }}</h4> <div ref="chart" style="height: 300px"></div> </div> </template> <script> import * as echarts from 'echarts' export default { props: ['title', 'option'], mounted() { this.chart = echarts.init(this.$refs.chart) this.chart.setOption(this.option) window.addEventListener('resize', this.resizeChart) }, methods: { resizeChart() { this.chart && this.chart.resize() } }, beforeDestroy() { window.removeEventListener('resize', this.resizeChart) this.chart && this.chart.dispose() } } </script>

需要注意,ECharts实例在组件销毁前要dispose掉,window的resize监听也要移除。不然页面反复切换路由,会积累一堆没销毁的图表实例,内存占用越来越高,页面越来越卡。

4. 分页查询、统计报表与文件上传这些难点怎么攻

4.1 多条件分页查询的性能优化实践

列表页的分页查询,大多数人用的是PageHelper。但我这个项目写的是纯MyBatis手写分页,limit #{pageNum}, #{pageSize},原因主要有两点:一是我这个项目每个列表的查询条件都不一样,手写SQL更直观;二是分页插件在复杂SQL(比如带子查询或动态条件)时偶尔有SQL改写问题,排查起来很费劲。

手写分页的核心是将参数封装成一个PageQuery对象,包含pageNum、pageSize以及各查询条件字段。Mapper接口的方法传这个对象,XML里拼动态SQL。

<select id="selectPersonPage" resultType="com.example.entity.PersonInfo"> SELECT * FROM person_info <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="healthStatus != null"> AND health_status = #{healthStatus} </if> <if test="riskLevel != null"> AND risk_level = #{riskLevel} </if> </where> ORDER BY create_time DESC LIMIT #{pageNum}, #{pageSize} </select>

COUNT查询单独写一条:

<select id="countPersonPage" resultType="int"> SELECT COUNT(*) FROM person_info <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="healthStatus != null"> AND health_status = #{healthStatus} </if> <if test="riskLevel != null"> AND risk_level = #{riskLevel} </if> </where> </select>

这里有个小优化点:MyBatis的if判断在拼接SQL时就已经把条件determined了,所以COUNT和LIST两条SQL的where片段其实一模一样。我后来把这些公共的SQL片段用<sql>标签抽出来,用<include>引用,避免了重复。

分页参数校验也不能忽视。pageNum最小为1,pageSize不能超过100(防止有人传10000直接把数据库打崩),这些校验放在Controller层用@Min和@Max注解搞定。

数据量方面,疾病防控系统单表数据量一般到不了百万级,分页查询性能瓶颈不明显。但如果未来数据量增长,可以继续做优化:比如从LIMIT #{offset}优化为LIMIT #{offset}, #{size}配合覆盖索引、或者改成基于游标的分页方案(WHERE id > #{lastId} LIMIT #{size}),后端代码要预先留好改造的余地。

4.2 统计报表的数据聚合SQL写法

统计模块最核心的SQL逻辑,是根据日期维度做GROUP BY聚合。比如“统计每日上报人数”,核心SQL长这样:

SELECT report_date, COUNT(*) AS total FROM health_report WHERE report_date BETWEEN #{startDate} AND #{endDate} GROUP BY report_date ORDER BY report_date

“统计健康状态分布”按health_status分组,然后前端用ECharts饼图渲染。代码上需要注意统计接口返回的数据格式,我统一封装成name和value的键值对数组,前端图表组件直接可用,不用做二次转换。

[ { "name": "正常", "value": 356 }, { "name": "发热", "value": 12 }, { "name": "隔离", "value": 4 }, { "name": "确诊", "value": 1 }, { "name": "康复", "value": 28 } ]

如果报表里的日期有连续性的要求,比如“近7天每日发热人数”,某一天没有任何数据时,GROUP BY的结果会缺这一天。这时候可以用MySQL 8.0的递归CTE先生成日期序列,再LEFT JOIN业务表:

WITH RECURSIVE date_range AS ( SELECT CURDATE() - INTERVAL 6 DAY AS date UNION ALL SELECT date + INTERVAL 1 DAY FROM date_range WHERE date < CURDATE() ) SELECT d.date, COUNT(r.id) AS fever_count FROM date_range d LEFT JOIN health_report r ON DATEDIFF(r.report_date, d.date) = 0 WHERE r.temperature >= 37.3 GROUP BY d.date ORDER BY d.date

这种SQL写法在MySQL 5.7里不支持,8.0就没问题。如果你的环境还是5.7,那就得在Java代码里补零填充:查出已有的数据后,遍历日期数组,缺失的日期补0。

报表查询的频率控制也要考虑。统计接口每次查询都实时跑一次SQL,数据量大以后会有点压力。我的做法是在Service层加了Spring Cache,设置缓存过期时间10分钟,同一个报表查询在10分钟内直接走缓存,不查数据库。对于“凌晨的数据才变”这种场景,10分钟级别的延迟完全可接受。

4.3 文件上传与图片预览的实践方案

防控系统里有几个地方需要文件上传:用户头像、核酸报告图片、物资导入Excel模板。文件上传我直接用的SpringBoot自带的MultipartFile机制,存储路径配置在application.yml里,上传后返回文件访问URL。

上传的核心流程:

  1. 校验文件类型和大小(图片限制5MB以内,后缀白名单jpg/png/gif)
  2. 生成唯一的文件名(UUID + 原后缀),防止重名覆盖
  3. 按日期分目录存储,比如 upload/2024/06/01/
  4. 文件元信息写入数据库(文件名、大小、类型、上传人、时间)
public String uploadFile(MultipartFile file) { if (file.isEmpty()) { throw new BusinessException("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); if (!ALLOWED_TYPES.contains(ext)) { throw new BusinessException("不支持的文件类型"); } String fileName = UUID.randomUUID().toString().replace("-", "") + ext; String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); File dir = new File(uploadPath + datePath); if (!dir.exists()) { dir.mkdirs(); } File dest = new File(dir, fileName); file.transferTo(dest); return "/upload/" + datePath + "/" + fileName; }

文件访问这块,我在WebConfig里配置了资源映射,把本地的upload路径映射到/upload/**访问路径,这样前端通过URL就能直接预览图片。

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath); }

上传文件的后缀校验,不仅要做白名单校验,还需要校验文件的真实类型。我看到过一些坑:改了后缀名的恶意文件直接通过后缀校验被传上服务器,然后被攻击者当成脚本执行。前端可以做类型检查,但后端一定要做二次校验,这种安全底线不能省。如果项目对安全要求更高,可以考虑集成OSS对象存储,或者参考MinIO做私有化部署,把文件存到专门的存储服务里。

5. 开发部署与常见问题排查实录

5.1 本地开发环境搭建完整步骤

整个项目从零开始跑通,环境这块分前端和后端两部分。

后端环境:

  1. 安装JDK,版本选1.8(SpringBoot 2.7完美兼容)或者17(如果用SpringBoot 3)
  2. 安装MySQL 8.0,初始化数据库时设置字符集为utf8mb4
  3. 新建数据库和账号,执行我提供的sql脚本导入表结构和基础数据(含一个账号admin/admin123)
  4. IDE我用IntelliJ IDEA,直接Import Maven项目,等待依赖下载完成
  5. 修改application.yml里的数据库连接信息
  6. 运行DiseaseControlApplication.java,看到“Started”日志即启动成功
  7. 浏览器访问http://localhost:8080/api/user/info确认后端接口通

MySQL 8.0的安装有几个常见的坑,我在Windows系统上多次踩过:一是安装后服务启动失败,多半是3306端口被占用,处理办法要么停掉冲突应用,要么改MySQL端口;二是连接报Public Key Retrieval is not allowed错误,连接URL里需要加allowPublicKeyRetrieval=true参数;三是数据库连接串里serverTimezone不设置的话,Java程序和MySQL之间的时间字段传输会有8小时时差。

前端环境:

  1. 安装Node.js,建议用16.x LTS版本(Vue 2.6 + Vue CLI 4/5这个组合很稳定)
  2. 全局安装Vue CLI:npm install -g @vue/cli
  3. 在项目根目录执行npm install,等待依赖安装完成
  4. npm run serve启动开发服务器,默认端口8080会跟后端冲突,我在vue.config.js里把devServer端口改成了3000,并配置了API代理
// vue.config.js module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

配置代理的主要原因是解决跨域。前端在3000端口,后端在8080端口,直接请求会触发CORS机制拦截,浏览器控制台报CORS error。通过devServer的proxy,前端所有请求都发到3000端口,代理转发到8080,前端视角里它访问的始终是同源地址,问题解除。除了这种方式,也可以在后端加CORS过滤器,但我更推荐proxy方式,这样后端不用额外处理跨域逻辑。

5.2 项目打包与部署上线方案

开发完成后要交付给用户跑起来,打包部署这块也不难。

前端执行npm run build,打包完成后dist目录里就是纯静态文件,扔给nginx托管即可。我在nginx里配了反向代理,把/api路径转发到后端服务。

server { listen 80; server_name your-domain.com; root /var/www/dist; 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; } location /upload/ { alias /var/www/upload/; } location / { try_files $uri $uri/ /index.html; } }

这里最后一个location里的try_files规则很关键。Vue是单页应用,如果用户直接访问某个子路由地址(比如/person/list),刷新页面时nginx找不到这个物理路径就会404。try_files $uri $uri/ /index.html的作用是把所有不存在的路径都回退到index.html,由前端路由接管,这样刷新就不会白屏了。

后端我用Maven打包:mvn clean package -DskipTests,生成jar包后扔到服务器上执行:

nohup java -jar disease-control-1.0.0.jar --spring.profiles.active=prod >/dev/null 2>&1 &

多环境配置这个习惯一定要有。application.yml放公共配置,application-dev.yml和application-prod.yml分开管理不同环境的数据库地址、日志级别、文件上传路径。开发环境连接本地数据库,生产环境连接服务器数据库,走了多环境配置后,不同环境切换只需要一个启动参数。

生产环境我建议再加一层系统服务托管,用systemd配置成开机自启。虽然现在直接nohup也一样能用,但宕机后不会自动拉起,体验还是差了不少。

5.3 高频报错与排障技巧速查记录

这个项目我前后遇到过不少问题,挑几个典型的写出来,大家以后碰到了少走弯路。

报错1:启动报Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost'

这个99%是数据库账号密码不对,或者连接串的host/port配置错误。排查步骤:先确认MySQL服务有没有启动,再确认能否用客户端的图形工具连接成功,最后看application.yml里的用户名密码是否匹配。还有一种隐蔽原因:MySQL 8.0默认用caching_sha2_password插件,如果连接驱动是mysql-connector-java 5.x老版本,认证方式不兼容,也要报Access denied。解决方法是升级驱动到8.x版本,对应driverClassName要写com.mysql.cj.jdbc.Driver。

报错2:npm install卡住不动,或者下载依赖特别慢

Node.js的npm源都在境外,国内网络访问经常超时。解决方法是切换淘宝镜像源:

npm config set registry https://registry.npmmirror.com

或者用cnpm命令。Vue CLI创建项目时,网络不通还会导致模板下载失败,这时候先检查能不能访问github(模板是从github拉取的),再考虑用离线模板的方式。

报错3:前端调接口报401,但是后端接口用Postman测是通的

401说明请求没带token或者token失效。从Postman能够通,说明后端接口本身没问题,问题出在请求拦截器没生效。最常见的场景是:Axios拦截器里拿到了localStorage.getItem('token')是null,而token实际存在了sessionStorage或者cookie里。两个存储方式搞混了,就会导致每一层看起来都没错但结果不对。登录后统一封装一个存储token的方法,全项目只通过这个方法读写token,别在页面里直接散落localStorage操作。

报错4:文件上传时被Spring Security拦截,返回403

Spring Security拦截了multipart请求,需要对/upload/**路径放行。在SecurityConfig里添加:

.antMatchers("/upload/**").permitAll()

文件上传路径、静态资源路径、登录注册接口属于“匿名可访问”的范畴,需要配置进permitAll列表。不然每次上传都返回403,排查半天才发现是安全配置的问题。

报错5:分页插件使用不当导致SQL查询结果总数不对

我这个项目虽然是手写分页,但也有人用PageHelper遇到过类似问题:PageHelper.startPage后面的第一条SQL才会被自动分页,如果你在startPage和查询之间插了其他数据库操作,SQL就会被截断或者查询不到分页效果。原则就是startPage紧挨着要分页的那条查询。

报错6:ECharts图表在弹窗里初始化为宽度100%后渲染异常

弹窗里的表格初始隐藏状态时宽度是0,等弹出后ECharts初始化太早,图表就渲染成一个很窄的条或者干脆不显示。解决办法是在弹窗打开完成后调用图表实例的resize()方法,或者用setTimeout延后初始化。也可以在弹窗的nextTick里重新执行setOption。

5.4 项目二次开发可以扩展的方向

系统做出来之后,如果还想继续完善或者作为毕设的亮点,我有几个方向推荐。

数据导出功能。很多医疗场景需要把数据导出成Excel交差,可以结合EasyExcel或者POI实现人员信息、上报记录、物资台账的导出。EasyExcel针对大量数据的导出性能比POI好很多,但两者都需要额外加依赖。导出这块要注意的是字段映射和格式控制:日期格式要用字符串模板,不然Excel显示的是序列号;数值超过11位会自动变成科学计数法,如身份证号,需要设置单元格为文本格式。EasyExcel写法的核心是定义导出模型,用@ExcelProperty指定表头名称和排序,模型字段和数据实体类字段可以不一一对应。

数据大屏展示。把统计报表从普通的页面图表升级为可视化大屏,用ECharts的多个图表拼一个dashboard页面,加上自动轮播功能。大屏的视觉元素装配在一些科技公司尤其吃香,作为项目亮点展示的时候能加分不少。我做大屏时的经验是:数据过大屏展示时要预先Aggregation好,尽量用后端接口返回已经聚合好的数据,别用前端循环过滤,否则大屏上的数字一多页面会卡。

消息通知功能。往消息队列或者WebSocket方向扩展,实现预警信息的即时推送。比如新增发热人员后,当天值班的管理员账号能实时收到弹窗提醒,这个比轮询刷新更高效,扩展后系统的实用性和技术含量都上一个档次。WebSocket的用法在SpringBoot里不难,一个@ServerEndpoint注解,但要注意Spring注入问题:@ServerEndpoint生成的实例不在Spring容器管理范围,需要配合ApplicationContextAware去拿Service实现类。这个坑我在其他项目里踩过,当时连不上数据库,排查一圈才发现是Bean注入失败。

移动端适配。如果有条件,可以基于Vue做一套移动端H5页面,或者用uni-app封装App。疾控一线的很多场景发生在社区、学校,大家真正用手机操作,比守在电脑前方便得多。uni-app是目前国内比较热门的方案,一套代码可以打包成H5、小程序和App,技术栈也是Vue语法,学习成本低。移动端的重点页面就是健康上报和异常查询,一个表单加一个列表,可以快速做出来。

这些扩展方向,绝大多数后端逻辑不用大改,改动主要集中在前端的页面展示和数据接口的返回结构上。整体架构当初设计的时候留好了扩展位,后续做起来轻松不少。

6. 项目演示与交付使用的注意事项

6.1 本地启动演示的完整配置清单

如果这个系统是给学校、医院或者客户演示用的,现场环境稳定性很重要。我一般会提前准备一个演示环境,把整套配置固定好:

  1. MySQL:新建专用账号disease_user,授权DML权限,密码用单独的强度规则,避免用root账号演示(存在安全风险)
  2. 后端:通过--spring.profiles.active=dev启动,数据库地址用127.0.0.1,端口保持8080
  3. 前端:npm run build打包好dist目录,用nginx托管,这样演示时不需要额外启动Vue开发服务,直接在浏览器输入localhost:80就能访问,体验更好

演示之前,先用真实数据走一遍核心流程:新建用户、登录、录入人员信息、填报健康上报、查看预警、看统计报表、物资入库出库。每个流程都要逐一检查,尤其是明细数据效果,容易发现接口返回和页面展示格式不一致的问题。我发现很多临时演示翻车的都是在折腾环境、公网穿透或者配置上花费太多时间,系统的演示反而草草收场。

6.2 交付时配套的说明文档清单

源码交付的时候光给代码和大宽库SQL是不够的,我一般会附这几份文档:

  • 系统部署文档:环境要求、部署步骤、常见启动问题处理
  • 数据库设计说明:所有表的字段含义、表间关系和设计原因
  • 接口文档:每个接口的参数、返回值、错误码(推荐用Apifox或Swagger管理)
  • 操作手册:给最终使用人员看的,图文并茂介绍每个页面的功能

这些文档在毕设答辩或者项目验收时是潜在得分点,也体现职业素养,后来给真实客户运维起来确实省心不少。

接口文档我推荐用Swagger(SpringFox或springdoc)自动生成,写好了Controller层的注解,让接口文档随着代码同步更新。一开始觉得写注解麻烦,接口多了之后才发现写注解的收益比手写Word文档高太多。

提示:如果项目涉及真实人员数据,演示和交付时建议全部换成测试数据,不要使用真实姓名和身份证号。个人隐私保护在真实项目验收时经常被检查,用假数据也能避免试验过程中产生不必要的问题。这个我从实习生时代就养成的习惯,后来帮到了很多次。

6.3 这个项目对标商业软件的差距分析

做完这个系统之后,我自己也复盘过:和市面上商业化的疾控平台相比,还差了哪些东西?说是差距,倒不如给了明确的改进方向。

商业平台普遍有这些能力:消息推送的精细化(短信、App推送、站内信多通道)、完善的审批流引擎(上报需要多级审批)、更细粒度的数据权限(每个人只能看自己辖区内的数据)、操作日志审计(谁在什么时间改了什么数据)、高并发下的性能保障。个人项目不需要一口吃成一个胖子,但从简历和项目质量出发,操作日志审计和数据权限这两块值得优先做。数据权限实现的思路是:人员表里加一个region_code字段,查询时根据当前用户的区域编码自动拼接条件,就能实现“不同区域管理员看到不同数据”的效果。这在面试时属于一个亮点,认真做了能讲出东西。

另一方面,个人项目也有自己的优势,就是灵活。模块裁剪、界面调整、流程定制都能快速响应,不像大平台改个字段要走很长的流程。如果能把某一个小功能做到极致,比如针对大学生群体的每日上报,反而比大而全的平台更有竞争力。

7. 最后的实操总结与几点真心建议

项目从零到一完整做下来,我的体会有几点想分享。

第一,技术栈不求新,求稳。SpringBoot+Vue+MySQL+MyBatis这套组合我能闭着眼睛写,踩过的坑都有现成解决方案。换一套新框架可能看起来更酷,但开发周期和不确定性都会增加。真实项目的成功交付比炫技重要得多。

第二,数据库是整个系统的地基,想不清楚表结构就不要急着敲代码。我刚开始做第一个管理系统的时候,建表随心所欲,结果写业务代码时发现关联查询缺字段、类型不匹配、索引缺失,返工好几次。后来老老实实把表设计清楚再动手,效率反而更高。ER图设计这一步不该省,哪怕你用Excel画模块结构的草图,也能看出表间关系是否合理。

第三,前端和后端的接口约定要尽早确定。我习惯先用Apifox写好接口的请求参数和返回值,前后端并行开发时各自mock数据,谁也不用等谁。这种方式比先写后端再写前端快得多,也减少了联调时改来改去的麻烦。

最后聊一句题外话:做这种管理系统,最容易被低估的是测试环节。代码写完能跑通主流程不算完,边界情况(搜索无结果、重复提交、超大文件上传)都要测试到位。我前几次做项目就是吃了没测好边界情况的亏,交付后用户一用就出问题,搞得自己很被动。这次我把测试用例前置到了开发计划里,每个功能开发完立刻写测试,自测通过才往下走,整体质量明显上了一个档次。

这个系统的完整源码、数据库脚本和部署文档我都整理好了,需要的朋友直接拿去参考搭建。按照文章里的步骤操作一遍,跑通项目是没有问题的。后面的扩展功能,遇到具体的报错或者设计上的疑问,欢迎在评论区里交流,我看到了都会回复。

返回列表