☰
SSM疫情打卡管理系统毕业设计:核心业务约束与工程落地细节
2026/10/26 8:34:10 网站建设 项目流程

简介:这是一套基于SSM框架的疫情打卡管理系统完整项目,面向计算机相关专业做毕业设计的学生,以及需要Java项目实战练习的学习者,也可用于课程设计与期末大作业。项目已通过导师指导并获高分评价,下载后无需修改即可直接运行,对新手较为友好。压缩包共139个文件,约45.88MB,涵盖xml配置、jar依赖、class编译文件、jsp页面、java源码、css与js前端资源、properties配置、sql数据库脚本及docx项目说明等,构成从后端逻辑到前端展示的完整工程结构。系统围绕用户登录注册、打卡数据管理等核心模块展开,源码与数据库脚本配套齐全,便于理解SSM三层架构的实际落地方式。目前已有164人学习关注,适合希望快速获得可运行毕设项目、对照源码梳理开发流程并完成答辩准备的读者参考使用。

1. 疫情打卡管理系统:SSM 毕业设计里最容易被低估的工程细节

很多同学拿到「基于 SSM 的疫情打卡管理系统」这个题目时,第一反应是「不就是增删改查吗」。我带过几届毕业设计,见过太多人三天写完 CRUD,然后在答辩现场被老师一句「你这个打卡记录怎么防止同一个人一天提交两次」问到哑口无言。疫情打卡这个场景看着简单,但它天然带着几个真实业务约束:一人一天一条记录、体温数值有合理区间、打卡时间要卡在当天、异常状态要能追溯。这些约束才是这个题目真正的技术含量所在。

SSM 指的是 Spring + SpringMVC + MyBatis 这套经典组合,虽然现在新项目更多用 Spring Boot,但毕业设计里 SSM 依然是主流,因为它的分层结构清晰,答辩时好讲,老师也熟悉。这套系统通常包含学生端打卡、教师端查看统计、管理员端管理用户和导出数据三类角色。适合正在做计算机毕业设计、需要一套能跑通、能讲清、能改动的 Java Web 项目的同学。下面我按实际落地的顺序,把选型、建库、核心打卡逻辑、统计导出和踩坑一条条讲清楚。

2. 环境与选型:为什么这套 SSM 组合在答辩时最稳

2.1 技术栈版本怎么定才不翻车

毕业设计最怕的不是功能少,而是环境跑不起来。我一般会推荐一套经过大量项目验证的稳定组合,而不是追最新版本。JDK 用 8,Tomcat 用 8.5 或 9,MySQL 用 5.7 或 8.0,Maven 用 3.6 以上。这套组合的兼容性最好,网上能查到的报错解决方案也最多。

组件推荐版本选它的理由
JDK1.8SSM 生态最成熟,注解和泛型支持够用
Spring5.2.x与 JDK8 配合稳定,注解配置完善
SpringMVC5.2.x与 Spring 同版本,避免依赖冲突
MyBatis3.5.x支持注解和 XML 两种写法,答辩好讲
MySQL5.7 / 8.05.7 兼容老驱动,8.0 注意驱动类名变化
Tomcat8.5 / 98.5 对老项目友好,9 需要留意 javax 包
Maven3.6+依赖管理清晰,打包 war 方便

这里有个血泪经验:MySQL 8.0 的驱动类是com.mysql.cj.jdbc.Driver,而 5.x 是com.mysql.jdbc.Driver,连接串还要加时区和 SSL 参数。很多同学本地用 5.7 跑通,换到 8.0 就报「Unknown system variable」,就是驱动和连接串没同步改。

2.2 分层结构怎么划分才经得起追问

SSM 的标准分层是 Controller、Service、Mapper(DAO)、Entity。但答辩老师常问的是「你为什么要分这么细」。我的回答逻辑是:Controller 只负责接收参数和返回结果,不写业务判断;Service 承担事务和业务规则,比如「一天只能打一次卡」这种校验必须放在 Service;Mapper 只做数据库操作。这样分层的好处是,当老师让你改一个规则时,你只需要动 Service 一层,不会牵一发动全身。

包结构我一般这样组织:

com.example.epidemic ├── controller // 接收请求 ├── service // 业务接口 │ └── impl // 业务实现,事务在这里 ├── mapper // MyBatis 接口 ├── entity // 数据库实体 ├── vo // 前端展示对象 └── util // 工具类,如日期处理

这个结构不是摆设。比如统计功能需要把打卡记录按班级聚合,返回的字段和数据库表不一致,这时候就用 VO 承接,而不是硬改 Entity。老师看到你有 VO 层,会认为你理解「数据库模型和展示模型要分离」这个点。

2.3 依赖配置里最容易漏的三处

pom.xml 里除了 Spring、SpringMVC、MyBatis 三大件,还有几个依赖经常被漏掉,导致运行时报 ClassNotFound。第一个是jackson-databind,返回 JSON 时没有它会报 406;第二个是commons-fileupload,做 Excel 导入导出时要用;第三个是druid或c3p0连接池,不配连接池在高并发测试时会连接泄漏。

<!-- JSON 转换,Controller 返回对象时必需 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> <!-- 文件上传,Excel 导入用 --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <!-- 连接池,避免每次请求新建连接 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency>

参数说明:jackson 版本要和 Spring 版本大致匹配,2.13 配 Spring 5.2 没问题;fileupload 1.4 修复了早期版本的安全问题;druid 1.2.8 稳定,配置里记得设initialSize和maxActive。漏掉连接池的后果是,答辩演示时多人同时点,系统直接卡死,这个翻车现场我见过不止一次。

3. 数据库设计:打卡表的时间字段和唯一约束怎么定

3.1 核心表结构与字段类型选择

疫情打卡系统的表不多,但每张表的字段类型都有讲究。用户表、打卡记录表、班级表、健康状态字典表,这四张基本够用。重点是打卡记录表,它承载了业务核心。

CREATE TABLE `clock_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint(20) NOT NULL COMMENT '打卡用户ID', `clock_date` date NOT NULL COMMENT '打卡日期,精确到天', `temperature` decimal(4,1) NOT NULL COMMENT '体温,如36.5', `health_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '0正常 1发热 2咳嗽 3其他', `location` varchar(100) DEFAULT NULL COMMENT '当前位置', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_date` (`user_id`,`clock_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日打卡记录';

逻辑说明:clock_date用 date 而不是 datetime,因为业务规则是「一天一条」,用日期做唯一约束的一部分最直接。temperature用 decimal(4,1),能存 36.5 这种一位小数,范围到 99.9,够用且不会出现浮点误差。health_status用 tinyint 做枚举,比 varchar 省空间且查询快。

参数说明:唯一索引uk_user_date是防重复打卡的第一道防线,但光靠它不够,因为用户重复提交时数据库会抛异常,体验差。所以 Service 层要先查一次,数据库约束作为兜底。create_time默认当前时间,方便统计提交时段。

3.2 唯一约束和业务校验的分工

很多同学只靠数据库唯一索引,结果用户点两次提交,第二次直接报 500 错误,页面上显示一堆异常堆栈。正确做法是两层防护:Service 层先根据 userId 和当天日期查一次,存在就返回「今日已打卡」;数据库唯一索引作为最后保障,防止并发下的极端情况。

// Service 层校验,先查后插 public Result clockIn(Long userId, ClockRecord record) { // 查询今天是否已打卡 ClockRecord exist = clockRecordMapper.selectByUserAndDate(userId, LocalDate.now()); if (exist != null) { return Result.fail("今日已打卡,请勿重复提交"); } record.setUserId(userId); record.setClockDate(LocalDate.now()); // 体温合理性校验 if (record.getTemperature().compareTo(new BigDecimal("35.0")) < 0 || record.getTemperature().compareTo(new BigDecimal("42.0")) > 0) { return Result.fail("体温数值不在合理范围"); } clockRecordMapper.insert(record); return Result.success("打卡成功"); }

逻辑说明:先查后插能给出友好提示,体温校验放在插入前,避免脏数据进库。LocalDate.now()取服务器当天日期,注意如果部署在时区不对的服务器上,可能差一天,这个坑后面会讲。

参数说明:体温下限设 35.0、上限设 42.0 是常见做法,低于 35 可能是测量错误,高于 42 在医学上极少见。这个范围可以根据实际需求调整,但一定要有,否则用户输入 999 也能存进去。

3.3 统计查询需要的索引

教师端要按班级、按日期统计打卡率,这涉及clock_record和user的关联查询。如果数据量到几千条,没有索引会明显变慢。除了主键和唯一索引,建议在clock_date上单独加一个普通索引,因为按日期范围筛选是高频操作。

ALTER TABLE `clock_record` ADD INDEX `idx_clock_date` (`clock_date`);

这个索引在「查询某天所有打卡记录」时生效。如果还要按班级统计,通常先查班级下的用户 ID 列表,再用user_id in (...)查打卡记录,这时候user_id已经在唯一索引的最左列,能复用。不要盲目加一堆索引,写入时会变慢,毕业设计的数据量下,两三个索引足够。

4. 打卡与统计功能落地:从 Controller 到 Mapper 的完整链路

4.1 打卡接口的参数接收与返回格式

打卡接口是学生端最核心的入口。Controller 接收前端传来的体温、健康状态、位置,从 Session 里取当前登录用户 ID,然后调用 Service。返回格式统一用Result对象,包含 code、msg、data 三个字段,前端好处理。

@RestController @RequestMapping("/api/clock") public class ClockController { @Autowired private ClockService clockService; @PostMapping("/in") public Result clockIn(@RequestBody ClockRecord record, HttpSession session) { // 从 Session 取登录用户,避免前端伪造 userId Long userId = (Long) session.getAttribute("userId"); if (userId == null) { return Result.fail(401, "请先登录"); } return clockService.clockIn(userId, record); } }

逻辑说明:userId 从 Session 取而不是前端传,这是安全底线。如果前端传 userId,别人改一下就能替别人打卡。@RequestBody接收 JSON,前端用 axios 提交时 content-type 要设 application/json。

参数说明:Result里的 code 用 200 表示成功,401 表示未登录,500 表示业务失败。前端根据 code 判断,不要只判断 HTTP 状态码,因为业务失败时 HTTP 仍是 200。

4.2 MyBatis 映射文件里的日期处理

MyBatis 处理LocalDate需要额外配置,否则会报「No typehandler found」。在 MyBatis 3.5 以上,内置了 JSR-310 支持,但要在配置文件里确认没有关掉。如果用的是老版本,需要自己写 TypeHandler。

<!-- ClockRecordMapper.xml 片段 --> <select id="selectByUserAndDate" resultType="com.example.epidemic.entity.ClockRecord"> SELECT * FROM clock_record WHERE user_id = #{userId} AND clock_date = #{date} </select> <select id="countByDateRange" resultType="int"> SELECT COUNT(*) FROM clock_record WHERE clock_date BETWEEN #{start} AND #{end} </select>

逻辑说明:selectByUserAndDate用于打卡前的重复校验,参数是 userId 和 LocalDate。countByDateRange用于统计某段时间的打卡总数,返回 int。

参数说明:#{date}传 LocalDate 时,MyBatis 会自动转成 java.sql.Date。如果报类型错误,检查 mybatis-config.xml 里是否注册了LocalDateTypeHandler,或者升级 MyBatis 到 3.5 以上。

4.3 教师端统计的聚合查询

教师端要看到「某天某班级的打卡人数和未打卡名单」。这个查询分两步:先查班级下所有学生,再查这些学生当天的打卡记录,最后做差集。不要试图用一条 SQL 搞定,可读性差且不好维护。

public StatVO getClassStat(Long classId, LocalDate date) { // 1. 查班级所有学生 List<User> students = userMapper.selectByClassId(classId); // 2. 查当天已打卡的用户ID List<Long> clockedIds = clockRecordMapper.selectUserIdsByDate(date); // 3. 计算未打卡名单 List<User> notClocked = students.stream() .filter(s -> !clockedIds.contains(s.getId())) .collect(Collectors.toList()); StatVO vo = new StatVO(); vo.setTotal(students.size()); vo.setClocked(students.size() - notClocked.size()); vo.setNotClockedList(notClocked); return vo; }

逻辑说明:用 Java 做差集而不是 SQL,是因为逻辑清晰,老师一眼能看懂。数据量大时可以在 SQL 层用 LEFT JOIN 优化,但毕业设计的数据量下,这种写法完全够用。

参数说明:selectUserIdsByDate只查 user_id 字段,减少数据传输。StatVO里的notClockedList用于前端展示未打卡名单,方便教师催报。

4.4 Excel 导出用 POI 还是 EasyExcel

教师端常要求导出打卡数据。用 Apache POI 写起来繁琐,但可控性强;用 EasyExcel 代码少,但多一个依赖。我一般推荐 EasyExcel,因为毕业设计时间紧,少写代码少出错。

// EasyExcel 导出示例 public void export(HttpServletResponse response, LocalDate start, LocalDate end) throws IOException { List<ClockExportVO> list = clockService.listForExport(start, end); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=clock_data.xlsx"); EasyExcel.write(response.getOutputStream(), ClockExportVO.class) .sheet("打卡数据") .doWrite(list); }

逻辑说明:设置响应头让浏览器识别为下载文件,ClockExportVO上用@ExcelProperty注解指定列名。doWrite一次性写入,数据量大时可以用分页写入。

参数说明:文件名用英文避免乱码,中文文件名需要 URLEncoder 编码。Content-Disposition里的 filename 如果含中文,不同浏览器表现不一致,稳妥做法是编码后传。

5. 避坑与排查:这套系统跑不起来时先看这几处

5.1 现象:启动报「No qualifying bean of type」

原因:Spring 没扫描到 Service 或 Mapper。常见于包路径写错,或者@Service、@Mapper注解漏加。解决:检查 spring 配置文件里的component-scan路径是否覆盖到 service 包,MyBatis 的MapperScannerConfigurer里basePackage是否指向 mapper 包。如果用的是注解式@MapperScan,确认写在配置类上。

5.2 现象:打卡时间比实际早或晚一天

原因:服务器时区不是东八区,LocalDate.now()取的是 UTC 日期。解决:在 JVM 启动参数加-Duser.timezone=Asia/Shanghai,或者在数据库连接串里指定serverTimezone=Asia/Shanghai。这个坑在云服务器上特别常见,本地跑没事,一部署就出问题。

5.3 现象:中文存入数据库变成问号

原因:数据库字符集不是 utf8mb4,或者连接串没指定编码。解决:建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,连接串加useUnicode=true&characterEncoding=utf8。注意 utf8 在 MySQL 里最多存 3 字节,emoji 存不了,用 utf8mb4 才彻底。

5.4 现象:重复提交导致数据库唯一键冲突报 500

原因:只靠数据库唯一索引,没在 Service 层做前置校验,或者并发下两个请求同时通过了前置校验。解决:Service 层先查后插,捕获DuplicateKeyException返回友好提示。极端并发下可以用INSERT ... ON DUPLICATE KEY UPDATE或者分布式锁,但毕业设计场景下,前置校验加异常捕获就够了。

5.5 现象:Excel 导出打开是乱码或提示文件损坏

原因:响应头没设对,或者输出流里混入了其他内容。解决:确认Content-Type是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,并且在写 Excel 之前没有response.getWriter()输出过任何字符。一旦用了 Writer,再拿 OutputStream 就会冲突。

6. 让答辩加分的一个技巧:把打卡数据做成可视化趋势图

功能跑通只是及格线,想让老师记住你,得有一个亮点。我一般会建议在教师端加一个「近 7 天打卡率趋势图」,用 ECharts 在前端渲染,后端只返回日期和打卡率两个数组。这个功能代码量不大,但视觉效果强,答辩时演示很抓眼球。

后端接口这样写:

@GetMapping("/trend") public Result trend(@RequestParam Long classId) { List<String> dates = new ArrayList<>(); List<BigDecimal> rates = new ArrayList<>(); LocalDate today = LocalDate.now(); for (int i = 6; i >= 0; i--) { LocalDate d = today.minusDays(i); dates.add(d.format(DateTimeFormatter.ofPattern("MM-dd"))); int total = userMapper.countByClassId(classId); int clocked = clockRecordMapper.countByClassAndDate(classId, d); // 避免除以零 BigDecimal rate = total == 0 ? BigDecimal.ZERO : BigDecimal.valueOf(clocked * 100.0 / total).setScale(1, RoundingMode.HALF_UP); rates.add(rate); } Map<String, Object> data = new HashMap<>(); data.put("dates", dates); data.put("rates", rates); return Result.success(data); }

逻辑说明:循环取近 7 天,每天算一次打卡率。setScale(1, HALF_UP)保留一位小数,前端显示更整洁。除以零的判断必须有,否则新班级没有学生时会抛异常。

参数说明:classId从请求参数取,前端下拉框选择班级后触发。日期格式用MM-dd,前端 X 轴显示短日期更清爽。返回的 rates 是百分比数值,ECharts 的 y 轴设max: 100即可。

前端用 ECharts 的折线图配置,把 dates 和 rates 塞进 series 就行。这个功能的价值在于,它把「打卡」这个动作变成了「可观察的趋势」,老师会认为你不仅会写 CRUD,还有数据意识。我自己的习惯是,每做一个管理系统,都至少留一个可视化出口,因为答辩时间有限,一张图比十页代码更能说明问题。

最后说个教训:别等到答辩前一天才部署到服务器。本地 Tomcat 和服务器 Tomcat 的版本差异、时区差异、数据库权限差异,每一个都能让你熬夜。提前一周在目标环境跑一遍,把上面那些坑挨个踩一遍,比事后补救强得多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询