基于Spring Boot的智慧校园防疫信息服务平台设计与实现

发布时间:2026/9/28 9:21:34
基于Spring Boot的智慧校园防疫信息服务平台设计与实现 做这个项目的起因很直白大学里每天的健康上报靠辅导员手动汇总实在太折磨人了。早上起来群里催一遍收上来的数据有Excel、有群接龙、有私聊的纯数字光统一格式就要花掉大量时间。更要命的是就算某个学生体温偏高靠人眼逐条去盯也很难及时发现。这个Java毕设项目就是在这样的背景下做的——一个智慧校园防疫信息服务平台把每日健康上报、异常预警、通行码、数据统计报表串成一个完整闭环整个流程从人工核对变成系统自动跑。它不是简单做个增删改查而是把现实中真实会踩的坑都趟了一遍重复提交、并发打卡、数据权限、报表导出刚好也是Java面试里高频考察的点。如果你正在挑毕设选题或者想找个能写进简历的完整Java项目这篇文章把这个项目的设计思路、核心实现和踩坑记录完整拆开可以直接照着落地。1. 项目启动前从手工统计到线上闭环的需求设计1.1 需求方真正想要的是什么项目启动前我找了几位辅导员和院系信息员聊过他们反馈的诉求听上去很简单能不能少花点时间收数据但深挖下去会发现少花时间收数据背后其实藏着四个层面的缺口。第一个是收集渠道分散。学生习惯五花八门有人用在线文档、有人发消息、有人让同学代填数据散在各个入口汇总本身就费劲。第二个是汇总口径不统一。有的人填的是当天早上体温有的人填的是昨晚体温还有的人随便填了个数字应付了事这些人眼判断的成本往往被低估了。第三个是异常发现滞后。体温超过37.3度、有接触史、行程涉及风险区域这些关键信息混在几百条记录里靠人工筛查既容易漏又不够及时。第四个是流程不闭环。常规做法是上报完就结束了异常的人有没有被跟进处理、后续复查结果如何没有完整记录事情常常不了了之。所以这个系统的需求不能简单定义为做一个打卡功能而是要回答一个问题怎么把上报、甄别、预警、处置、统计这一个链条完整线上化。后来我把需求总结成一句话系统应当自动完成信息的采集、判定、通知和留痕把人工介入的环节压缩到最小。所有功能设计都围绕这句话展开。1.2 参与角色与核心业务流程校园场景下的角色比一般企业系统更固定梳理清楚才能往下设计权限和菜单。我把角色分成四类学生负责每日上报和查看自己的记录及通行码辅导员负责查看本班学生的上报情况、处理异常预警院系管理员负责全院的汇总统计和导出校级管理员负责全局配置打卡规则、预警规则、查看全校数据、维护用户和角色。另外还有一类访客或临时入校人员需要走预约申请流程这个可以单列或并入学生模块处理不复杂。业务流程上我按照这个闭环来设计学生每日上报健康信息上报数据落入健康打卡表系统根据配置好的规则引擎自动判定存在异常的生成预警记录并推送给对应辅导员同时根据上报结果动态生成当天的通行码绿码正常通行黄码和红码需要复查处理辅导员处理完预警记录并填写处理结果所有数据汇总为各类统计报表支持导出存档。这整条链路里规则判断和预警推送是系统的核心价值点而不是打卡本身。1.3 从需求到功能模块的映射把上面的业务梳理映射到技术模块一张表说清楚需求点对应模块关键技术点每日健康上报健康打卡模块幂等控制、唯一约束、并发处理异常自动发现预警规则模块可配置规则表、规则判定引擎通行凭证通行码模块动态生成、缓存策略、状态更新班级/院系数据隔离权限模块RBAC角色权限、行级数据权限汇总分析和归档统计报表模块SQL聚合、ECharts、POI导出系统配置和用户管理系统管理模块用户、角色、菜单、日志管理模块划分不去追求微服务那样的极致解耦但要保证每个模块的边界清楚、依赖单向。这件事比我预想的重要后面开发到权限模块和报表模块时如果前面没有独立拆开代码就会耦合得很难受。2. 技术选型复盘为什么这套Java技术栈最适合这个场景2.1 框架选型的三个理由选型是毕设里最容易被追问的问题也是我花时间最多的地方之一。最终我选了Spring Boot做基础框架理由有三个维度。第一是生态和部署的成熟度。高校信息化环境里Java系应用部署最省事JDK加Tomcat这种组合本身就是很多校园系统的标准配置。换成Python系或者Node系不是不行但在做一个信息管理系统这个传统定位上Java生态的资源、案例、资料密度都明显更高遇到问题搜解决办法的成本也更低。第二是框架自带的中间件能力。Spring Boot把事务管理、AOP切面、定时任务、参数校验这些都打包好了单看某个功能点好像没什么了不起但它们组合在一起恰好覆盖了这种业务系统的大部分诉求打卡数据要事务保障、记录审核要切面记录日志、通行码过期要定时任务处理。第三是面试和答辩的考点重合度。这个项目做完你顺手就能把几个Java面试高频知识点串起来数据一致性怎么保证、并发问题怎么处理、权限怎么做、动态代理用在哪里。项目是Excel信息管理、饭卡系统回答这一类问题就有底气支撑。我用的版本组合是Spring Boot 2.7.x JDK 8 MyBatis-Plus 3.5.x MySQL 5.7 Redis 6。没有追求最新版稳定过了就行。2.2 核心依赖清单依赖用途备注spring-boot-starter-webWeb接口与Tomcat容器内置mybatis-plus-boot-starterORM与CRUD增强分页插件好用spring-boot-starter-data-redis缓存、锁、计数器打卡锁和通行码缓存jjwtJWT Token生成与解析选0.9.1或0.11.x看JDK版本apache poiExcel/Word导出4.1.2即可hutool工具类集合日期、ID生成很方便lombok简化实体类代码答辩时可选择性去掉增强理解spring-boot-starter-validation参数校验接口入参防呆必用2.3 两个被反复问到的架构决策单体还是微服务我直接选了单体应用。微服务在真实开发里是为了解决多团队协作和独立扩容问题的一个毕设项目强行拆微服务只会增加服务调用的复杂度还会把问题引到部署和运维上。单体应用不等于代码不清晰按照controller、service、mapper、entity分层把业务模块用包名隔离完全够用答辩也很好解释。前后端分离还是模板渲染我选了前后端分离前端用Vue3加Element Plus后端纯REST接口。理由是数据报表页面、卡片式布局这些东西用Vue操作起来比JSP顺手太多而且前后端分离之后后端只需要暴露接口表达逻辑的测试也更好写。如果前端技术不熟也可以退一步用Thymeleaf模板渲染整个项目代码量会减少但那种接口驱动前后端协作的经验就少了。这个取舍后面在权限控制那一块也影响到了设计因为前端菜单和按钮权限全都要依赖接口返回的权限标识。3. 数据建模把业务规则落到字段级设计3.1 用户、角色与扩展信息表用户体系我采用了最经典的RBAC模型三张基础表加两张关联表sys_user用户表、sys_role角色表、sys_menu菜单权限表、sys_user_role用户角色关联表、sys_role_menu角色菜单关联表。有一点容易忽略不要把学生、辅导员这些业务属性直接堆在sys_user表里。我单独设计了student_info扩展表存储学号、学院、专业、班级、宿舍、手机号这些字段再通过user_id和主表关联。这么做的原因是学生扩展字段会随着业务调整比如新增一个疫苗信息字段不改主表就能扩展更重要的是后面做行级数据权限时按班级、按学院过滤数据都集中在student_info表查询路径很清晰。辅导员和院系管理员的扩展信息我用了另一张staff_info表同样是这个思路。3.2 健康打卡表唯一约束和decimal字段的讲究健康打卡是整个系统的数据核心表结构我斟酌最多CREATE TABLE health_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 上报人ID, report_date DATE NOT NULL COMMENT 上报日期, temperature DECIMAL(3,1) NOT NULL COMMENT 体温, health_status TINYINT COMMENT 1正常 2异常, has_contact TINYINT COMMENT 是否有异常接触 0/1, location_code VARCHAR(64) COMMENT 定位区域编码, location_name VARCHAR(255) COMMENT 定位区域名称, symptom VARCHAR(255) COMMENT 症状描述, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, report_date), KEY idx_report_date (report_date) ) COMMENT健康打卡记录表;有两个字段设计值得解释。第一个是体温用了DECIMAL(3,1)而不是float或double。体温精度一位小数就够了DECIMAL存的是精确值做大小比较不会出现0.1加0.2不等于0.3这种浮点问题规则引擎判断异常时不用再处理精度误差。第二个是加了一个联合唯一约束uk_user_date保证一个用户一天只能有一条上报记录。这个约束在并发场景下至关重要它是我幂等控制最后一道兜底防线后面会详细讲。3.3 预警规则与通行码的存储方案预警相关的表有三张alert_rule规则表、alert_record预警记录表、pass_code通行码表。CREATE TABLE alert_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(64) COMMENT 规则名称, field_name VARCHAR(32) COMMENT 判断字段, operator VARCHAR(8) COMMENT 操作符 in, threshold VARCHAR(64) COMMENT 阈值, alert_level TINYINT COMMENT 1黄 2橙 3红, rule_desc VARCHAR(255), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE alert_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, report_id BIGINT COMMENT 打卡记录ID, user_id BIGINT NOT NULL, rule_id BIGINT COMMENT 命中规则ID, alert_level TINYINT COMMENT 预警级别, content VARCHAR(255) COMMENT 预警内容, status TINYINT COMMENT 0待处理 1已处理 2已忽略, handle_user_id BIGINT COMMENT 处理人, handle_time DATETIME, handle_remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_alert_status (status, create_time) ) COMMENT预警记录表;通行码表pass_code字段包括user_id、code_date、code_level、code_value、update_time加一个(user_id, code_date)唯一键。通行码的设计原则是当天有效、动态可更新它的值是从当天打卡数据和预警记录计算出来的不是单纯存一个静态字符串所以后面我会单独讲它的生成逻辑。3.4 查询索引加对索引比什么都值钱项目在后端阶段做过一次明显的性能优化核心就是索引。健康打卡表在(user_id, report_date)上有唯一约束可以直接命中单用户查询但全校按日期统计时走idx_report_date就能快速过滤。预警记录表最常用的查询是待处理的某类预警(status, create_time)联合索引让拉取待办列表的查询从秒级降到毫秒级。一个心得设计表的时候就要想清楚高频查询条件是什么索引按查询条件来建不要一股脑给所有字段加索引。加索引会拖慢写操作打卡这种频繁写入的表尤其要注意留两三个必要索引就够了。4. 打卡高峰幂等控制与数据一致性实战4.1 不处理重复提交会发生什么打卡最热闹的时间集中在早上7点到9点全校几千个学生同时操作各种客户端环境都有重复提交的问题根本躲不掉学生连点两下提交按钮、弱网重试、在不同设备上各提交一次都可能让同一个人同一天出现多条数据。如果不做任何控制最直接的结果就是规则引擎对同一个人的同一天判了两次生成两条预警辅导员待办列表里看到同一条异常记录重复出现。再严重一点两个请求同时穿过检查逻辑都认为当天还没打卡数据库就会存在重复记录统计报表的数据口径也就乱了。要解决这个问题的思路很明确前端防、后端防、数据库防三层一起上。4.2 三层幂等防线我在打卡提交接口里做了三层防护。第一层是前端按钮置灰。点击提交后按钮立即变为不可用状态同时显示提交中这能挡掉大部分手误点击。第二层是后端Redis锁。以用户的ID加日期作为锁的key在Redis里用setIfAbsent加锁。加锁成功的请求才能继续往下走失败的直接提示重复提交PostMapping(/submit) public Result? submit(RequestBody ReportSubmitRequest req) { Long userId AuthContext.getUserId(); String date req.getReportDate().toString(); String lockKey lock:report: userId : date; // setIfAbsent 加锁10秒自动过期防止异常情况死锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (!Boolean.TRUE.equals(locked)) { throw new BizException(今日健康信息已提交请勿重复操作); } try { return reportService.submit(userId, req); } finally { redisTemplate.delete(lockKey); } }这里有两个细节要注意。锁的过期时间不能太久我设了10秒正常情况下打卡接口几十毫秒就完成了过期时间只是为了兜底异常场景防止代码卡住导致锁一直占着。finally里一定要主动删锁否则正常请求每次都要等锁自然过期浪费时间。Redis锁本质上是利用了单线程原子操作底层相关并发知识在Java里对应的是锁和CAS那一套答辩时可以往深了讲。第三层是数据库唯一约束兜底。哪怕前面两层都失效两条请求都穿过了Redis锁理论上极难但如果是集群模式节点间不同步数据库的uk_user_date也会在插入时报一个DuplicateKey异常。我在Service层捕获这个异常并转成友好提示彻底堵死重复数据。4.3 事务边界与批量数据一致性大队打卡的高峰期除了重复提交还容易踩事务的坑。我的提交逻辑是这样的先写健康打卡记录再触发规则判定命中则生成预警记录。最初我把规则判定写在了同一个Transactional方法里结果压测时发现接口响应时间很长原因是规则引擎里有一些远程映射查询比如根据location_code查询区域风险等级走的是网络IO把网络耗时也包进了事务。正确的做法是事务只管数据库写操作规则判定放到提交之后异步执行。我后来用了Spring的事件监听机制健康打卡记录保存成功后发布一个事件监听方法里再做规则判定。用到的关键注解是TransactionalEventListener它支持事务提交后再执行保证数据一定已经落库才去判规则避免读到不存在的打卡记录。具体代码结构// 发布事件打卡提交成功 transactionalEventPublisher.publishReportSubmitted(report); // 监听器事务提交后被调用 TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onReportSubmitted(ReportSubmittedEvent event) { ruleEngine.evaluate(event.getReport()); }另外辅导员批量导入历史打卡数据时也要注意事务边界。我按每500条一个事务批次来插入避免一个大事务持锁时间过长影响其他请求也让导入失败时的回滚范围可控。处理这类批量任务时事务里不要做任何非数据库操作这是我从这个项目里学得最深的一条纪律。5. 预警规则与通行码可配置业务规则的设计5.1 规则为什么不能写死在代码里预警逻辑是整个系统值钱的部分但它的业务规则变化很快。今天说37.3度要预警明天说某两个区域接触需要重点关注后天可能又要加一条新规则。如果这些判断硬编码在Java代码里每次调整都要改代码、重新打包、重新部署在运维上完全不可接受。所以我把规则做成了配置化数据库里存规则规则引擎启动时加载到内存并缓存业务调整时管理员在配置页面改规则表系统无需重启即可生效。可能会有人问为什么不直接用Drools这种重量级规则引擎我的观点是规则还是离散条件判断为主数量也不过十几条用一个模板模式加规则表达解析就能跑得又快又稳而且代码逻辑对答辩和源码阅读都更友好。引入Drools会显著拉高项目复杂度在这个复杂度下回报却不明显。5.2 规则表结构与判定逻辑规则表在前面已经给过结构field_name存判定字段operator存操作符threshold存阈值alert_level存预警级别。比如体温大于37.3度黄色预警对应一条记录field_nametemperatureoperatorthreshold37.3alert_level1。判定逻辑的核心是一个evaluate方法根据操作符和阈值对上报数据做判断public boolean evaluate(AlertRule rule, HealthReport report) { Object fieldValue ReflectUtil.getFieldValue(report, rule.getFieldName()); if (fieldValue null) { return false; } String valueStr fieldValue.toString(); switch (rule.getOperator()) { case : return Double.parseDouble(valueStr) Double.parseDouble(rule.getThreshold()); case : return Double.parseDouble(valueStr) Double.parseDouble(rule.getThreshold()); case : return valueStr.equals(rule.getThreshold()); case in: return Arrays.asList(rule.getThreshold().split(,)).contains(valueStr); default: return false; } }规则引擎返回结果后还要做一次去重处理同一个人同一天只保留最高级别的预警避免一个异常报告同时触发体温高、接触史两条记录在辅导员代办里刷屏。这个去重逻辑在生成预警记录时同时完成。5.3 通行码的动态生成与缓存策略通行码的生成逻辑是典型的读多写少场景。每天早上学生要看码校门口值班人员也要查验访问频率高必须把生成结果缓存起来。生成规则是三档判断当天没有任何打卡记录显示未上报禁止通行当天有红色预警输出红码有橙色或黄色预警对应橙码或黄码完全正常则输出绿码。具体的code_value我用用户ID加日期加盐做了一次MD5取前8位加日期拼接这样同一个用户同一天的值是稳定的就不需要每天重复计算。缓存我用Redis的String结构存key是pass:{userId}:{date}设置12小时过期。这样一个学生上午看过码下午再看直接从缓存命中不用每次重新查库。但要注意缓存失效策略如果某个学生上午被判定为绿码中午辅导员处理了一条新的异常记录预警级别改变了通行码必须能够及时刷新。我的做法是在预警记录新增、处理时主动删除该用户当天对应的通行码缓存让下次访问重新生成。这个主动失效比缓存过期时间靠谱得多一定要在改数据的同时清缓存否则就会出现人都变黄码了扫码还显示绿码的严重问题。6. 权限体系JWT认证与行级数据权限的落地6.1 登录、Token与拦截器权限模块我选了JWT加拦截器的轻量方案没有上Spring Security原因是这个项目的角色和权限模型相对固定用拦截器加注解能控制得更直观调试也方便。登录接口校验用户名密码后生成一个JWT TokenToken里放了用户ID、用户名、角色标识过期时间设为2小时。前端把Token存在localStorage里每次请求在Header里带上Authorization: Bearer xxx。后端加一个全局拦截器先校验Token是否过期再解析出用户信息放到ThreadLocal里方便Service层直接取当前登录人。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登录); } LoginUser user jwtUtil.parseToken(token.replace(Bearer , )); AuthContext.set(user); // 设置到ThreadLocal return true; } Override public void afterCompletion(...) { AuthContext.clear(); // 请求结束必须清理防止线程池复用数据错乱 } }afterCompletion里清理ThreadLocal这一步特别重要如果漏了Tomcat线程池复用时下一个请求可能读到上一个登录用户的信息这是真实线上会发生的严重Bug。6.2 RBAC角色权限的划分RBAC部分我按数据级别和操作级别两种维度划分权限。菜单权限控制一个角色能进入哪些页面按钮权限控制页面上的具体操作新增、修改、删除、导出。一个辅导员账号登录进来看不到用户管理菜单也看不到导出全校数据的按钮校级管理员则全部可见。后端在登录时一次性返回菜单列表和按钮权限标识集合前端根据权限标识决定是否渲染按钮。但后端接口也要做同样的校验不能只依赖前端隐藏。我在需要权限的接口上加了自定义注解RequirePermission(report:export)拦截器里校验当前用户是否包含该权限没有就直接拒绝。这样做才真正防止了越权调用比如绕过前端直接POST接口。6.3 行级数据权限辅导员只能看本班菜单和按钮权限解决的是能不能进这个页面的问题但进了页面后能不能看到比自己权限范围更大的数据是另一种权限问题。比如辅导员A登录后数据列表里不能出现别的班级的学生数据否则就是严重越权。我这里用到了三种角色对应的数据范围划分辅导员只看本班、院系管理员看本院、校级管理员看全校。查询列表时按角色动态拼接过滤条件public PageHealthReportVO pageReports(PageQuery query) { LambdaQueryWrapperHealthReport wrapper Wrappers.lambdaQuery(); LoginUser user AuthContext.get(); if (user.isCounselor()) { // 行级权限只查询本班学生 ListLong studentIds studentMapper.selectIdsByClassId(user.getClassId()); if (CollUtil.isEmpty(studentIds)) { return Page.empty(); } wrapper.in(HealthReport::getUserId, studentIds); } else if (user.isCollegeAdmin()) { wrapper.inSql(HealthReport::getUserId, SELECT user_id FROM student_info WHERE college_id user.getCollegeId()); } wrapper.orderByDesc(HealthReport::getReportDate); return reportMapper.selectPage(new Page(query.getPage(), query.getSize()), wrapper); }这里有个细节辅导员场景下我把学生ID先用SQL查出来再放进in查询如果班级人数很大比如几千人in列表就会很长。更稳妥的做法是直接用子查询inSql让数据库完成院系管理员就是这种写法。SQL里拼接数值型参数问题不大但要注意防止SQL注入字符串和日期类型的条件一定要用参数绑定不要直接拼字符串。6.4 AOP与Java动态代理在权限日志中的实际应用做完整系统后我发现每个接口都手动写日志代码是件非常啰嗦的事。这块我用了Spring AOP本质上就是Java动态代理的应用场景。我写了一个切面对所有带指定注解的接口自动记录操作日志——谁在什么时间调了什么接口、参数是什么、返回结果如何。业务代码里不需要写一行日志代码切面统一处理。这正好是观察Java动态代理的好机会Spring默认对接口使用JDK动态代理对没有接口的类使用CGLIB代理。在权限日志这个场景里它是横切逻辑和业务逻辑解耦的标准做法。项目答辩时如果被问到动态代理在哪里用过这就是最直接的答案。7. 可视化报表与文档导出统计模块的设计7.1 统计接口从SQL到数据的口径统一报表模块有几个高频统计口径必须一开始就定义清楚。打卡率分母是某个范围内应打卡人数分子是实际打卡人数体温趋势按日分组取平均温度或异常温度数量异常分布按预警级别统计各类异常的学生数量学院健康状态汇总按学院维度统计正常、异常、未上报人数。设计统计接口时我尽量把聚合计算放到数据库里完成而不是查全量数据到Java里循环算。例如过去14天全校平均体温趋势这个接口SQL大概是SELECT report_date, ROUND(AVG(temperature), 1) AS avg_temp, COUNT(*) AS total_cnt FROM health_report WHERE report_date 2022-05-01 AND report_date 2022-05-14 GROUP BY report_date ORDER BY report_date用索引覆盖日期范围后14条数据毫秒级返回。统计口径上如果前端和后端各有各的算法规避定义不一致就会很乱所以我做了一张统计口径说明文档打卡率、异常率这些指标的计算标准全部统一写死在后端前端只做展示。7.2 前端图表选型与数据对接前端图表选了ECharts功能成熟、文档多、示例丰富。接入方式很简单通过npm或CDN引入后端统计接口返回的JSON结构直接映射到ECharts的data字段比如折线图对应{date, value}[]饼图对应{name, value}[]。图表加载性能上要注意一点多个图表同时渲染时不要每个图表都单独请求一次接口我把仪表盘页面的数据汇总成一个聚合接口一次请求返回所有图表所需的数据。这样首屏加载时间从三次请求缩短到一次对校园网环境下体验改善很明显。7.3 Excel导出POI的选型与内存控制导出Excel是辅导员使用频率很高的功能每次汇总表格都要导出来存档。一开始我直接用了POI的XSSFWorkbook数据量小的时候完全没问题。但导出全校一个月的打卡数据时几万行数据直接卡到内存溢出后来换成了SXSSFWorkbook流式写入问题解决。try (SXSSFWorkbook workbook new SXSSFWorkbook(100)) { Sheet sheet workbook.createSheet(健康上报汇总); // 写表头 Row header sheet.createRow(0); String[] headers {日期, 姓名, 学号, 学院, 体温, 健康状态}; for (int i 0; i headers.length; i) { header.createCell(i).setCellValue(headers[i]); } // 写数据行每100条刷一次到磁盘 int rowNum 1; for (HealthReportVO vo : dataList) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(vo.getReportDate().toString()); row.createCell(1).setCellValue(vo.getStudentName()); // ... } workbook.write(outputStream); } finally { workbook.dispose(); // 释放临时磁盘文件 }SXSSFWorkbook和XSSFWorkbook的核心区别是前者只保留窗口内的行在内存其余行写入磁盘临时文件所以可以处理很大数据量的导出。需要注意导出文件流结束后调用dispose释放磁盘临时文件否则会出现临时文件堆积问题。7.4 Word日报图表怎么处理最省心很多学校需要每天生成一份上报情况日报存档Word格式。有同行问过我Java的POI能不能直接在Word里生成图表答案是能做但体验很差。POI对Word图表XWPFChart的支持很弱样式调整困难生成的图表美观度也不行。我最终的方案是先用ECharts在服务端生成图表图片再把图片插入Word文档。具体做法是写一个简单的浏览器截图工具或使用Java图形库生成PNG图片然后把图片通过XWPFDocument的addPicture方法插入文档中再配一段文字说明。这样Word里的图表看着和前端完全一致生成逻辑也简单稳定唯一要注意的是图片大小需要控制单张宽度别超过Word页面宽度否则排版会乱。8. 部署上线与复盘从打包到踩坑记录8.1 打包、部署与会话时效项目最终要能一键运行起来才算是完整交付。我用Maven打包成jar在2核4G的云服务器上部署环境是JDK8、MySQL5.7、Redis6前端打包后由Nginx托管静态文件并反向代理后端接口。启动命令很简单mvn clean package -DskipTests nohup java -jar target/campus-health-1.0.0.jar --spring.profiles.activeprod app.log 21 Nginx配置里把/api路径代理到本地的8080端口前端只通过Nginx访问也顺便解决了跨域问题。环境变量配置这块我在application-prod.yml里统一管理数据库连接、Redis连接和JWT密钥用环境变量占位部署时写进服务器的系统环境变量避免密钥硬编码在代码里。8.2 用Jmeter压测暴露的三个问题上线前我用Jmeter模拟1000人并发打卡压了一把结果暴露了三个问题。第一个是数据库连接池不够用。默认HikariCP连接池大小是10并发一上来连接就不够用了大量请求排队等待连接。我把maximum-pool-size调到30空闲连接不释放打卡高峰明显改善。这是任何高并发系统都会遇到的第一道坎。第二个是慢查询。统计日报接口在数据量到了几十万条后日期范围查询开始变慢。我用EXPLAIN看了执行计划发现没有走索引优化后加了(report_date, user_id)联合索引查询时间从1.2秒降到80毫秒。第三个是缓存穿透。如果有人恶意循环请求一个不存在的用户ID查询通行码Redis缓存总是查不到请求全部打到数据库。我加上空值缓存策略查不到的行缓存一个空标记设置短过期时间挡掉了穿透的请求。8.3 印象最深的几个坑和修复复盘整个开发过程有几个坑值得单独记录下来都很有代表性。第一个坑是日期字符串比较。前端传日期时用的是字符串格式直接拿2022-08-02和2022-8-3比较结果因为月和日没有补零排序完全乱了。后来统一用LocalDate类型接收和比较这个问题才根除从此所有日期参数后端一律用LocalDate而不是String。第二个坑是MyBatis-Plus逻辑删除与唯一约束冲突。我给打卡记录加了逻辑删除字段is_deleted又保留了(user_id, report_date)唯一约束结果用户删除记录后想重新提交因为逻辑删除的记录还在表里唯一约束冲突导致无法插入。解决方式是去除逻辑删除字段打卡数据本身就要求不可覆盖、只能追加再有就是唯一约束的范围要重新设计。第三个坑是JWT时钟不一致。开发时Token过期时间用了绝对时间部署的服务器和本机时钟偏差了五分钟导致上午生成的Token下午就过期了。后来改成基于当前时间的相对时长计算并统一了所有服务器的NTP时钟同步。第四个坑是多实例定时任务重复执行。我写了一个每日凌晨清理过期预警的定时任务后来部署了两个实例结果任务两边都执行了。解决方式是加一个基于Redis的分布式锁让同一时间只有一个实例执行定时任务。第五个坑是POJO之间直接拷贝导致脏数据。DTO转换时图省事直接引用赋值结果修改了一个对象的字段另一个也变了。后来做深浅拷贝区分转换DTO一律用BeanUtil拷贝属性不在原对象上动手脚。这些坑没有一个是高深的理论问题但每个都会让程序在线上表现出奇怪的行为排查起来相当费时间。把这个项目从零部署到真实环境完整跑一遍比单纯刷题理解Java并发和对象生命周期要深刻得多。最后再分享一个我个人的体会这个项目做完我最大的收获不是掌握了某个框架的某个冷门API而是建立了一种业务闭环的思维方式。数据从采集、校验、判级、通知、处理到归档每一步都要有明确归属和记录权限和异常情况都要有兜底方案。如果你也要做类似选题我的建议是先把业务角色和流程想透再写代码数据表设计时多想想查询条件接口开发时把幂等、事务、权限、日志这些横切能力一步做到位后面测试和答辩都会轻松很多。把这个项目的过程沉淀成经验你会发现它不只是毕设也是你系统性掌握Java后端开发的一次完整演练。