从零搭一套SSM人事考勤请假系统三个角色如何分工、权限怎么控、踩过哪些坑做Java后端这几年SSMSpring SpringMVC MyBatis这套组合一直是中小型管理系统的常青树。最近刚好完成了一个“人事员工考勤签到请假管理系统”的项目角色拆成三种员工、部门经理、系统管理员。整个系统从数据库设计到权限控制再到考勤统计和请假审批流基本把企业里最常见的办公场景都覆盖了。如果你正准备用SSM做毕设或者公司内部需要一套轻量级的人事管理工具这篇文章应该能帮你省不少时间。我会把项目结构、核心表设计、接口实现、权限控制思路以及我在实际开发中遇到的那些坑全部摊开来讲。先说结论这套系统的核心不复杂真正考验人的是业务边界的拆分和状态机的设计。员工要能签到签退、提交请假部门经理要能看到本部门成员的考勤记录并审批请假管理员则要管理员工信息、部门结构同时能做全局考勤数据的统计和导出。三个角色如果权限没控好后面所有功能都会乱套。1. 项目概述与核心需求拆解1.1 SSM框架为什么还值得选很多人在2024年问Spring Boot都这么成熟了为什么还要用SSM答案很现实大量高校课程设计和中小公司的存量系统仍然以SSM为主而且SSM的配置式开发能让你把Spring的IOC、AOP、SpringMVC的请求流转、MyBatis的SQL映射这些底层原理看得更清楚。SSM框架和SSM框架教程至今还是搜索热词说明这套技术栈的学习价值远没结束。从实际开发体验来说SSM和Spring Boot的核心区别在于“自动配置”与“手动组装”。Spring Boot帮你省掉了大量XML配置但如果你不理解背后的机制出了问题很难定位。SSM则逼着你把每一块配置写明白比如数据源、SqlSessionFactory、Mapper扫描、视图解析器、拦截器所有东西都在你掌控之中。做完这个项目你对Spring容器和MyBatis代理机制的理解会上升一个台阶。1.2 三个角色的业务边界划分这个系统的角色设计可以说是人事考勤场景的“标准答案”。权限边界一定要从需求阶段就梳理清楚否则后面写接口的时候会反复改。员工Employee是这个系统里最基础的角色。他们要做的操作包括查看自己的考勤记录、上班签到、下班签退、提交请假申请、查看请假审批结果。员工只能看到自己的数据不能触碰其他同事的信息更不能看到部门统计。部门经理Manager是在员工基础上扩展出来的管理层角色。经理除了能做员工的所有操作外还需要看到本部门所有员工的考勤汇总和请假申请。审批请假是经理最重要的职责他需要根据请假类型、时长和部门当前的人员安排来决定通过还是驳回。这里要注意一个细节经理审批的范围只限本部门跨部门的申请要流转到更高层级处理。系统管理员Admin则是全局的掌控者。管理员负责维护部门信息、账号管理、员工信息的增删改查还要能查看全公司所有部门的考勤统计、请假汇总并能把考勤数据导出成报表。管理员的权限是最高的所以系统中的敏感操作比如删除员工、修改考勤记录必须做操作日志记录。2. 系统设计从表结构到核心业务流2.1 数据库表设计详解数据库设计是整个项目的地基。我推荐用MySQL 5.7以上版本字符集统一用utf8mb4。核心表我拆了六张员工表employee、部门表department、考勤表attendance、请假表leave_request、角色表role、用户表user。先看员工表和用户表的关系这是很多人第一次做管理系统容易搞混的地方。我这里的做法是拆成两张表user表负责登录认证用户名、密码、角色IDemployee表负责人事信息姓名、部门、职位、入职时间、手机号。两者通过employee_id字段关联。这样拆的好处是认证逻辑和业务逻辑解耦以后接入LDAP或者OAuth2时只动user表就行。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT BCrypt加密后的密码, role_id int(11) NOT NULL COMMENT 角色ID:1-员工 2-经理 3-管理员, employee_id int(11) DEFAULT NULL COMMENT 关联员工ID, status tinyint(1) DEFAULT 1 COMMENT 账号状态 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;考勤表attendance是每天打卡数据的载体。这里我加了一个work_date字段用来标记这张记录属于哪一天。为什么不直接用create_time来判断因为存在补卡的情况而且如果员工晚上加班跨天create_time已经是第二天了但考勤归属还是前一天的。所以work_date必须单独存最好在业务层就判断好再写入。请假表leave_request需要重点设计状态字段。我用一个status字段来标记审批状态0-待审批、1-已通过、2-已驳回、3-已撤销。请假类型我用type字段区分1-事假、2-病假、3-年假、4-调休。这里有个经验之谈状态字段尽量不要用字符串更不要用中文用数字枚举值在代码里定义成常量类既省空间又方便扩展。2.2 三层架构与项目目录规划SSM项目的目录结构如果一开始没规划好代码写多了会很难维护。我习惯按“controller → service → mapper”三层来组织每一层的职责必须单一。controller层只做参数接收和结果返回不写任何业务逻辑。service层是核心处理业务规则、事务、状态流转。mapper层就是MyBatis的接口SQL写在XML里复杂的联表查询都在这一层完成。另外加一个common包放公共类统一返回结果、分页对象、异常处理一个interceptor包放登录和权限拦截器一个util包放日期处理、Excel导出等工具类。有个细节要注意entity实体类和VO视图对象不要混用。比如员工在查询自己的考勤记录时前端可能需要展示“迟到/早退/正常”这样的状态但数据库里存的是打卡时间。如果在实体类里额外加一个statusName字段那这个实体类就变得不纯净MyBatis的自动映射也会受到影响。我的做法是单独建一个AttendanceVO继承或组合实体类的字段再扩展展示用的属性。2.3 请假审批状态机设计请假状态流转是这个系统里最容易写乱的地方。一开始很多新手会直接在controller里写一堆if-else后来发现状态组合实在太多还是得用状态机思想。我把请假的状态流转设计成四个节点待审批、已通过、已驳回、已撤销。员工提交请假时状态为待审批经理审批通过后变为已通过驳回则变为已驳回在经理尚未审批时员工可以主动撤销申请状态变为已撤销。这里严禁出现的跳变是已驳回的申请不能被员工再次直接修改提交需要重新发起一条新申请。这么设计是为了保留完整的审批痕迹避免数据被篡改。在代码里我封装了一个LeaveStatusEnum枚举和状态流转校验方法。每次更新状态前先判断当前状态是否允许变更为目标状态。虽然这增加了代码量但上线之后几乎没有出过状态错乱的bug。对于这类企业管理系统的核心业务宁可代码写多几行也坚决不能图省事。3. 核心功能实现与实操细节3.1 员工端签到签退与请假申请的实现要点签到功能看似简单实际要考虑的问题很多。最大的坑是“一天只允许一条考勤记录”。我的设计思路是在attendance表中以employee_id和work_date做联合唯一索引然后利用数据库的唯一约束来防止重复插入。这样即使两个请求并发到达数据库也能挡掉一条。签到的业务逻辑分三步。第一步根据当前用户ID和当天日期查询考勤记录如果不存在则新增一条记录签到时间如果已存在则更新签退时间。第二步判断签到时间是否晚于规定上班时间如果晚于则置迟到状态签退时间早于规定下班时间则置早退状态。第三步计算工作时长签退时间减签到时间去掉午休时长需补逻辑。这里补充说明工时计算不是简单的减法项目里需要在配置表里维护午休起止时间。中小企业如果管理比较粗放可以直接用两段时间差但这样加班时长统计会不准确建议还是把午休时长扣除。请假申请的实现相对简单只要填好请假类型、开始时间、结束时间、请假事由然后提交即可。但关键点在于假期余额校验。比如年假是5天提交的时候必须校验剩余可用天数是否足够。我用一个假期余额表leave_balance来记录员工每种假期的剩余天数而不是直接去统计历史请假记录。这样查起来快而且数据一致性更好控制。Override Transactional(rollbackFor Exception.class) public Integer submitLeave(LeaveRequest request) { // 校验时间合法性 if (request.getStartTime().after(request.getEndTime())) { throw new BusinessException(请假开始时间不能晚于结束时间); } // 校验余额 LeaveBalance balance leaveBalanceMapper.selectByEmpIdAndType( request.getEmployeeId(), request.getType()); if (balance null || balance.getRemainingDays() request.getTotalDays()) { throw new BusinessException(假期余额不足无法提交); } // 扣减余额并保存申请 leaveBalanceMapper.deduct(request.getEmployeeId(), request.getType(), request.getTotalDays()); leaveRequestMapper.insert(request); return request.getId(); }注意这段代码启用了Transactional注解。扣减余额和保存申请必须在同一个事务里执行否则会出现申请提交成功但余额没扣或者反过来余额扣了但申请失败的情况。这种数据一致性问题在开发环境不一定能暴露生产环境一上量就完蛋。3.2 审批端经理如何高效处理请假与查看团队考勤经理端的核心诉求是快速看到所有待审批的申请并做出处理同时能按日期查看部门成员的考勤情况。审批列表的查询要注意分页和条件组合。前端传过来的筛选条件可能包括申请状态、请假类型、员工姓名、申请时间范围。我在Mapper XML里用动态SQL拼接WHERE条件这正好是MyBatis最擅长的部分。如果你用的MyBatis版本比较老写动态SQL时注意 标签里的test条件字符串判断要写status ! null and status ! 数字类型直接判断null即可。经理审批的操作接口设计得越简单越好。前端只需要传leaveRequestId和审批结果通过/驳回加上一个可选的审批意见。后端在修改状态之前必须重新校验该申请当前是否处于待审批状态。严谨的做法是用乐观锁update语句带where id #{id} and status 0如果受影响行数为0说明已经被处理过直接返回“申请已处理请勿重复操作”。部门考勤汇总对经理来说也是一个高频功能。我提供了一个按部门、按日期的考勤统计接口返回该部门当天的应到人数、实到人数、迟到人数、早退人数、请假人数和缺勤人数。这个功能用一条带条件聚合的SQL就能实现。如果数据量大也可以提前用定时任务生成每日汇总表查询性能会好很多。3.3 管理端员工档案、考勤汇总与数据导出的完整方案管理员模块的功能较多我从实用角度挑三个重点讲员工账号初始化、考勤异常处理、数据导出。员工账号初始化是管理员最常操作的功能。新员工入职后管理员在系统里录入员工信息并创建登录账号。这里有个安全细节初始密码不要用明文存储必须用BCrypt加密后再存入数据库。Spring Security的BCryptPasswordEncoder可以直接使用或者用jBCrypt库也行。我建议初始密码设置为身份证后六位或者随机生成并要求员工首次登录后强制修改。考勤异常处理是管理员最需要小心的功能。比如员工反馈打卡机坏了导致漏打卡管理员需要手动补录一条考勤记录。这个操作必须做标记我加了一个is_manual字段值为1表示人工补录。同时管理员修改考勤数据必须要校验权限和操作理由而且每次修改都要记录操作日志。这些设计可能不会直接影响功能上线但审计的时候非常有用。数据导出我用的方案是Apache POI生成Excel文件。实现思路很简单先查询出需要导出的考勤数据List然后通过POI的XSSFWorkbook创建Excel文件最后通过HttpServletResponse将文件流写回前端。这里有一个性能优化点如果数据量超过几万行建议用SXSSFWorkbook流式导入导出它不会把所有数据都放在内存里而是利用临时文件。公司几百号人的月度考勤导出用XSSFWorkbook问题不大但如果做全公司全年导出就会内存溢出。3.4 登录认证与角色权限控制的两种实现思路权限控制是三个角色系统能否稳定运行的生命线。我推荐先用最简单、最直观的方案拦截器加角色判断。写一个LoginInterceptor重写preHandle方法检查session或Token中是否存在登录用户。如果未登录直接返回401状态码并提示“请先登录”。接着再写一个PermissionInterceptor读取当前请求的URI通过权限配置判断该角色是否能访问该URI。我这里没有引入Spring Security或Shiro因为对于一个学习型或中小型项目这两个框架的配置复杂度反而会分散你对业务本身的注意力。等到系统真正需要细粒度权限比如按钮级别权限再集成Spring Security也不迟。角色权限的配置文件可以用一个Map在Java代码中维护也可以用数据库的权限表和角色权限关联表来管理。如果是给公司做的内部系统建议用数据库管理因为管理员可能需要在后台动态调整权限。如果是课程设计或毕设用Java配置Map就够了省掉一大堆CRUD代码。前端也要做对应的控制。比如员工端不显示“部门管理”菜单经理端不显示“系统管理”菜单。前后端双重校验是必须的前端控制只能起到优化用户体验的作用真正保护数据安全的一定是后端。很多人只做了前端隐藏菜单接口照样能调用这在真实项目中是很严重的安全漏洞。4. 关键技术代码与配置深度解析4.1 SSM整合的核心配置SSM整合最让大家头疼的是配置文件太多、版本兼容问题多。我把自己调通的完整配置思路讲一遍。首先创建spring-context.xml作为根容器配置组件扫描排除Controller、数据源、SqlSessionFactory、事务管理器然后创建spring-mvc.xml作为子容器配置Controller扫描、注解驱动、视图解析器、静态资源映射最后在web.xml里配置ContextLoaderListener和DispatcherServlet。这里要注意一个很多人踩过的坑Spring父容器和SpringMVC子容器的扫描范围不能重叠。父容器扫描service和mapper子容器只管controller。如果两个容器都扫描了service类会导致事务代理失效因为子容器会优先用自己的bean。我用一个简单的方法避免这个问题在spring-context.xml的context:component-scan里加一个排除Controller的过滤器在spring-mvc.xml里只扫描controller包。数据源我用的是阿里巴巴的Druid连接池。配置Druid的监控页面是一个加分项只需要在web.xml里配置一个StatViewServlet即可。我建议把监控页面设置访问密码避免内部信息泄露。Druid的SQL监控对于排查慢查询和连接泄漏问题很有帮助尤其是系统运行一段时间后出现连接数不够时Druid能直接告诉你哪些SQL占用了连接。4.2 我的Batis映射文件编写经验MyBatis的Mapper XML是整个数据访问层最需要细心的地方。我习惯把所有的SQL都写在XML里而不是用注解。原因很简单XML支持动态SQL可读性也更好。后来接手过用注解写SQL的项目查询条件稍微一复杂整个代码块就变得很难维护。下面是部门考勤汇总的核心查询SQLselect idselectDeptAttendanceSummary resultTypejava.util.Map SELECT COUNT(DISTINCT e.id) AS totalCount, SUM(CASE WHEN a.status 0 THEN 1 ELSE 0 END) AS normalCount, SUM(CASE WHEN a.status 1 THEN 1 ELSE 0 END) AS lateCount, SUM(CASE WHEN a.status 2 THEN 1 ELSE 0 END) AS leaveEarlyCount, SUM(CASE WHEN a.status 3 THEN 1 ELSE 0 END) AS absentCount FROM employee e LEFT JOIN attendance a ON e.id a.employee_id AND a.work_date #{workDate} WHERE e.department_id #{deptId} /selectSQL里用了LEFT JOIN这样没有打卡记录的员工也会出现在结果里才能算出“缺勤人数”。被注释的统计要特别注意大小写MySQL在Linux下是区分表名的Windows不区分。很多人在本地开发是Windows环境部署到Linux服务器就报“表不存在”排查半天发现是表名大小写问题。建议项目里统一使用小写表名。4.3 签到接口的并发控制与时间判定逻辑签到接口要考虑并发重复提交的问题。员工连续点了两次签到按钮如果不用并发控制数据库里就会插入两条记录。除了前面提到的联合唯一索引代码层面也可以用Redis的SETNX做分布式锁。考虑到签到是高频操作直接把一天内多次打卡的请求变成幂等的操作最稳妥。还有一个容易忽略的细节服务器时间和前端时间的校准。如果员工电脑的时间不准前端传过来的时间戳就会有问题。解决办法是签到的打卡时间以后端服务器接收到请求的时间为准前端传的时间只作为参考。前端展示时再通过返回的服务器时间戳做倒计时或判断避免“我刚签的到怎么时间不对”这种投诉。时间判断逻辑上我用一个简单的规则上班时间设置为09:00那么签到时间在09:00之前算正常09:00到10:00算迟到10:00之后没签到算缺勤。下班时间设置为18:0018:00之前签退算早退。更严格的企业可能还要区分弹性工作制这类规则可以抽取到系统配置表里让管理员能灵活调整。5. 典型问题与排查技巧实录5.1 常见问题速查表开发过程中遇到的问题五花八门我整理了几类典型问题方便你对照排查。问题现象根本原因解决方案登录后页面反复跳回登录页拦截器放行的静态资源路径未配置完整检查拦截器排除路径加上/css /js /images等前端传的时间参数是null日期格式不匹配SpringMVC无法转换在Controller使用DateTimeFormat注解或配置全局日期转换器数据库中有重复考勤记录未设置联合唯一索引或并发请求未被控制添加employee_idwork_date唯一索引业务层加锁或Redis控制经理能看到其他部门的请假申请SQL查询未加部门维度过滤审批查询必须带部门ID条件最好在SQL层强制拼接事务回滚不生效Spring容器和SpringMVC容器重复扫描service调整component-scan扫描范围避免父子容器bean重叠导出Excel内存溢出使用XSSFWorkbook加载全量数据数据量大改用SXSSFWorkbook流式写入修改密码后旧密码还能登录密码加密方式不一致或Redis缓存了登录态确认加密算法统一修改密码后清除相关缓存5.2 我实际踩过的坑与避坑建议第一个坑是MyBatis的Mapper接口和XML文件同名但不在同一个目录时无法自动映射。我后来在pom.xml的build节点添加了resource配置把xml目录下的文件也打进classpath这个问题就解决了。如果你用Maven构建项目一定记得检查target目录里有没有Mapper.xml文件这是很多人排查半天才发现的问题。第二个坑是懒加载和事务的配合。MyBatis的延迟加载在开启了事务的Service方法里通常没问题但如果事务提交后再访问延迟加载的属性就会抛LazyInitializationException。这个坑让我花了一整个下午。解决办法有两个一是关闭延迟加载二是在事务范围内提前访问需要的数据三是配置OpenSessionInViewFilter。我后来直接使用关联查询避免懒加载虽然SQL多写了一点但胜在稳定。第三个坑是角色判断硬编码。刚开始我用if (admin.equals(roleCode))这种方式在代码里到处判断角色后来角色一多代码里到处都是不可读的字符串。建议创建RoleConstant常量类集中管理角色编码需要判断时引用常量这样以后修改角色名只改一处即可。5.3 性能优化与安全加固经验这个项目虽然不大但上线后还是做了一些优化。第一列表查询的SQL要避免SELECT *只查当前页面需要的字段配合分页插件PageHelper能显著减少无效字段的网络传输和内存占用。第二考勤数据会随时间增长很快建议对attendance表按月份做分区或者定期归档历史数据到备份表。分区表在查询某个月数据时能直接走分区裁剪性能提升很明显。第三登录接口增加简单的防暴力破解机制比如同一个账号连续失败5次就锁定10分钟我在这里使用了Redis记录失败次数并设置过期时间。安全问题里还有一个容易被忽视的密码传输。项目里如果直接使用HTTP明文传输密码非常容易被抓包。我是在前端先调用一个公钥接口获得RSA加密后的密码再传回后端用私钥解密最后和数据库中的BCrypt密文比对。这样即使密文被截获也无法直接解密出明文密码。6. 从课程设计到生产部署的扩展建议如果你是用这个项目做课程设计或毕业设计我建议你在答辩时可以额外展示这些亮点第一Druid监控页面能直观展示SQL执行情况第二Excel导入导出功能第三操作日志记录模块。这些都是企业级的常见需求面试官或答辩老师看到的会是一个不仅完成了CRUD还考虑了安全性和可用性的项目。如果想往生产级系统靠拢还有几个方向可以扩展。一是引入Redis做会话共享为后面多实例部署做准备二是引入消息队列比如考勤数据异步写入避免签到高峰期数据库压力过大三是把权限模型升级为RBAC支持多角色多权限的细粒度控制四是增加移动端适配因为实际考勤场景中大量员工是用手机打卡的。这里再分享一个个人经验无论技术选型是什么都不要一上来就埋头写代码。先花一天时间把角色梳理清楚把核心数据流画出来把表结构设计好后面实现就会非常顺畅。我第一次做类似系统就是没做设计直接上手结果做到一半发现员工和用户表分不清部门维度没有考虑请假余额也不知道放哪张表几乎返工了一半代码。有了第二次经验这次我先把所有流程在纸上演算一遍才动手写项目整体效率提升非常明显。做这类管理系统最容易被低估的是细节。一个签到时间的判定、一个状态流转的校验、一个权限边界的控制看起来都是几行代码的事但真正决定系统好不好用、稳不稳定的恰恰是这些细节。希望这篇文章能帮你少走一些弯路。