ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java企业人事管理系统实战:从需求文档到部署上线全流程

Java企业人事管理系统实战:从需求文档到部署上线全流程 简介这份资源是面向计算机专业学生与Java Web初学者的人事管理系统毕业设计文档围绕企业人事部门日常的档案、薪酬、绩效等信息管理需求给出从系统分析到详细设计的完整实现思路。压缩包内共1个doc文件约48KB内容涵盖绪论、系统分析、总体设计与详细设计等章节并配有功能模块划分、系统模块设计图与实体属性图等图示说明。文档以Java语言为基础依托MyEclipse平台结合JSP技术与SqlServer2008数据库展开论述重点讲解人员档案管理、培训管理、职称评定管理、奖惩管理、人员调动等模块的设计方法同时涉及系统功能要求、可行性分析与非功能性需求等分析环节。目前已有80人浏览学习适合需要参考同类课题结构、撰写开题报告或课程设计的学生也可为准备搭建基础人事管理系统的开发者提供模块划分与数据库设计的思路借鉴。1. 从一份 .doc 需求文档到能跑的人事系统Java 企业级项目落地全流程很多做 Java 后端的同学都遇到过这种场景产品或者行政丢过来一份 Word 文档标题写着「企业人事管理系统需求说明」里面是几十条功能点从员工档案、考勤、薪资到组织架构密密麻麻。你打开一看需求是中文的但怎么把它变成一套能跑起来、能演示、能交付的 Java 系统文档里一个字没提。这份「基于 Java 企业人事管理系统.doc」本质上就是这类需求文档的典型代表它描述的是业务不是技术方案。真正要落地需要你自己把文档里的业务语言翻译成数据库表、Spring Boot 接口、权限模型和前端页面。这篇文章面向的是拿到类似需求文档、需要独立完成一套企业人事管理系统的 Java 开发者不管你是要交课程设计、做公司内部工具还是准备面试时拿一个完整项目练手下面这套从需求拆解到部署上线的路径都能直接照着走。我会把选型理由、建表脚本、核心接口代码、参数配置和踩过的坑都摊开讲新手能跟步骤复现熟手能直接拿去改。2. 需求文档怎么拆成数据库表和接口先定边界再动手2.1 从 .doc 里提取实体和关系的实操方法拿到一份人事管理系统的需求文档第一件事不是打开 IDEA 建工程而是拿一张纸或者一个在线表格把文档里出现的名词全部圈出来。员工、部门、职位、考勤记录、请假单、薪资条、社保、合同、培训记录这些名词大概率就是你要建的实体。动词则是你要暴露的接口比如「录入员工信息」「审批请假」「计算月度薪资」「导出考勤报表」。我一般会按下面这个顺序做拆解通读文档把所有业务名词列成一张清单不要漏掉任何出现过的名词。对每个名词判断它是不是独立实体。比如「员工姓名」不是实体它属于员工这个实体「部门名称」属于部门实体。确定实体之间的关系。一个部门有多个员工一个员工属于一个部门这是典型的一对多。一个员工可以有多条考勤记录也是一对多。请假单和审批人之间可能是多对一。把每个实体的关键属性列出来标注哪些是必填、哪些是可选、哪些需要唯一约束。最后再回头看文档里的动词每个动词对应一个或一组接口。这个过程不需要任何工具但一定要在写代码之前完成。我见过太多人直接打开 Navicat 就开始建表建到一半发现部门层级没设计好又回头改改完发现员工表的外键要跟着动连锁反应能把人逼疯。2.2 人事系统核心表结构设计与建表 SQL下面这套表结构是我在多个中小型企业人事系统里反复用过的版本覆盖了员工、部门、职位、考勤、请假、薪资六个核心模块。字段类型以 MySQL 8.0 为准其他数据库自行调整。-- 部门表支持树形结构parent_id 为 0 表示顶级部门 CREATE TABLE sys_department ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父部门ID0为顶级, dept_name VARCHAR(64) NOT NULL COMMENT 部门名称, dept_code VARCHAR(32) NOT NULL COMMENT 部门编码唯一, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序号, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_dept_code (dept_code), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门表; -- 职位表和部门关联一个部门有多个职位 CREATE TABLE sys_position ( id BIGINT NOT NULL AUTO_INCREMENT, dept_id BIGINT NOT NULL COMMENT 所属部门ID, position_name VARCHAR(64) NOT NULL COMMENT 职位名称, position_level TINYINT NOT NULL DEFAULT 1 COMMENT 职级1初级 2中级 3高级 4管理, base_salary DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 基础薪资, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_dept_id (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT职位表; -- 员工表核心表关联部门和职位 CREATE TABLE sys_employee ( id BIGINT NOT NULL AUTO_INCREMENT, emp_no VARCHAR(32) NOT NULL COMMENT 工号唯一, real_name VARCHAR(64) NOT NULL COMMENT 真实姓名, gender TINYINT NOT NULL DEFAULT 1 COMMENT 性别1男 2女, birth_date DATE DEFAULT NULL COMMENT 出生日期, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, dept_id BIGINT NOT NULL COMMENT 部门ID, position_id BIGINT NOT NULL COMMENT 职位ID, hire_date DATE NOT NULL COMMENT 入职日期, leave_date DATE DEFAULT NULL COMMENT 离职日期, emp_status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1在职 2试用 3离职, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no), KEY idx_dept_id (dept_id), KEY idx_position_id (position_id), KEY idx_emp_status (emp_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工表; -- 考勤记录表每天每人一条 CREATE TABLE att_attendance ( id BIGINT NOT NULL AUTO_INCREMENT, emp_id BIGINT NOT NULL COMMENT 员工ID, attend_date DATE NOT NULL COMMENT 考勤日期, check_in_time DATETIME DEFAULT NULL COMMENT 上班打卡时间, check_out_time DATETIME DEFAULT NULL COMMENT 下班打卡时间, attend_status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 2迟到 3早退 4缺勤 5请假, work_hours DECIMAL(4,1) DEFAULT NULL COMMENT 工作时长, remark VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_emp_date (emp_id, attend_date), KEY idx_attend_date (attend_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表; -- 请假单表 CREATE TABLE att_leave ( id BIGINT NOT NULL AUTO_INCREMENT, emp_id BIGINT NOT NULL COMMENT 申请人ID, leave_type TINYINT NOT NULL COMMENT 1事假 2病假 3年假 4调休, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, leave_days DECIMAL(4,1) NOT NULL COMMENT 请假天数, reason VARCHAR(500) DEFAULT NULL, approve_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回, approver_id BIGINT DEFAULT NULL COMMENT 审批人ID, approve_time DATETIME DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_emp_id (emp_id), KEY idx_approve_status (approve_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT请假单表; -- 薪资记录表按月生成 CREATE TABLE sal_salary ( id BIGINT NOT NULL AUTO_INCREMENT, emp_id BIGINT NOT NULL, salary_month VARCHAR(7) NOT NULL COMMENT 薪资月份格式2026-01, base_salary DECIMAL(12,2) NOT NULL DEFAULT 0.00, performance_salary DECIMAL(12,2) NOT NULL DEFAULT 0.00, deduction DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 扣款, social_security DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 社保个人部分, actual_salary DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 实发薪资, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未发放 1已发放, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_emp_month (emp_id, salary_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT薪资记录表;这套表结构有几个设计点需要说明。部门表用parent_id实现树形结构查询某个部门及其所有子部门的员工时要么用递归 CTE要么在应用层做递归组装MySQL 8.0 支持WITH RECURSIVE可以直接在 SQL 里搞定。员工表的emp_status字段区分在职、试用、离职离职员工不删除记录只改状态这样历史考勤和薪资数据不会断链。考勤表的uk_emp_date唯一索引保证一个人一天只有一条考勤记录避免重复打卡导致数据混乱。薪资表的uk_emp_month同理一个人一个月只有一条薪资记录。2.3 Spring Boot 项目分层与接口定义表建好之后项目结构按经典的三层架构来搭Controller 层负责接收请求和参数校验Service 层写业务逻辑Mapper 层做数据库操作。用 Spring Boot MyBatis-Plus 的组合省去大量手写 CRUD 的时间。// EmployeeController.java RestController RequestMapping(/api/employee) Validated public class EmployeeController { Autowired private EmployeeService employeeService; // 分页查询员工列表支持按部门、状态、关键字筛选 GetMapping(/page) public ResultPageEmployeeVO page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long deptId, RequestParam(required false) Integer empStatus, RequestParam(required false) String keyword) { return Result.ok(employeeService.pageQuery(pageNum, pageSize, deptId, empStatus, keyword)); } // 新增员工 PostMapping(/add) public ResultLong add(RequestBody Valid EmployeeAddDTO dto) { return Result.ok(employeeService.addEmployee(dto)); } // 修改员工信息 PutMapping(/update) public ResultVoid update(RequestBody Valid EmployeeUpdateDTO dto) { employeeService.updateEmployee(dto); return Result.ok(); } // 员工离职只改状态不删记录 PutMapping(/leave/{empId}) public ResultVoid leave(PathVariable Long empId, RequestParam NotBlank String leaveDate) { employeeService.leaveEmployee(empId, leaveDate); return Result.ok(); } }Controller 层只做参数接收和返回封装不写业务逻辑。Validated和Valid配合 DTO 上的注解做参数校验比如NotBlank、NotNull、Pattern这些能在进入 Service 之前就把非法参数拦掉。返回结果统一用ResultT包装前端拿到code、message、data三个字段处理起来一致。Service 层的核心方法是pageQuery和addEmployee。pageQuery用 MyBatis-Plus 的LambdaQueryWrapper拼条件关键字搜索对real_name和emp_no做like匹配。addEmployee需要做几件事校验工号是否重复、校验部门是否存在、插入员工记录、如果开启了试用期还要自动计算转正日期。这些逻辑写在 Service 里Controller 不感知。Service public class EmployeeServiceImpl implements EmployeeService { Autowired private EmployeeMapper employeeMapper; Autowired private DepartmentMapper departmentMapper; Override public PageEmployeeVO pageQuery(Integer pageNum, Integer pageSize, Long deptId, Integer empStatus, String keyword) { LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); if (deptId ! null) { wrapper.eq(Employee::getDeptId, deptId); } if (empStatus ! null) { wrapper.eq(Employee::getEmpStatus, empStatus); } if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Employee::getRealName, keyword) .or().like(Employee::getEmpNo, keyword)); } wrapper.orderByDesc(Employee::getCreateTime); PageEmployee page employeeMapper.selectPage(new Page(pageNum, pageSize), wrapper); return page.convert(this::toVO); } Override Transactional(rollbackFor Exception.class) public Long addEmployee(EmployeeAddDTO dto) { // 工号唯一性校验 Long count employeeMapper.selectCount( new LambdaQueryWrapperEmployee().eq(Employee::getEmpNo, dto.getEmpNo())); if (count 0) { throw new BizException(工号已存在); } // 部门存在性校验 Department dept departmentMapper.selectById(dto.getDeptId()); if (dept null) { throw new BizException(部门不存在); } Employee employee new Employee(); BeanUtils.copyProperties(dto, employee); employee.setEmpStatus(1); employeeMapper.insert(employee); return employee.getId(); } }Transactional(rollbackFor Exception.class)保证异常时回滚rollbackFor指定Exception而不是默认的RuntimeException是因为业务里可能抛受检异常不指定的话不会回滚。工号唯一性校验放在插入之前但高并发下仍可能重复所以数据库层面uk_emp_no唯一索引是最后一道防线捕获DuplicateKeyException再转成友好提示。3. 权限控制与数据隔离人事系统最容易翻车的地方3.1 基于角色的接口权限设计人事系统的权限比普通业务系统复杂因为不同角色能看到的数据范围完全不同。普通员工只能看自己的考勤和薪资部门主管能看本部门所有员工的数据HR 能看全公司系统管理员能改配置但不能看薪资明细。这种「功能权限 数据权限」的双层控制是人事系统最容易出问题的地方。功能权限用 Spring Security 或者 Sa-Token 都能做核心是给每个接口打上权限标识用户登录后查出他拥有的权限集合请求进来时比对。下面是一个基于注解的权限校验示例// 自定义权限注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); // 权限标识如 employee:view, salary:edit } // AOP 切面校验权限 Aspect Component public class PermissionAspect { Autowired private PermissionService permissionService; Around(annotation(requirePermission)) public Object check(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable { Long userId UserContext.getCurrentUserId(); if (userId null) { throw new BizException(未登录); } boolean has permissionService.hasPermission(userId, requirePermission.value()); if (!has) { throw new BizException(无权限访问); } return joinPoint.proceed(); } }接口上直接标注RequirePermission(salary:view)切面在方法执行前校验。权限数据存在sys_permission表里角色和权限多对多用户和角色多对多这是标准的 RBAC 模型。3.2 行级数据权限让主管只能看本部门数据功能权限解决「能不能访问这个接口」行级权限解决「能看哪些数据」。人事系统里部门主管调/api/employee/page时不应该看到其他部门的员工。实现方式是在 Service 层拼查询条件时根据当前用户的角色动态追加dept_id过滤。// 数据权限拦截器在 MyBatis 查询前追加部门条件 Component public class DataScopeInterceptor implements InnerInterceptor { Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 从当前登录用户获取数据权限范围 DataScope dataScope UserContext.getDataScope(); if (dataScope null || dataScope.isAll()) { return; // 全公司权限不加限制 } // 获取原始 SQL追加 dept_id 条件 String originalSql boundSql.getSql(); String newSql SELECT * FROM ( originalSql ) tmp WHERE tmp.dept_id IN ( dataScope.getDeptIds() ); // 通过反射修改 BoundSql 的 sql 字段 ReflectUtil.setFieldValue(boundSql, sql, newSql); } }这段代码用 MyBatis-Plus 的InnerInterceptor在 SQL 执行前改写语句把原始查询包一层子查询外层加上dept_id IN (...)的条件。UserContext里存的是当前登录用户的角色和数据范围HR 角色isAll()返回 true 不加限制主管角色返回他管理的部门 ID 列表。注意这种 SQL 改写方式对复杂查询比如带 UNION、子查询、GROUP BY 的语句可能出问题包一层子查询后外层再过滤性能会下降。更稳妥的做法是在 Service 层显式拼条件而不是在拦截器里改 SQL。我一般只在简单列表查询上用拦截器复杂报表查询手动控制。3.3 登录认证与密码安全登录接口用 Spring Security 的AuthenticationManager或者自己写一个简单的 Token 方案。密码存储必须用 BCrypt绝对不能在数据库里存明文或者 MD5。BCrypt 每次加密结果不同但校验时能匹配上这是它的设计特性。// 密码加密与校验 Configuration public class SecurityConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } } // 登录逻辑 public String login(String username, String rawPassword) { SysUser user userMapper.selectByUsername(username); if (user null) { throw new BizException(用户名或密码错误); } if (!passwordEncoder.matches(rawPassword, user.getPassword())) { throw new BizException(用户名或密码错误); } // 生成 Token存入 Redis设置过期时间 String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(token: token, user.getId(), 2, TimeUnit.HOURS); return token; }BCryptPasswordEncoder(10)里的 10 是强度参数值越大加密越慢10 是性能和安全的平衡点。Token 存 Redis 并设置 2 小时过期前端每次请求带在 Header 里后端拦截器校验 Redis 里是否存在。用户登出时删除 Redis 里的 Token 即可。4. 考勤与薪资计算业务逻辑最密集的两个模块4.1 考勤打卡记录的处理与异常判定考勤模块的核心是每天定时跑批把打卡记录和排班规则比对判定正常、迟到、早退、缺勤。假设公司规定 9:00 上班、18:00 下班打卡时间在 9:00 之后算迟到18:00 之前算早退。// 考勤判定逻辑 public void judgeAttendance(Long empId, LocalDate date) { Attendance att attendanceMapper.selectByEmpAndDate(empId, date); if (att null || att.getCheckInTime() null) { // 没有打卡记录判定缺勤 saveOrUpdateStatus(empId, date, 4); return; } LocalTime standardIn LocalTime.of(9, 0); LocalTime standardOut LocalTime.of(18, 0); LocalTime actualIn att.getCheckInTime().toLocalTime(); LocalTime actualOut att.getCheckOutTime() ! null ? att.getCheckOutTime().toLocalTime() : null; int status 1; // 默认正常 if (actualIn.isAfter(standardIn)) { status 2; // 迟到 } if (actualOut ! null actualOut.isBefore(standardOut)) { status 3; // 早退 } // 工作时长计算 if (att.getCheckInTime() ! null att.getCheckOutTime() ! null) { long minutes Duration.between(att.getCheckInTime(), att.getCheckOutTime()).toMinutes(); att.setWorkHours(BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP)); } att.setAttendStatus(status); attendanceMapper.updateById(att); }这段逻辑每天凌晨跑一次遍历前一天所有在职员工。实际项目中还要考虑弹性工作制、加班、外勤打卡等情况但核心判定逻辑就是时间比对。work_hours用BigDecimal保留一位小数避免浮点精度问题。4.2 月度薪资计算从考勤和请假数据汇总薪资计算是人事系统里逻辑最复杂的部分因为它依赖考勤、请假、社保、个税等多个数据源。我一般把薪资计算拆成三步先算应发再算扣款最后算实发。// 月度薪资计算 Transactional(rollbackFor Exception.class) public void calculateSalary(String salaryMonth) { // 1. 查出当月所有在职员工 ListEmployee employees employeeMapper.selectList( new LambdaQueryWrapperEmployee().in(Employee::getEmpStatus, 1, 2)); for (Employee emp : employees) { // 2. 获取基础薪资和绩效薪资 Position position positionMapper.selectById(emp.getPositionId()); BigDecimal baseSalary position.getBaseSalary(); BigDecimal performance getPerformanceSalary(emp.getId(), salaryMonth); // 3. 统计考勤扣款迟到早退各扣50缺勤扣日薪 BigDecimal attendanceDeduction calcAttendanceDeduction(emp.getId(), salaryMonth); // 4. 统计请假扣款事假扣日薪病假扣50%日薪 BigDecimal leaveDeduction calcLeaveDeduction(emp.getId(), salaryMonth); // 5. 社保个人部分简化按固定比例 BigDecimal socialSecurity baseSalary.multiply(new BigDecimal(0.105)) .setScale(2, RoundingMode.HALF_UP); // 6. 实发 基础 绩效 - 考勤扣款 - 请假扣款 - 社保 BigDecimal actual baseSalary.add(performance) .subtract(attendanceDeduction) .subtract(leaveDeduction) .subtract(socialSecurity); // 7. 写入薪资表 Salary salary new Salary(); salary.setEmpId(emp.getId()); salary.setSalaryMonth(salaryMonth); salary.setBaseSalary(baseSalary); salary.setPerformanceSalary(performance); salary.setDeduction(attendanceDeduction.add(leaveDeduction)); salary.setSocialSecurity(socialSecurity); salary.setActualSalary(actual); salaryMapper.insert(salary); } }日薪的计算方式是baseSalary / 21.7521.75 是法定月计薪天数。事假扣一天日薪病假扣半天。迟到早退每次扣 50 元缺勤扣一天日薪。这些规则每个公司不同实际项目中应该做成可配置的存在sys_config表里而不是硬编码。提示薪资计算涉及金额所有BigDecimal运算必须指定精度和舍入模式setScale(2, RoundingMode.HALF_UP)是标准做法。不要用double做金额计算浮点误差会导致对不上账。4.3 定时任务与批量处理考勤判定和薪资计算都是定时任务用 Spring 的Scheduled或者 Quartz 都能做。Scheduled简单够用Quartz 适合需要动态调整执行时间的场景。Component public class ScheduleTask { Autowired private AttendanceService attendanceService; Autowired private SalaryService salaryService; // 每天凌晨1点跑前一天考勤判定 Scheduled(cron 0 0 1 * * ?) public void dailyAttendance() { LocalDate yesterday LocalDate.now().minusDays(1); attendanceService.judgeAll(yesterday); } // 每月1号凌晨2点算上个月薪资 Scheduled(cron 0 0 2 1 * ?) public void monthlySalary() { String lastMonth YearMonth.now().minusMonths(1).toString(); salaryService.calculateSalary(lastMonth); } }cron表达式0 0 1 * * ?表示每天 1:00:00 执行0 0 2 1 * ?表示每月 1 号 2:00:00 执行。批量处理时要注意事务边界如果一次处理几千条数据不要把所有操作放在一个大事务里分批提交每批 500 条避免长事务锁表。5. 部署上线与常见问题排查5.1 从本地到服务器的部署清单项目开发完部署到服务器上需要准备这些东西JDK 17、MySQL 8.0、Redis、Nginx。Spring Boot 打成 jar 包用nohup java -jar启动或者用 systemd 做成服务。# 打包 mvn clean package -DskipTests # 启动 nohup java -jar hr-system.jar \ --spring.profiles.activeprod \ --server.port8080 \ /var/log/hr-system.log 21 # 查看启动日志 tail -f /var/log/hr-system.log--spring.profiles.activeprod指定生产环境配置数据库连接、Redis 地址这些敏感信息放在application-prod.yml里不要提交到代码仓库。Nginx 做反向代理把 80 端口的请求转发到 8080同时配置静态资源缓存。5.2 人事系统上线后最容易踩的五个坑坑一员工工号重复插入导致 500 错误。现象是前端提交新增员工时偶尔报「系统异常」日志里是DuplicateKeyException。原因是并发请求下两个线程同时通过了工号唯一性校验都执行了 insert。解决方式是在 Service 层捕获DuplicateKeyException转成「工号已存在」的友好提示同时数据库唯一索引必须保留。坑二考勤定时任务跑完后数据对不上。现象是某些员工明明打了卡却显示缺勤。原因是打卡记录的时间字段有时区问题服务器用 UTC 时间数据库存的是本地时间比对时差了 8 小时。解决方式是在 JDBC 连接串里加serverTimezoneAsia/Shanghai所有时间字段统一用LocalDateTime不要用Date。坑三薪资计算重复执行导致数据翻倍。现象是某个月薪资表里同一个员工有两条记录。原因是定时任务被触发了两次或者手动执行时没有做幂等。解决方式是在sal_salary表的uk_emp_month唯一索引基础上计算前先删除当月已有记录或者用INSERT ... ON DUPLICATE KEY UPDATE。坑四部门主管能看到其他部门数据。现象是行级权限拦截器没生效。原因是UserContext里的数据范围在异步线程里丢失了ThreadLocal没有传递到子线程。解决方式是用TransmittableThreadLocal替代ThreadLocal或者在异步任务里手动设置上下文。坑五导出 Excel 时内存溢出。现象是导出全公司几千条考勤记录时服务卡死。原因是用了 POI 的XSSFWorkbook把整个工作簿加载到内存。解决方式是换成 EasyExcel 的流式导出或者分页查询、分批写入。6. 把 .doc 变成可演示项目的最后一步接口文档与联调技巧项目能跑起来之后最后一步是让前端或者测试能顺利联调。我一般用 Knife4j 或者 SpringDoc 自动生成接口文档省去手写文档的时间。在pom.xml里加依赖启动类上加注解所有 Controller 的接口就会自动出现在/doc.html页面上。!-- Knife4j 依赖 -- dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi3-jakarta-spring-boot-starter/artifactId version4.4.0/version /dependencyConfiguration public class Knife4jConfig { Bean public OpenAPI openAPI() { return new OpenAPI() .info(new Info() .title(企业人事管理系统接口文档) .version(1.0) .description(员工、考勤、请假、薪资模块接口)); } }联调时最容易扯皮的是参数格式。日期用yyyy-MM-dd还是时间戳金额用字符串还是数字分页从 0 开始还是从 1 开始这些必须在接口文档里写清楚。我的习惯是在 DTO 的字段上加Schema(description 入职日期格式yyyy-MM-dd)前端一看就明白。还有一个技巧是准备一套测试数据。写一个data.sql插入几个部门、十几个员工、一个月的考勤和薪资记录每次重建数据库后自动执行。这样前端联调时不用手动造数据直接就能看到效果。-- data.sql 测试数据示例 INSERT INTO sys_department (parent_id, dept_name, dept_code, sort_order, status) VALUES (0, 技术部, TECH, 1, 1), (0, 人事部, HR, 2, 1), (1, 后端组, TECH_BACKEND, 1, 1); INSERT INTO sys_position (dept_id, position_name, position_level, base_salary, status) VALUES (1, Java开发工程师, 2, 15000.00, 1), (1, 前端开发工程师, 2, 13000.00, 1), (2, HR专员, 1, 8000.00, 1); INSERT INTO sys_employee (emp_no, real_name, gender, dept_id, position_id, hire_date, emp_status) VALUES (E001, 张三, 1, 1, 1, 2024-03-01, 1), (E002, 李四, 2, 1, 2, 2024-05-15, 1), (E003, 王五, 1, 2, 3, 2023-11-20, 1);这套测试数据配合spring.sql.init.modealways配置每次启动都会执行开发阶段非常方便。生产环境记得关掉这个配置改成never。最后说一个我自己的习惯每次改完代码不管多小的改动都先跑一遍完整的流程——登录、查员工列表、新增员工、审批请假、算薪资、导出报表。这六个动作走通基本能覆盖 90% 的回归场景。人事系统的业务逻辑不复杂但数据关联多一个字段改错可能影响好几个模块。我在这上面翻过车改完员工表结构忘了同步改薪资计算的 SQL结果算出来的薪资全是错的被财务追着问了一下午。从那以后改表结构必查所有引用该表的 SQL这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表