如果只看标题里的那一串技术名词,可能会觉得“又是一个堆框架的CRUD系统”。但真正做过的同学都知道,企业考勤管理系统这种题目,是javaweb方向里面最考验综合能力的实战项目之一,它同时牵扯到权限设计、流程状态机、时间计算、报表聚合、前端交互,还要把springboot、jsp、layui、mysql、maven这条链路完整跑通。我前后带过不少同学做同类型项目,也接手过几个企业里正在用的考勤系统,今天就把这套技术选型下的考勤管理系统拆开揉碎,从架构思路到表结构再到部署排查,完整走一遍。
这套系统听起来很“课程设计”,但实际上并不简单。很多人在答辩或者上线的时候被问倒,问题往往不是集中在“会不会用springboot写接口”,而是集中在:考勤这种强规则的业务,你是怎么把“迟到”“早退”“请假”“加班”这些状态算清楚的?数据表怎么设计的?多人同时打卡的时候怎么保证不串数据?这些才是真正拉开差距的地方。
适合谁来参考:正在做毕设或课程设计的同学,想转java开发准备找工作、需要一个完整项目经历的初学者,以及已经会用springboot写增删改查、想提升业务建模能力的人。看完之后,你可以直接照着这套思路落地一个能跑的考勤系统,也能搞懂背后的设计逻辑。
1. 项目定位与技术栈选型
1.1 这个项目到底是做什么的
企业考勤管理系统的核心价值,是把员工每天的到岗情况数据化,再转成管理层能用的考勤结果。粗看它只是替代“纸质打卡机”,但实际上它要解决的是四个核心问题:一是员工身份识别与打卡记录;二是考勤规则计算(迟到、早退、缺勤、加班);三是异常考勤处理(请假、补卡、外勤);四是统计报表输出(月报、部门汇总、导出)。
如果用一句话概括业务闭环,就是:员工打卡产生原始数据,系统按规则计算成考勤结果,管理员审核异常数据,最终生成工资核算与人事管理依赖的统计报表。这个链路里,每一个环节都能扩展出很多细节,也正是这些细节决定了系统是“能演示的demo”还是“能用的系统”。
这个项目本身没有什么复杂算法,难点集中在“状态转化”和“时间维度计算”这两块。理清楚这两块,整个系统就已经完成了一大半。
1.2 技术组合背后的真实逻辑
很多同学拿到标题里的技术栈,第一反应是“为什么用jsp,现在不都前后端分离吗?”。这个疑问是合理的,但从实际项目角度看,这套组合并不是落后的,而是非常适合快速交付、单人维护、权限简单的企业内部系统。
Spring Boot在这里承担的是后端基础框架的角色。它的starter机制把springmvc、事务管理、参数校验、json序列化这些基础能力自动装配好,让开发者不用像早期ssh那样写一堆xml配置。对于考勤系统这种业务复杂度中等、并发量不高的系统来说,springboot的开发效率优势非常明显。
JSP在这里并不是用来写复杂页面的,而是配合模板渲染,快速生成服务端页面。javaweb的经典流程是浏览器请求发到servlet,servlet调业务层,再forward到jsp渲染html返回。springboot原生不推荐jsp,但通过配置依然能完美支持。选择jsp而不是freemarker或thymeleaf,通常是因为教程生态成熟、调试直观、部署简单,而且很多企业内部老系统就是jsp技术栈,学会它反而更容易上手。
Layui和Bootstrap的定位需要分开看。Layui这个框架自带一套后台管理风格的组件,表格、表单、弹层、日期选择器这些都是现成的,非常适合快速搭出一个“像样的系统界面”。Bootstrap则主要用来做基础布局和栅格系统,以及弹窗、按钮等基础样式。在实际开发中,我一般用Bootstrap定全局样式,用Layui的table组件和后端接口对接渲染动态数据,这套组合能省掉大量手工拼html的精力。
MySQL承担持久化任务,Maven负责依赖管理和构建打包。MySQL在考勤这种读写比高、事务要求严格的场景下非常合适;Maven则是java项目标配,不用多说。
这套技术栈的组合逻辑是:springboot解决后端开发效率,jsp解决页面服务端渲染,layui+bootstrap解决界面交互,mysql解决数据可靠性,maven解决工程化。每一层解决一层的问题,没有多余的组件。
1.3 为什么不做前后端分离
这里单独说一下前后端分离的问题,因为这是选型时被问得最多的点。使用Vue/React+Spring Boot是目前互联网项目的绝对主流,那为什么还要用jsp+layui这套“老方案”?
首先是交付成本问题。前后端分离意味着要维护两套工程,需要处理跨域、token鉴权、接口文档维护,这些对单人开发的小系统来说是额外负担。而jsp的模式天然把页面和后端放在同一个工程里,编译、打包、发布都是单体的操作,部署只需要一个jar+外部静态资源,复杂度低很多。
其次是这个项目本身的定位。企业考勤系统的使用场景集中在局域网、公司内网或者云服务器上的单点部署,用户量几十到几百人,根本没有必要拆出独立前端工程。用一个单体应用把请求-渲染-响应整条链路打通,反而更容易维护和升级。
最后是学习路径问题。很多同学的毕设或培训项目要求完整走一遍javaweb核心知识点,jsp正好把request、response、session、cookie、servlet这些概念全部体现出来了,对理解web底層原理非常有帮助。等这套跑通了,再去学前后端分离,理解深度完全不一样。
2. 核心功能模块拆解与数据库设计
2.1 权限模型与功能清单
考勤系统的用户角色,一般拆成三种:普通员工、部门管理员、系统管理员。不需要做太复杂的RBAC权限表,用一张用户表里的角色字段就能控制。
普通员工的核心动作是打卡、查看个人考勤记录、提交请假和加班申请。部门管理员可以做部门维度的考勤审批和记录查询。系统管理员拥有全部权限,包括员工信息管理、考勤规则设置、部门管理、全公司报表导出。
功能清单拆下来是这么六块:
- 登录与个人中心:账号密码登录、修改密码、个人信息展示
- 员工管理:员工信息增删改查、按部门筛选、账号启停用
- 考勤打卡:上班卡、下班卡、外勤打卡、补卡申请
- 考勤查询:个人日/月视图、管理员部门维度视图、异常记录筛选
- 请假与加班管理:申请、审批(通过/驳回)、状态跟踪
- 统计报表:按月汇总出勤天数、迟到次数、加班时长,导出Excel
实际开发时,不建议一上来就想着“全功能平铺”,先把打卡记录这个主链路跑通,再逐步加审批和报表,这样开发节奏更可控。
2.2 六张核心表怎么设计
数据库设计是考勤系统的重中之重。表设计直接决定后面写SQL的复杂度,也决定系统能不能支持后期的统计需求。我按实际项目经验整理了一套最少可用、但足够覆盖业务的表结构。
员工表(employee):存员工的基本信息和账号信息。核心字段是id、工号、姓名、部门id、职位、手机号、邮箱、账号、密码、角色(0员工/1管理员/2超级管理员)、状态(0在职/1离职)、入职时间。注意工号要唯一,建议用工号作为业务主键,而不是直接用自增id。
部门表(department):id、部门名称、负责人id、创建时间。这张表很简单,但一定要建,因为考勤统计是按部门聚合的。
考勤记录表(attendance):这是全系统的核心表。字段包括id、员工id、考勤日期、上班打卡时间、下班打卡时间、上班状态(正常/迟到/缺卡)、下班状态(正常/早退/缺卡)、工作时长(分钟)、加班时长(分钟)、考勤来源(1打卡/2补卡/3管理员手工录入)。这里有一个关键设计点:一条记录对应员工某一天,而不是把打卡动作拆成多次记录,这样写统计SQL会省事非常多。
请假申请表(leave):id、员工id、请假类型(事假/病假/年假/调休)、开始时间、结束时间、请假时长(天)、请假原因、审批状态(0待审批/1通过/2驳回)、审批人id、审批时间、审批意见。
加班申请表(overtime):字段结构和请假表类似,多一个加班时段字段,用来区分工作日加班、周末加班、节假日加班。
补卡申请表(repair):id、员工id、补卡日期、补卡类型(上班卡/下班卡)、补卡时间、补卡原因、审批状态、审批人id、审批时间。补卡这个模块很容易被忽略,但实际企业里用的非常频繁,员工忘打卡是常态,一定要有。
这六张表之间通过employee_id关联,逻辑清晰且能满足绝大多数查询需求。如果后期需要做工资核算,可以在考勤记录表的基础上联查请假和加班汇总,不需要再做更复杂的表结构。
2.3 考勤规则怎么落成代码
考勤规则是整个系统最容易出错的地方,原因在于“规则表达”和“时间计算”天然容易产生歧义。我在项目里通常用一套配置参数的方式解决,把规则抽到一张配置表或者配置文件里,而不是写死在代码里面。
需要配置的核心参数是这些:上班时间(如09:00)、下班时间(如18:00)、迟到容忍分钟数(如10分钟)、早退容忍分钟数(如10分钟)、标准工作时长(如8小时)、是否需要强制午餐休息扣减(如扣1小时)、加班最小计算单位(如30分钟起算,不足半小时不计)。
以“迟到判断”举例,代码的逻辑是这样:
// 从配置中心获取上班时间和容忍分钟数 LocalTime workStart = configService.getWorkStartTime(); int lateToleranceMinutes = configService.getLateToleranceMinutes(); // 判断是否迟到 if (attendance.getPunchInTime() == null) { attendance.setPunchInStatus("缺卡"); } else if (attendance.getPunchInTime().isAfter(workStart.plusMinutes(lateToleranceMinutes))) { attendance.setPunchInStatus("迟到"); } else { attendance.setPunchInStatus("正常"); }这套逻辑的核心思路是把“制度规则”翻译成“时间比较”和“状态赋值”,整个系统所有跟规则相关的代码都遵循这个模式。后面如果要调整规则,不用改代码,只改数据库配置即可。
3. 前后端整合与关键代码实现
3.1 Spring Boot的pom依赖核心配置
用Maven管理依赖是这个项目的基本功。如果你用的IDE是IDEA,创建Spring Boot项目时实际上会自动生成pom.xml,但里面的依赖需要手动梳理清楚。这里给出一份精简但完整的依赖清单。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.11</version> </dependency> </dependencies>这里面的关键依赖有三块:tomcat-embed-jasper是Spring Boot内嵌Tomcat支持JSP编译的核心,缺了它JSP页面会直接下载而不是解析;JSTL是JSP页面里用的标签库,用来做条件判断和循环遍历;Druid是阿里连接池,比默认的HikariCP多了一个可视化监控页面,调试SQL很方便。
另外记得在application.properties中配置JSP视图的前缀和后缀:
spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp server.port=8080 spring.datasource.url=jdbc:mysql://localhost:3306/attendance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456特别提醒:MySQL 8.0以上的驱动连接URL里必须加serverTimezone=Asia/Shanghai,否则会报“Cannot resolve serverTimezone”这个经典错误。这跟考勤系统强相关,因为涉及大量时间计算,统一时区是绝对底线。
3.2 登录会话与拦截器实现
考勤系统不推荐用前后端分离的token方案,因为JSP页面天然依赖Session,用好Session控制登录态是最稳的方案。
我习惯的做法是写一个Spring MVC的HandlerInterceptor,在请求进入Controller之前拦截,判断Session里有没有登录用户。如果没有就直接重定向到登录页。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } return true; } }然后在配置类里注册拦截器,并排除登录相关路径和静态资源路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/css/**", "/js/**", "/layui/**", "/static/**"); } }这里有一个很容易踩的坑:如果你用了JSP而静态资源放在src/main/resources/static目录下,拦截器的排除路径一定要把静态目录放行,否则页面上所有的css和js都会被拦截,页面会变得“全部裸奔”。这个问题的排查方式简单粗暴,打开浏览器F12看console里的网络请求,如果是403说明被拦截了,直接去检查excludePathPatterns。
3.3 打卡接口与异常判断
打卡功能是系统的核心入口。前端我用Layui的按钮和地图定位插件做交互,后端只接收一个坐标参数和时间参数。
打卡接口的完整逻辑可以拆成四个步骤:
- 从Session中拿到当前登录员工ID,校验员工状态
- 通过上班时间或下班时间判断这次打卡是上班卡还是下班卡
- 查询当天是否已有记录,决定是插入还是更新
- 通过规则引擎计算打卡状态,返回给前端
核心代码逻辑大致是这样的:
@PostMapping("/punch") @ResponseBody public R punch(HttpServletRequest request, Double latitude, Double longitude) { // 1. 获取当前登录员工 Employee employee = (Employee) request.getSession().getAttribute("loginUser"); LocalDate today = LocalDate.now(); // 2. 查询今天的考勤记录 Attendance attendance = attendanceMapper.selectByEmployeeIdAndDate(employee.getId(), today); // 3. 如果recordMac为空,则是第一次打卡,记为上班卡 if (attendance == null) { attendance = new Attendance(); attendance.setEmployeeId(employee.getId()); attendance.setWorkDate(today); attendance.setPunchInTime(LocalTime.now()); // 在这里判断是否迟到(使用配置的时间规则) attendance.setPunchInStatus(checkPunchIn(LocalTime.now())); attendanceMapper.insert(attendance); return R.ok("上班打卡成功", attendance); } // 4. 同一天第二次打卡,记为下班卡 if (attendance.getPunchOutTime() == null) { attendance.setPunchOutTime(LocalTime.now()); attendance.setPunchOutStatus(checkPunchOut(LocalTime.now())); // 计算工作时长:下班时间-上班时间,再减去午休时长 long workMinutes = Duration.between(attendance.getPunchInTime(), attendance.getPunchOutTime()).toMinutes(); long standardLunchBreak = configService.getLunchBreakMinutes(); attendance.setWorkMinutes((int) (workMinutes - standardLunchBreak)); // 计算加班时长(如果存在加班配置) attendance.setOvertimeMinutes(calculateOvertime(attendance)); attendanceMapper.update(attendance); return R.ok("下班打卡成功", attendance); } return R.error("今日已打卡完成"); }这段代码看起来只处理了一次打卡和二次打卡,但实际业务里还有几个特别容易漏的边界情况:
- 员工跨天工作时(比如凌晨下班),判断日期不能只靠本机时间,要引入“考勤班次日期”的概念
- 员工一天打多次卡(比如出门吃午饭回来再打),这里用第一次和最后一次作为标准,中间次数直接忽略,避免数据混乱
- 补卡之后如果当天又补了一次,应该覆盖原记录而不是新增,否则统计会产生重复数据
实操中发现,道路上大多数考勤项目死在“忽略重复打卡”这个问题上,务必在表设计时给employee_id + work_date加一个唯一索引,从数据库层面兜底防重复。
3.4 请假审批的状态流转设计
请假审批和加班申请本质上是同一个流程模型:提交 -> 待审批 -> 通过/驳回。我一般把状态字段设计成int类型:
- 0:待审批(提交后默认状态)
- 1:已通过(审批人点击通过)
- 2:已驳回(审批人点击驳回,并填写意见)
- 3:已撤销(申请人自己撤销)
在Controller层,审批操作的核心代码是状态校验加更新的过程:
@PostMapping("/approve") @ResponseBody public R approve(Integer leaveId, Integer approveStatus, String approveComment, HttpSession session) { Leave leave = leaveMapper.selectById(leaveId); // 校验当前状态必须是待审批 if (!leave.getStatus().equals(0)) { return R.error("该申请已处理,请勿重复审批"); } leave.setStatus(approveStatus); leave.setApproverId(((Employee) session.getAttribute("loginUser")).getId()); leave.setApproveComment(approveComment); leave.setApproveTime(new Date()); leaveMapper.update(leave); return R.ok("审批完成", null); }这段代码的逻辑不复杂,真正需要关注的是事务一致性。当一个请假申请被审批通过之后,这个时间段就应该自动回写考勤状态,把员工对应日期的记录从“缺卡”改成“请假”,否则员工明明请假了,月底统计仍然显示缺勤,这就非常恼火。
我们项目里用的是Spring的事务注解,把审批和考勤更新放在一个事务里面:
@Transactional(rollbackFor = Exception.class) public void approveLeaveAndUpdateAttendance(Leave leave, int approveStatus) { leaveMapper.update(leave); if (approveStatus == 1) { // 把员工请假日期内的考勤记录状态更新为“请假” attendanceMapper.updateStatusByDateRange( leave.getEmployeeId(), leave.getStartTime(), leave.getEndTime(), "请假" ); } }这种“审批通过后自动联动考勤”的逻辑,是很典型的业务闭环设计,也是答辩和面试时非常加分的细节。
3.5 Layui表格与后端数据接口对接
前端的核心交互集中在打卡页、记录查询页、审批页和管理报表页。用Layui的table组件可以非常高效地渲染服务端分页数据,具体的配合方式是:Layui的table请求后端接口,后端返回特定格式的JSON,前端自动分页渲染。
Layui默认的JSON格式是这样的:
{ "code": 0, "msg": "", "count": 100, "data": [...] }后端只需要构造一个统一返回对象,把分页的total和rows填进去就行。我在项目中写了一个PageResult类来统一处理:
public class PageResult { private Integer code; // 0是成功 private String msg; private Long count; // 总条数 private List<?> data; // 分页数据 public static PageResult success(List<?> list, Long total) { PageResult result = new PageResult(); result.setCode(0); result.setMsg(""); result.setCount(total); result.setData(list); return result; } }Controller里配合PageHelper分页非常方便:
@GetMapping("/records") @ResponseBody public PageResult records(Integer page, Integer limit, Integer employeeId, String dateRange) { PageHelper.startPage(page, limit); List<Map<String, Object>> list = attendanceMapper.selectRecordsWithDeptName(employeeId, dateRange); PageInfo<Map<String, Object>> pageInfo = new PageInfo<>(list); return PageResult.success(pageInfo.getList(), pageInfo.getTotal()); }前端页面对应的表格配置直接配置url、cols、page,Layui自己就完成分页、排序、刷新这些功能:
layui.use('table', function(){ var table = layui.table; table.render({ elem: '#attendanceTable', url: '/attendance/records', page: true, cols: [[ {field: 'workDate', title: '日期'}, {field: 'punchInTime', title: '上班时间'}, {field: 'punchOutTime', title: '下班时间'}, {field: 'punchInStatus', title: '上班状态'}, {field: 'punchOutStatus', title: '下班状态'}, {field: 'workMinutes', title: '工作时长'} ]] }); });这一套做下来,前后端联调基本是顺滑的,唯一需要提前约定的是JSON字段名和日期时间格式。建议后端统一返回“HH:mm:ss”这种字符串格式,前端才能直接展示,否则Java的LocalTime默认会把秒数串成“09:00:00”这种形式,界面展示上稍显冗余。
4. 部署上线与常见问题排查
4.1 Maven打包与部署方式
JavaWeb考勤系统的部署方式非常简单,因为用的是Spring Boot内嵌Tomcat,所以打包成可执行的jar即可,不需要额外安装Tomcat服务器。
打包命令就一条:
mvn clean package -Dmaven.test.skip=true打包完的jar文件在target目录下,直接运行:
java -jar attendance-system-0.0.1-SNAPSHOT.jar启动之后访问http://服务器IP:8080/login就能看到登录页。
没有装Maven的环境可以直接用IDEA右侧的Maven面板,双击package即可。如果打包过程中遇到依赖下载失败的问题,优先检查本地的Maven settings.xml是否配置了国内镜像源,我实际遇到过很多次因为从中央仓库下载失败导致打包失败的情况。
还需要额外说一点:用java -jar运行的时候,如果想让JSP能正常渲染,务必确认jar包里面包含/META-INF/resources/WEB-INF/jsp/这个路径下的页面。Maven默认打包时,如果jsp文件放在src/main/webapp/WEB-INF/jsp/目录下,需要加上一段构建配置来确保webapp资源被同步复制到target目录:
<build> <finalName>attendance-system</finalName> <resources> <resource> <directory>src/main/webapp</directory> <targetPath>META-INF/resources</targetPath> <includes> <include>**/*.*</include> </includes> </resource> </resources> </build>如果这一步漏掉,就会遇到IDE里启动正常、打包运行后页面404的问题。这是JavaWeb项目踩坑排行榜里前三名的位置。
4.2 时区问题与数据库时间字段选择
考勤系统的所有统计都跟时间相关,时区不一致会直接造成“打卡时间偏差8小时”,这个问题非常严峻。
MySQL 8.0的驱动需要配置serverTimezone=Asia/Shanghai,前面也提到过。在数据库建表时,考勤记录表的时间字段建议用DATETIME而不是TIMESTAMP,因为TIMESTAMP只支持1970-2038年,而DATETIME的范围更大。对于考勤这种历史数据需要长期保存的场景,DATETIME更稳妥。
实体类字段我统一使用的是java.time.LocalDateTime和java.time.LocalTime,不用java.util.Date,原因很简单:LocalDateTime更直观、线程安全、并且和MySQL的DATETIME映射非常自然。
private LocalDateTime createTime; // 创建时间 private LocalTime punchInTime; // 上班打卡时间 private LocalTime punchOutTime; // 下班打卡时间这里再强调一个容易踩的小坑:前端提交的时间参数传到后端时,字符串转LocalTime如果格式不对会直接报DateTimeParseException。建议在Controller入口统一做格式化处理:
@DateTimeFormat(pattern = "HH:mm:ss") private LocalTime punchInTime;或者在配置类中添加一个全局的日期时间格式转换器,这么处理之后Ajax传参就再也不会因为格式问题报400错误了。
4.3 数据统计与Excel导出
考勤系统的最终输出物是报表,统计功能是否好用直接决定了系统在企业里的使用感受。
这里说两个非常核心的统计需求。第一个是“当月员工考勤汇总”,SQL可以用GROUP BY + 子查询来实现当月每一天的迟到/早退/请假汇总。我通常是先查出每一条考勤记录的状态,然后在Java层做聚合,这样代码可读性更好,也方便后续扩展页面展示维度。但是如果数据量很大需要SQL直查,也可以用这么一段核心SQL:
SELECT employee_id AS empId, SUM(CASE WHEN punch_in_status = '迟到' THEN 1 ELSE 0 END) AS lateCount, SUM(CASE WHEN punch_out_status = '早退' THEN 1 ELSE 0 END) AS earlyCount, SUM(CASE WHEN punch_in_status = '缺卡' OR punch_out_status = '缺卡' THEN 1 ELSE 0 END) AS missCount, SUM(work_minutes) AS totalWorkMinutes FROM attendance WHERE DATE_FORMAT(work_date, '%Y-%m') = #{month} GROUP BY employee_id第二块是Excel导出。Java里有现成的POI依赖,这里我们需要对统计结果做格式化输出,设置导出文件前端自动下载:
@RequestMapping("/export") public void export(HttpServletResponse response, String month) throws IOException { List<MonthlyStatVO> list = attendanceService.selectMonthStat(month); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("考勤月报"); // 设置表头... // 填充数据... response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=attendance_"+month+".xlsx"); workbook.write(response.getOutputStream()); }导出报表的时候有几个容易忽略的小地方:文件名里不要直接用中文字符,最好用拼音或英文+日期拼接,否则很容易乱码;如果你用POI导出超过10万条数据,要考虑用SXSSFWorkbook来减少内存占用,我们这个考勤系统一般是几千到几万条,XSSFWorkbook就够用了,但是面试时被问到最好能说出区别。
4.4 项目开发中实际遇到的疑难杂症
最后把这几年实际做过考勤项目,踩过的坑集中整理一下,这几条是教科书里看不到但实际价值极高的。
第一个坑:连接池用完把服务拖垮。一个小型考勤系统并发量不会太高,但如果Druid连接池的连接回收设置不对,高并发打卡时会报GetConnectionTimeoutException。我通常会在Druid的配置中设置maxActive=20、initialSize=5、minIdle=5,并开启removeAbandoned避免连接泄漏。
第二个坑:Layui表格默认请求方式冲突。用Layui的table.render时,默认传参方式是Get,如果后端接口用的是@RequestBody接收POST的JSON,联调就接不上。这个问题非常隐蔽,排查时记得先看网络请求的query string是?page=1&limit=10还是JSON字符串,根据实际请求方式调整接口签名。
第三个坑:打卡日期跨界问题。很多考勤系统在处理夜班员工时容易出错,因为晚上22点之后的“日期”和次日早上的“日期”已经变了。如果没有专门配置“考勤跨天归属”的逻辑,就会把一段完整的工时拆成两天,统计和审批都会乱。我早期的处理方式是固定按自然日归属,不做跨天;如果你的系统有明确夜班场景,一定要在需求阶段说清楚边界规则,否则后期返工成本很高。
第四个坑:JSP页面不能直接访问WEB-INF目录下的文件。很多人第一次写跳转的时候,把iframe的src指向/WEB-INF/jsp/main.jsp,结果页面空白。这个问题的原因是WEB-INF目录下的文件对外是不可见的,只能通过服务端forward或redirect。所以页面跳转一定要走Controller返回视图名,而不是直接写文件路径,这一点对于刚上手项目的新手来说极其关键。
5. 写在最后的工程化建议
5.1 代码分层要做到什么程度
考勤系统虽然是单人开发的项目,但代码分层一定要规整。项目结构我建议按照这样的方式组织:
com.company.attendance ├── controller // 接口层,接收请求、返回数据 ├── service // 业务逻辑层,处理事务和规则 ├── mapper // 数据访问层,对应MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象(视图模型) ├── utils // 工具类(时间计算、导出) └── config // 全局配置(拦截器、跨域、日期格式)这种分层方式让JavaWeb的条理非常清楚,Controller不写业务逻辑,Service只负责规则,Mapper只管数据读写。刚开始不习惯的同学会觉得“这个简单的功能为什么要写三层”,但项目一旦超过10个功能模块,这种分层的维护价值就体现出来了。面试讲项目的时候,这种分层方式也更容易被认可。
5.2 单点登录权限控制如果再细一点
对于企业考勤系统,如果并发人数到几百人,也可以给权限加上“数据范围”的概念,即管理员只能看自己部门的数据,超级管理员才能看全部。这个可以在SQL层面加一个部门过滤条件。如果想再进一步,可以学习Spring Security整合进来做接口权限,但考勤项目本身复杂度不高,用拦截器已经能覆盖绝大部分需求。
5.3 数据库初始化脚本建议
项目交接或者答辩前,一定要准备好一份完整的schema.sql,包含建库、建表、初始化测试数据的全部语句。我见过太多项目代码写得不错,但是没有初始化脚本,导致评委那边部署的时候卡在“空数据库无法启动”,这个印象分损失非常大。
-- 初始化管理员账号,密码建议用MD5或BCrypt加密后存储 INSERT INTO employee (employee_no, name, account, password, role, status) VALUES ('admin', '系统管理员', 'admin', 'e10adc3949ba59abbe56e057f20f883e', 2, 0);很多同学直接明文存密码,这在毕设和面试里都容易被打低分。至少要用MD5加盐或者说BCrypt加密,虽然系统不大,但防御意识要从做项目起就建立起来。
做考勤系统这类项目,最大的价值不是“把某个框架用了一遍”,而是把一个业务需求从抽象变成具体的过程。从考勤规则怎么定义,到表结构怎么设计,再到状态流转怎么控制,到最后报表怎么出,每一步都不是单纯的技术堆叠,而是对需求的理解和对数据建模的把握。如果你正在做同类型的项目,我强烈建议你不要直接复制某个现成项目,而是自己从需求梳理开始把它完整做一遍。过程中踩的那些坑,都会变成你以后面试和工作中最值钱的经验。