news 2026/9/20 21:10:29

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>

不需要额外引入 mybatis 依赖,starter 会自动带。application.yml里重点配置两项:逻辑删除和分页插件。逻辑删除是疫情管理系统的刚需,因为学生异动和上报记录不能物理删除,否则事后审计没法追溯。

spring: datasource: url: jdbc:mysql://localhost:3306/campus_epidemic?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: ${DB_PASSWORD:root} jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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 IService<HealthReport> { boolean reportToday(HealthReport report); Page<HealthReportVO> pageWithStudent(Page<HealthReport> page, String college, Integer isAbnormal); } @Service public class HealthReportServiceImpl extends ServiceImpl<HealthReportMapper, 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 模块默认请求方式是把pagelimit作为普通参数发到后端,返回格式要求是{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 Map<String, Object> page(@RequestParam(defaultValue = "1") long page, @RequestParam(defaultValue = "10") long limit, String college, Integer isAbnormal) { Page<HealthReport> p = new Page<>(page, limit); Page<HealthReportVO> result = reportService.pageWithStudent(p, college, isAbnormal); Map<String, 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 的pagescurrentoptimizeCountSql这些内部字段暴露给前端。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-event="detail"<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 class="layui-table-link" lay-event="studentDetail">{{ d.name }}</a>' }, { field: 'temperature', title: '体温', width: 80, templet: function(d) { if (d.temperature !== null && Number(d.temperature) >= 37.3) { return '<span style="color:#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 里手写了selectListselectPage对应的方法,并且拼接了${ew.customSqlSegment},同时查询条件里带了last或自己写了LIMIT,就会出现分页和自定义条件互相覆盖。

<select id="selectReportPageWithCollege" resultType="com.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 test="college != 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 LambdaQueryWrapper<HealthReport>() .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 排查时一眼能看懂。这一层算是"少即是多"——复杂状态机对校内审批场景属于过度设计,statusupdate_timeaudit_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: false

schema.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() { List<DailyStatistic> list = reportMapper.selectDailyGroupByCollege(LocalDate.now().minusDays(1)); statisticService.saveBatch(list); } }

日汇总表加定时任务,是一个从"功能能用"到"扛得住早高峰"的分水岭。如果不想加表,至少给统计 SQL 涉及的report_datecollege建联合索引。排查慢 SQL 用 MySQL 的slow_query_log即可,定位到具体 SQL 后,用EXPLAIN看 type 字段是否是ref以上,而不是ALL

6. 一个顺手的小技巧:用 MybatisPlus 的 condition 参数收敛查询条件

LambdaQueryWrappereq方法有一个重载接收boolean condition参数,这个参数在接 layui 表格筛选时特别好用。前端可能传college=空字符串isAbnormal=null,如果不处理就会变成WHERE college = '',导致查不到数据。常见写法是每个字段都if判断一次,代码又臭又长。

public Page<HealthReportVO> pageWithStudent(Page<HealthReport> page, String college, Integer isAbnormal) { LambdaQueryWrapper<HealthReport> 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 端口,剩下的交互细节在日志里见真章。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 21:10:02

用自然语言写量化策略:Vibe-Trading多智能体回测工作台解析

1. 为什么是“Vibe”&#xff1a;自然语言写策略背后的产品逻辑先聊一个我自己的困扰。做量化交易的人每天面对的是什么&#xff1f;不是行情&#xff0c;不是K线&#xff0c;而是代码。一个策略从想法到落地&#xff0c;中间隔着数据清洗、因子计算、回测框架、参数调优&#…

作者头像 李华
网站建设 2026/9/20 21:09:09

猫抓新手完全指南:浏览器视频下载与资源嗅探四步搞定

猫抓新手完全指南&#xff1a;浏览器视频下载与资源嗅探四步搞定 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 网页上的视频想存下来&#xff0c…

作者头像 李华
网站建设 2026/9/20 21:08:39

合肥四季沐歌太阳能上门维修电话|传感器故障排查|欧米到家咨询热线

太阳能热水器使用时间长了&#xff0c;容易出现不上水、水箱水位不准、水温升不上去、热水出得少、上水不停、仪表不显示、控制器报警、管道漏水、冬季冻堵、电加热不能使用等情况。尤其是合肥气候湿润、四季分明&#xff0c;多雨潮湿且冬季低温湿冷&#xff0c;部分家庭太阳能…

作者头像 李华
网站建设 2026/9/20 21:06:07

基于FFT与自适应滤波的语音分离实战指南

简介&#xff1a;本资源是一套基于MATLAB实现语音分离的完整代码方案&#xff0c;面向计算机、电子信息工程及数学等专业的本科生与研究生&#xff0c;解决课程设计、期末大作业及毕业设计中语音信号处理的实际问题。代码依托FFT频域分析&#xff0c;结合带通/带阻等可调滤波器…

作者头像 李华
网站建设 2026/9/20 21:05:22

宇通客车供应链变革:从诊断到落地的SCM体系升级之道

简介&#xff1a;《宇通客车供应链变革&#xff1a;诊断与解决之道》是一份聚焦SCM全球运营与供应链管理的PPT资源&#xff0c;适合供应链管理者、企业战略人员及物流管理专业学生研读。该PPT以宇通客车为案例&#xff0c;系统梳理了客车行业特征、企业供应链结构&#xff08;实…

作者头像 李华