Java企业人事管理系统实战:从数据库设计到Spring Boot开发
发布时间:2026/9/30 5:21:36来源:尧图网络
简介基于Java的企业人事管理系统毕业设计文档面向计算机相关专业学生、Java Web开发学习者和毕业设计选题人员。文档从人事管理系统的现实意义谈起强调企业人力资源数字化带来的效率提升随后完整展开系统分析、总体设计、详细设计与功能模块划分。设计基于MyEclipse集成开发环境采用JSP动态页面技术与SqlServer2008数据库覆盖人员档案、培训、职称评定、奖惩、人员调动等业务模块并提供可行性分析、非功能性需求、实体属性图及模块设计说明可帮助读者快速理解从需求到实现的关键链路。资源共1个doc文件压缩包大小48KB虽然体量精简但内容结构完整适合作为毕业设计论文框架或小型人事管理项目的参考资料。文档中对各功能模块的划分与数据库设计思路清晰特别适合在缺少完整项目参考时作为起步模板。已有80人学习下载可用于课程设计、答辩准备或系统开发前的需求梳理。1. “基于java企业人事管理系统.doc”到底在解决什么问题“基于java企业人事管理系统.doc”这个标题我在课程设计、毕业设计题目里见过很多次也在一些小企业的项目需求文档里看到过。它指向的不是高并发中间件或复杂算法而是一套面向人事专员的信息化工具员工档案能录、能改、能查部门结构能调整考勤和薪资能按规则计算合同到期能提醒最后还要能把结果导成 Word 或 Excel 报表。标题里的“.doc”很常见因为这类项目交作业或交项目时习惯把需求分析、数据库设计、核心接口说明写进一份 Word 文档代码和文档一起交付。适合三类人准备课程设计或毕业设计的 java 学习者要给小团队做内部系统的一线开发者以及正被“java事务、权限、数据一致性”这些问题问住、想用真实系统验证一遍的转行者。下面按一套最常见也最可靠的方案拆开讲目标是让读者能照着建表、写接口、跑起来。2. 先别写代码把人事领域拆成表数据库设计定成败2.1 技术栈选型Spring Boot MyBatis 为什么是这类系统的主流答案这类系统我不建议一上来就上微服务也不建议把 Redis 当成核心存储用。人事管理系统的人数级通常就是几十到几千人业务量不大但字段多、状态多、报表条件多真正要命的是数据一致性而不是性能。Spring Boot 把配置收敛成少数几个文件内置 Web 服务器最后交付物就是一个 jar 包小公司没有专职运维也能跑。MyBatis 虽然要写 XML但人事系统里的统计 SQL 基本都是动态条件手写 SQL 比 JPA 的派生查询更容易调优、更容易给后来的维护者解释。前端如果只给人事专员用服务端渲染的 Thymeleaf 足够如果还要让员工自己提交请假、查看工资条再拆出一个 Vue 项目也不迟但这不是人事系统的必需品。数据库选 MySQL云厂商 RDS 和自建都默认支持网上能查到的踩坑记录也最多。还有一件事容易被新人卡住java 环境变量配置。JDK 装完不配 JAVA_HOME 和 PATHMaven 编译时直接报“java 不是内部或外部命令”这不是题目难点但会消耗一小时。先把环境确认好再往下做。2.2 核心表结构与主外键关系从部门、员工到考勤、薪资行业里做这类系统最少需要这几类表账号权限、组织架构、员工档案、考勤、薪资以及一套流程记录。下面是我常用的核心表结构表名关键字段关系与业务规则departmentid, parent_id, name, manager_id, status自关联树形结构部门允许停用但不允许物理删除employeeid, dept_id, name, gender, id_card, phone, hire_date, contract_end_date, status, deleted员工与部门多对一员工表保留逻辑删除标记sys_userid, employee_id, username, password_hash, role_id登录账号与员工一对一密码只存哈希sys_roleid, role_code, role_name角色与权限点通过 sys_role_menu 关联attendanceid, employee_id, work_date, first_in_time, last_out_time, status, overtime_hours每个员工每天一条用 employee_id work_date 做唯一键salaryid, employee_id, salary_month, base_salary, allowance, bonus, deduction, net_salary, status, version每个员工每月一条用 employee_id salary_month 做唯一键design_dept 的 parent_id 自关联可以表达公司、部门、小组三级结构employee 里不要直接存上级经理姓名应该存 manager_id 指向 employee.id这样汇报关系才能跟着组织调整走。考勤和薪资表都要有状态字段考勤至少分正常、迟到、早退、缺卡、请假薪资至少分草稿、已确认、已发放否则报表里对不上账。2.3 初始化 SQL 脚本建库建表与关键参数说明下面是建库和四张核心表的初始化 SQL字段做了裁剪但保留了最容易踩坑的参数设置。CREATE DATABASE hr_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE hr_system; CREATE TABLE department ( id BIGINT AUTO_INCREMENT PRIMARY KEY, parent_id BIGINT NOT NULL DEFAULT 0, name VARCHAR(64) NOT NULL, manager_id BIGINT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_parent_id (parent_id) ) ENGINE InnoDB COMMENT 部门表; CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY, dept_id BIGINT NOT NULL, name VARCHAR(32) NOT NULL, id_card VARCHAR(18) NULL, phone VARCHAR(20) NULL, hire_date DATE NULL, contract_end_date DATE NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 2离职, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_dept_id (dept_id), KEY idx_phone (phone), CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department (id) ) ENGINE InnoDB COMMENT 员工表; CREATE TABLE attendance ( id BIGINT AUTO_INCREMENT PRIMARY KEY, employee_id BIGINT NOT NULL, work_date DATE NOT NULL, first_in_time DATETIME NULL, last_out_time DATETIME NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1迟到 2早退 3缺卡 4请假, overtime_hours DECIMAL(4,1) NOT NULL DEFAULT 0, UNIQUE KEY uk_emp_date (employee_id, work_date), CONSTRAINT fk_att_emp FOREIGN KEY (employee_id) REFERENCES employee (id) ) ENGINE InnoDB COMMENT 考勤日表; CREATE TABLE salary ( id BIGINT AUTO_INCREMENT PRIMARY KEY, employee_id BIGINT NOT NULL, salary_month CHAR(7) NOT NULL COMMENT 格式 2025-06, base_salary DECIMAL(10,2) NOT NULL DEFAULT 0, allowance DECIMAL(10,2) NOT NULL DEFAULT 0, bonus DECIMAL(10,2) NOT NULL DEFAULT 0, deduction DECIMAL(10,2) NOT NULL DEFAULT 0, net_salary DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已确认 2已发放, version INT NOT NULL DEFAULT 0, UNIQUE KEY uk_emp_month (employee_id, salary_month), CONSTRAINT fk_sal_emp FOREIGN KEY (employee_id) REFERENCES employee (id) ) ENGINE InnoDB COMMENT 薪资表;这里有几个参数值得单独说。字符集用 utf8mb4 而不是 utf8是因为员工姓名和备注里可能出现生僻字或 emojiutf8 在 MySQL 里最多存三个字节遇到四字节字符会直接报错。COLLATE 用 general_ci 还是 utf8mb4_unicode_ci 影响排序和中文等值比较企业内部系统选 general_ci 已经够用性能上差不了多少。DECIMAL 是给所有金额字段用的这是人事系统里不能商量的事情用 DOUBLE 存工资会在累计计算时出现 0.10.2 不等于 0.3 的问题。attendance 和 salary 的唯一键是防重复计算的第一道保险后面谈数据一致性时还会再提到。3. 把后端骨架搭起来登录认证、员工档案与组织管理3.1 Maven 工程结构与依赖搭一个能启动的最小骨架一个标准的 Spring Boot 工程目录上先保持 maven 的约定src/main/java、src/main/resources、src/main/java 下面按 controller、service、mapper、entity、config 分包。下面是我常用的 pom.xml 精简版版本号不写死交给父 pom 或 BOM 统一管理。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency /dependenciesSpring Boot 2.7.18 是 2.x 这条线里很稳的一个版本JDK 8 到 17 都能跑如果你们公司强制 JDK 17 以上直接换 Spring Boot 3.x 也是同一套代码只是 jakarta 包名需要同步改。POI 5.2.5 用于后面导出 Excel 和 Word这个依赖会在第 4 章和第 6 章真正用到。jjwt 三个包拆开是 API、实现和 JSON 序列化写登录模块时缺一个都会在运行期抛 ClassNotFoundException。配置文件 application.yml 里我习惯把数据源参数分开写方便排查现场问题server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/hr_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl 里的 serverTimezone 必须和数据库所在时区一致。国内服务器一般直接写 Asia/Shanghai不写的话LocalDateTime 存入 DATETIME 字段后查询出来可能相差 8 小时这是人事系统里最常见的“数据不对”的玄学来源。allowPublicKeyRetrievaltrue 是在 MySQL 8 使用 caching_sha2_password 认证时避免连接失败的常用参数本地开发可以开生产环境建议配合白名单使用。map-underscore-to-camel-case 打开后数据库的 create_time 可以直接映射到实体的 createTime不用每个字段写 TableField 注解。3.2 用 JWT 做登录认证把“谁能看哪个部门”在接口层收敛登录认证是人事系统所有接口的前置条件。常见做法是用 token 保存登录态JWT 只是其中一种实现好处是后端不用维护 session坏处是 token 无法主动失效所以密码修改后要强制 Redis 记录黑名单。人事系统中我更看重的是“行级权限”也就是一个分公司的人事专员登录后只能看到自己分公司和下级部门的员工不能全表看。行级权限不是靠前端隐藏按钮而是靠 SQL 条件。下面是一个极简的登录接口密码用 BCrypt 哈希校验登录成功后生成 JWT并把用户可见的部门范围放进 token 或缓存中。RestController RequestMapping(/api/auth) public class AuthController { private final LoginService loginService; public AuthController(LoginService loginService) { this.loginService loginService; } PostMapping(/login) public Result login(RequestBody LoginRequest req) { LoginResult result loginService.login(req.getUsername(), req.getPassword()); return Result.success(result); } }Service public class LoginService { Autowired private SysUserMapper userMapper; Autowired private DeptScopeService deptScopeService; public LoginResult login(String username, String rawPassword) { SysUser user userMapper.findByUsername(username); if (user null) { throw new BusinessException(用户名或密码错误); } if (!BCrypt.checkpw(rawPassword, user.getPasswordHash())) { throw new BusinessException(用户名或密码错误); } // 查出该用户能看到哪些部门生成权限范围 ListLong deptScope deptScopeService.getVisibleDeptIds(user.getRoleId()); String token JwtUtil.createToken(user.getId(), user.getRoleCode(), deptScope); return new LoginResult(token, user.getUsername(), user.getRoleCode()); } }逻辑说明BCrypt.checkpw 是对明文密码做校验用户表里永远不要存原始密码。token 里塞部门范围有个前提就是部门调整后旧 token 可能带旧范围所以我的习惯是 token 里只放 userId 和 roleId每次请求时从 Redis 读一次权限范围权限变更后能立刻生效。LoginRequest 和 LoginResult 是两个普通 DTO这里省略了字段定义实际项目里它们就是 getter/setter 的 POJO。参数上有一个容易忽略的点JWT 过期时间不要只设一天人事系统里专员常常几天不退出登录我一般设 7 天并要求服务端每隔一天自动续签一次。续签接口要放到认证拦截器的白名单里否则前端拿过期 token 换新 token 会被自己的过滤器拦死。3.3 员工档案管理Controller-Service-Mapper 三层实现员工档案是人事系统的“主数据”部门、考勤、薪资全都围绕它展开。下面是一个带行级权限的查询接口核心在 Mapper XML 的查询条件上。RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword, RequestAttribute(deptScope) ListLong deptScope) { PageResultEmployeeVO result employeeService.page(pageNum, pageSize, keyword, deptScope); return Result.success(result); } }select idpage resultTypecom.example.hr.entity.EmployeeVO SELECT e.id, e.name, e.phone, e.hire_date, e.contract_end_date, d.name AS dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.id WHERE e.deleted 0 AND e.dept_id IN foreach collectiondeptScope itemdeptId open( separator, close) #{deptId} /foreach if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.phone LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY e.hire_date DESC LIMIT #{offset}, #{pageSize} /selectdeptScope 是拦截器从当前登录人的权限范围里取出来放进请求属性的SQL 里用 foreach 展开成 IN 条件这不是可选条件是强制条件。哪怕登录的人是超级管理员deptScope 里也要显式放入所有部门 ID避免代码分支里“管理员不走权限”这种写法留下遗漏。LIMIT #{offset}, #{pageSize} 是手动分页offset 的计算在 service 层做offset (pageNum - 1) * pageSize。如果 list 用 PageHelper最外层用 PageHelper.startPage(pageNum, pageSize)原理一样只是帮你拼好了 limit。Service 层我一般把更新和删除包进事务里。删除员工不能真的 DELETE而是 update deleted1否则考勤表、薪资表的外键会大面积报错历史报表也全部对不上。另一个坑是修改员工时如果用 BeanUtils.copyProperties 把整个 DTO 拷贝到实体上包含的 List 子对象会被浅拷贝共享引用改一个对象的子列表另一处也变了。对深层嵌套对象要么自己写 clone要么重新查库再赋值。这就是 java 面试里常问的“对象深度拷贝”在真实业务里的体现不是八股文是真会翻车。4. 考勤与薪资人事系统的业务核心别做成单纯 CRUD4.1 考勤的“日汇总—月统计”结构为什么月度表必须单独建很多新人会认为考勤只要一张表每天往里插打卡记录就行。实际上人事专员要的不是流水是“某人这个月迟到几次、缺卡几次、加班几小时”。如果把月统计结果靠程序临时算每当考勤日期跨月、请假状态被修正、漏打卡补录时报表就乱了。所以在 attendance 日表之上我习惯再加一条 ATTENDANCE_MONTH 月统计表字段包括 employee_id、month、late_count、early_leave_count、absent_days、overtime_hours唯一键是 employee_id month。月汇总可以由日表定时聚合生成。下面 SQL 是一句典型生成逻辑INSERT INTO attendance_month ( employee_id, month, late_count, early_leave_count, absent_days, overtime_hours, update_time ) SELECT employee_id, DATE_FORMAT(work_date, %Y-%m) AS month, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS early_leave_count, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS absent_days, SUM(overtime_hours) AS overtime_hours, NOW() FROM attendance WHERE work_date BETWEEN 2025-06-01 AND 2025-06-30 GROUP BY employee_id, DATE_FORMAT(work_date, %Y-%m) ON DUPLICATE KEY UPDATE late_count VALUES(late_count), early_leave_count VALUES(early_leave_count), absent_days VALUES(absent_days), overtime_hours VALUES(overtime_hours), update_time NOW();ON DUPLICATE KEY UPDATE 依赖 attendance_month 表上的唯一键 employee_id month所以重复执行这个 SQL 不会产生两条记录只会刷新统计值。这就解决了“考勤补录后重新统计”的问题。要注意的一点是这条 SQL 只适合跑当月数据不能把三个月前的数据重新整体洗一遍因为历史月份可能已确认状态字段不在这个 SQL 里更新确认过的月份应该走单独的重算接口。4.2 薪资计算BigDecimal、事务与幂等控制薪资计算是人事系统里最不能含糊的模块。计算规则看起来简单基本工资 补贴 奖金 - 扣款但涉及的数值必须用 BigDecimal。java 的 double 和 float 在二进制里无法精确表示 0.1累加几次后出现 8.999999 这种数字工资条上就说不清。BigDecimal 也不允许直接 new BigDecimal(0.1)因为 0.1 的二进制近似值已经被算进去了正确姿势是 BigDecimal.valueOf(0.1) 或 new BigDecimal(0.1)。下面是薪资计算的 service 层核心片段Service public class SalaryService { Autowired private SalaryMapper salaryMapper; Transactional(rollbackFor Exception.class) public void calcOneMonth(String salaryMonth, Long employeeId) { // 1. 先查是否已经存在存在且状态已确认则不允许覆盖 Salary salary salaryMapper.findByEmployeeIdAndMonth(employeeId, salaryMonth); if (salary ! null salary.getStatus() 1) { throw new BusinessException(该月薪资已确认不能重复计算); } // 2. 查考勤月表算出扣款和加班费 AttendanceMonth att attendanceMonthMapper.findByEmployeeIdAndMonth(employeeId, salaryMonth); BigDecimal deduction BigDecimal.ZERO; if (att ! null) { deduction att.getLateCount() 0 ? BigDecimal.ZERO : BigDecimal.valueOf(att.getLateCount()).multiply(new BigDecimal(20)); } BigDecimal netSalary BigDecimal.valueOf(6000) .add(BigDecimal.valueOf(500)) .subtract(deduction); // 3. 不存在则插入存在但状态为草稿则更新 if (salary null) { salary new Salary(); salary.setEmployeeId(employeeId); salary.setSalaryMonth(salaryMonth); salary.setBaseSalary(BigDecimal.valueOf(6000)); salary.setAllowance(BigDecimal.valueOf(500)); salary.setDeduction(deduction); salary.setNetSalary(netSalary); salary.setStatus(0); salary.setVersion(0); salaryMapper.insert(salary); } else { salary.setDeduction(deduction); salary.setNetSalary(netSalary); salaryMapper.updateById(salary); } } }Transactional 保证“查重、计算、插入”三步要么全部成功要么全部回滚。这里还有一个更隐蔽的问题两个管理员同时在页面上点击“计算薪资”事务只能保证数据库操作原子性不能阻止两个请求读到同样状态后各自插入。所以在 salary 表里有唯一键 uk_emp_month 和 version 字段。唯一键保证重复插入时数据库直接报错version 则用于更新时乐观锁UPDATE salary SET net_salary #{netSalary}, version version 1 WHERE id #{id} AND version #{oldVersion}如果更新返回行数为 0说明有另外的请求已经改过这条薪资当前请求要重新加载再算或者直接报错。salary 的 status 字段从草稿到已确认到已发放每一步都往前走不允许从“已发放”回退到“草稿”这是防止“算错了重新覆盖”的最后一道闸。万不得已要更正也要新建一条负数冲抵记录再另算一条对账才有痕迹。4.3 用 Java POI 把薪资数据导成 Excel样式、字段类型和行数限制人事系统里导出工资条是刚需POI 是 java 生态里最常用的办公文档库。这里先讲 Excel 导出因为工资条、考勤统计用 Excel 比 Word 更合适Word 报表放到第 6 章讲。Excel 导出不要用 XSSFWorkbook 把全部数据写进内存员工过万时很容易 OutOfMemory。常见做法是用 SXSSFWorkbook它内部用临时文件落盘只保留窗口期内可见的行。GetMapping(/export/salary) public void exportSalary(HttpServletResponse response, RequestParam String salaryMonth) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamesalary_ salaryMonth .xlsx); try (SXSSFWorkbook workbook new SXSSFWorkbook(100)) { Sheet sheet workbook.createSheet(薪资明细); String[] headers {员工编号, 姓名, 基本工资, 补贴, 奖金, 扣款, 实发工资}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 分页读取避免一次性加载全部员工 int pageNum 1; int pageSize 1000; ListSalaryVO list; do { list salaryMapper.pageSalary(salaryMonth, pageNum, pageSize); int startRow (pageNum - 1) * pageSize 1; for (int i 0; i list.size(); i) { Row row sheet.createRow(startRow i); SalaryVO vo list.get(i); row.createCell(0).setCellValue(String.valueOf(vo.getEmployeeId())); row.createCell(1).setCellValue(vo.getEmployeeName()); row.createCell(2).setCellValue(vo.getBaseSalary().doubleValue()); row.createCell(3).setCellValue(vo.getAllowance().doubleValue()); row.createCell(4).setCellValue(vo.getBonus().doubleValue()); row.createCell(5).setCellValue(vo.getDeduction().doubleValue()); row.createCell(6).setCellValue(vo.getNetSalary().doubleValue()); } pageNum; } while (list.size() pageSize); workbook.write(response.getOutputStream()); } }逻辑说明SXSSFWorkbook 构造参数 100 表示内存中最多保留 100 行超过部分写入磁盘临时文件适合大数据量导出。这里分页查询是为了不让 List 一次装几万条页面每翻一次查 1000 条写完后继续查。金额字段在这里用 doubleValue() 转成数字只是为了 Excel 单元格显示和求和方便doubleValue 本身会有精度风险但只是导出发送不是再次用于计算可以接受。如果连显示都不允许有 0.1 偏差就把单元格设成字符串用 vo.getBaseSalary().toPlainString() 写入代价是不能直接在 Excel 里求和。这一类导出最容易翻车的是长数字列比如手机号、身份证号。它们如果按数值写入 Excel会被自动转成科学计数法手机号还会丢尾数。解决办法是把这些列统一用 row.createCell(i).setCellValue(vo.getPhone()) 字符串写入千万别图省事转 Long。POI 导出的内容不是自己打开看一眼没问题就完事还要关注 Excel 的“单元格格式”那一列到底存的是数值还是文本。5. 避坑与常见问题排查从部署到使用的 5 个血泪经验5.1 日期时间慢 8 小时页面显示与数据库不一致现象员工入职时间录的是 2025-06-01 09:00数据库里存成 2025-06-01 01:00前端页面查出来又变回 09:00。原因MySQL 连接串没指定 serverTimezone驱动和数据库会话时区各用一套。解决在连接池 url 里固定加上 serverTimezoneAsia/Shanghai并把数据库实例的 time_zone 也改掉命令是 SET GLOBAL time_zone 08:00。改了之后重启应用再查历史数据如果仍偏移说明写入时已经错了只能对异常月份的数据做一次批量补偿。5.2 Excel 导出手机号变成 1.39E10 和身份证后四位变成 0000现象导出的员工通讯录里手机号列显示 1.39E10身份证后四位全变成 0。原因POI 把手机号当作 double 写入单元格Excel 数值精度只有 15 位有效数字而身份证是 18 位超出的位数全被丢弃。解决把这类长数字列全部用字符串写入导出前统一格式化用 cell.setCellValue(String.valueOf(vo.getIdCard()))。这是最容易复现也最尴尬的坑通常发生在交付前一刻。5.3 导出报表时内存溢出几万员工一起查出来现象点击“导出全员通讯录”页面卡死后台日志出现 java.lang.OutOfMemoryError: Java heap space。原因导出代码直接 select * from employee再在循环里取 department 名称几万条记录和嵌套对象全堆在 List 里。解决分页读取并流式写入代码见 4.3 的 SXSSFWorkbook 例子。另外把 JVM 堆内存设大一点只是治标根本办法是“查一批、写一批、释放一批”不要让导出接口和普通查询接口共用同一套大数据量方法。5.4 删除部门或员工时外键报错历史数据被物理删除现象删除一个已经离职三年的员工时数据库抛 Cannot delete or update a parent row: a foreign key constraint fails。原因员工表被考勤表、薪资表外键引用物理删除员工等于把历史对账依据一并删了。解决员工表加 deleted 字段所有查询默认带上 deleted0离职走“状态改为离职”而不是删除部门删除时先检查下是否存在在职员工有就不允许删只能停用。外键在业务上相当于一个强约束反过来也要求代码里不能有绕过约束的物理删除路径。5.5 薪资被重复发放事务和唯一键缺一不可现象同一月份的薪资被算了两次salary 表出现两条相同 employee_id 和 salary_month 的记录或者一个管理员在计算时另一个管理员同步修改最终工资条对不上。原因Transactional 只能保证单次事务内一致性不能防止两个并发请求读到同样状态后一起插入。解决数据库唯一键 uk_emp_month 守住插入代码里的乐观锁版本字段 version 守住更新业务代码里再增加状态判断status 为非草稿时禁止重算。这三层少一层最终都会以“这月工资多了 500”的形式出现在老板面前。这也是 java 面试里“怎么保证数据一致性”最接地气的答案事务、唯一索引、乐观锁按场景三层配合。6. 最后一步用导出的 Word 报表把“doc”落到实际业务里6.1 模板填充还是代码生成先分清楚再动手很多标题带 .doc 的人事系统最后都要交付一份像样的 Word 报表。常见做法有两种一种是用 Word 做一个模板在占位符处填充数据适合“格式固定、每周都出”的员工花名册另一种是全程用代码生成段落和表格适合“表头和数据列不固定”的临时报表。实际项目里我做新人入职登记表时用模板做全公司组织架构图时用代码生成因为树形结构每层行数不可控。POI 的 XWPF 用来生成 .docx 是成熟的但旧 .doc 格式的生成在 POI 里只有 HWPF接口少、维护慢所以真落地的项目里基本都是生成 docx再由终端的 Word 打开另存为 .doc 交付不要硬扛二进制后缀。6.2 用 XWPF 生成员工信息 Word 报表的最小代码下面是一个生成员工信息 Word 表格的最小示例表头固定数据行动态增加import org.apache.poi.xwpf.usermodel.*; try (XWPFDocument doc new XWPFDocument(); FileOutputStream out new FileOutputStream(employee_report.docx)) { // 标题段落 XWPFParagraph title doc.createParagraph(); XWPFRun titleRun title.createRun(); titleRun.setText(员工信息报表); titleRun.setBold(true); titleRun.setFontSize(16); // 创建 1 行 4 列表格表头 XWPFTable table doc.createTable(1, 4); table.getRow(0).getCell(0).setText(姓名); table.getRow(0).getCell(1).setText(部门); table.getRow(0).getCell(2).setText(入职日期); table.getRow(0).getCell(3).setText(合同到期日); // 动态追加数据行 for (EmployeeVO vo : employeeList) { XWPFTableRow row table.createRow(); row.getCell(0).setText(vo.getName()); row.getCell(1).setText(vo.getDeptName()); row.getCell(2).setText(String.valueOf(vo.getHireDate())); row.getCell(3).setText(String.valueOf(vo.getContractEndDate())); } doc.write(out); }table.createRow() 会在表格末尾追加行并自动补齐与表头一样的列数。这里 employeeList 建议控制在几百条以内超过千行时 Word 表格本身的渲染会变慢更合理的做法是按部门拆成多个 Word 文档或直接转 PDF 再合并。标题段落的 setBold 和 setFontSize 只作用于当前 run如果之后又往同一段落加 run样式要重新设一遍这是新人最容易忽略的细节。6.3 验证方法别让导出的文档变成黑匣子导出 Word 后不要只双击打开看格式我自己的习惯是写一个简易校验方法用 POI 重新读回生成的文件断言标题段落存在、表格行数等于员工数加一。这样可以避免导出时静默截断、字符集错乱这类问题在交付现场才暴露。生成的 docx 本质是一个 zip 包也可以用 unzip 打开后直接查看 word/document.xml 的文本内容判断占位符是否被正确替换。这套系统的最后一段路往往不是写业务代码而是把结果文档的每个细节都验证一遍。希望这些从建表到导出的习惯能帮到你至少让你在下次接到“java 企业人事管理系统”这类题目时不用再从零开始踩一遍我当年踩过的坑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网