去年接手了一个中型企业的人力资源管理系统重构项目,让我深刻体会到SpringBoot在快速开发企业级应用中的优势。传统HR系统往往面临几个典型痛点:手工操作效率低下、数据分散在不同Excel表中难以整合、权限管理混乱导致敏感信息泄露风险。这套基于SpringBoot的HR系统从零开始构建,仅用8周就完成了核心模块上线,目前稳定支撑着该企业3000+员工的日常管理。
系统采用经典的分层架构设计:
实体类设计时特别注意了JPA关联关系的使用技巧。比如员工与部门的@ManyToOne关联,实际开发中发现三个优化点:
java复制@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
注意:必须配合@Transactional使用,否则会出现no session异常
java复制// Department实体中
@OneToMany(mappedBy = "department")
@JsonIgnore // 关键!避免JSON序列化死循环
private List<Employee> employees;
java复制@Entity
@Table(name = "employee")
@SecondaryTable(name = "employee_profile",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "emp_id"))
public class Employee {
@Column(table = "employee_profile", length = 2000)
private String resume;
}
RBAC模型我们做了增强设计,在标准五表结构基础上增加了:
权限拦截的两种实现方式对比:
方案一:注解方式
java复制@PreAuthorize("hasRole('HR_ADMIN') or #emp.dept.id == authentication.deptId")
public void updateSalary(Employee emp) {
// ...
}
方案二:AOP方式
java复制@Around("@annotation(org.springframework.security.access.prepost.PreAuthorize)")
public Object checkPermission(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
PreAuthorize auth = signature.getMethod().getAnnotation(PreAuthorize.class);
// 解析SpEL表达式...
}
实测发现注解方式更直观但灵活性差,最终采用混合方案:基础权限用注解,特殊规则走AOP。
考勤模块最复杂的是异常情况处理:
核心计算逻辑:
java复制public AttendanceResult calculate(Employee emp, LocalDate date) {
// 获取当天所有打卡记录
List<ClockRecord> records = recordRepo.findByEmpAndDate(emp.getId(), date);
// 异常检测
if (records.isEmpty()) {
return markAbsence(emp, date);
}
// 排序获取最早和最晚记录
records.sort(Comparator.comparing(ClockRecord::getTime));
LocalDateTime first = records.get(0).getTime();
LocalDateTime last = records.get(records.size()-1).getTime();
// 规则引擎匹配
Rule rule = ruleService.matchRule(emp, date);
return rule.evaluate(first, last);
}
月初计算全公司考勤时遇到性能瓶颈,通过三步优化将耗时从6分钟降到23秒:
java复制// 原始:N+1查询问题
employees.forEach(emp -> {
List<Record> records = recordRepo.findByEmpId(emp.getId());
// ...
});
// 优化:一次加载所有数据
Map<Long, List<Record>> recordMap = recordRepo.findAll()
.stream()
.collect(Collectors.groupingBy(Record::getEmpId));
java复制List<Employee> employees = employeeService.listAll();
employees.parallelStream().forEach(emp -> {
calculateForEmployee(emp);
});
java复制@Cacheable(value = "attendance_rules", key = "#deptId")
public List<Rule> getRulesByDept(Long deptId) {
return ruleRepo.findByDeptId(deptId);
}
国内个税计算的特殊性在于累计预扣法,我们抽象出TaxCalculator接口:
java复制public interface TaxCalculator {
BigDecimal calculate(BigDecimal income,
BigDecimal insurance,
BigDecimal specialDeduction,
Month month);
}
// 2023年新版个税实现
public class ChinaTaxCalculator2023 implements TaxCalculator {
private static final BigDecimal[] RATES = { /*...*/ };
private static final BigDecimal[] QUICK_DEDUCTIONS = { /*...*/ };
@Override
public BigDecimal calculate(BigDecimal income,
BigDecimal insurance,
BigDecimal specialDeduction,
Month month) {
BigDecimal taxable = income.subtract(insurance)
.subtract(specialDeduction)
.subtract(BigDecimal.valueOf(5000));
// 累计应纳税额计算...
}
}
sql复制INSERT INTO payroll (emp_id, base_salary, bonus, tax, status)
VALUES (?, ?, ?, ?, 'PENDING')
java复制@Transactional
public void approvePayroll(Long payrollId) {
Payroll payroll = payrollRepo.findById(payloadId)
.orElseThrow(...);
if (!"PENDING".equals(payroll.getStatus())) {
throw new IllegalStateException("只能审核待处理薪资单");
}
payroll.setStatus("APPROVED");
payroll.setApprover(SecurityUtils.getCurrentUser());
}
java复制@Async
public void processBankTransfer(List<Long> payrollIds) {
// 调用银行接口
bankService.batchTransfer(payrollIds);
// 更新状态
payrollRepo.updateStatus(payrollIds, "COMPLETED");
}
生产环境采用双活部署:
code复制 [Nginx]
/ \
[北京集群] [上海集群]
/ | \ / | \
[App1][App2][App3][App4][App5]
| | | | |
[MySQL北京主] [MySQL上海主]
\ /
[双向同步]
关键配置项:
yaml复制spring:
datasource:
url: jdbc:mysql:loadbalance://bj-db,sh-db/hr_system?loadBalanceAutoCommitStatementThreshold=5
redis:
cluster:
nodes: bj-redis1:6379,bj-redis2:6379,sh-redis1:6379
Prometheus监控指标示例:
yaml复制- job_name: 'springboot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['bj-app1:8080', 'sh-app1:8080']
Grafana看板重点监控:
现象:多名HR同时计算某部门薪资时,结果出现串数据。
根因:Service方法未加@Transactional,导致Hibernate一级缓存混乱。
解决方案:
java复制@Transactional(isolation = Isolation.READ_COMMITTED)
public void calculateDeptPayroll(Long deptId) {
// ...
}
现象:系统运行一周后响应变慢,OOM频发。
排查步骤:
修复方案:
java复制@GetMapping("/employees")
public Page<Employee> listEmployees(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
if (page < 1) page = 1;
if (size > 500) size = 500; // 限制最大页大小
return employeeService.list(page, size);
}
与法大大对接的合同签署流程:
mermaid复制sequenceDiagram
系统->>法大大: 发起签约(员工信息+合同模板)
法大大-->>员工短信: 签署链接
员工->>法大大: 完成签署
法大大->>系统: 签署回调通知
系统->>数据库: 更新合同状态
核心代码:
java复制@PostMapping("/contract/sign")
public String initiateSign(@RequestBody Contract contract) {
// 填充模板变量
Map<String, String> vars = Map.of(
"employeeName", contract.getEmployee().getName(),
"salary", contract.getSalary().toString()
);
// 调用SDK
FadadaResponse resp = fadadaClient.createContract(
contract.getTemplateId(),
vars,
contract.getEmployee().getMobile()
);
if (resp.isSuccess()) {
contract.setStatus("SIGNING");
contract.setSignId(resp.getSignId());
contractRepo.save(contract);
}
return resp.getSignUrl();
}
使用Apache POI导出考勤统计报表的优化技巧:
java复制public void exportAttendanceReport(LocalDate start, LocalDate end,
HttpServletResponse response) {
// 1. 创建workbook(流式)
SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 保留100行在内存
// 2. 预定义样式
CellStyle headerStyle = createHeaderStyle(workbook);
// 3. 分批写入数据
int page = 0;
while (true) {
Page<Employee> emps = employeeRepo.findAll(PageRequest.of(page, 100));
if (emps.isEmpty()) break;
writePageData(workbook, headerStyle, emps.getContent());
page++;
}
// 4. 流式输出
response.setContentType("application/vnd.ms-excel");
workbook.write(response.getOutputStream());
workbook.dispose();
}
这个项目给我最深的体会是:SpringBoot确实能极大提升开发效率,但要构建健壮的HR系统,还需要在以下方面重点投入:
代码规范方面,我们团队形成了这些最佳实践:
这套系统后续计划扩展AI能力: