ARTICLE DETAIL

资讯详情

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

SSM+JSP+layui实战:学生课堂考勤管理系统设计与实现

SSM+JSP+layui实战:学生课堂考勤管理系统设计与实现 简介这套学生课堂考勤管理系统基于SSMJSPlayui架构整合MySQL数据库适合Java学习者、课程设计或毕业设计场景。系统内置辅导员、管理员、教师、学生四种角色覆盖班级管理、学生管理、请假审批、签到管理、密码修改等完整业务链条前端采用layui组件库交互友好可帮助读者理解SSM整合、权限设计与分层开发思路。资源包共1802个文件包含JSP页面、Java源码、XML配置、CSS/JS前端样式、依赖JAR包及数据库脚本等整体压缩包33.32MB目录结构清晰便于查找与二次开发。已有765人学习下载适合需要快速搭建SSM项目模板或参考完整考勤功能实现的读者。1. 学生课堂考勤管理系统为什么SSMJSPlayui这套组合还是Java Web项目的经典答案学生课堂考勤管理系统用JavaSSMJSPlayuiMySQL这套技术栈实现本质上是在解决一个很具体的问题把课堂点名从纸质表格变成系统记录让老师实时看到谁到了、谁缺席、谁请假期末能一键算出出勤率。这套组合在Java Web领域非常典型Spring管理业务对象SpringMVC接收请求MyBatis操作数据库JSP渲染后端页面layui把后台界面做得像模像样。对正在学Java的开发者、做课程设计的学生、需要给中小型机构做内部系统的人这是性价比最高的起步方案。技术不算新但每一层都是Java面试八股文绕不开的东西做完一遍SSM基本功就扎实了。2. SSM三件套在考勤系统里的分工Spring容器、SpringMVC路由、MyBatis持久层2.1 Spring容器考勤业务中Service层的Bean管理与事务Spring在考勤系统里干两件事管理Service层对象以及管理事务。比如AttendanceService处理签到逻辑StudentService维护学生信息。如果不用Spring你得在每个Controller里手动new这些对象还要自己处理数据库连接关闭代码会非常啰嗦。常见做法是在Service类上加Service注解然后在Spring配置里配好组件扫描和事务管理器context:component-scan base-packagecom.campus.attendance / bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource / /bean tx:annotation-driven transaction-managertransactionManager /这段配置的逻辑是Spring启动时扫描com.campus.attendance包及子包把所有标注了Service、Repository、Controller的类纳入容器。事务管理器绑定数据源调用签到方法时Spring自动开启事务方法结束无异常就提交有异常就回滚。参数说明base-package必须是你实际放代码的包路径别扫成com.campus这种父级目录否则会把不该加载的组件也注册进去。DataSourceTransactionManager是JDBC和MyBatis时代最常用的事务实现SSM手动装配阶段必须显式声明SpringBoot里虽然会自动配置但SSM中没有这一步事务完全不起作用。Service层注入Mapper也非常简洁Service public class AttendanceService { Autowired private AttendanceMapper attendanceMapper; Autowired private StudentMapper studentMapper; }Autowired按类型注入Spring在容器里找出匹配的Bean。这里的隐藏前提是MyBatis的Mapper接口需要被扫描注册通常用MapperScan注解或MapperScannerConfigurer配置。漏掉这步启动时会直接报NoSuchBeanDefinitionException这是新手特别容易踩的第一道门槛。2.2 SpringMVC请求映射从前端按钮到Controller方法SpringMVC负责处理HTTP请求。考勤系统里典型的请求链是学生在JSP页面点击「签到」按钮表单提交到/attendance/signInSpringMVC根据RequestMapping映射找到Controller方法执行业务逻辑后返回JSON数据或页面视图。Controller层的标准写法Controller RequestMapping(/attendance) public class AttendanceController { Autowired private AttendanceService attendanceService; RequestMapping(value /signIn, method RequestMethod.POST) ResponseBody public Result signIn(RequestParam(studentId) Integer studentId, RequestParam(courseId) Integer courseId, HttpSession session) { Integer userId (Integer) session.getAttribute(userId); if (userId null) { return Result.error(未登录无法签到); } boolean success attendanceService.signIn(studentId, courseId); return success ? Result.ok(签到成功) : Result.error(签到失败请检查是否重复签到); } }关键参数说明value/signIn是路由地址前端表单action必须和它一致methodRequestMethod.POST限定只能POST请求进来防止用户在浏览器地址栏直接GET访问造成脏数据GET请求在考勤这种写操作里必须禁掉RequestParam绑定请求参数参数名必须和前端提交的name属性完全一致大小写敏感ResponseBody表示直接返回JSON而不是跳转页面配合layui的Ajax请求是SSM项目里最顺手的协作方式。这里有个SpringMVC配置细节不能被忽略视图解析器的配置决定了JSP的存放位置mvc:annotation-driven / bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views/ / property namesuffix value.jsp / /bean配置了它Controller里return attendance/list时SpringMVC会转发到/WEB-INF/views/attendance/list.jsp。习惯上把JSP放在WEB-INF下是为了防止用户绕过Controller直接访问页面好处是获取数据的入口被统一管控这一点在做权限校验时特别有用。2.3 MyBatis持久层考勤记录的SQL与动态查询MyBatis在整套里就是数据访问层比JDBC省大量样板代码比Hibernate更容易控制SQL。签到插入是最典型的用法。insert idinsertAttendance parameterTypecom.campus.attendance.entity.Attendance useGeneratedKeystrue keyPropertyattendanceId INSERT INTO t_attendance (student_id, course_id, sign_in_time, status, remark) VALUES (#{studentId}, #{courseId}, NOW(), #{status}, #{remark}) /insertuseGeneratedKeystrue表示要MySQL自增主键回填keyProperty指定回填到Java对象的哪个属性。这里有一个容易被忽视的参数细节sign_in_time字段不要用Java代码里的new Date()传参直接在SQL里用NOW()更稳妥因为数据库服务器和应用服务器的时间可能存在误差一旦应用服务器时钟不准所有考勤记录的时间都会偏移统计迟到时会产生冤假错案。MyBatis的高频需求是按条件查询考勤记录比如老师查某个学生某门课程的所有缺勤记录。动态SQL能解决条件不确定的问题select idselectAttendanceByCondition resultTypecom.campus.attendance.entity.Attendance SELECT a.*, s.student_name, c.course_name FROM t_attendance a LEFT JOIN t_student s ON a.student_id s.student_id LEFT JOIN t_course c ON a.course_id c.course_id where if teststudentId ! null AND a.student_id #{studentId} /if if testcourseId ! null AND a.course_id #{courseId} /if if teststatus ! null AND a.status #{status} /if /where ORDER BY a.sign_in_time DESC /select标签会自动去掉多余的AND 标签根据参数是否为空决定是否拼接条件。这个写法在考勤查询场景里几乎是标配老师可能只按课程查、只按学生查、或者学生加课程一起查。用#{}是预编译参数占位MyBatis会替换成?再做参数绑定这是防SQL注入的标准姿势。绝对不要用${}来拼参数${}是字符串直接替换攻击者可以在参数里拼接SQL片段造成数据泄露或删库风险。3. 数据库表设计与layui前端骨架把数据模型和页面框架立住3.1 学生、课程、考勤记录三张核心表的设计考勤系统的数据模型核心是三张表学生表、课程表、考勤记录表。这里给出实际项目中验证过的建表语句CREATE TABLE t_student ( student_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 学生ID, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, student_name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(11) COMMENT 手机号, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE t_course ( course_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher_id INT COMMENT 授课教师ID, course_time VARCHAR(100) COMMENT 上课时间描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE t_attendance ( attendance_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 考勤记录ID, student_id INT NOT NULL COMMENT 学生ID, course_id INT NOT NULL COMMENT 课程ID, sign_in_time DATETIME COMMENT 签到时间, sign_date DATE COMMENT 签到日期, status TINYINT NOT NULL COMMENT 1正常 2迟到 3缺勤 4请假, remark VARCHAR(200) COMMENT 备注, FOREIGN KEY (student_id) REFERENCES t_student(student_id), FOREIGN KEY (course_id) REFERENCES t_course(course_id), INDEX idx_student_course (student_id, course_id), INDEX idx_course_date (course_id, sign_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;设计说明status字段用TINYINT存数字状态1正常、2迟到、3缺勤、4请假。不要直接存「正常」「迟到」这样的中文原因有两个数字占用空间极小后期扩展状态机比如加一个「早退」只加数字不改表结构。我在很多项目里见过直接用varchar存状态的最后统计时用字符串比较效率差而且容易写错。sign_date这个字段是为了配合唯一索引解决重复签到问题原理在下一章详细展开。联合索引idx_student_course覆盖「查某个学生在某门课程所有考勤记录」的场景idx_course_date覆盖按课程和日期范围做统计的场景。如果你不加这些索引等数据量到几万条后查询时间会明显上升老师期末导出成绩时页面转圈圈。外键要不要加是个取舍。加了外键MySQL在插入、删除时会做完整性约束检查保证数据一致但带来额外性能开销在高并发写入下可能产生锁等待。考勤系统并发量极低数据完整性比性能更重要建议加上。以后如果要做高并发版本的微服务拆分外键再摘掉也不迟。3.2 layui表格组件如何对接后端JSON数据layui的核心优势是不用写大量JavaScript逻辑只要把数据按约定格式返回表格就能自动渲染。这对于后端为主的Java开发者来说非常友好不用深入研究Vue或React。layui表格要求的JSON结构如下{ code: 0, msg: , count: 100, data: [ {studentId: 1, studentName: 张三, courseName: 数据结构, status: 1, signInTime: 2025-05-20 08:32:00}, {studentId: 2, studentName: 李四, courseName: 数据结构, status: 2, signInTime: 2025-05-20 08:45:00} ] }页面里渲染表格的代码layui.use([table, layer], function() { var table layui.table; var layer layui.layer; table.render({ elem: #attendanceTable, url: /attendance/list, page: true, cols: [[ {field: studentId, title: 学生ID, width: 80}, {field: studentName, title: 姓名, width: 100}, {field: courseName, title: 课程, width: 150}, {field: status, title: 状态, width: 100, templet: function(d) { if (d.status 1) return span stylecolor:green正常/span; if (d.status 2) return span stylecolor:orange迟到/span; if (d.status 3) return span stylecolor:red缺勤/span; if (d.status 4) return span stylecolor:blue请假/span; return d.status; }}, {field: signInTime, title: 签到时间, width: 170} ]], limit: 10, limits: [10, 20, 30] }); });这段代码的逻辑要点layui的table组件自动向/attendance/list发起GET请求请求参数中自动携带page和limit分页参数后端接收到后返回上面的JSON结构。templet函数用于对status字段做列渲染转换把数字显示成带颜色的文字这是layui中最常用的自定义列模板接口。limit和limits控制每页条数及可选的每页条数选项。特别注意code字段的值。layui约定code为0才算请求成功如果你后端返回的code是200或别的值表格会一直显示「数据加载失败」。这个细节是排查表格问题时的首要检查点。如果前后端是同一团队建议在后端统一封装Result工具类时就把code含义对齐0成功非0失败。3.3 用layui的弹出层实现签到弹窗签到操作常见的交互方式是弹窗确认里面显示当前课程和学生信息点击确认后提交签到请求。layui的layer模块提供了轻量API。var index layer.open({ type: 1, title: 签到确认, content: $(#signInModal), area: [400px, 300px], btn: [确认签到, 取消], yes: function() { var studentId $(#hiddenStudentId).val(); var courseId $(#hiddenCourseId).val(); $.ajax({ url: /attendance/signIn, type: POST, data: {studentId: studentId, courseId: courseId}, success: function(res) { if (res.code 200) { layer.close(index); layer.msg(签到成功); table.reload(attendanceTable); } else { layer.msg(res.msg, {icon: 2}); } } }); } });type:1表示页面层content传入页面中已有的div元素area控制弹窗尺寸btn定义确认和取消两个按钮yes回调里执行Ajax提交。注意layer.msg的icon参数2表示错误图标1表示正确图标细节能提升使用体验。踩坑提示content直接传$(#signInModal)时这个div必须存在于body直接子节点并且通常是display:none隐藏状态如果把它嵌套在表格容器内部layer获取内容时会把外层容器的样式也带进去导致弹窗内容错位或样式异常。4. 核心考勤流程的实现签到、请假、统计三个关键模块的代码落地4.1 签到接口防止重复签到不能只靠if-else签到是最核心的功能也是最容易出bug的地方。一个学生同一节课只能签到一次如果没做防重逻辑学生刷新一下页面就产生两条考勤记录期末统计出勤率全错。防重逻辑放在Service层核心思路是「先查再插」public boolean signIn(Integer studentId, Integer courseId) { // 第一步查询是否已存在今天的签到记录 Attendance existing attendanceMapper.selectTodayAttendance(studentId, courseId); if (existing ! null) { return false; // 已签到返回false } // 第二步插入新记录 Attendance attendance new Attendance(); attendance.setStudentId(studentId); attendance.setCourseId(courseId); attendance.setStatus(1); attendance.setRemark(正常签到); int rows attendanceMapper.insertAttendance(attendance); return rows 0; }这里的逻辑正确性依赖一个前提查询和插入之间没有并发。但实际场景中如果一个学生快速双击签到按钮或者两台设备同时登录同一个账号操作就可能出现「先查没有同时插入」的竞态条件。解决这个问题不能只靠Java代码判断要在数据库层面做约束。最稳妥的做法是加唯一索引。注意不能直接在sign_in_time上加唯一索引因为DATETIME包含时分秒两次操作时间不同就不冲突。要单独加一个sign_date字段只记录日期ALTER TABLE t_attendance ADD UNIQUE INDEX uk_student_course_date (student_id, course_id, sign_date);然后在insert SQL里给sign_date赋值CURDATE()。这样数据库确保同一个学生在同一天同一门课只能有一条记录即使Java代码有并发数据库也会直接拒绝第二条插入。这才是真正不依赖业务代码的兜底方案。经验之谈应用层的防重是辅助数据库的唯一约束才是保证。这个原则在做支付、券码、考勤这类「事件记录型」功能时都适用。我见过很多团队只在Service层判重压测一上来就出现大量重复数据最后靠唯一索引把脏数据洗干净再堵住。4.2 请假审批状态机的流转不能跳步请假流程设计学生提交请假申请状态为「待审批」教师审批通过后变成「已通过」拒绝则变成「已驳回」。这是典型的状态机流转。请假表结构CREATE TABLE t_leave ( leave_id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, leave_date DATE NOT NULL, reason VARCHAR(500) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待审批 1已通过 2已驳回, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, approve_time DATETIME, approve_remark VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT请假表;审批逻辑public boolean approveLeave(Integer leaveId, Integer approveStatus, String approveRemark) { Leave leave leaveMapper.selectById(leaveId); if (leave null) { throw new BusinessException(请假记录不存在); } // 状态机校验只有待审批状态才能流转 if (leave.getStatus() ! 0) { throw new BusinessException(该记录已审批不能重复操作); } Leave updated new Leave(); updated.setLeaveId(leaveId); updated.setStatus(approveStatus); updated.setApproveTime(new Date()); updated.setApproveRemark(approveRemark); return leaveMapper.updateStatus(updated); }BusinessException是一个自定义运行时异常配合全局异常处理器返回给前端友好提示。状态机流转的要点是每步操作前先判断当前状态是否允许这次操作。「待审批」可以流转到「通过」或「驳回」但一旦变成「已通过」就不能再改。没有这个保护老师误点审批按钮就会覆盖状态追溯记录全乱。进一步加固可以加乐观锁字段version每次更新时比对并递增。不过考勤这种低频系统状态机校验已经够了。如果未来做高并发审批再考虑乐观锁。4.3 考勤统计出勤率计算与Excel导出统计模块是期末老师最依赖的功能。计算四个数字总课次数、出勤次数、迟到次数、缺勤次数。出勤率口径和业务约定清楚这里采用出勤率等于正常出勤次数除以总课次数请假的算缺勤分母。不同业务口径只需调整SQL。统计SQLselect idstatisticsByStudent resultTypemap SELECT student_id, COUNT(*) AS total_count, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN status IN (3, 4) THEN 1 ELSE 0 END) AS absent_count FROM t_attendance WHERE student_id #{studentId} AND course_id #{courseId} GROUP BY student_id /select这个SQL用CASE WHEN加SUM实现条件计数是统计出勤率最高效的写法不用在Java循环里多次查库一次把四个指标全查出来。导出Excel用Apache POI核心骨架public void exportAttendanceExcel(Integer courseId, HttpServletResponse response) { ListMapString, Object stats attendanceMapper.statisticsByCourse(courseId); XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(考勤统计); Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(总课次); header.createCell(3).setCellValue(出勤数); header.createCell(4).setCellValue(迟到数); header.createCell(5).setCellValue(缺勤数); header.createCell(6).setCellValue(出勤率); int rowNum 1; for (MapString, Object stat : stats) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(String.valueOf(stat.get(student_id))); row.createCell(1).setCellValue((String) stat.get(student_name)); row.createCell(2).setCellValue(((Number) stat.get(total_count)).intValue()); row.createCell(3).setCellValue(((Number) stat.get(normal_count)).intValue()); row.createCell(4).setCellValue(((Number) stat.get(late_count)).intValue()); row.createCell(5).setCellValue(((Number) stat.get(absent_count)).intValue()); double rate ((Number) stat.get(normal_count)).doubleValue() / ((Number) stat.get(total_count)).doubleValue(); row.createCell(6).setCellValue(rate); } }XSSFWorkbook对应Excel 2007以上的xlsx格式老版本Excel需要改用HSSFWorkbook输出xls。两个细节要注意数字单元格用setCellValue传入double或int日期类型的单元格要设置cellStyle格式否则导出后是序列化数字。response要设置Content-Disposition头且文件名做URL编码否则浏览器下载时中文文件名乱码。5. 避坑专题JSPlayuiSSM混编场景最常翻车的5个问题5.1 现象JSP页面layui表格渲染不出任何数据页面上表格区域空白浏览器控制台没报错但Network里请求已经发出去了。原因通常是后端返回的JSON结构和layui约定的结构不一致。layui要求{code:0, msg:, count:100, data:[...]}但很多SSM项目的公共返回类封装的格式是{code:200, message:成功, data:{list:[...], total:100}}layui不认这种嵌套直接放弃渲染。解决办法有两种。第一种改后端返回结构把Result的code字段改成0代表成功count字段单独赋值。第二种在前端用parseData做数据解析不改变后端接口table.render({ elem: #attendanceTable, url: /attendance/list, parseData: function(res) { return { code: res.code 200 ? 0 : 1, msg: res.msg, count: res.data.total, data: res.data.list }; }, cols: [[...]] });我建议直接在前端用parseData因为后端接口可能被多个页面复用改动返回结构会影响所有调用方。排查顺序打开浏览器Network面板看请求响应体比对实际JSON结构和layui期望结构把字段对齐全再谈其他。5.2 现象MyBatis查询出的时间字段变成一串数字考勤记录的signInTime查询出来应该是2025-05-20 08:30:00但页面显示的是1710892200000这种毫秒时间戳或者2025-05-20T08:30:00这种ISO格式。老师看到直接懵期末报表全废。原因是Java对象里的Date字段序列化成JSON时Jackson默认按时间戳或ISO格式输出没有配置格式化。这不是MyBatis的问题是JSON序列化的配置问题。使用SpringMVC默认的Jackson在实体类日期字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date signInTime;如果是Fastjson用JSONField(formatyyyy-MM-dd HH:mm:ss)注解。timezone参数不能省否则可能和北京时间差8小时。每次写代码时看到实体类里有Date字段先问一句这个字段会出现在JSON响应里吗会就提前加上格式化注解。5.3 现象POST请求中文乱码页面上提交的学生姓名到后端变成乱码或者响应数据里的中文在页面上成了问号。这个现象在把项目从Windows开发环境部署到Linux服务器时尤其容易暴露。几乎都是编码不一致导致的。JSP页面本身是UTF-8请求通过浏览器发送时默认按页面编码但服务器端接收时按ISO-8859-1解码中文自然乱了。MySQL连接串没指定characterEncoding数据库层也会再乱一次。三处编码必须对齐。第一处JSP页面顶部声明% page contentTypetext/html;charsetUTF-8 %第二处SpringMVC配置CharacterEncodingFilterfilter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping第三处MySQL连接串加编码参数jdbc:mysql://localhost:3306/attendance_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai排查顺序浏览器Console看请求头Content-Type有没有charset服务端过滤器配没配数据库连接串有没有编码参数最后看表字符集是不是utf8mb4。四层逐个检查总有一层在漏。5.4 现象Maven打包后JSP页面404在IDEA里启动一切正常但用Maven打成war包部署到Tomcat之后页面访问全部404JSP文件变成找不到资源。原因通常是Maven没有把JSP文件作为资源打包。Maven默认只把src/main/resources下的内容当资源src/main/webapp里的JSP在开发时由IDEA的Tomcat插件直接识别但打包时如果pom.xml里显式配置了resources标签且没包含webapp目录JSP就不会进入war包。检查pom.xml如果配置了resources确保webapp目录在构建范围内build resources resource directorysrc/main/resources/directory filteringtrue/filtering /resource resource directorysrc/main/webapp/directory filteringfalse/filtering /resource /resources /build排查方法很简单用解压工具打开war包看WEB-INF下面有没有JSP文件。没有就是打包配置问题。另外JSP放WEB-INF/views下时访问路径要通过Controller转发只能由SpringMVC控制放webapp根目录则可以被直接访问。SSM项目推荐放WEB-INF下减少未经过Controller的裸访问也避免JSP路径暴露给前端用户。5.5 现象数据库连接池耗尽导致应用卡死应用运行一段时间后所有请求卡住日志报Connection is not available, request timed out after 30000ms重启后才恢复。原因是连接池的配置泄漏。最常见的路径在Mapper或DAO里手动获取了Connection但没在finally里关闭或者SqlSession没关闭连接一直占着连接池。另一个常见路径是事务方法里调用了外部接口外部接口慢且没设超时时间事务一直挂着连接一直被占用。如果是Druid连接池检查配置spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: truemax-active别拍脑袋填100。连接池上限不是越大越好每个连接都会占用MySQL的内存和文件描述符超出MySQL端的max_connections配置反而报错。考勤系统并发量不高max-active20足够。代码层面排查如果直接在业务里用了SqlSessionTemplate确保在finally里关闭用了Spring声明式事务注意方法粒度不要在大事务里做网络IO。Druid监控页面能实时看到连接池的活跃连接数和占用来源定位到具体DAO是最快的办法。6. 把这个系统从「能跑」做到「能用」回归验证、校验分层与索引优化6.1 用Postman做接口回归验证把考勤系统的全部接口整理成Postman集合用环境变量管理测试数据。每次改代码后在Collection Runner里跑一遍全量接口确认签到、请假审批、统计报表核心链路没有回归。额外增加一个「重复签到」测试用例验证唯一索引真的把第二笔插入挡住了。接口验证不仅是开发阶段的动作也是项目交接时给维护者的一份系统行为说明比文档直观得多。6.2 参数校验放在Controller层还是Service层我的习惯是Controller层做格式校验参数为空、学号格式不对这些Service层做业务校验学生是否存在、是否已签到。如果一个Service方法只被一个Controller调用你可以把校验全放Service层Controller变薄。一旦Service方法将来被多个入口调用业务校验必须下沉到Service否则每个入口都要写一套漏掉一个就会出事故。6.3 给查询加一条覆盖索引考勤统计列表经常按course_id加sign_date过滤在t_attendance表加一条索引能让统计查询直接走索引不需要回表访问整行数据ALTER TABLE t_attendance ADD INDEX idx_course_date (course_id, sign_date);加完索引后执行EXPLAIN查看执行计划的Extra列如果出现Using index就说明这条索引被覆盖了。这可能是整个考勤项目里性价比最高的一次优化几十行SQL的执行时间能从几百毫秒降到个位数。我自己的习惯是每写完一个模块花十分钟思考「下次再做一遍这个系统哪些坑要一开始就避开」。目前排前三的是数据库字符集从一开始用utf8mb4而不是utf8签到防重的唯一索引建表时就加上而不是等出现脏数据再补请假状态机保留version字段为以后并发预埋冗余。这个习惯帮我接手新项目时少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表