尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

SpringBoot+Vue疾病防控管理系统开发实战与部署指南 做疾病防控类的管理系统这几年需求一直很稳定。医院、疾控中心、社区卫生服务站、学校校医室甚至一些企业的健康管理部门都需要一套能管人员信息、能记录防控动态、能出统计报表的工具。用SpringBootVue这套组合来做后端跟前端分离职责清晰后期维护和二次开发都方便JavaMySQLMyBatis这条技术链路也足够稳。这篇文章我就基于自己做过的这类项目把设计思路、核心实现、踩过的坑完整梳理一遍给要做类似系统或者正在准备相关毕设的朋友一个可参考的完整方案。1. 项目背景与整体设计思路1.1 疾病防控系统的核心需求到底有哪些别一上来就想着“做一个大而全的疾控平台”那是机构级别的事。个人开发或者中小型项目核心盯住几个真实场景就够了人员信息建档、健康状态跟踪、异常情况上报、防控物资管理、统计报表展示。把这五件事做扎实系统就能真正用起来。我做这个项目之前专门跟社区卫生服务中心的工作人员聊过。他们最头疼的不是“有没有系统”而是“系统能不能减少Excel操作”。比如每天要统计辖区内发热门诊人数、隔离人员健康情况、物资库存余量这些数据分散在不同表格里汇总靠手工。所以系统的核心价值就一条让数据录入更简单、查询更快、报表自动生成。基于这个需求定位功能模块我拆成了这几个用户管理管理员、医护人员、普通用户分级权限支持账号启停用人员信息管理姓名、身份证号、联系方式、住址、健康状态、风险等级健康上报每日体温、症状、行程轨迹、接触史记录异常预警体温超标或风险等级变化时自动提醒物资管理口罩、消毒液、防护服等物资的入库、出库、库存预警数据统计按区域、按时间、按人群维度的图表统计这套模块划分既有业务闭环又不会把开发量撑爆。每个模块对应若干张表表之间通过外键关联逻辑清晰写代码的时候也不容易乱。1.2 为什么选SpringBootVue这套技术组合技术选型这事很多人喜欢追新但我一直坚持一个原则稳定、熟悉、生态成熟。SpringBootVueMySQLMyBatis这个组合在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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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.StdOutImplmapper-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 ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT 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接收后调用ServiceService里要做这几件事根据personId查询人员信息确认档案存在判断体温是否超过37.3超过则自动更新person_info表中的health_status为2发热如果人员risk_level为高风险且体温异常自动生成一条预警记录插入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.vueapi目录把每个模块的接口请求集中管理组件里不直接写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 serviceVuex里我用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 classchart-card h4{{ title }}/h4 div refchart styleheight: 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 idselectPersonPage resultTypecom.example.entity.PersonInfo SELECT * FROM person_info where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testhealthStatus ! null AND health_status #{healthStatus} /if if testriskLevel ! null AND risk_level #{riskLevel} /if /where ORDER BY create_time DESC LIMIT #{pageNum}, #{pageSize} /selectCOUNT查询单独写一条select idcountPersonPage resultTypeint SELECT COUNT(*) FROM person_info where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testhealthStatus ! null AND health_status #{healthStatus} /if if testriskLevel ! null AND risk_level #{riskLevel} /if /where /select这里有个小优化点MyBatis的if判断在拼接SQL时就已经把条件determined了所以COUNT和LIST两条SQL的where片段其实一模一样。我后来把这些公共的SQL片段用sql标签抽出来用include引用避免了重复。分页参数校验也不能忽视。pageNum最小为1pageSize不能超过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。上传的核心流程校验文件类型和大小图片限制5MB以内后缀白名单jpg/png/gif生成唯一的文件名UUID 原后缀防止重名覆盖按日期分目录存储比如 upload/2024/06/01/文件元信息写入数据库文件名、大小、类型、上传人、时间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 本地开发环境搭建完整步骤整个项目从零开始跑通环境这块分前端和后端两部分。后端环境安装JDK版本选1.8SpringBoot 2.7完美兼容或者17如果用SpringBoot 3安装MySQL 8.0初始化数据库时设置字符集为utf8mb4新建数据库和账号执行我提供的sql脚本导入表结构和基础数据含一个账号admin/admin123IDE我用IntelliJ IDEA直接Import Maven项目等待依赖下载完成修改application.yml里的数据库连接信息运行DiseaseControlApplication.java看到“Started”日志即启动成功浏览器访问http://localhost:8080/api/user/info确认后端接口通MySQL 8.0的安装有几个常见的坑我在Windows系统上多次踩过一是安装后服务启动失败多半是3306端口被占用处理办法要么停掉冲突应用要么改MySQL端口二是连接报Public Key Retrieval is not allowed错误连接URL里需要加allowPublicKeyRetrievaltrue参数三是数据库连接串里serverTimezone不设置的话Java程序和MySQL之间的时间字段传输会有8小时时差。前端环境安装Node.js建议用16.x LTS版本Vue 2.6 Vue CLI 4/5这个组合很稳定全局安装Vue CLInpm install -g vue/cli在项目根目录执行npm install等待依赖安装完成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.activeprod /dev/null 21 多环境配置这个习惯一定要有。application.yml放公共配置application-dev.yml和application-prod.yml分开管理不同环境的数据库地址、日志级别、文件上传路径。开发环境连接本地数据库生产环境连接服务器数据库走了多环境配置后不同环境切换只需要一个启动参数。生产环境我建议再加一层系统服务托管用systemd配置成开机自启。虽然现在直接nohup也一样能用但宕机后不会自动拉起体验还是差了不少。5.3 高频报错与排障技巧速查记录这个项目我前后遇到过不少问题挑几个典型的写出来大家以后碰到了少走弯路。报错1启动报Caused by: java.sql.SQLException: Access denied for user rootlocalhost这个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。报错2npm 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拦截返回403Spring Security拦截了multipart请求需要对/upload/**路径放行。在SecurityConfig里添加.antMatchers(/upload/**).permitAll()文件上传路径、静态资源路径、登录注册接口属于“匿名可访问”的范畴需要配置进permitAll列表。不然每次上传都返回403排查半天才发现是安全配置的问题。报错5分页插件使用不当导致SQL查询结果总数不对我这个项目虽然是手写分页但也有人用PageHelper遇到过类似问题PageHelper.startPage后面的第一条SQL才会被自动分页如果你在startPage和查询之间插了其他数据库操作SQL就会被截断或者查询不到分页效果。原则就是startPage紧挨着要分页的那条查询。报错6ECharts图表在弹窗里初始化为宽度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 本地启动演示的完整配置清单如果这个系统是给学校、医院或者客户演示用的现场环境稳定性很重要。我一般会提前准备一个演示环境把整套配置固定好MySQL新建专用账号disease_user授权DML权限密码用单独的强度规则避免用root账号演示存在安全风险后端通过--spring.profiles.activedev启动数据库地址用127.0.0.1端口保持8080前端npm run build打包好dist目录用nginx托管这样演示时不需要额外启动Vue开发服务直接在浏览器输入localhost:80就能访问体验更好演示之前先用真实数据走一遍核心流程新建用户、登录、录入人员信息、填报健康上报、查看预警、看统计报表、物资入库出库。每个流程都要逐一检查尤其是明细数据效果容易发现接口返回和页面展示格式不一致的问题。我发现很多临时演示翻车的都是在折腾环境、公网穿透或者配置上花费太多时间系统的演示反而草草收场。6.2 交付时配套的说明文档清单源码交付的时候光给代码和大宽库SQL是不够的我一般会附这几份文档系统部署文档环境要求、部署步骤、常见启动问题处理数据库设计说明所有表的字段含义、表间关系和设计原因接口文档每个接口的参数、返回值、错误码推荐用Apifox或Swagger管理操作手册给最终使用人员看的图文并茂介绍每个页面的功能这些文档在毕设答辩或者项目验收时是潜在得分点也体现职业素养后来给真实客户运维起来确实省心不少。接口文档我推荐用SwaggerSpringFox或springdoc自动生成写好了Controller层的注解让接口文档随着代码同步更新。一开始觉得写注解麻烦接口多了之后才发现写注解的收益比手写Word文档高太多。提示如果项目涉及真实人员数据演示和交付时建议全部换成测试数据不要使用真实姓名和身份证号。个人隐私保护在真实项目验收时经常被检查用假数据也能避免试验过程中产生不必要的问题。这个我从实习生时代就养成的习惯后来帮到了很多次。6.3 这个项目对标商业软件的差距分析做完这个系统之后我自己也复盘过和市面上商业化的疾控平台相比还差了哪些东西说是差距倒不如给了明确的改进方向。商业平台普遍有这些能力消息推送的精细化短信、App推送、站内信多通道、完善的审批流引擎上报需要多级审批、更细粒度的数据权限每个人只能看自己辖区内的数据、操作日志审计谁在什么时间改了什么数据、高并发下的性能保障。个人项目不需要一口吃成一个胖子但从简历和项目质量出发操作日志审计和数据权限这两块值得优先做。数据权限实现的思路是人员表里加一个region_code字段查询时根据当前用户的区域编码自动拼接条件就能实现“不同区域管理员看到不同数据”的效果。这在面试时属于一个亮点认真做了能讲出东西。另一方面个人项目也有自己的优势就是灵活。模块裁剪、界面调整、流程定制都能快速响应不像大平台改个字段要走很长的流程。如果能把某一个小功能做到极致比如针对大学生群体的每日上报反而比大而全的平台更有竞争力。7. 最后的实操总结与几点真心建议项目从零到一完整做下来我的体会有几点想分享。第一技术栈不求新求稳。SpringBootVueMySQLMyBatis这套组合我能闭着眼睛写踩过的坑都有现成解决方案。换一套新框架可能看起来更酷但开发周期和不确定性都会增加。真实项目的成功交付比炫技重要得多。第二数据库是整个系统的地基想不清楚表结构就不要急着敲代码。我刚开始做第一个管理系统的时候建表随心所欲结果写业务代码时发现关联查询缺字段、类型不匹配、索引缺失返工好几次。后来老老实实把表设计清楚再动手效率反而更高。ER图设计这一步不该省哪怕你用Excel画模块结构的草图也能看出表间关系是否合理。第三前端和后端的接口约定要尽早确定。我习惯先用Apifox写好接口的请求参数和返回值前后端并行开发时各自mock数据谁也不用等谁。这种方式比先写后端再写前端快得多也减少了联调时改来改去的麻烦。最后聊一句题外话做这种管理系统最容易被低估的是测试环节。代码写完能跑通主流程不算完边界情况搜索无结果、重复提交、超大文件上传都要测试到位。我前几次做项目就是吃了没测好边界情况的亏交付后用户一用就出问题搞得自己很被动。这次我把测试用例前置到了开发计划里每个功能开发完立刻写测试自测通过才往下走整体质量明显上了一个档次。这个系统的完整源码、数据库脚本和部署文档我都整理好了需要的朋友直接拿去参考搭建。按照文章里的步骤操作一遍跑通项目是没有问题的。后面的扩展功能遇到具体的报错或者设计上的疑问欢迎在评论区里交流我看到了都会回复。
返回列表