
说实话刚看到“springboot疫情打卡健康评测系统11676”这个题目标题的时候我第一反应是又一个大四孩子要开始折腾毕设了这个题目编号风格太典型了几乎每个学校毕设管理系统里都能翻出几页类似的选题。但说句公道话这个题目比起那些“图书管理系统”“考勤管理系统”要有意思得多因为它的核心不在“打卡”这个动作而在“健康评测”这四个字上。我见过太多人把这个项目做成一个简单的增删改查用户填个姓名、体温、是否咳嗽存进数据库管理员看个列表完事。这确实也能答辩过关但做完之后你会发现自己什么都没学会。这篇文章我就从一个完整落地项目的视角把这个“健康评测系统”从需求到代码、从数据库到前端部署一条线全部拆开讲明白。不管你是拿它当毕设、课设还是真的要在公司里做一个类似的小型健康管理模块这套思路都能直接用上。1. 项目到底是什么拆解需求背后的真实意图1.1 健康评测不是一个表单而是一条业务闭环先把这个项目的本质说清楚。它表面上是一个打卡登记系统但如果你想让它配得上“评测”两个字背后一定要有一套规则运算逻辑。业务流程大概是这样的每天到了打卡时间窗口用户学生/员工提交当天的健康信息包括体温、症状、行程轨迹、接触史、当前所在位置等系统拿到这些数据之后不是简单丢进数据库而是根据预先配置好的评测规则算出一个健康状态比如“正常”“关注”“高风险建议隔离”再把结果回传给用户同时把异常数据实时推送给管理端。这个闭环拆开之后系统就分出了三个核心价值点一是数据采集解决“每天有没有人漏报”的问题二是规则评测解决“这一堆上报数据到底意味着什么”的问题三是异常预警解决“发现问题之后管理员怎么第一时间知道”的问题。很多同学做这个项目只做了第一点剩下两个根本没想清楚所以最后做出来的东西自然也就平平无奇。1.2 从用户角色反推功能模块系统里至少有四种角色每种角色的诉求完全不同角色核心诉求对应功能模块普通用户学生/员工快速完成每日填报能查看自己的历史记录和当前状态健康打卡、我的记录、个人资料班级/部门管理员查看本组人员填报情况处理异常记录数据看板、异常处理、催报推送系统管理员维护用户、配置评测规则、查看全局统计用户管理、规则配置、报表导出访客/匿名用户无一般不开放登录注册、找回密码有意思的是很多同学会把“用户管理”做成一个独立的首页大banner好像这个系统就是为了管人而存在的。但实际上对健康评测系统来说“用户管理”只是基础设施业务核心永远是打卡数据和评测规则。这个顺序如果你在设计的时候搞反了后面数据库表结构都会跟着歪。2. 技术选型为什么SpringBoot是这个题目的最优解2.1 框架选型背后的逻辑SpringBoot对这个题目来说几乎就是标准答案。原因不复杂首先它是当前Java方向就业需求量最大的框架选它做毕设有直接功利价值其次它内置Tomcat不用你单独去配置服务器一个打包好的jar直接就能跑这对课设和毕设来说省了太多事第三Spring生态全家桶齐全你要接MySQL、接Redis、接消息队列、接第三方登录都有现成的starter可以加依赖。我还看到有些人会纠结“SSHSpringStrutsHibernate”还是“SSMSpringSpringMVCMyBatis”。我的建议是直接别纠结SSH已经是上一代的技术堆栈了现在写简历上不仅不加分反而会让面试官觉得你技术视野停留在七八年前。SpringBoot 2.x MyBatis再做前端Vue这个组合对单体项目来说就是最优解。2.2 项目结构规范一开始就别把包建乱了我见过太多项目从包结构开始就注定要乱。这里给出一套我踩过坑之后沉淀下来的基础结构不管项目大小都建议按这个来com.example.healthcheck ├── HealthCheckApplication.java # 启动类 ├── common/ # 通用工具、统一返回结果、异常处理 │ ├── Result.java │ ├── ResultCode.java │ ├── GlobalExceptionHandler.java │ └── utils/ ├── config/ # 配置类拦截器、跨域、静态资源映射 ├── controller/ # 控制层只做参数接收和结果封装 ├── service/ # 业务层放核心业务逻辑 │ └── impl/ ├── mapper/ # MyBatis的Mapper接口 ├── entity/ # 数据库实体字段与表一一对应 ├── dto/ # 前端传来的请求参数对象 ├── vo/ # 返回给前端的视图对象 └── task/ # Spring定时任务很多人不理解为什么既要entity又要dto又要vo感觉就是在造轮子。你写久了就会明白数据库实体如果直接被Controller拿来接收请求参数一旦表结构调整接口也会跟着变这种耦合在项目里非常痛苦。比如打卡接口接收的参数里有一个”异常备注”但表里可能根本没有这个字段而是需要经过服务层拆解之后分别写入异常表。这时候dto和entity的价值就体现出来了。2.3 自动装配原理面试官最爱问的SpringBoot核心既然题目是SpringBoot那就绕不开自动装配原理。这个原理其实没那么高深简单说就是SpringBoot启动时会去读spring.factories或者AutoConfiguration.imports文件里的自动配置类列表再根据类上的条件注解去判断哪些配置要生效。Bean ConditionalOnMissingBean public ObjectMapper objectMapper() { return new ObjectMapper(); }上面代码里这段就是典型的自动配置思路如果容器里没有ObjectMapper那我就帮你创建一个如果你自己定义了我就不干扰你。这就是SpringBoot“约定优于配置”的底气来源。做项目时不用管这些也能跑通但如果你准备拿这个项目去面试至少得能说出“SpringBootApplication由SpringBootConfiguration、EnableAutoConfiguration、ComponentScan三个注解组成”这句话然后再补一句“自动配置的核心是条件注解加starter依赖”。这比背什么面试宝典有用得多。3. 数据库设计健康评测系统的地基工程3.1 核心业务链路设计在定表结构之前建议先把数据流动的链路画出来。我设计的链路如下用户打开页面 - 登录鉴权 - 查看“今日是否已打卡” - 如果没打填写表单 - 后端校验参数 - 写入打卡记录表 - 调用评测规则服务 - 评测出健康状态 - 更新当次打卡结果并写入评测记录表 - 如果评测结果为“异常/高风险”额外插入一条异常预警记录 - 管理端轮询或接受推送提醒。这条链路决定了至少需要五张核心表用户表、打卡记录表、评测规则表、评测结果表也可以合并进打卡表、异常预警表。再加上基础的四张辅助表角色表、部门/班级表、系统配置表、操作日志表。3.2 表结构设计要点表名关键字段设计要点t_userid、username、password、real_name、role_id、dept_id、phone注意密码不能明文至少要MD5加盐t_deptid、name、parent_id支持班级/部门/园区多级结构t_checkin_recordid、user_id、checkin_date、temperature、has_symptom、symptom_desc、location、health_status、create_time一天一条记录用user_id和checkin_date做唯一索引t_evaluation_ruleid、rule_name、condition_expression、score、description、enabled核心表规则引擎的数据来源t_alert_recordid、user_id、record_id、level、content、status、handler_id、handle_time异常记录和处理状态分离有几个细节要特别注意。第一checkin_date为什么要单独设一个字段而不是直接用create_time因为create_time是时间戳包含时分秒而业务上判断“今天是否已经打卡”用的是自然日。如果直接用时间你还要考虑时区转换直接在代码里写一个LocalDate.now()再和create_time比较也行但有索引的情况下单独存一个日期字段查询性能更好逻辑也更清晰。第二temperature这种浮点字段建议用decimal(4,1)比如36.5、37.2这种格式。用float和double容易出精度问题虽然体温这里影响不大但这是一种好习惯。3.3 健康评测规则怎么建模这是整个项目最核心、也最容易被低估的部分。常见的评测规则可以抽象成三种形式阈值型体温超过37.3度自动判定异常条件组合型体温偏高且伴有咳嗽判定高风险只有接触史但无症状判定观察权重打分型每个指标设置权重分值最后汇总得分对应不同健康等级我推荐用“打分制”来建模因为它的扩展性最强后期加规则不用改代码。比如这样设计基础分100分体温大于等于37.3扣30分出现“咳嗽”“乏力”等任一症状扣20分14天内去过风险地区扣40分有明确接触史扣50分。最终得分80以上为正常60到79为观察60以下为高风险。这套逻辑用Java实现的时候最优雅的做法并不是写一长串if-else而是把规则条件存进数据库表通过一个规则执行器去循环解析。当然如果项目周期紧直接用if-else封装到service里也完全没问题毕竟是单体小项目不用为了设计模式而设计模式。只是你要让评测逻辑独立出一个方法别跟打卡保存的逻辑混在一起。4. 核心实现实录打卡、评测、统计、前端整合4.1 打卡接口的实现与当天去重这块是最常见的功能但里面有一个非常隐蔽的坑并发重复提交。用户手快点了两次提交或者前端网络重试就会造成同一个人同一天生成两条打卡记录。解决办法分两层数据库加唯一索引兜底代码里先查后插。先看Mapper层面的去重查询select idcountByUserAndDate resultTypeint select count(1) from t_checkin_record where user_id #{userId} and checkin_date #{checkinDate} /select再看Service层的核心逻辑Override Transactional(rollbackFor Exception.class) public ResultVO submitCheckin(CheckinDTO dto, Long userId) { // 先判断今天是否已打卡 int count checkinRecordMapper.countByUserAndDate(userId, LocalDate.now()); if (count 0) { throw new BizException(今日已完成打卡请勿重复提交); } // 组装实体并保存打卡记录 CheckinRecord record new CheckinRecord(); BeanUtils.copyProperties(dto, record); record.setUserId(userId); record.setCheckinDate(LocalDate.now()); record.setCreateTime(LocalDateTime.now()); checkinRecordMapper.insert(record); // 调用评测服务计算出健康状态 Integer score evaluationService.evaluate(record); String status score 80 ? 正常 : (score 60 ? 观察 : 高风险); record.setHealthStatus(status); // 如果异常插入预警记录 if (!正常.equals(status)) { alertService.createAlert(record, score); } // 回传评测结果给前端 MapString, Object data new HashMap(); data.put(healthStatus, status); data.put(score, score); data.put(tips, evaluationService.getAdvice(status)); return ResultVO.success(data); }这里有几个细节值得说道。第一Transactional一定要加上因为打卡记录、评测结果、预警记录这三个写操作必须保证原子性否则可能出现打卡成功但异常预警没生成的情况。第二别用if (count 0)这种判断因为数据库唯一索引才是最终防线如果你觉得并发写不严重也可以直接靠唯一索引捕获DuplicateKeyException效果更简单粗暴。4.2 评测规则执行器的简化版实现评测规则如果全写在Service里会很难维护。我习惯写一个独立的Evaluator组件把规则逻辑集中处理Component public class HealthEvaluator { private static final int BASE_SCORE 100; private static final double TEMP_THRESHOLD 37.3; public EvaluationResult evaluate(CheckinRecord record) { int score BASE_SCORE; ListString reasons new ArrayList(); // 体温阈值规则 if (record.getTemperature() ! null record.getTemperature().doubleValue() TEMP_THRESHOLD) { score - 30; reasons.add(体温异常( record.getTemperature() ℃)); } // 症状组合规则有症状就扣分 if (Boolean.TRUE.equals(record.getHasSymptom())) { score - 20; reasons.add(存在咳嗽/乏力/咽痛等疑似症状); } // 行程风险规则 if (StringUtils.hasText(record.getTravelHistory()) risk.equals(record.getTravelRiskLevel())) { score - 40; reasons.add(14天内途经/到达风险地区); } // 接触史规则 if (Boolean.TRUE.equals(record.getContactHistory())) { score - 50; reasons.add(有疑似/确诊人员接触史); } score Math.max(score, 0); String level; if (score 80) { level 正常; } else if (score 60) { level 观察; } else { level 高风险; } return new EvaluationResult(score, level, reasons); } }你可能会问如果以后需求说“体温37.2但咽喉痛也算高风险”这种硬编码还能改吗答案是改起来要动代码重新发布。如果你真的想做得更“高级”一点可以把体温阈值、扣分值、等级边界都放进数据库的配置表里执行器从配置表中读取。我建议至少把“36.5到37.3”这种阈值抽成配置至于规则完全动态化说实话对单体毕设项目有点过度设计写上反而像是在炫技工作中也未必有人真的会去改规则。4.3 定时任务生成每日汇总报表管理端需要一个今天打卡率、异常人数的统计总览这个数据可以实时统计但对于一天只变几次的数据实时查询每次都去扫全表显然不划算。更优雅的做法是用Spring自带的定时任务每天凌晨把昨天的汇总数据算好存到一张统计表里管理端直接查表。Component public class ReportTask { Resource private CheckinRecordMapper checkinRecordMapper; Resource private DailyReportMapper dailyReportMapper; /** * 每天凌晨1点执行统计前一天的打卡汇总 */ Scheduled(cron 0 0 1 * * ?) public void generateDailyReport() { LocalDate targetDate LocalDate.now().minusDays(1); int totalUsers userMapper.countAll(); int checkedUsers checkinRecordMapper.countByDate(targetDate); int abnormalUsers checkinRecordMapper.countByDateAndStatus(targetDate, 观察); int highRiskUsers checkinRecordMapper.countByDateAndStatus(targetDate, 高风险); DailyReport report new DailyReport(); report.setReportDate(targetDate); report.setTotalUsers(totalUsers); report.setCheckedUsers(checkedUsers); report.setAbnormalUsers(abnormalUsers highRiskUsers); report.setCreateTime(LocalDateTime.now()); dailyReportMapper.insertOrUpdate(report); } }定时任务虽然好写但有几个问题要提前想好。第一如果你用的服务器有多个节点部署Scheduled会每个节点各执行一遍造成重复数据。单体部署没问题分布式就要引入xxl-job这类外部调度框架了。第二cron表达式建议用在线生成器确认一下别自己脑补我自己就曾经把“0 0 1 * * ?”写成“0 1 0 * * ?”结果每天凌晨1点整变成每天凌晨0点1分跑排查了半天。4.4 Vue打包放进SpringBoot中前后端一体化部署很多人的项目是前端一套端口8080后端一套端口9090部署和访问都很麻烦。其实Vue项目在build之后就是一个纯静态文件夹完全可以扔进SpringBoot的静态资源目录里实现一个jar包同时提供页面和接口。操作流程很简单先在前端工程目录执行npm run build会生成一个dist文件夹然后把dist里面的内容复制到SpringBoot的src/main/resources/static目录下重新打包即可。SpringBoot启动后直接访问http://localhost:8080就能打开前端页面。但这里有一个巨大的坑Vue如果是使用history模式的路由刷新一个子页面时会直接404。原因是前端路由是虚拟的SpringBoot没有对应这个路径的后端接口。解决办法是在SpringBoot里做一个history模式回退的配置Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { // 把非api、非静态资源的路径全部转发到index.html registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); } }注意这个配置要在所有接口都以/api前缀开头的前提下才能用。如果你把接口路径写成/user/list这种根路径那这个转发配置就会把你的接口请求也给拦截了。所以接口统一加/api前缀不仅仅是为了好看更是为了让静态资源转发规则不被误伤。5. 常见问题与排查技巧实录5.1 SpringBoot版本太高导致的依赖冲突这个坑在热词里出现的频率非常高因为我见过太多人直接用最新版的Spring Boot 3.x配MyBatis starter结果依赖下载失败或者bean注入报错。Spring Boot 3.x基于Jakarta命名空间很多老版本的第三方starter还没适配好。如果你做这个项目是为了快速交付我的建议是选Spring Boot 2.7.x这个最终2.x版本配JDK 8或11生态最稳。把下面这个依赖放进去基本不会出错parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent如果你非要用Spring Boot 3.x那就要确保所有依赖都升级到Jakarta版本比如MyBatis的starter要用mybatis-spring-boot-starter的3.0版本。这不是不能做只是你要有心理准备排查依赖坑的时间会比开发业务的时间还长。5.2 数据库连接相关的启动报错SpringBoot项目启动时报“Failed to configure a DataSource”十有八九是配置文件写错了。我遇到最多的问题是application.yml中数据库URL里带有特殊字符没转义比如密码里有符号导致连接串截断。遇到这种问题最直接的排查方式是先在navicat或命令行工具里用同样的账号密码试连一次数据库确认数据库本身没问题再回头检查配置文件的写法。另外用MySQL 8.0以上版本驱动类要写com.mysql.cj.jdbc.DriverURL里建议加上serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8否则容易出现中文乱码和时区报错。5.3 MyBatis XML中大于小于号的坑在XML文件里写SQL如果你直接在where条件里写“temperature #{value}”MyBatis解析XML时会把当作标签结束符的一部分轻则解析失败重则报错。解决办法有两个方案一是用转义字符 和 二是写CDATA区块。select idselectHighTemp resultTypeCheckinRecord ![CDATA[ select * from t_checkin_record where temperature 37.3 ]] /select我推荐用CDATA因为SQL里面有多个比较符号时转义写法读起来太费劲。不过也要注意一点CDATA区块内的#{}占位符在MyBatis里仍然会被正确解析因为它是MyBatis在XML解析之后和SQL预处理之前处理的不是数据库层面的原生语法所以该用#{}还是用不存在“CDATA里不能用#{}”的说法这一点经常被误解。5.4 前端打包后页面白屏或404除了上面说的history路由回退问题还有一个常见问题是静态资源路径不对。Vue默认的publicPath是/如果你的SpringBoot加了context-path比如server.servlet.context-path/health那前端所有静态资源的路径就会变成/health/js/xxx.js如果publicPath不对就会白屏。解决思路是要么前端把publicPath改成./让资源走相对路径要么把SpringBoot的context-path去掉直接用根路径部署。我倾向于去掉context-path因为加了它以后前端的接口请求路径、静态资源路径、路由跳转全部要跟着改一遍徒增复杂度。做一个单体毕设系统不需要这个隔离。5.5 跨天与并发重复打卡的边界情况打卡系统最怕出现“用户凌晨12点踩点提交两次”“计算日期时跨越了自然日”这些边界情况。我在实际开发中吃过亏那时用的是LocalDateTime.now()去判断当天结果用户在23:59:59提交了一次00:00:01又提交了一次数据库里两天各有一条记录表面上看没问题但业务逻辑上“昨天没打”和“今天打了”就完全对不上了。所以我的建议是判断“今天是否已打卡”用LocalDate.now()做业务日期而不是取时间戳再转换。表里单独存checkin_date字段让这个字段成为业务的标尺所有统计报表也用这个字段做分组避免SQL里写date(create_time)这种函数。这个坑平时不显眼但一到月底出统计报表时就会漏人。5.6 IDEA里怎么配置启动端口、环境变量这个看起来很简单但好多第一次用SpringBoot的同学就是卡在这一步。启动端口不要改来改去直接在src/main/resources/application.yml里配server: port: 8080如果你想在IDEA里临时指定端口可以在启动类的Run Configuration里加VM options-Dserver.port8081甚至还能配置环境变量启动参数--spring.profiles.activedev开发环境一套配置生产环境一套配置通过profile切换。我建议至少建三个配置文件application-dev.yml、application-prod.yml主配置里写spring.profiles.activedev。这样你在一台机器上调试本地数据库和部署到服务器上连线上库之间切换时不用改代码只用改一个参数。6. 实操心得与几个可以加分的设计细节6.1 我踩过的坑和体会做这类项目我最大的体会是越是一个“看起来很简单”的增删改查系统越要在一开始就把边界类问题想清楚。打卡系统里最折磨人的不是“正常提交”而是“重复提交”“漏打卡补报”“异常撤销”“管理员驳回重报”这几类少见的操作。我第一版就只做了提交和列表结果测试阶段用户稍微点几下就暴露出一堆问题。第二个体会是健康评测系统的核心在于“规则”所以规则的维护方式决定了这个系统的上限。如果你有时间可以把评测规则接口单独拉一个管理页面出来让管理员能在页面上调整“体温阈值”“扣分分值”这些参数。这个功能代码量不大但它是整个项目中第一个“脱离增删改查”的功能不管写在简历上还是答辩演示时都能明显提升项目的技术含量。6.2 后续扩展方向参考如果你做完基础版本还有余力可以考虑这么几个方向。第一是接入消息通知用户打卡异常时自动发送站内信甚至接入企业微信机器人第二是引入Redis做今日打卡状态的缓存减少对数据库的重复查询第三是做一个导出Excel的功能让管理员能按部门导出每日打卡明细。这三个方向都有现成方案也符合实际使用场景。还有一点值得提一下就是如果一个项目后续可能要接其他系统比如对接钉钉通讯录同步组织机构就会引出“对接接口设计”的问题。这时候如果把对接逻辑写进Controller里后面会很痛苦。我建议在与第三方系统交互的场景中单独抽一层client/service这比什么都写在主Service里更容易维护。6.3 答辩展示时的三个加分点最后说点答辩或者给领导演示时的小技巧。第一不要只演示“我提交了一条记录”而是要把评测规则展示出来先给领导看规则页面再演示提交一条体温37.4度的记录让系统自动给出异常状态这个演示过程很有说服力。第二准备一组本地的测试数据覆盖“正常”“观察”“高风险”三种状态演示的时候一键切换不同用户登录查看结果。第三如果时间允许准备好一个“异常预警”的演示场景比如提交一条高风险记录之后切到管理员账号看到这条预警然后执行处理动作完整展示闭环。这三点看着不起眼但正是它们决定了别人看到的是一个“填表系统”还是一个“健康评测系统”。说实话做项目到最后拼的往往不是谁代码写得花而是谁更能把自己系统里的逻辑讲清楚、演示到位。我个人的建议是动手写代码之前先花半天时间把上面的链路、表结构和规则模型整理成一份文档哪怕只是写在纸上。这个准备阶段省下的时间大概率比你晚几天动工所耽误的时间要多得多。项目本身不难难的是你愿不愿意在写第一行代码之前先让自己想明白整个系统到底在干什么。