ARTICLE DETAIL

资讯详情

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

校园疫情管理系统二次开发实践:数据模型、权限与性能优化

校园疫情管理系统二次开发实践:数据模型、权限与性能优化 简介这是一套基于 Spring Boot、MyBatis 与 MySQL 的校园疫情管理系统课程设计在若依开源框架上二次修改而来适合 Java Web 课程设计、毕业设计、期末实训及入门练手。系统包含个人中心、登录注册、学生信息管理、疫苗接种情况、健康打卡记录、请假审批管理等模块能将若依权限体系与实际业务场景结合便于理解后台管理系统的完整开发流程也可作为学习工程化项目组织的参考。整个压缩包共 1469 个文件约 9.07MB其中 Java 源码 257 个、HTML 页面 305 个、XML 配置 184 个另有前端 JS/CSS、SQL 脚本及启动脚本项目依赖与数据库脚本均已内置便于在 IDEA 中快速部署验证。目前已有 1344 人学习/下载。借助该项目可掌握若依脚手架的目录结构、MyBatis 数据持久化写法、疫情相关信息的增删改查与审批流转思路也能直接作为课程设计报告、答辩演示和二次开发的基础。1. 修改版校园疫情管理系统从信息登记表到可运营数据平台校园疫情管理系统的“修改版”这三个字在绝大多数情况下指的不是官方发布的新版本而是学校信息化团队或外包开发者在开源项目、历史遗留系统之上做的二次开发。换句话讲你拿到的往往是一套能跑但不敢直接用的代码基础表格齐全、打卡流程能走通但权限模型混乱、统计口径不一致、数据导出靠手工改 SQL。这类系统最核心的诉求是“用得住”每天几千人上报的健康数据不能丢、不能错辅导员能在五分钟内定位到体温异常的学生校领导看到的汇总数字和院系提交上来的报表对得上。本文会从数据模型设计、核心业务链路、权限与隐私、部署与性能优化四个维度把一套可运营的校园疫情管理系统应该有的骨架讲清楚。适合接手了这类系统但发现到处是坑的开发者也适合准备从零搭建但不想走弯路的技术负责人。文中涉及的代码以 Java Spring Boot 和 MySQL 为例但设计思路对 Python、Node.js 同样适用。2. 数据模型与数据库设计把健康打卡、出入校和异常上报装进同一套表结构2.1 为什么“改版系统”最先要动的一定是表结构校园疫情管理系统表面上是业务流程问题骨子里是数据模型问题。原版系统最常见的毛病是“一张大表打天下”把姓名、学号、学院、每日体温、是否咳嗽、是否接触过中高风险地区人员、当前位置全部塞进健康打卡表。这种设计在数据量小时没有任何问题但一旦需要回答“第三轮全员核酸应检未检名单”“7 天内有发热记录的学生分布在哪些宿舍楼”这类问题时SQL 会写得痛不欲生。另一个典型问题是缺少维度表。学院、专业、班级、宿舍楼这些信息散落在用户表或打卡表的备注字段里导致统计时需要大量的字符串匹配。修改版优先要做的是把业务实体拆开用户维度一张表、健康上报一张表、出入校申请一张表、异常记录一张表再加一张字典表维护学院、风险等级、症状类型等可枚举数据。2.2 核心表字段设计与 MySQL 建表脚本2.2.1 用户表与组织维度拆分用户表不直接存学院名称和班级名称而是存组织 ID。组织表采用 parent_id 自关联结构一级节点是学校二级是学院三级是专业或年级四级是班级。这样的设计让“按学院汇总”“按班级筛选”都只需要一次递归查询或路径匹配。学生、辅导员、院系管理员、校防控办成员统一放在同一张用户表里用 role_type 区分。2.2.2 健康上报表与出入校表的字段取舍健康上报表每名学生每天一条记录设计上必须有唯一约束防止重复提交。体温字段用 DECIMAL(4,2) 而不是 FLOAT避免浮点误差。症状字段用多选编码例如 1发热、2咳嗽、4乏力用整型位运算存储既能满足多选又不引入额外的关联表。出入校申请表必须留审批链路字段当前审批人、审批状态、审批时间戳否则后续做审批流时会被迫改表。2.2.3 可执行的建表脚本与参数说明-- 组织维度表支持学校-学院-专业-班级四层结构 CREATE TABLE sys_org ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父级组织ID顶级为0, org_name VARCHAR(100) NOT NULL COMMENT 组织名称, org_level TINYINT NOT NULL COMMENT 1学校 2学院 3专业 4班级, path VARCHAR(500) NOT NULL DEFAULT COMMENT 从根到当前节点的ID路径如 /1/23/456, status TINYINT NOT NULL DEFAULT 1 ); -- 每日健康上报主表一人一天一条 CREATE TABLE health_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, report_date DATE NOT NULL, temperature DECIMAL(4,2) NOT NULL COMMENT 体温范围 35.00 ~ 42.00, symptom_mask TINYINT NOT NULL DEFAULT 0 COMMENT 症状位图1发热 2咳嗽 4乏力, risk_level TINYINT NOT NULL DEFAULT 0 COMMENT 行程风险0正常 1低风险 2中风险 3高风险, location_code VARCHAR(20) NOT NULL COMMENT 当前所在地行政区划代码, report_time DATETIME NOT NULL, UNIQUE KEY uk_user_date (user_id, report_date) );建表有两个关键参数需要说明temperature 字段用 DECIMAL(4,2) 而非 DOUBLE是因为浮点数在范围比较时会出精度问题用 DECIMAL 存精确小数查询temperature 37.3不会漏数据。symptom_mask 用 TINYINT 位图目的是把原本需要多对多关联表的“多选症状”压缩成一个字段查询时用symptom_mask 1判断是否发热既省空间又能在异常筛选时走索引。2.3 索引设计哪些字段必须建组合索引健康上报表最频繁的查询模式是“某天内体温异常的人”和“某人某段时间的上报历史”。前者建议建(report_date, temperature)组合索引后者建议建(user_id, report_date)组合索引这与唯一约束的 uk_user_date 正好复用。出入校申请表按“审批人 状态”查询最多建(approver_id, status, create_time)组合索引。注意不要对 status 这类低基数字段单独建索引MySQL 优化器大概率会放弃它组合索引中把它放在第二或第三位更合理。3. 核心功能实现健康打卡闭环、异常预警与出入校审批3.1 健康打卡的幂等设计每日健康打卡是系统压力最大的接口。早上 7 点到 9 点是提交高峰如果接口不做幂等控制学生端网络抖动重试一次就会生成两条记录。常规做法是在 Service 层用“先查后插”配合数据库唯一索引兜底。先查的逻辑不是简单 select 一次而是利用 INSERT ... ON DUPLICATE KEY UPDATE 保证只有第一次插入生效后续重复提交只更新上报时间。Transactional public boolean submitHealthReport(HealthReportDTO dto) { HealthReport report new HealthReport(); report.setUserId(dto.getUserId()); report.setReportDate(LocalDate.now()); report.setTemperature(dto.getTemperature()); // 构造唯一索引冲突时的更新行为 String sql INSERT INTO health_report (user_id, report_date, temperature, symptom_mask, risk_level, location_code, report_time) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE temperature VALUES(temperature), report_time VALUES(report_time); // 这里用 JdbcTemplate 执行返回值为 1 表示插入2 表示更新 return jdbcTemplate.update(sql, ...) 0; }这个做法的精妙之处在于把幂等控制从应用层下沉到了数据库层。即使两个请求同时通过代码里的 select 判断最终插入时唯一索引也只会放行一条。接口响应上第一次提交返回“提交成功”重复提交返回“今日已提交信息已更新”前端根据返回码区分提示文案即可。3.2 异常预警从“人等数据”变成“数据找人”异常预警的常见误区是每次查询都全表扫描“今天所有上报记录”然后在内存里逐个判断体温是否大于 37.3。正确做法是建立预警规则表把判定逻辑从代码里抽出来让运营人员可以在后台配置阈值。规则表结构包含规则名称、比对字段、操作符、阈值、通知对象模板。规则引擎执行时使用一个每分钟跑一次的定时任务扫描最近 15 分钟内新提交的上报记录将体温 37.3、症状位图非 0、风险等级 2 的三种情况分别命中不同规则然后通过消息队列异步推送通知给对应班级的辅导员和校医院值班人员。-- 预警规则表设计 CREATE TABLE alert_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL, field_name VARCHAR(30) NOT NULL COMMENT 匹配字段temperature / symptom_mask / risk_level, operator VARCHAR(10) NOT NULL COMMENT gt / gte / eq / bitand, threshold_value VARCHAR(10) NOT NULL COMMENT 阈值位图规则填掩码, notify_role VARCHAR(20) NOT NULL COMMENT 通知对象角色counselor / doctor / admin, notify_template TEXT NOT NULL COMMENT 消息模板占位符用 {name} {student_no} {value}, is_active TINYINT NOT NULL DEFAULT 1 );预警链路里最容易忽略的是触达确认。消息推送到辅导员微信或短信后系统需要记录一条待办任务辅导员查看后标记“已处理”超过 30 分钟未处理的自动升级到院系管理员。这样整个流程是可追踪的而不是消息发出去就结束了。3.3 出入校审批状态机比 if-else 可靠出入校申请涉及的状态有待审批、辅导员通过、辅导员驳回、院系通过、院系驳回、已离校、已返校。直接用 if-else 判断当前状态允许执行哪些操作代码会随着状态增多迅速腐化。推荐用轻量级状态机定义当前状态、触发事件、目标状态、允许的操作者四个元素的二维表。// 状态机核心配置当前状态 事件 - 目标状态 MapStateEvent, State transitions Map.of( new StateEvent(State.PENDING, Event.COUNSELOR_APPROVE), State.SCHOOL_APPROVED, new StateEvent(State.PENDING, Event.COUNSELOR_REJECT), State.REJECTED, new StateEvent(State.SCHOOL_APPROVED, Event.ADMIN_APPROVE), State.APPROVED, new StateEvent(State.APPROVED, Event.LEAVE_SCHOOL), State.LEFT, new StateEvent(State.LEFT, Event.RETURN_SCHOOL), State.RETURNED );状态机的好处是非法流转在配置层面就被拒掉比如辅导员驳回后不允许直接进入已离校状态。每次状态变更写入一条 flow_log记录操作人、操作时间、备注方便事后追溯。这里注意一点状态机的校验必须在事务里完成先查当前状态再执行更新配合行级锁防止并发下两个人同时审批通过同一张申请单。4. 权限模型与隐私保护RBAC 落地和健康数据脱敏4.1 五种角色的权限边界校园疫情管理系统涉及的角色至少有学生、辅导员、院系管理员、校防控办、系统管理员。RBAC 模型在这里不能只做到“角色-权限”两级因为同是辅导员只能看自己带的学生数据同是院系管理员只能看本学院数据。所以必须引入“数据范围”概念。权限设计的核心是两层过滤第一层是功能权限决定用户能访问哪些菜单和接口第二层是数据权限决定用户在某个接口里能看到哪些行的数据。学生角色只能查询自己的上报记录辅导员的数据权限是“所在班级的学生”院系管理员是“本学院所有专业和班级”校防控办是全校数据但操作受限只能查看和导出不能修改。4.2 基于 MyBatis 拦截器的数据权限自动拼接数据权限如果写在每个业务方法的 SQL 里会漏且难以维护。常见的做法是用 MyBatis 拦截器在 SQL 执行前自动拼接数据范围条件。拦截器解析当前登录用户的角色和归属组织如果是辅导员就在 SQL 的 WHERE 条件后面追加AND user_id IN (SELECT id FROM sys_user WHERE org_id IN (当前辅导员管理的班级ID集合))。Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String sql boundSql.getSql(); // 从上下文获取当前用户数据权限childOrgIds 是当前用户可管理的组织ID列表 DataScopeContext ctx DataScopeContext.get(); if (ctx ! null ctx.needFilter() sql.contains(health_report)) { String newSql sql AND user_id IN (SELECT id FROM sys_user WHERE org_id IN ( ctx.childOrgIds() )); // 通过反射替换 BoundSql 中的 SQL Field field BoundSql.class.getDeclaredField(sql); field.setAccessible(true); field.set(boundSql, newSql); } return invocation.proceed(); } }这种全局拦截方案需要注意死循环问题拦截器拼进去的子查询如果也命中拦截规则会造成无限递归。解决办法是在查询的数据权限表、用户表、组织表时设置一个标识位让拦截器跳过或者在 SQL 特征中加上/* no-scope */注释标记拦截器检测到该注释直接放行。4.3 隐私合规健康数据的字段级加密与展示脱敏体温、症状、行程风险等级属于敏感健康数据数据库存储必须加密。推荐用字段级 AES 加密而非整表加密因为整表加密会让普通查询的索引失效。体温字段加密后原来temperature 37.3的查询无法直接执行所以需要在应用层做两层设计数据库里明文和密文各存一份或者加一个冗余的“是否异常”标记列查询异常名单走标记列查询详细体温走解密。展示层脱敏同样重要。校防控办导出全校数据时手机号要保留前 3 位和后 4 位中间用星号代替学生的家庭住址只显示到区和街道级别。脱敏操作放在服务端返回前统一处理而不是在 SQL 层截断否则内部人员在排查问题时查数据库会看到完整明文。5. 定时任务与数据大屏让每日汇总从手工统计变成可验证的自动化流程5.1 未上报名单的定时扫描与催报校园疫情管理系统每天都有硬性任务截至上午 10 点统计哪些学生未完成健康打卡并通知辅导员催报。实现方案是 Spring Boot 的 Scheduled 定时任务配合任务状态表防止重复执行。任务执行时先查询“今天已上报的学生 ID 集合”再查“所有在校状态学生的 ID 集合”做差集得到未上报名单按辅导员分组推送提醒。任务状态表记录每次执行的开始时间、结束时间、处理人数、执行结果。这个表看起来多余但没有它定时任务在重启恢复时会重复触发学生会收到多条催报通知。-- 定时任务执行日志表 CREATE TABLE task_execution_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_name VARCHAR(50) NOT NULL, trigger_time DATETIME NOT NULL, finish_time DATETIME, total_count INT, success_count INT, fail_count INT, status TINYINT COMMENT 1执行中 2成功 3失败 );5.2 数据大屏的接口设计原则学校领导层看的数据大屏压力来自两个方向一是首次打开时加载全年趋势数据二是每隔 30 秒轮询当天实时上报率。全年日均体温曲线这类不常变化的数据应该在每日凌晨定时把聚合结果刷入大屏汇总表查询接口只读汇总表当天实时数据走 Redis 缓存上报接口写入时同步更新 Redis 计数器大屏接口从 Redis 读取。大屏接口的响应速度目标应该在 200ms 以内。如果超过这个值优先检查查询是否命中索引其次看是否需要引入独立的聚合表。不要把大屏查询接口直接打在明细表上明细表数据量达到千万级后即使有索引COUNT(*)和GROUP BY也会拖垮业务库。5.3 报表导出的异步化大屏之外辅导员和院系管理员每天都要导出 Excel 报表。Excel 导出不能在请求线程里同步生成因为数据量大时生成耗时几十秒前端请求早超时了。做法是提交导出任务到线程池生成文件后上传到对象存储前端轮询任务状态完成后直接下载。导出模块还需要做行数上限保护超过 5 万行的查询必须分页查询并限制单次导出大小否则 JVM 内存直接被打满。6. 上线前验证用并发压测和故障演练检验系统的真实承载力6.1 用 Locust 模拟全校 2 万人在 30 分钟内集中打卡校园疫情管理系统真正会让系统崩掉的场景只有一个早上打卡高峰。压测不能只测单接口的最大 QPS要模拟真实节奏在指定的 30 分钟窗口内把 2 万个用户按照正态分布逐步发起打卡请求。关注的核心指标不是平均响应时间而是 P95 响应时间和错误率。压测结果如果 P95 超过 800ms优先排查数据库连接池。Spring Boot 默认的 HikariCP 最大连接数是 10在 2 万人集中提交时数据库连接必然成为瓶颈。建议把 maximum-pool-size 调到与 CPU 核数相关的值cores * 2 1同时打开连接池的 leak-detection-threshold及时发现连接泄漏。6.2 三个最容易忽略的故障点第一个是数据库主从延迟。健康打卡的写入走主库统计查询走从库如果从库同步延迟超过 5 秒学生刚提交的打卡在“我的记录”里看不到会反复提交。解决方法是强制写后读走主库或者设置从库同步延迟告警。第二个是 Redis 缓存击穿。未上报名单和全校已上报人数这些热点 key 如果同时过期会把压力全部打到数据库。用互斥锁重建缓存或者给缓存 key 的过期时间加随机偏移两者选其一即可。第三个是消息队列表膨胀。预警通知如果推送失败重试机制要设置最大重试次数和死信队列否则失败消息堆积在 MySQL 表中会拖慢所有消息的消费速度。每天凌晨清理超过 7 天的执行日志和消息记录保持表体积稳定。6.3 最后一步核对统计口径系统上线前做一次全链路核对手工选 3 个学院用 SQL 直接从明细表统计今日上报率、发热人数、出入校申请数与大屏展示数据和辅导员导出的 Excel 对应。这三个数字对不上说明某个环节存在缓存不一致或脱敏逻辑覆盖遗漏。把统计口径核对写成一个自动化巡检脚本每天凌晨跑一次差异超过阈值自动告警而不是依赖人工发现。本文还有配套的精品资源点击获取
返回列表