☰
基于Java的员工管理系统开发实战:Spring Boot+MyBatis Plus全流程
2026/10/1 3:19:19 网站建设 项目流程

1. 项目定位与核心需求拆解

先别急着写代码。拿到“基于Java的员工管理系统”这个标题,第一反应是又一个课程设计/毕业设计项目,但真正动手做的时候才发现,这个题目坑不少——它看着简单,但涉及到的知识点几乎覆盖了Java后端开发的全链路:数据库设计、ORM框架、权限控制、数据校验、分页查询、异常处理……做完一个完整版本,基本相当于把你学的Java东西从头串了一遍。我自己带过的课程设计小组里,凡是踏踏实实把这类系统写完的,后面出去面试或者做实习项目,明显比那些只写过零散Demo的同学底气足得多。

什么样的人适合拿这个题目练手?我的看法是:已经学完Java基础(集合、IO、多线程)、Servlet/Spring基础,但还没独立做过一个完整Web系统的人。它不像高并发秒杀、分布式事务那种项目一上来就抽象到劝退,员工管理系统本身业务逻辑不复杂,但五脏俱全,非常适合作为第一个“完整工程”。不管是拿来交课程设计、写在简历上作为实战经历,还是纯想学一溜Spring Boot + MyBatis的开发流程,这个题目都能给你足够的成长空间。

核心需求方面,我做了这么多年开发,见到形形色色的员工管理系统,总结下来逃不开三块:

  • 基础数据管理:员工信息的增删改查,一定要有部门归属,不然存储就是一盘散沙结构,后续统计没法搞。
  • 流程类功能:最常见的就是考勤。签到、签退、上下班时间计算,时间一旦算不准,系统直接被用户喷死。
  • 辅助管理功能:薪资管理、公告发布、个人中心查看自己信息这些,属于锦上添花,但决定了系统“看起来”专不专业。

如果再往企业实际场景延伸,还有员工调动、离职交接、社保信息维护、合同管理等,但在课程设计和练手阶段,我会建议先聚焦上面三块,把每一块做得扎实,再考虑扩展。贪多嚼不烂,这是做这类管理系统最容易犯的错。

从场景角度说,这个系统解决的核心痛点是:纸质表格管理员工信息效率低、难检索、易出错。所以你这套系统设计出来,第一个要回答的问题就是——“怎么让HR/管理员比用Excel更爽”?如果回答不了这个问题,说明你没理解系统的价值。比如,Excel做不到跨部门实时协同看同一份数据,做不到点击一次就自动生成当月的考勤统计报表,做不到按时间维度去追踪一个员工从入职到转正再到离职的完整生命周期。系统能做到了,价值就出来了。

2. 技术选型方案与架构设计对比

2.1 为什么优先选Spring Boot搭后端

以前的老课程设计很多是SSH(Struts2 + Spring + Hibernate)或者SSM(Spring + SpringMVC + MyBatis)组合,现在再抱着XML配置文件来回折腾,纯属跟自己过不去。我自己的经验是,Spring Boot带来的开发效率提升,对单人开发一个中小型管理系统来说是碾压级的。

核心理由有这么几个:

  • 起步依赖省心:一个spring-boot-starter-web就把Spring MVC、内嵌Tomcat、Jackson序列化全带上了,不用再手工下载十几个jar包还担心版本冲突。我当时做SSM项目时,光调jar包冲突就花了两天,Spring Boot基本两三分钟就把环境跑起来了。
  • 内嵌容器直接运行:以前要把项目打成war包丢到Tomcat的webapps目录下,现在java -jar一条命令启动,部署和调试都简化了很多。
  • 自动配置帮你省掉大量样板配置:数据库连接池、MyBatis、Redis等组件的装配,Starter一引入,配置项填一下就完事,虽然有些同学觉得这“不透明”、学不到原理,但我一直认为先会用再学原理,效率更高。

需要注意的是,Spring Boot版本选择上,我建议直接上2.7.x或者3.x。但3.x要求JDK17,如果你装了JDK8,那就选2.7.x,这两个版本目前都有大量企业在用,学完不亏。我自己第一次做员工管理系统用的是Spring Boot 2.7.6 + JDK8,非常稳。

2.2 ORM框架:MyBatis Plus还是JPA

ORM这块,我把话说直白点:个人项目、课程设计、中小企业管理系统,MyBatis Plus是首选。

JPA/Hibernate封装度高,自动建表、自动管理关联关系确实爽,但一旦需要写复杂查询(比如员工表按部门、姓名、状态多个条件组合过滤),JPQL和QueryDSL的学习成本一开始会觉得过于抽象。MyBatis Plus在MyBatis基础上把单表CRUD、分页、条件构造器都封装好了,写业务代码时能明显感到“省力的部分正是业务开发里最频繁的部分”。

给你看一个对比感受一下。用MyBatis Plus查询“市场部入职一年以上的正式员工”,代码大概是:

LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Employee::getDeptId, deptId) .lt(Employee::getHireDate, LocalDate.now().minusYears(1)) .eq(Employee::getStatus, 1); List<Employee> list = employeeMapper.selectList(wrapper);

条件构造器还天然规避了Column名拼写错误的问题——Lambda表达式指向的是实体类字段名,编译期就能发现错误。JPA其实也能做同样的事,但至少对我接触的大量初学者来讲,MyBatis Plus的上手曲线明显更友好,排查问题时SQL也能直连去跑,透明度高。

2.3 前端方案:前后端分离还是服务端渲染

前端方案直接决定你的开发工作量和复杂程度,我也在这个选择上犹豫过。两条路线都试过,各说优缺点:

方案优点缺点适合场景
前后端分离(Vue + Element UI / Element Plus)页面交互体验好,组件成熟,招聘市场主流;接口文档清晰,前端后端独立部署需要同时掌握Vue语法和前端工程化(npm、Vite等),学习成本高毕设/实训想做完整展示型项目、简历上想突出全栈能力
服务端渲染(Thymeleaf + Bootstrap + JQuery)后端一个包搞定,不用跨域,页面直接由Controller返回,开发链路短交互复杂时前端代码混乱,不好维护,面试时显得技术栈相对传统偏后端能力提升、重点在业务逻辑/数据存储设计时

我个人的建议:如果你做这个项目的时间在两个星期以上,果断上前后端分离。Vue3 + Vite + Element Plus的组合现在非常成熟,B站搜一下教程就有配套项目。更重要的是,前后端分离模式下你会自然地理解“接口设计”与“数据契约”的概念,这在企业工作里特别重要——上班后你会发现,全栈开发一个人干的场景越来越少,前后端联调才是常态,早点习惯Flux这套节奏会让你后面适应得更快。

但如果你时间紧,只有三四天,Thymeleaf方案也不是不能选。毕竟系统核心价值在后端的业务逻辑和数据处理,前端只是表现层。我见过不少同学用Thymeleaf + Bootstrap把页面做得干干净净的,展示效果完全够用。只要不是完全不会前端,这个方案能让你把有限的时间优先砸在真正重要的后端逻辑上。

3. 数据库设计详解:一张张表铺开聊

3.1 员工管理系统的核心表结构

数据库设计是整个项目的根基——根基打歪了,后面前端接口设计、统计报表、权限开发都会跟着歪。我做这个项目时第一步不是写代码,而是把表结构敲定,反复琢磨字段之间的约束关系。核心表一共五张,我先给你看一下设计和当时的考虑:

部门表 department

id bigint 主键 dept_name varchar(50) 部门名称,唯一约束 leader varchar(20) 负责人姓名 phone varchar(20) 联系电话 create_time datetime 创建时间

第一版我根本没想到要建部门表,想着员工表里放一个“部门名称”字符串字段就行,后来发现一旦员工有几十人、部门十几个,统计“各部门人数”就要在Java里做字符串匹配,还容易脏数据(“技术部”和“技术 部”就匹配不上了)。单抽出一张部门表后,员工表用dept_id外键关联,统计就变成了GROUP BY dept_id一条SQL的事。

员工表 employee

id bigint 主键 emp_no varchar(20) 工号,唯一索引 name varchar(20) 姓名 gender tinyint 性别(0未知,1男,2女) birthday date 出生日期 id_card varchar(18) 身份证号 phone varchar(20) 手机号 email varchar(100) 邮箱 dept_id bigint 部门ID,外键关联 position varchar(50) 岗位 hire_date date 入职日期 status tinyint 状态(0离职,1在职,2试用) create_time datetime 创建时间 update_time datetime 更新时间

注意几个细节:emp_no工号和id_card身份证号都加了唯一索引,这是为了让业务数据天然具备防重能力。有些同学只设主键,后续代码里再手动查重,等你遇到并发场景就来不及了。status字段我特意设计了好几种状态而不是简单的0/1——试用期、在职、离职三种状态在统计和权限逻辑里都是不一样的,只放两个值后面加需求的时候改表结构会非常头疼。

考勤表 attendance

id bigint 主键 emp_id bigint 员工ID work_date date 出勤日期 check_in datetime 签到时间 check_out datetime 签退时间 status tinyint 状态(0正常,1迟到,2早退,3缺勤) create_time datetime 创建时间

考勤表每天每个员工会生成一条记录,一年的数据量大概就是员工数乘以220个工作日。这个量级用普通索引就可以。我在emp_id + work_date上建了联合唯一索引,保证同一员工同一天只有一条考勤记录,从数据库层面堵住了重复签到的可能。这也是很多教程里容易忽略的——不建唯一约束,代码里防重写得再狠也总有漏网之鱼,数据库兜底才是最稳的。

薪资表 salary

id bigint 主键 emp_id bigint 员工ID base_salary decimal(10,2) 基本工资 bonus decimal(10,2) 奖金 deductions decimal(10,2) 扣除 pay_date date 发放日期 create_time datetime 创建时间

薪资表简单,但注意金额字段必须用decimal而不是float/double。二进制的浮点数在金融计算里会出大问题——你拿double算个0.1 + 0.2就知道,结果是0.30000000000000004,给员工发工资多出来几个千分之一厘也会被吐槽。这类字段设计上的细节就是业务经验和教科书知识的差别。

用户表 sys_user

id bigint 主键 username varchar(50) 登录名,唯一索引 password varchar(100) 密码(BCrypt加密后的哈希值) emp_id bigint 关联员工ID role tinyint 角色(1管理员,2普通员工) create_time datetime 创建时间

用户表是权限体系的基础。把登录账号和员工信息拆开,好处是灵活:一个员工可以绑定一个登录账号,未来还能扩展多个账号(比如一个员工管理端的、一个移动端的)。密码一定要加密存储,千万别明文保存。我见过有人图省事直接把用户输入的密码字段存进库里的写法,这要是被同行看到,妥妥被挂在网上。密码加密用BCrypt(Spring Security里自带BCryptPasswordEncoder),它每次生成的哈希值都带随机盐,同一个密码存出来都长得不一样,防彩虹表攻击比MD5加固定盐强得多。

3.2 主键策略和通用字段处理

主键这块,以前老项目喜欢用数据库自增,现在新项目更多倾向用雪花算法Long型主键。原因在于:员工管理系统虽然大概率用不到分布式,但万一后面拆成微服务或者数据要迁移,自增主键在处理合库、合并数据时会产生冲突,雪花ID是全局唯一的,天然不怕这种情况。MyBatis Plus的ID生成策略用IdType.ASSIGN_ID一行配置就搞定。

通用字段方面,create_time、update_time几乎每张表都有,我在MyBatis Plus里用@TableField(fill = FieldFill.INSERT)配合MetaObjectHandler实现自动填充。这样代码里插入数据时压根不用手动set时间,统一由框架接管,既省代码又保证每个表的时间字段格式一致。做这个设计的时候我当时还走了个弯路,最初在Service层手动set了一个new Date(),后来另一个同事接手维护时发现新增数据没有更新,谁都不记得还有这层逻辑,改成自动填充后才彻底杜绝了漏赋值。

表关系总结:部门表 1 对 N 员工表;员工表 1 对 N 考勤表、薪资表;用户表 1 对 1 员工表。五张表的关系建好,后端的CRUD逻辑就井井有条了,不用手写复杂Join也能很快查询。

4. 后端接口设计与核心功能实现

4.1 接口统一返回结构与全局异常处理

做接口设计之前,我先定了一个统一的响应封装类,这在企业开发里属于家常便饭,但在学生项目里看到的大多是Controller直接返回一个Map或者裸的Model,前端接口根本无法用一种统一的方式做错误处理和状态判断。我习惯的写法是:

public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; // 提示信息 private T data; // 业务数据 // 静态方法 success()、error() 省略 }

前端的 Vue 里写一个axios拦截器,根据code字段统一处理跳转登录页、弹出错误提示、展示成功消息。这样Controller里就只需要专心处理业务逻辑,不用每个接口都写一套成功/失败返回逻辑。

异常处理方面,用全局@RestControllerAdvice+@ExceptionHandler兜住所有未捕获异常。这样就算你的业务代码里忘了try-catch,系统也会返回一个格式统一的JSON错误给前端,不至于把500错误堆栈直接打到页面上——那个丑,而且暴露内部细节给用户,既不专业也不安全。我做了个拦截:

@ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常", e); return Result.error(500, "系统异常,请联系管理员"); }

4.2 员工CRUD与多条件分页查询的实现

员工查询功能看起来简单,真正做起来有个地方特别容易翻车——多条件组合查询加模糊查询加排序。同时员工姓名、部门、入职时间范围、状态都有可能作为过滤条件,而且条件有可能是空,前端传null也是常见的。最原始的写法是用字符串拼接SQL,拼到一半忘了WHERE 1=1就完蛋了,还得防止SQL注入。MyBatis Plus的LambdaQueryWrapper正是来解决这个烦恼的:

@PostMapping("/page") public Result<IPage<EmployeeVO>> page(@RequestBody EmployeeQuery query) { Page<Employee> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(query.getName()), Employee::getName, query.getName()) .eq(query.getDeptId() != null, Employee::getDeptId, query.getDeptId()) .eq(query.getStatus() != null, Employee::getStatus, query.getStatus()) .ge(query.getHireStart() != null, Employee::getHireDate, query.getHireStart()) .le(query.getHireEnd() != null, Employee::getHireDate, query.getHireEnd()) .orderByDesc(Employee::getHireDate); IPage<Employee> result = employeeMapper.selectPage(page, wrapper); // 后面的vo转换省略 return Result.success(convertedPage); }

需要注意两个点:

  • 条件里面的StringUtils.hasText()和query.getXxx() != null的短路判断不能省。少了这个,用户的筛选项没填,SQL就会拼上WHERE name = '',查出来结果为空,用户一脸懵。
  • 分页必须用IPage。不要自己在内存里list然后截取子列表,数据量一多内存直接爆掉,而且游标直接在数据库分页,性能完全不是一个量级。

新增员工时要注意事务:插入employee表的同时初始化一条默认考勤数据(可选)或者写日志表,这两步要么都成功要么都回滚。Spring Boot面试天天问事务,项目里加个@Transactional(rollbackFor = Exception.class)让你的系统变得更加专业。

删除员工这里我强烈建议逻辑删除——用一个deleted标记,而不是真从数据库里物理删除。因为一个员工关联了考勤、薪资等历史数据,你物理删了,以后查历史报表就会出现外键悬空。MyBatis Plus中配置逻辑删除非常方便:

mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

4.3 考勤签到签退与状态判定规则

考勤是员工管理系统里最有业务感的功能,我特意在这里加了几层逻辑,复杂度一下就上来了。后端接口我设计成:

  • POST /attendance/check-in:签到,记录当前时间为签到时间,判断是否迟到。
  • PUT /attendance/check-out:签退,更新签退时间,判断是否早退。
  • GET /attendance/my?month=2024-05:查询本人当月考勤汇总。
  • GET /attendance/statistics?deptId=xxx:管理员查询部门考勤率。

上班时间判定规则,以朝九晚六为例(9:00上班,18:00下班,午休12:00-13:00)。

public AttendanceStatus computeStatus(LocalDateTime checkIn, LocalDateTime checkOut) { LocalTime officeStart = LocalTime.of(9, 0); LocalTime officeEnd = LocalTime.of(18, 0); if (checkIn == null) return AttendanceStatus.ABSENT; if (checkIn.toLocalTime().isAfter(officeStart.plusMinutes(5))) { return AttendanceStatus.LATE; } if (checkOut != null && checkOut.toLocalTime().isBefore(officeEnd.minusMinutes(5))) { return AttendanceStatus.EARLY_LEAVE; } return AttendanceStatus.NORMAL; }

这套规则的业务细节在于:给了一点容错时间,晚到5分钟内不算迟到。做考勤功能时不能脑子一热,设9点整就9点整,员工走个路/打个卡网络延迟几秒钟就被标迟到,一定会被喷到怀疑人生。所以当时我特意加了这5分钟弹性,实际试运行下来用户体验好了很多。具体容错多少按公司制度来,但这思路本身很重要——业务规则和代码判定的边界要留一点合理的“缓冲”。

签到防重复也很关键。前端按钮点击一下就禁用了,但后端还是要再校验一次当天是否有记录,防的就是用户刷新页面或恶意请求接口绕过前端。实现方式也很简单:

// 校验当天是否已签到 Long count = attendanceMapper.selectCount( new LambdaQueryWrapper<Attendance>() .eq(Attendance::getEmpId, empId) .eq(Attendance::getWorkDate, LocalDate.now())); if (count > 0) { throw new BusinessException("今天已经签到过了"); }

当然单靠查询+插入在高并发下还是有可能重复插入,你可以在数据库层面用联合唯一索引兜底,我前面也提到了这个设计。表格设计的价值在这里就体现出来了。

考勤数据还有一个隐藏的设计点:过了零点之后怎么算。比如员工值夜班签到时间跨天、或者加班的签退在凌晨1点,这个如果在业务初期不考虑,后面扩展到多班次时就得重构整个表结构。我当时的处理方式比较务实——考勤日期work_date以签到的自然日为准,签退时间只作为计算工作时长的字段,不决定日期归属。夜班需求真来了在这个架构上扩展会顺畅一些,不会推倒重来。

5. 权限控制方案与登录认证

权限控制这块,很多人一上来就是Spring Security + JWT,但说实话,做一个员工管理系统,如果只有“管理员”和“普通员工”两种角色,引入全套Spring Security框架,配置起来反而是负担,很多概念一辈子用不上。我建议分两种做法:

如果时间紧:自己写一个HandlerInterceptor拦截器,根据Session或JWT中的用户角色做URL级别的权限控制。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null) { throw new BusinessException(401, "未登录"); } // 省略JWT解析和用户查询 UserContext.setCurrentUser(user); return true; } }

然后注册到WebMvc配置里,指定哪些放行(登录接口)、哪些必须登录、哪些必须管理员。我在拦截器里根据role字段再判断“访问某个URL需要管理员权限”。

如果时间充裕:还是建议整合Spring Security + JWT。虽然配置多一点,但这是面试高频,也是企业通用方案,学一遍怎么用不亏。Spring Security的过滤器链、认证管理器、授权决策这些概念,光看教程很难得到直观感受,通过项目把它们串起来会清晰很多。

登录密码这块我前面提了一句,这里得多提醒一句:永远不要在数据库里存明文密码。BCrypt加密后存哈希,登录验证用passwordEncoder.matches(原始密码, 库里的哈希)来比对。这个过程中你还能顺带理解为什么不能只做MD5——MD5不加盐的话,彩虹表一查就破。

另外登录后拿到用户信息,后端往前端返回数据时,别把密码字段序列化出去——我在EmployeeVO/UserVO里直接就不设计密码字段了,从源头杜绝泄漏。

6. 前端页面设计与联调经验

6.1 Vue3 + Element Plus页面结构规划

如果你选前后端分离,前端部分让我给你搭个结构参考。Vue3 + Vite + Element Plus + Pinia + Axios是现在的基础组合。页面大概分这么几块:

  • 登录页:用户名密码输入,调后端/auth/login拿到token,存到localStorage里。
  • 首页Dashboard:展示员工总数、各部门人数、今日考勤情况等统计卡片。这块配合后端一个统计接口,用SQLGROUP BY就能算出数据,前端再用ECharts画饼图柱状图。可视化是亮点,答辩时加分。
  • 员工管理页:表格展示员工列表,顶部筛选项(部门、姓名、状态),右侧“新增”“编辑”“删除”按钮,配合弹窗表单。这套交互是Element Plus的el-table+el-dialog+el-form三板斧,照着官方文档写很快。
  • 考勤管理页:普通员工看自己的月度考勤日历;管理员看部门考勤列表,能按日期筛选。
  • 薪资管理页:管理员可录入/修改薪资记录,员工只能查看自己的薪资历史。

我个人的经验是:页面不要多,五个左右就够,每个页面都要完整。10个残缺页面远不如5个精致页面带来的展示效果好。

6.2 Axios拦截器与跨域配置

前端必配一个Axios实例,封装baseURL和拦截器。拦截器里做两件事:请求头自动携带token,响应里统一处理错误码。

axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['token'] = token; } return config; }); axios.interceptors.response.use( response => { const res = response.data; if (res.code === 401) { window.location.href = '/login'; return Promise.reject(new Error('未登录')); } if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { ElMessage.error('网络异常,请稍后重试'); return Promise.reject(error); } );

跨域问题,开发阶段最简单的方式是后端加一个CORS配置类(允许前端dev server的http://localhost:5173访问),生产上部署为同域就不用操心了。初次联调最容易遇见的不是跨域是JSON格式不匹配——前端传的是FormData,后端接收类型是@RequestBody,或者日期格式对不上,字符串转LocalDate失败。遇到这种错误,先从网络请求面板看Payload,再到后端日志看异常信息,链路排查一遍,一般几分钟就能定位。

7. 部署上线实战与常见问题排查

7.1 最简单的部署方案:项目打包与运行

很多人卡在这一步,以为部署很复杂。其实现在的Spring Boot项目配合Vue构建产物,部署非常简单,核心流程如下:

后端打包:

mvn clean package -DskipTests # 生成 target/employee-system.jar

前端构建:

npm run build # 生成 dist 目录

然后把dist里的静态资源文件拷贝到后端src/main/resources/static/目录下,重新打包后端;或者把前端dist丢到Spring Boot同级的目录用Nginx托管,把/api路径反向代理到后端端口。后一种方案更贴近真实场景(前端静态文件与后端服务分离),但前一种对演示/答辩场景最省事——一个jar包里面既有接口又有页面,扔服务器上执行这条命令就完事:

java -jar employee-system.jar --server.port=8080 --spring.profiles.active=prod

部署前记得创建生产数据库,并执行schema.sql初始化表结构。我第一次部署时漏了这一步,应用启动后提示Table 'employee' doesn't exist,排查了一会儿才发现是初始化脚本没跑,挺尴尬的。

7.2 我踩过的高频坑:时区问题、连接池、逻辑删除

下面这几个坑是我做这个项目时真实遇到过,而且不止一次被同类问题坑到,列出来供你直接避雷。

时区问题:Spring Boot连接MySQL时,jdbc-url里的serverTimezone必须配好。我自己遇到过传一个2024-05-20 09:00:00进去,存到库里变成2024-05-20 17:00:00,查出来又莫名多一天之类的情况。正确的连接串是这样的:

jdbc:mysql://localhost:3306/employee_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

而且在代码里,接收前端传的日期字符串时统一使用LocalDateTime,配合Jackson的spring.jackson.date-format配置以及JavaTimeModule,避免java.util.Date导致的时区偏移。

数据库连接池连接超时:HikariCP默认连接超时时间没那么宽裕,如果程序中某个操作耗时太长,连接空闲一久会被MySQL服务端主动断开。项目上线后我遇到过“运行一段时间后第一个请求就报错,重启又好了”的诡异现象,最后定位到是连接池活跃链接失效。解决方案也简单,在配置里加:

spring: datasource: hikari: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 maximum-pool-size: 10

逻辑删除与唯一索引冲突:我前面提到员工表emp_no加唯一索引,但配合逻辑删除时就悲剧了——同一工号的员工被“删除”后,想重新入职录同一个人,插入时提示唯一索引冲突,因为老数据还在表里只是打了个删除标记。这个问题我当时查了很久,最后用两种方案结合解决:

  • 方案一:唯一索引改成(emp_no, deleted)联合索引,deleted是删除标记,这样同一个人删除后deleted=1,新插入的记录deleted=0,两者不冲突。但进一步想,如果同一个人删了两次(中间重新入职一次),还是会撞。于是更彻底——
  • 方案二:工号生成时带一个随机后缀或按时间戳编号,比如“20240520-001”,从生成策略上杜绝重复。

方案二的思路更简单:业务上工号本来就应该全局唯一,所以新员工录用的时候按照规则生成新工号,不重用人删除掉的旧工号。这样一来,逻辑删除和唯一索引天然共存,不需要在数据库层面做妥协。

8. 写在最后:项目扩展方向与个人体会

这个项目做完,我心里最大的感受是:“做出来”和“做得完整”之间的差距,比想象中大得多。一个能跑起来的CRUD很简单,但一个拿得出手的系统,背后是对业务边界的思考、对异常场景的处理、对数据一致性的考量。我见过不少人完成了功能演示,但一问到“部门被删除时员工怎么处理”“员工连续几天没打卡系统怎么告诉HR”“登录态过期的时机怎么计算”,就答不上来——这些才是设计里真正有分量、也真正会暴露水平的地方。

如果你有余力,这个项目后面还能继续扩展几个方向:

  • 往统计分析方向走:给Dashboard加更深的报表,比如各部门调薪趋势、考勤异常预警、员工工龄分布图,这里会用到更多分组聚合SQL,也对前端图表库使用有更高要求。
  • 往工作流方向走:请假审批、离职审批,引入简单的状态机,让申请单在“待审批、已通过、已驳回”之间流转,这是企业系统的经典玩法,做出来简历可以加分不少。
  • 往工程化方向走:引入Redis缓存部门数据,用XXL-Job做定时任务每天凌晨自动生成考勤日报,或者拆出独立的认证服务——这三样虽然对这个小系统有点“过度设计”,但学到的技能是通用的。

最后再分享一个小技巧:写代码过程中把你的错误和解决过程记在一个地方。我跟你说,这些东西比代码本身值钱——面试官更想听“你踩过什么坑、怎么解决的”,而不是“你用了什么框架”。祝你的系统早日跑起来,答辩顺利。

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

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

立即咨询