ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

SpringBoot+MybatisPlus+layui 构建校园疫情管理系统的完整实践

SpringBoot+MybatisPlus+layui 构建校园疫情管理系统的完整实践 简介这是一套基于Spring Boot、MyBatis-Plus与Layui的校园疫情管理系统面向高校信息化管理人员、Java全栈学习者以及毕业设计开发者。系统围绕疫情背景下的校园管理需求完成健康数据采集、审批流转、多角色权限管控等核心功能。资源包为RAR压缩格式共850个文件整体容量约58.11MB主要文件包括Java源码、XML映射与配置文件、HTML页面、CSS样式、JavaScript脚本、图片图标以及SQL数据库脚本等目录结构完整导入IDE即可运行或二次开发。在功能层面系统接入了腾讯API可展示中国疫情确诊分布地图和实时疫情新闻并通过饼图、折线图、柱状图对疫情数据进行可视化集成Shiro安全框架实现学生、教师、院系管理员等不同角色的动态菜单权限分配。业务模块覆盖健康打卡、疫情数据管理、院系班级管理、核酸检测管理、疫苗接种管理以及学生请假串行审批等同时支持Excel拖拽上传与导出具备条件查询与分页的增删改查能力。资源包已有1122人学习适合用作毕业设计参考也可帮助开发者深入理解RBAC权限设计与数据可视化实现。1. 校园疫情管理系统为什么还在用 SpringBoot 三件套校园疫情管理系统不只是一张今日健康打卡表。它的真实形态是学生每日上报体温和行程、辅导员审核异常、校医跟进隔离状态、管理员看全院统计大屏。这句话拆开之后你会发现它同时踩中了三个后端项目最常见的问题数据写入频繁但逻辑简单、权限层级多但又不适合上重型工作流、统计查询要快但表结构天天变。SpringBoot MybatisPlus layui 这套组合恰恰把这三件事的性价比做到极致SpringBoot 管好接口生命周期和依赖注入MybatisPlus 用 ActiveRecord 风格把单表 CRUD 压到几乎没有样板代码layui 自带表格、弹层、日期组件不需要前端构建链就能在真实服务器上跑起来。对做毕设、做课设、做中小型校内系统的开发者来说这是最快能落地的路线。下面要写的不是项目介绍而是一套可以照着抄的落地方案从表设计、代码分层、layui 表格对接到分页失效、日期控件限制、自动建表这几个高频坑一次说清。2. SpringBoot 与 MybatisPlus 的数据层设计先定表再写代码2.1 表结构设计的取舍疫情管理系统的核心数据无非四类学生基础信息、每日健康上报、出校审批、异常记录。这四类数据在真实业务里的特点是字段多但都不复杂、状态流转明确、查询条件基本都是学院 日期 状态。所以表设计不需要过度范式化反而应该把高频查询字段直接冗余到主表里。CREATE TABLE tb_student ( id bigint(20) NOT NULL AUTO_INCREMENT, student_no varchar(32) NOT NULL COMMENT 学号, name varchar(32) NOT NULL, college varchar(64) DEFAULT NULL COMMENT 学院, grade varchar(16) DEFAULT NULL COMMENT 年级, phone varchar(16) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1在校 0离校, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE tb_health_report ( id bigint(20) NOT NULL AUTO_INCREMENT, student_id bigint(20) NOT NULL, report_date date NOT NULL COMMENT 上报日期, temperature decimal(4,2) DEFAULT NULL COMMENT 体温, health_code varchar(16) DEFAULT NULL COMMENT 绿码/黄码/红码, is_abnormal tinyint(1) DEFAULT 0 COMMENT 1异常 0正常, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_date_status (report_date, is_abnormal) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康上报表;字段不需要一个都不剩地照抄但两个点要注意一是report_date用 date 而不是 datetime因为业务维度就是按天上报用 datetime 反而让统计查询多一层转换二是唯一索引必须加在(student_id, report_date)上否则学生一天重复提交会留脏数据。MybatisPlus 的saveOrUpdate无法天然防重复最终要靠这个唯一索引兜底。2.2 SpringBoot 集成 MybatisPlus 的最小配置MybatisPlus 3.5.x 是目前主流版本SpringBoot 2.7.x 对应 JDK8 项目足够。在pom.xml里只需要引入两个依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId /dependency不需要额外引入 mybatis 依赖starter 会自动带。application.yml里重点配置两项逻辑删除和分页插件。逻辑删除是疫情管理系统的刚需因为学生异动和上报记录不能物理删除否则事后审计没法追溯。spring: datasource: url: jdbc:mysql://localhost:3306/campus_epidemic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:root} jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl密码这一行用了${DB_PASSWORD:root}的占位符形式意思是环境变量里有 DB_PASSWORD 就用它没有就 fallback 到 root。这个习惯尤其在部署到服务器后非常重要因为库里可能有学生手机号和身份证号明文密码写进 yml 再推到代码仓库就是安全事故。如果还想更稳妥一点用 Jasypt 把密码整体加密成密文串springboot yml密文是这个方向最常见的检索词。不过对于小项目环境变量占位符已经够用。2.3 Service 层代码别把所有逻辑堆在 Controller一个常见坏味道是 Controller 里直接注入 Mapper 然后写 CRUD。疫情管理系统的业务规则比 CRUD 多一点比如学生只能改自己当天的上报辅导员只能看本院数据这些必须收敛到 Service 层。MybatisPlus 的IService接口加上ServiceImpl实现类的写法public interface HealthReportService extends IServiceHealthReport { boolean reportToday(HealthReport report); PageHealthReportVO pageWithStudent(PageHealthReport page, String college, Integer isAbnormal); } Service public class HealthReportServiceImpl extends ServiceImplHealthReportMapper, HealthReport implements HealthReportService { Override public boolean reportToday(HealthReport report) { report.setReportDate(LocalDate.now()); report.setIsAbnormal(report.getTemperature() ! null report.getTemperature().compareTo(new BigDecimal(37.3)) 0 ? 1 : 0); return saveOrUpdate(report); } }这段代码的核心逻辑不在saveOrUpdate而在setIsAbnormal之前的那行判断。体温是否异常是一个业务规则把它放在 Service 层意味着将来调整阈值时只改一个地方。前端拿到上报成功与否就够了不应该由前端告诉后端这个人异常了因为前端可以被绕过。另外注意report.setReportDate(LocalDate.now())后端强制取服务器日期不信任客户端传的日期否则学生会改手机系统时间补填昨天的记录。3. layui 表格与后端接口的联调日期、分页、行点击事件3.1 layui 的 table 模块和 SpringBoot 分页接口对齐格式layui 的 table 模块默认请求方式是把page和limit作为普通参数发到后端返回格式要求是{code:0, msg:, count:100, data:[]}。这个约定和 SpringBoot 的 Page 对象天然匹配只需在 Controller 里做一个组装不要直接把 MybatisPlus 的 Page 对象返回出去。RestController RequestMapping(/api/report) public class HealthReportController { Autowired private HealthReportService reportService; GetMapping(/page) public MapString, Object page(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long limit, String college, Integer isAbnormal) { PageHealthReport p new Page(page, limit); PageHealthReportVO result reportService.pageWithStudent(p, college, isAbnormal); MapString, Object map new HashMap(); map.put(code, 0); map.put(msg, ); map.put(count, result.getTotal()); map.put(data, result.getRecords()); return map; } }把 Page 对象转成 Map 返回一方面是为了匹配 layui 的协议另一方面是避免把 MybatisPlus 的pages、current、optimizeCountSql这些内部字段暴露给前端。defaultValue要显式写上否则前端第一次加载表格没传 page 时SpringBoot 会直接 400。layui 表格默认每页 10 条但统计页通常需要更大所以前端 render 里我会显式写明page: true, limit: 20。3.2 layui 日期控件限制最大日期为今天健康上报表格里日期范围查询是最常用筛选条件。layui 的laydate组件默认允许选未来日期如果不限制用户选到明天甚至下个月查询结果永远为空而且会让人以为是 Bug。限制最大日期的写法是laydate.render({ elem: #reportDateRange, type: date, range: true, max: laydate.now() });max: laydate.now()是最容易被忽略的一行。laydate 的max参数支持三种格式固定字符串2025-12-31、函数laydate.now()、以及带偏移的写法max: -1。laydate.now()取的是客户端本地时间而查询接口对日期范围的上限约束在后端还要再校验一次。if (endDate.isAfter(LocalDate.now())) { throw new BizException(结束日期不能晚于今天); }前端限制只是体验优化后端校验才是数据正确性的保障。凌晨跨天时最容易出问题前端页面开着没过期但到了零点之后now()已经是新的一天用户选了昨天作为结束日期前端不会拦截后端如果不校验就直接放行了。所以在后端写这个校验不是形式主义。3.3 table 列点击事件不只是加个监听器需求里常见的点击学生姓名弹出详情放在 layui 里有两种做法。一种是在cols配置里给列加templet模板把名字渲染成带lay-eventdetail的a标签然后在table.on(tool(filter), ...)回调里操作。另一种是cellClick事件区别在于前者只能命中设置了lay-event的 HTML 元素后者可以精确到某个单元格。实际开发中我推荐第一种。table.render({ elem: #reportTable, url: /api/report/page, page: true, cols: [[ { field: studentNo, title: 学号, width: 120 }, { field: name, title: 姓名, width: 100, templet: a classlayui-table-link lay-eventstudentDetail{{ d.name }}/a }, { field: temperature, title: 体温, width: 80, templet: function(d) { if (d.temperature ! null Number(d.temperature) 37.3) { return span stylecolor:#FF5722 d.temperature ℃/span; } return d.temperature ℃; }}, { field: createTime, title: 上报时间, width: 170 } ]] }); table.on(tool(reportTable), function(obj) { if (obj.event studentDetail) { showStudentModal(obj.data.studentId); } });templet里的d就是当前行数据用字符串模板直接拼 HTML或者用函数方式返回带样式的结构。体温正红色显示这个细节可以一眼筛出异常记录比多写一列isAbnormal再判断更直观。注意tool监听的lay-filter值要和table.render里的elem对应如果 render 时没有写id: reportTable事件绑定是无效的。事件名和过滤器的对应关系是 layui 初学者最容易卡壳的地方。4. 单表 500 条限制、分页失效的表单提交问题4.1 超过 500 条数据 MybatisPlus 单页查不出来先确认是不是 SQL 写错接触mybatisplus单页500条限制这个热搜词反映了一个常见现象列表页翻到第二页之后数据重复或缺失。先说结论MybatisPlus 本身没有 500 条分页限制出问题的地方大多在PaginationInnerInterceptor没有注册。分页插件必须显式声明Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType.MYSQL会自动把分页改写为LIMIT没配这个 Bean 时Page传进去也是全表查询。还有一个隐蔽场景自定义 SQL 里的分页失效。当你在 Mapper.xml 里手写了selectList或selectPage对应的方法并且拼接了${ew.customSqlSegment}同时查询条件里带了last或自己写了LIMIT就会出现分页和自定义条件互相覆盖。select idselectReportPageWithCollege resultTypecom.example.vo.HealthReportVO SELECT r.*, s.name, s.college FROM tb_health_report r LEFT JOIN tb_student s ON r.student_id s.id where if testcollege ! null and college ! AND s.college #{college} /if /where ORDER BY r.create_time DESC /select分页拦截器是拦截执行层面的 SQL把整条查询包一层COUNT再追加LIMIT。上面的自定义 SQL 带LEFT JOIN如果发现总数对但列表数据偏移多半是 COUNT 语句里把 LEFT JOIN 关联出来的重复行算进去了。排查方法很简单把 MybatisPlus 日志打开看打印出来的 COUNT SQL 是什么单独拿去数据库执行一眼就能看出 count(*) 和表实际行数对不上。4.2 表单重复提交导致一天多条上报记录学生端最常见的一个 Bug 是点提交后接口响应慢心急再点一次结果生成了两条同一天的上报。这个问题在reportToday这个方法里用saveOrUpdate是防不住的因为第一次请求和第二次请求到达 Service 时数据库中都不存在当天记录两个线程都走 INSERT。数据库唯一索引uk_student_report_date会拦截第二次 INSERT 并抛 DuplicateKeyException。public boolean reportToday(HealthReport report) { try { return saveOrUpdate(report); } catch (DuplicateKeyException e) { // 再次尝试更新避免直接报错给用户 report.setId(getBaseMapper().selectOne(new LambdaQueryWrapperHealthReport() .eq(HealthReport::getStudentId, report.getStudentId()) .eq(HealthReport::getReportDate, LocalDate.now())).getId()); return updateById(report); } }前端按钮加上lay-disable状态在 ajax 回调前禁止重复点击只是缓解。真正的防线是唯一索引加 DuplicateKeyException 捕获二者缺一不可。注意捕获异常后的二次更新里report.getId()可能是 null需要先从库里把主键查出来再updateById这也是为什么建议保留唯一索引而不是去掉。4.3 出入校审批的状态流转不要用 int 裸奔出校审批的状态字段如果直接存0/1/2过三个月没人记得 1 是待辅导员审批还是待校医审批。常见做法是用枚举类管理public enum ApproveStatus { PENDING(0, 待审批), PASS(1, 已通过), REJECT(2, 已驳回); }MybatisPlus 默认的枚举映射需要加EnumValue注解。但更简单的方案是直接存字符串状态码PENDING / PASS / REJECT可读性比数字好得多而且在 SQL 排查时一眼能看懂。这一层算是少即是多——复杂状态机对校内审批场景属于过度设计status加update_time加audit_remark就足够审计了。5. 部署前的三个必查点字段自动建表、接口安全、统计口径5.1 要不要让 MybatisPlus 自动建表搜索热词里出现表不存在自动建表说明不少项目在开发阶段被建表 SQL 折磨过。MybatisPlus 本身没有内置 DDL 自动建表能力它只管 ORM。常见做法有两种一是集成mybatis-plus-ddl这类插件启动时比对实体类注解建表二是在application.yml配置spring.sql.init.mode: always配合schema.sql在启动时执行。我的建议是第二种因为 DDL 脚本可以被版本管理改动记录清楚。spring: sql: init: mode: always schema-locations: classpath:db/schema.sql continue-on-error: falseschema.sql里写CREATE TABLE IF NOT EXISTS启动报错时continue-on-error设为 false 可以快速暴露问题。生产环境务必把mode改成never否则每次启动都可能覆盖变更。5.2 接口层面的最小安全策略校园系统在公网暴露的情况不多但一旦暴露最先被扫描的就是/actuator/heapdump、/api/report/page这种无鉴权接口。最轻量的做法是加一个 HandlerInterceptor 做 token 校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !TokenStore.isValid(token)) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\unauthorized\}); return false; } return true; } }项目里如果引了 spring-boot-starter-actuator建议把暴露端点全部关掉或者至少加management.endpoints.web.exposure.include: health避免 heapdump 敏感信息被拉走。技术上你不是在处理理论威胁而是在处理真实发生过的事故——这个风险点已经不属于可选项了。5.3 统计报表的 Slow SQL 排查管理员首页的今日上报率各学院异常人数这类统计优化空间比想象中大。一个常见坏味道是前端加载页面时同时发十几个统计请求每个请求都执行一次GROUP BY COUNT数据库压力全在运行时的计算上。正确做法是加一张tb_report_statistics日汇总表每晚定时任务统计一次前端只查这张表Component public class StatisticTask { Scheduled(cron 0 10 0 * * ?) public void generateYesterdayStatistic() { ListDailyStatistic list reportMapper.selectDailyGroupByCollege(LocalDate.now().minusDays(1)); statisticService.saveBatch(list); } }日汇总表加定时任务是一个从功能能用到扛得住早高峰的分水岭。如果不想加表至少给统计 SQL 涉及的report_date和college建联合索引。排查慢 SQL 用 MySQL 的slow_query_log即可定位到具体 SQL 后用EXPLAIN看 type 字段是否是ref以上而不是ALL。6. 一个顺手的小技巧用 MybatisPlus 的 condition 参数收敛查询条件LambdaQueryWrapper的eq方法有一个重载接收boolean condition参数这个参数在接 layui 表格筛选时特别好用。前端可能传college空字符串、isAbnormalnull如果不处理就会变成WHERE college 导致查不到数据。常见写法是每个字段都if判断一次代码又臭又长。public PageHealthReportVO pageWithStudent(PageHealthReport page, String college, Integer isAbnormal) { LambdaQueryWrapperHealthReport wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(college), HealthReport::getCollege, college) .eq(isAbnormal ! null, HealthReport::getIsAbnormal, isAbnormal) .orderByDesc(HealthReport::getReportDate); return baseMapper.selectReportPageWithCollege(page, wrapper); }.eq(condition, column, value)里 condition 为 false 时整段条件不拼接到 SQL。这个 API 是 MybatisPlus 比较实用的设计之一能让查询接口少写 30% 的 if 判断。注意StringUtils.hasText是 Spring 提供的工具方法如果你在非 Spring 环境测试这段代码换成college ! null !college.isEmpty()也一样。另外一个细节是orderByDesc要放在最后否则LIMIT分页在排序字段有相同值时会出现翻页数据重叠这也是为什么要在排序字段后面追加一个唯一字段比如orderByDesc(HealthReport::getReportDate).orderByDesc(HealthReport::getId)。加上唯一字段兜底后翻页重复率基本归零。整套方案写到这里从表设计到前端联调再到部署排错核心链路已经完整覆盖。剩余的工作就是建两个测试账号跑一遍学生上报—辅导员审核—管理员看统计的主流程把异常体温阈值从 37.3 改成 37.5 验一下规则是否真的只改一处。运行mvn spring-boot:run把项目拉起来打开本地 8080 端口剩下的交互细节在日志里见真章。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表