ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SpringBoot+Vue的疾病防控综合管理系统设计与实现

基于SpringBoot+Vue的疾病防控综合管理系统设计与实现 做公共卫生信息化的朋友应该都有体会“企业级疾病防控综合管理系统”这几个字我第一次看到是在一个基层卫生信息化的需求单里。不少疾控中心、社区卫生服务中心还在用Excel加微信群做病例汇聚和物资统计每到报表周期就加班到深夜数据从几个入口进来字段口径对不上上级临时换一个统计维度又是几千条记录的重复劳动。这套基于SpringBoot Vue MyBatis MySQL架构构建的管理系统正是奔着解决这些问题去的。本文会从需求复盘、架构设计、核心模块、数据库表结构、关键代码实现、部署运维几个层面完整展开把我实际做这类项目时踩过的坑和验证过可行的方法一并写出来。适合准备做卫生信息化项目的开发者、想学习前后端分离项目的读者以及需要快速落地一套管理系统的团队参考。1. 为什么会有这套系统需求复盘与技术选型逻辑1.1 公共卫生管理到底在管什么很多人觉得疾病防控就是“报个病例”真做起来才知道是个多线程业务。一个基层机构日常要管的至少包括传染病监测与病例报告、慢性病随访管理、防控物资入库出库、疫苗接种安排与留观记录、健康宣教内容发布以及给上级领导看的数据分析报表。这些业务在传统Excel时代最大的问题不是某个环节慢而是数据各自为政填报表的医生一套模板做统计的科员另一套模板物资管理员手里的账本又是独立的同一批数据对不上是常态。这套系统在设计时就把这些业务拉到了一个平台里。医生在网页上填报病例审核人员在系统里完成审核物资出入库有独立模块统计报表由后端聚合后直接生成图表。业务数据从入口开始就是结构化的后面做任何统计和追溯都不需要再人工清洗。1.2 技术选型为什么是SpringBoot Vue MyBatis MySQL这个组合在中小型管理系统中几乎是标准答案每个成员都有明确分工。SpringBoot解决的问题是后端快速开发和部署成本。它内置Tomcat依赖管理靠starter就能搞定不需要像传统SSM那样维护一堆XML配置。我要暴露一个REST接口一个RestController加几个注解就行这对业务变化快、需求迭代频繁的管理系统来说非常关键。Vue负责前端交互。疾病防控系统的表单特别多病例填报、物资台账、接种记录全是密集型输入场景。Vue的组件化开发可以把手写重复的表单封装成通用组件配合Element UI这类现成UI库后台管理界面能节省大量工作量。MyBatis是持久层里最“看得见”的选择。防控系统有大量多表联查和动态统计SQL比如按月按机构按病种聚合数据。MyBatis把SQL直接写在Mapper XML里哪条SQL慢、需要怎么优化一眼就能看出来调起来非常直接。如果是JPA复杂查询的自动生成SQL有时候反而让你摸不着头脑。MySQL就是成本与稳定性的平衡点。5.7和8.0都有大量生产案例社区资料多员工入职上手快。对一个区县级机构或者中小型团队来说不需要一上来就上分布式数据库MySQL单库分表用好索引已经能支撑相当长时间。对比其他方案Node.js后端写小型接口很快但业务层一复杂类型约束和工程规范就跟不上Python Django适合原型快速验证但大规模并发和高密度事务处理的历史经验相对少PostgreSQL功能更强但如果团队以前都是用MySQL的迁移学习成本也是实打实的。这套技术栈最大的优势是不追求某一个环节最强而是保证整个链路最稳。1.3 这套系统到底适合谁第一种是做毕业设计或者课程项目的学生前后端完整、模块清晰、可以直接跑起来改比从零搭框架节省大量时间。第二种是中小型卫生机构的信息化负责人想用一套内部系统替代Excel流程不需要买几十万的商业平台。第三种是技术团队想快速交付类似的管理系统拿这套源码做基座替换业务模块就能复用。需要说清楚的是这类系统不是区域级全民健康信息平台不涉及跨机构大规模数据交换它的边界就是“一个机构或多个机构内部的防控业务闭环”。理解了这层定位后面看架构和表设计就不会觉得简单了。2. 整体架构与工程结构前后端分离到底怎么分2.1 前后端分离的协作逻辑现在这套源码采用标准的前后端分离方式。用户在浏览器里打开Vue页面页面通过axios发送HTTP请求到SpringBoot后端SpringBoot通过Controller接收请求、Service处理业务规则、Mapper访问MySQL最后把JSON数据返回给前端渲染。开发阶段前端用vue-cli的devServer代理解决跨域问题部署阶段可以独立用Nginx托管前端静态文件再把/api前缀的请求转发到后端。整个调用链路是这样的浏览器加载Vue页面和路由页面组件调用src/api目录下的模块函数这些函数通过封装好的axios实例发出HTTP请求SpringBoot的Controller接收参数并校验Service层执行业务规则Mapper接口对应的XML执行SQLMySQL返回结果逐层封装成ResponseResult结构的JSON前端拿到数据后更新页面状态2.2 后端工程结构每个目录都不是摆设后端工程的结构直接决定了项目后期好不好维护。我把这套源码的包结构列出来dd-system ├── pom.xml └── src/main/java/com/health/dd ├── DDApplication.java // 启动类 ├── config/ // CorsConfig、WebMvcConfig等配置 ├── controller/ // LoginController、ReportController... ├── service/ // 业务接口 ├── service/impl/ // 业务实现 ├── mapper/ // MyBatis Mapper接口 ├── entity/ // 数据库实体对象 ├── dto/ // 前端传入参数对象 ├── vo/ // 返回前端的数据对象 ├── common/ // Result、ResultCode、PageResult ├── utils/ // JwtUtil、PasswordUtil等 └── interceptor/ // JwtInterceptor这里我要重点强调一个原则Controller不要写业务逻辑。很多人图省事直接把数据库查询写在Controller里项目一大了Service层形同虚设。这个源码里Controller只做参数接收、简单校验和调用Service所有事务、权限、业务规则都封装在Service实现类中。这样做的直接好处是以后如果要暴露给第三方接口或者写定时任务直接复用Service即可不用重新实现一遍。2.3 前端工程结构前端的目录结构同样为长期维护做了划分dd-web ├── package.json ├── vue.config.js // 开发代理和构建配置 └── src/ ├── main.js ├── App.vue ├── api/ // request.js及按模块拆分的接口文件 ├── router/ // index.js和动态路由处理 ├── store/ // Vuex用户状态和token ├── views/ // login、dashboard、report、material、vaccine等页面 ├── components/ // 通用组件分页、上传、弹窗 └── utils/ // auth.js、validate.js前端模块划分的逻辑是api层统一管接口地址views层只关心页面渲染store统一管理用户登录态router负责页面跳转和权限拦截。这样前端团队开发时各自负责自己的模块互不干扰。2.4 接口设计规范统一返回结构全站接口统一返回一个Result对象前端axios拦截器只需要处理这一个结构不用每个接口单独判断。Result的核心字段是code、message、datacode为200表示成功401表示未认证403表示无权限500表示系统异常。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }分页接口统一返回PageResult包含total、records、current、size四个字段。前端分页组件直接对接这个结构不需要每张表单独做分页逻辑。这套规范看起来简单但实际团队协作时能省下大量联调时间。3. 核心业务模块与数据库设计疾病防控的闭环长什么样3.1 模块全景与用户角色这套系统的业务模块可以拆成七块每块解决一个具体场景模块核心功能系统管理用户、角色、菜单、机构管理传染病监测疾病字典维护、监测数据录入病例上报病例填报、提交、审核、驳回、归档防控物资管理物资台账、入库出库、库存预警疫苗接种管理接种人员登记、批次管理、接种记录健康宣教文章发布、公告推送、浏览统计数据统计病例趋势、病种占比、机构排名报表用户角色设计上至少需要区分系统管理员、疾控科人员、填报医生、机构管理员、普通查看者。不同角色看到的功能菜单不同这由前端动态路由配合后端返回的菜单数据实现。权限管理如果做得好后续给第三方机构开账号也方便。3.2 核心数据库表设计逻辑数据库是整个系统的地基。我挑几张核心表说一下设计思路。用户表sys_user用于认证和基础信息字段名类型说明idbigint主键usernamevarchar(50)登录名唯一索引passwordvarchar(100)BCrypt加密后的密码real_namevarchar(50)真实姓名org_idbigint所属机构IDstatustinyint1启用 0禁用deletedtinyint逻辑删除标记病例上报主表rep_case_report是关键业务表字段名类型说明idbigint主键report_novarchar(32)报告编号唯一org_idbigint上报机构IDdisease_idbigint疾病字典IDpatient_namevarchar(50)患者姓名patient_id_cardvarchar(20)身份证号report_datedate发病/上报日期report_statustinyint0草稿 1待审核 2已通过 3驳回 4归档reporter_idbigint填报人IDremarkvarchar(500)备注物资表mat_material和出入库表mat_stock_log用两张表实现库存和流水分离。mat_material只维护当前库存总量mat_stock_log记录每一次入库出库的明细。以后要查“某个批次什么时候入库、发给哪个科室了”直接查流水表即可不用翻Excel。这里有几个设计经验值得展开说第一统一主键用自增ID还是雪花ID如果系统只在一个机构内部跑自增ID完全够用如果以后要考虑数据合并或对接上级平台主键最好用类似雪花算法的分布式ID避免多库合并时冲突。第二时间字段统一用datetime类型并设置create_time和update_time所有表都加上方便排查数据和做增量同步。第三数量、金额用decimal而不是float/double。防控物资里哪怕库存数量出现0.1的误差都会让盘点非常难受数据库字段设计阶段就要避免浮点误差。第四所有核心业务表都加deleted逻辑删除字段。业务系统里物理删除记录会让审计追踪失效比如病例上报后如果被物理删除审核记录就断了后面想追溯根本找不到。第五字符集要求utf8mb4。别用utf8否则遇到患者姓名里的生僻字或特殊符号直接报错或乱码。3.3 病例上报状态流转设计病例从填报到归档不是一条直线而是一个状态机。我在源码里看到的状态流转是医生创建记录时是草稿可以随时修改保存确认无误后点击提交状态变为待审核疾控科人员查看后可以选择通过或驳回驳回时需要填写原因以便填报医生修改后重新提交通过后记录进入已归档状态不再允许修改只能查看。这个流程设计的要点在于“审核记录要单独建表”。有些人图方便只在主表上加一个status字段驳回原因直接覆盖。但真实业务中一次病例可能被驳回两次、修改三次所有历史状态都应该可追溯。所以我在设计时增加了rep_case_audit表每产生一次审核动作就插入一条记录主表的status只代表当前状态。这一个细节在项目上线后的实际使用中帮了大忙月底对账时所有驳回和修改记录都能翻出来责任清晰。3.4 多表联查与统计SQL口径一致是命根子统计报表最容易翻车的地方是“统计口径”。什么叫口径就是每条数据要不要过滤状态、要不要去重、时间范围怎么算。这套系统里统计都是走SQL聚合比如按机构按月统计上报病例数SELECT o.org_name, DATE_FORMAT(r.report_date, %Y-%m) AS month, COUNT(DISTINCT r.id) AS report_count FROM rep_case_report r LEFT JOIN sys_org o ON r.org_id o.id WHERE r.report_status 2 AND r.report_date BETWEEN #{startDate} AND #{endDate} GROUP BY o.org_name, DATE_FORMAT(r.report_date, %Y-%m) ORDER BY month DESC这里有几个关键点一是为什么用LEFT JOIN如果某个机构当月没有上报记录INNER JOIN会直接把它过滤掉领导想看的是“所有机构的完成情况”没上报的机构也要显示为0二是report_status 2表示只统计已审核通过的数据草稿和驳回中的脏数据不能混入统计三是COUNT(DISTINCT r.id)防止因为联查产生重复记录。疾病类型占比统计也是一样核心是GROUP BY加COUNTSELECT d.disease_name, COUNT(*) AS cnt FROM rep_case_report r LEFT JOIN dis_disease d ON r.disease_id d.id WHERE r.report_status 2 AND r.report_date DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY d.disease_name ORDER BY cnt DESC这种SQL在MyBatis XML里写清楚别用三层的select嵌套把可读性搞没了。统计逻辑写在SQL里而不是在Java内存里去数效率高得多。4. 关键功能实现拆解JWT登录、上报流程、图表报表4.1 基于JWT的登录鉴权实战登录是前后端分离系统第一道关卡。前端把用户名密码用POST方式传给后端后端从sys_user表查出用户校验密码是否正确正确则生成一个带签名和过期时间的JWT返回给前端。之后前端每一次请求都在Header里带上Authorization: Bearer token后端拦截器解析token拿到当前用户的userId和角色信息。JwtUtil的核心代码public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; // 24小时 public static String generateToken(Long userId, String username) { Date now new Date(); Date expireDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }JWT拦截器里要做的事很简单预检请求OPTIONS直接放行白名单路径如登录接口和静态资源放行其余请求解析Header中的token失败则返回401。解析成功后将userId放入request attribute里后续Controller通过参数绑定或者一个自定义注解取到当前用户。密码存储我建议用BCrypt而不是MD5加盐。MD5加盐虽然比明文好但是BCrypt每次生成的hash都带随机盐同样的密码两次加密结果不一样暴力破解成本高很多。Spring Security或者Shiro里都提供BCrypt实现这套源码中实际上也使用了BCrypt方式复制代码时注意别把PasswordEncoder漏掉。这里有几个坑我要特别提醒第一token过期时间别设太短也别太长。太短用户频繁掉线太长有安全风险。内网管理系统24小时比较合适如果你担心token被偷用建议在Redis中维护一个session状态token过期后强制重新登录。第二拦截器放行配置要非常小心。登录接口、验证码接口、前端静态资源、Swagger文档路径都要加白名单否则前端刷新一下页面就被重定向到登录页排查半天发现是拦截器把静态请求也拦了。第三前后端联调时最容易出的问题是token传了但没按规范带上Bearer前缀后端的解析逻辑要从Header中截取完整值再解析。4.2 病例上报核心流程多表写入的事务一致性病例上报不是简单insert一条记录。一次规范的填报除了主记录还要写审核日志、更新机构的日统计表如果有附件还要保存附件记录。这些操作要么全部成功要么全部失败。在Service实现类上加TransactionalTransactional(rollbackFor Exception.class) public Long createReport(ReportDTO dto) { ReportCase report new ReportCase(); BeanUtils.copyProperties(dto, report); report.setReportNo(generateReportNo()); report.setReportStatus(0); report.setReporterId(CurrentUser.get().getId()); reportMapper.insert(report); AuditLog audit new AuditLog(); audit.setReportId(report.getId()); audit.setActionType(1); // 创建 audit.setOperatorId(CurrentUser.get().getId()); auditMapper.insert(audit); statisticsMapper.updateDailyReport(report.getOrgId(), report.getReportDate(), 1); return report.getId(); }SpringBoot默认情况下Transactional只对RuntimeException回滚如果方法里抛出受检异常默认是不会回滚的。我这里显式指定rollbackFor Exception.class确保任何异常都触发回滚。这一点在真实业务中很重要尤其是后面接消息队列或者第三方接口时受检异常在调用链里很常见。再提一个容易被忽略的问题事务不要开在Controller层。Controller把事务注解加上虽然也能回滚但拦截器、参数校验等操作也会被纳入事务范围白白拉长数据库连接占用时间。把事务放在Service实现类才是标准姿势。4.3 Vue端权限控制路由守卫与按钮级指令前端要做两级权限。第一级是页面访问权限。用户登录后后端根据角色返回一个菜单树前端拿到菜单后动态注册路由。这样角色是填报医生的用户根本不会加载出“系统管理”页面。路由守卫的关键代码router.beforeEach((to, from, next) { const token getToken() if (token) { if (to.path /login) { next(/) } else { next() } } else { if (to.path /login) { next() } else { next(/login) } } })注意动态路由不能一进入系统就全部注册否则刷新页面路由丢失会出现白屏。常见做法是把后端返回的菜单数据缓存到store里刷新时先调一次获取用户信息接口再动态addRoutes。第二级是按钮级权限。比如审核按钮只能疾控科人员看到填报医生看不到。实现方式是用vue自定义指令把需要的权限编码挂在按钮上Vue.directive(permission, { inserted(el, binding) { const required binding.value const userPerms store.getters.permissions if (!userPerms.includes(required)) { el.parentNode.removeChild(el) } } })前端权限只是体验优化真正的安全边界在后端。后端接口必须在查询条件里加上机构ID和角色过滤防止登录用户直接改URL越权访问。4.4 数据可视化报表后端聚合、前端图表展示统计报表前端用的是ECharts。后端提供一个聚合接口返回结构化的统计数据前端拿到后直接塞给图表组件。后端统计接口伪代码GetMapping(/statistics/reportTrend) public ResultListMapString, Object reportTrend(String startDate, String endDate) { ListMapString, Object list statisticService.getReportTrend(startDate, endDate); return Result.success(list); }Vue页面中初始化图表时要注意在组件销毁时调用chart.dispose()否则页面来回切换会导致内存泄漏。现在很多图表库在SPA里的卡顿问题都出在没及时释放实例。我这里给出一个标准写法mounted() { this.chart echarts.init(this.$refs.chart) this.loadData() }, beforeDestroy() { if (this.chart) { this.chart.dispose() } }, async loadData() { const res await api.getReportTrend() this.chart.setOption({ xAxis: { data: res.data.map(item item.month) }, yAxis: {}, series: [{ type: line, data: res.data.map(item item.count) }] }) }5. 从零到一部署运行环境准备、数据库初始化与典型踩坑5.1 环境版本清单要跑起这套源码首要任务是版本匹配。我把推荐的版本列成一张表组件推荐版本说明JDK1.8Spring Boot 2.x默认支持Maven3.6依赖管理MySQL5.7或8.0建议8.0性能更好Node.js14或16vue-cli 4/5的稳定环境npm6可用国内镜像加速前端构建产物dist目录Nginx或SpringBoot static目录托管很多人上来就卡在版本问题。老源码跑不起来最常见的原因就是JDK版本太高Spring Boot 2.x在高版本JDK下会出现模块访问报错。如果pom.xml里spring-boot-starter-parent版本是2.2.x或2.3.x建议直接升到2.7.x同时保持JDK 1.8。Spring Boot 3.x改成了jakarta.*命名空间很多老代码的javax.*导入要批量替换那又是一轮折腾不建议在起步阶段碰。5.2 数据库初始化的正确姿势第一步创建数据库注意字符集CREATE DATABASE dd_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二步导入SQL文件mysql -u root -p dd_system dd_system.sqlWindows下执行这个命令之前先确认SQL文件本身是UTF-8编码cmd终端可以执行chcp 65001切到UTF-8代码页否则中文字段注释可能变成乱码。另外MySQL 8.0的认证插件默认是caching_sha2_password如果后端用的还是旧版mysql-connector-java启动时会报Public Key Retrieval is not allowed。解决办法是把驱动升级到8.x版本或者在JDBC连接串上加allowPublicKeyRetrievaltrue。数据库连接字符串里的时区参数也不能省。MySQL 8.0默认时区可能导致日期字段相差8小时我一般这样写url: jdbc:mysql://localhost:3306/dd_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue5.3 后端启动步骤与报错排查数据库准备好后打开application.yml检查数据源配置server: port: 8080 spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dd_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl启动命令mvn spring-boot:run或者打包后运行mvn clean package -DskipTests java -jar target/dd-system-1.0.0.jar我遇到过的后端启动报错基本就几类一是端口被占用报错信息里有Port 8080 was already in use。用netstat -ano | findstr 8080查PID然后结束进程。二是数据库连接失败检查MySQL服务有没有启动、密码对不对、防火墙有没有放行3306。如果本机装了多个MySQL实例端口可能不是3306要在连接串里改。三是MyBatis提示找不到SQL检查mapper-locations是否配置正确以及Mapper接口和XML文件的namespace是否完全匹配。四是SQL打印不出来看log-impl配置。StdOutImpl会直接在控制台输出SQL联调阶段很有用生产环境记得关掉或者改成logback输出。5.4 前端启动与打包部署前端环境准备阶段npm install是最大的一道坎。网络不好时建议用淘宝镜像npm config set registry https://registry.npmmirror.com npm install启动开发模式npm run serve开发模式下跨域靠vue.config.js代理解决module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }注意前端请求的接口地址必须带/api前缀后端Controller的RequestMapping也要统一用/api开头代理规则才生效。这个约定如果前后端不一致开发环境看起来能跑通一打包部署就白屏。构建生产包npm run build产物在dist目录。部署可以选择两种方式第一种是用Nginx托管静态文件反向代理APIlocation / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }try_files $uri $uri/ /index.html;这句必须有否则Vue用history模式路由时刷新二级页面会报404。第二种是把dist里的文件直接复制到SpringBoot的src/main/resources/static目录重新打包后端然后直接访问8080端口就能打开系统。这种方式适合内网临时使用但不适合正式环境因为静态文件和API混在同一个服务里不方便做独立扩容。5.5 部署后一定要验证的三件事系统跑起来之后不要急着提测先按真实业务走一遍主流程。我在上线前的检查清单基本固定登录后能否获取菜单和权限填报一个病例后审核流程能否正常流转统计报表的数字是否和数据库手工查出来一致。这三件事只要有一件对不上后面返工成本很高。尤其在统计报表这块我吃过亏。有一次系统上线后领导问某月份上报数系统显示100但Excel底档是103后来排查发现是统计口径没统一Excel里把草稿状态也算了进去。从那以后我的做法是先写统计SQL再用同样的条件在数据库里手工验证一次确认无误再给前端接图表。6. 生产环境必须注意的细节性能、安全与演进方向6.1 数据库索引和性能调优这套系统的核心业务表是病例上报表数据量一旦上十万查询速度就会明显下降。我建议在以下几个字段上建联合索引ALTER TABLE rep_case_report ADD INDEX idx_org_date_status (org_id, report_date, report_status);这个索引针对的是最常见的查询模式按机构查某段时间的上报记录同时过滤状态。创建索引后用EXPLAIN看一下执行计划EXPLAIN SELECT * FROM rep_case_report WHERE org_id 1 AND report_date 2024-01-01 AND report_status 2;执行计划里type应该至少是range或ref而不是ALL。如果是ALL说明全表扫描索引没建对或者SQL写法有问题。统计查询里尽量避免SELECT *只需要id、org_id、disease_id这些字段就只查这些。MySQL的联合索引可以把查询条件都覆盖在索引里减少回表次数这个优化对报表类接口特别明显。另外当月数据量特别大时可以考虑按月做分表比如rep_case_report_202401但分表会带来跨表查询复杂度前期数据量没到百万级别不建议引入。6.2 MyBatis缓存与N1查询问题MyBatis的缓存是面试题常客实际项目里也要小心用。一级缓存是SqlSession级别的同一事务内两次相同查询会命中缓存。这看起来是好事但如果你在事务里先查了一条记录然后通过别的SQL更新了这条记录再用同一个SqlSession查询拿到的是缓存里的旧值。所以事务内部要改数据时别依赖一级缓存的新鲜度。二级缓存是跨SqlSession的在Mapper XML里加cache/就开启了。听起来很爽但实际上如果表经常更新二级缓存反而会产生脏读。我的建议是只对基本不变化的基础数据表开二级缓存比如疾病字典、机构表病例上报这种频繁插入更新的表坚决不开。另外开二级缓存的实体类要实现Serializable接口否则反序列化时直接报错。N1问题是新手最容易踩的。比如查列表时先查10条病例再循环每一条的机构名称和疾病名称每条触发一条SQL一共11条查询。正确做法是一步联查或者用Mapper的嵌套结果映射。MyBatis支持类似resultMap idReportDetailMap typeReportCaseVO id propertyid columnid/ result propertypatientName columnpatient_name/ association propertyorg javaTypeSysOrg id propertyid columnorg_id/ result propertyorgName columnorg_name/ /association /resultMap这样一条SQL就能把主表和关联表的数据都装进来日志里SQL数量从11降到1接口响应时间能快一个数量级。6.3 安全加固事项卫生信息系统的数据涉及个人隐私安全不是可选项。第一是SQL注入防护。MyBatis的#{}是预编译参数直接用没问题但${}拼接的字段要特别小心。最典型的是排序字段用户传一个orderColumn如果直接ORDER BY ${orderColumn}就有注入风险。我的做法是做一个白名单映射后端收到字符串后先去Map里查对应的真实字段名查不到就返回默认字段。第二是接口越权。分页接口如果只按条件查询但没限定机构ID一个医生登录后把请求里的参数改一改就能看到其他机构的数据。所有业务查询都必须从token里解析当前用户所属机构SQL条件里强制带上机构范围。这个规矩在代码评审时要反复强调。第三是敏感数据脱敏。系统日志和接口返回值里手机号、身份证号不能全量打印。身份证至少要隐藏中间8位手机号隐藏中间4位。日志脱敏工具在logback里配置PatternLayout自定义规则或者干脆在DTO输出实体上对敏感字段打JsonSerialize注解。第四是附件上传限制。病例填报经常需要上传检验单截图上传接口必须校验文件扩展名白名单、文件大小上限、文件名过滤特殊字符。我见过一个项目因为没限制上传类型被人传了JSP木马直接导致服务器被控制这个教训很沉重。6.4 这套系统后续可以怎么扩展源码的优势在于可以基于完整骨架继续生长。我认为比较典型的扩展方向有几个一是对接上级平台。通过定时任务把审核通过的病例记录按标准XML或JSON格式推送出去或者用消息队列异步传输避免影响主业务流程。二是做可视化大屏。把统计接口的数据接到大屏模板上展示机构上报完成率、疾病趋势、物资库存实时预警对管理层汇报时非常加分。三是增加消息通知。病例被驳回、物资库存低于预警线时通过短信或企业微信通知到负责人。这个功能可以让系统从“被动录入”变成“主动提醒”。四是移动端适配。实际使用中医生更多在门诊间隙填报PC端操作不如手机方便。做一个H5版本或者小程序版本复用后端接口前端的表单校验和草稿本地缓存做一套就能用。五是集成Swagger或者Knife4j生成接口文档。前端和后端联调时接口文档是刚需。一个更新及时的接口文档能省下大量沟通成本特别是团队里有新人加入时。最后说点个人体会。这套系统从最初的需求调研到跑起来我最大的感触是业务口径必须在数据库设计阶段就定死。状态字段的取值、统计报表的过滤条件、机构层级关系这些如果一开始不明确后面每改一个地方都要波及好几张表和十几行SQL。我后来再做类似管理系统先把状态机和统计口径画在纸上找业务方逐个确认确认完再动手写代码返工次数明显少了很多。如果你也在做这类业务建议先花两天梳理流程不要急着开IDE这部分前置工作比写一万行代码都值钱。
返回列表