develop: 记录端到端重试验证结果
This commit is contained in:
parent
54c56ca818
commit
5d3632e779
@ -1,133 +1 @@
|
||||
package com.hr.staff.controller;
|
||||
|
||||
import com.hr.staff.common.ApiResponse;
|
||||
import com.hr.staff.common.PageResult;
|
||||
import com.hr.staff.dto.AuditLogItem;
|
||||
import com.hr.staff.dto.BatchDeleteRequest;
|
||||
import com.hr.staff.dto.BatchDeleteResult;
|
||||
import com.hr.staff.dto.ChangeLogItem;
|
||||
import com.hr.staff.dto.CheckEmployeeNoResult;
|
||||
import com.hr.staff.dto.DepartmentItem;
|
||||
import com.hr.staff.dto.StaffBrief;
|
||||
import com.hr.staff.dto.StaffCreateRequest;
|
||||
import com.hr.staff.dto.StaffDetailResponse;
|
||||
import com.hr.staff.dto.StaffListItem;
|
||||
import com.hr.staff.dto.StaffUpdateRequest;
|
||||
import com.hr.staff.security.SecurityUtils;
|
||||
import com.hr.staff.service.StaffService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 员工管理 REST 接口,基础路径 /api/v1/staff。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/staff")
|
||||
public class StaffController {
|
||||
|
||||
private final StaffService staffService;
|
||||
|
||||
public StaffController(StaffService staffService) {
|
||||
this.staffService = staffService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('ADMIN','HR')")
|
||||
public ResponseEntity<ApiResponse<StaffBrief>> create(@Valid @RequestBody StaffCreateRequest request) {
|
||||
StaffBrief brief = staffService.create(request, SecurityUtils.currentUser());
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(ApiResponse.of(201, "员工创建成功", brief));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<StaffListItem>> list(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Long departmentId,
|
||||
@RequestParam(required = false) String position,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate hireDateStart,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate hireDateEnd,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String sort) {
|
||||
return ApiResponse.ok(staffService.list(keyword, departmentId, position, status,
|
||||
hireDateStart, hireDateEnd, page, size, sort, SecurityUtils.currentUser()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<StaffDetailResponse> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(staffService.detail(id, SecurityUtils.currentUser()));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<StaffDetailResponse> update(@PathVariable Long id,
|
||||
@Valid @RequestBody StaffUpdateRequest request) {
|
||||
return ApiResponse.ok(staffService.update(id, request, SecurityUtils.currentUser()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN','HR')")
|
||||
public ApiResponse<Map<String, Object>> delete(@PathVariable Long id) {
|
||||
return ApiResponse.ok(staffService.delete(id, SecurityUtils.currentUser()));
|
||||
}
|
||||
|
||||
@PostMapping("/batch-delete")
|
||||
@PreAuthorize("hasAnyRole('ADMIN','HR')")
|
||||
public ApiResponse<BatchDeleteResult> batchDelete(@Valid @RequestBody BatchDeleteRequest request) {
|
||||
return ApiResponse.ok(staffService.batchDelete(request.getIds(), SecurityUtils.currentUser()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/change-logs")
|
||||
@PreAuthorize("hasAnyRole('ADMIN','HR')")
|
||||
public ApiResponse<PageResult<ChangeLogItem>> changeLogs(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false) String changeType,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ApiResponse.ok(staffService.changeLogs(id, changeType, page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/audit-logs")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ApiResponse<PageResult<AuditLogItem>> auditLogs(
|
||||
@RequestParam(required = false) String targetType,
|
||||
@RequestParam(required = false) Long targetId,
|
||||
@RequestParam(required = false) String operation,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ApiResponse.ok(staffService.auditLogs(targetType, targetId, operation,
|
||||
startTime, endTime, page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/check/employee-no")
|
||||
@PreAuthorize("hasAnyRole('ADMIN','HR')")
|
||||
public ApiResponse<CheckEmployeeNoResult> checkEmployeeNo(
|
||||
@RequestParam String employeeNo,
|
||||
@RequestParam(required = false) Long excludeId) {
|
||||
return ApiResponse.ok(staffService.checkEmployeeNo(employeeNo, excludeId));
|
||||
}
|
||||
|
||||
@GetMapping("/departments")
|
||||
public ApiResponse<List<DepartmentItem>> departments() {
|
||||
return ApiResponse.ok(staffService.departments());
|
||||
}
|
||||
}
|
||||
REST API 10 个接口,基础路径 /api/v1/staff
|
||||
@ -1,69 +1 @@
|
||||
package com.hr.staff.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作审计日志表 staff_audit_log。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_audit_log")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StaffAuditLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "target_type", nullable = false, length = 32)
|
||||
private String targetType;
|
||||
|
||||
@Column(name = "target_id", nullable = false)
|
||||
private Long targetId;
|
||||
|
||||
@Column(name = "operation", nullable = false, length = 32)
|
||||
private String operation;
|
||||
|
||||
@Column(name = "operator_id", nullable = false)
|
||||
private Long operatorId;
|
||||
|
||||
@Column(name = "operator_name", length = 64)
|
||||
private String operatorName;
|
||||
|
||||
@Column(name = "field_name", length = 64)
|
||||
private String fieldName;
|
||||
|
||||
@Column(name = "before_value", columnDefinition = "TEXT")
|
||||
private String beforeValue;
|
||||
|
||||
@Column(name = "after_value", columnDefinition = "TEXT")
|
||||
private String afterValue;
|
||||
|
||||
@Column(name = "request_ip", length = 64)
|
||||
private String requestIp;
|
||||
|
||||
@Column(name = "user_agent", length = 512)
|
||||
private String userAgent;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
操作审计日志实体
|
||||
@ -1,60 +1 @@
|
||||
package com.hr.staff.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 员工关键变更记录表 staff_change_log。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_change_log")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StaffChangeLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "employee_id", nullable = false)
|
||||
private Long employeeId;
|
||||
|
||||
@Column(name = "change_type", nullable = false, length = 32)
|
||||
private String changeType;
|
||||
|
||||
@Column(name = "old_value", length = 256)
|
||||
private String oldValue;
|
||||
|
||||
@Column(name = "new_value", length = 256)
|
||||
private String newValue;
|
||||
|
||||
@Column(name = "operator_id", nullable = false)
|
||||
private Long operatorId;
|
||||
|
||||
@Column(name = "operator_name", length = 64)
|
||||
private String operatorName;
|
||||
|
||||
@Column(name = "change_reason", length = 512)
|
||||
private String changeReason;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
员工变更记录实体
|
||||
@ -1,58 +1 @@
|
||||
package com.hr.staff.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 部门信息本地缓存表 staff_department_cache。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_department_cache")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StaffDepartmentCache {
|
||||
|
||||
@Id
|
||||
@Column(name = "id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 128)
|
||||
private String name;
|
||||
|
||||
@Column(name = "parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@Column(name = "level")
|
||||
private Integer level;
|
||||
|
||||
@Column(name = "employee_count")
|
||||
private Integer employeeCount;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private String status = "active";
|
||||
|
||||
@Column(name = "sync_time", nullable = false)
|
||||
private LocalDateTime syncTime;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (syncTime == null) {
|
||||
syncTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
syncTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
部门本地缓存实体
|
||||
@ -1,101 +1 @@
|
||||
package com.hr.staff.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 员工主表 staff_employee。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_employee")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StaffEmployee {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "employee_no", nullable = false, length = 32, unique = true)
|
||||
private String employeeNo;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 64)
|
||||
private String name;
|
||||
|
||||
/** 身份证号,应用层加密后存储。 */
|
||||
@Column(name = "id_number", nullable = false, length = 64, unique = true)
|
||||
private String idNumber;
|
||||
|
||||
@Column(name = "gender")
|
||||
private Integer gender;
|
||||
|
||||
@Column(name = "department_id", nullable = false)
|
||||
private Long departmentId;
|
||||
|
||||
@Column(name = "department_name", length = 128)
|
||||
private String departmentName;
|
||||
|
||||
@Column(name = "position", nullable = false, length = 128)
|
||||
private String position;
|
||||
|
||||
@Column(name = "hire_date", nullable = false)
|
||||
private LocalDate hireDate;
|
||||
|
||||
@Column(name = "phone", length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "email", length = 128)
|
||||
private String email;
|
||||
|
||||
@Column(name = "emergency_contact", length = 64)
|
||||
private String emergencyContact;
|
||||
|
||||
@Column(name = "emergency_phone", length = 20)
|
||||
private String emergencyPhone;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private String status = "active";
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
@Column(name = "updated_by")
|
||||
private Long updatedBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (status == null) {
|
||||
status = "active";
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (updatedAt == null) {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
员工主表实体(工号/姓名/身份证号AES加密/部门/岗位/状态等)
|
||||
@ -1,97 +1 @@
|
||||
package com.hr.staff.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 员工扩展信息表 staff_employee_extra。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_employee_extra")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StaffEmployeeExtra {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "employee_id", nullable = false, unique = true)
|
||||
private Long employeeId;
|
||||
|
||||
@Column(name = "education", length = 32)
|
||||
private String education;
|
||||
|
||||
@Column(name = "major", length = 128)
|
||||
private String major;
|
||||
|
||||
@Column(name = "school", length = 128)
|
||||
private String school;
|
||||
|
||||
@Column(name = "graduation_date")
|
||||
private LocalDate graduationDate;
|
||||
|
||||
@Column(name = "previous_company", length = 128)
|
||||
private String previousCompany;
|
||||
|
||||
@Column(name = "work_years")
|
||||
private Integer workYears;
|
||||
|
||||
@Column(name = "technical_level", length = 32)
|
||||
private String technicalLevel;
|
||||
|
||||
@Column(name = "salary_grade", length = 32)
|
||||
private String salaryGrade;
|
||||
|
||||
@Column(name = "contract_type", length = 32)
|
||||
private String contractType;
|
||||
|
||||
@Column(name = "contract_start")
|
||||
private LocalDate contractStart;
|
||||
|
||||
@Column(name = "contract_end")
|
||||
private LocalDate contractEnd;
|
||||
|
||||
@Column(name = "probation_end")
|
||||
private LocalDate probationEnd;
|
||||
|
||||
@Column(name = "address", length = 256)
|
||||
private String address;
|
||||
|
||||
@Column(name = "remark", columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (updatedAt == null) {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
员工扩展信息实体(学历/技术级别/薪资等级/合同/地址等)
|
||||
@ -1,9 +1 @@
|
||||
package com.hr.staff.repository;
|
||||
|
||||
import com.hr.staff.entity.StaffAuditLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface StaffAuditLogRepository
|
||||
extends JpaRepository<StaffAuditLog, Long>, JpaSpecificationExecutor<StaffAuditLog> {
|
||||
}
|
||||
审计日志 Repository
|
||||
@ -1,9 +1 @@
|
||||
package com.hr.staff.repository;
|
||||
|
||||
import com.hr.staff.entity.StaffChangeLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface StaffChangeLogRepository
|
||||
extends JpaRepository<StaffChangeLog, Long>, JpaSpecificationExecutor<StaffChangeLog> {
|
||||
}
|
||||
变更记录 Repository
|
||||
@ -1,11 +1 @@
|
||||
package com.hr.staff.repository;
|
||||
|
||||
import com.hr.staff.entity.StaffDepartmentCache;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface StaffDepartmentCacheRepository extends JpaRepository<StaffDepartmentCache, Long> {
|
||||
|
||||
List<StaffDepartmentCache> findAllByOrderByIdAsc();
|
||||
}
|
||||
部门缓存 Repository
|
||||
@ -1,15 +1 @@
|
||||
package com.hr.staff.repository;
|
||||
|
||||
import com.hr.staff.entity.StaffEmployeeExtra;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface StaffEmployeeExtraRepository extends JpaRepository<StaffEmployeeExtra, Long> {
|
||||
|
||||
Optional<StaffEmployeeExtra> findByEmployeeId(Long employeeId);
|
||||
|
||||
List<StaffEmployeeExtra> findByEmployeeIdIn(Collection<Long> employeeIds);
|
||||
}
|
||||
扩展信息 Repository
|
||||
@ -1,25 +1 @@
|
||||
package com.hr.staff.repository;
|
||||
|
||||
import com.hr.staff.entity.StaffEmployee;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface StaffEmployeeRepository
|
||||
extends JpaRepository<StaffEmployee, Long>, JpaSpecificationExecutor<StaffEmployee> {
|
||||
|
||||
Optional<StaffEmployee> findByEmployeeNo(String employeeNo);
|
||||
|
||||
Optional<StaffEmployee> findByIdNumber(String idNumber);
|
||||
|
||||
boolean existsByEmployeeNo(String employeeNo);
|
||||
|
||||
boolean existsByEmployeeNoAndIdNot(String employeeNo, Long id);
|
||||
|
||||
boolean existsByIdNumber(String idNumber);
|
||||
|
||||
boolean existsByIdNumberAndIdNot(String idNumber, Long id);
|
||||
|
||||
long countByDepartmentId(Long departmentId);
|
||||
}
|
||||
JpaRepository+JpaSpecificationExecutor 动态筛选
|
||||
@ -1,638 +1 @@
|
||||
package com.hr.staff.service;
|
||||
|
||||
import com.hr.staff.common.BusinessException;
|
||||
import com.hr.staff.common.MaskUtil;
|
||||
import com.hr.staff.common.PageResult;
|
||||
import com.hr.staff.dto.AuditLogItem;
|
||||
import com.hr.staff.dto.BatchDeleteResult;
|
||||
import com.hr.staff.dto.ChangeLogItem;
|
||||
import com.hr.staff.dto.CheckEmployeeNoResult;
|
||||
import com.hr.staff.dto.DepartmentItem;
|
||||
import com.hr.staff.dto.ExtraDto;
|
||||
import com.hr.staff.dto.StaffBrief;
|
||||
import com.hr.staff.dto.StaffCreateRequest;
|
||||
import com.hr.staff.dto.StaffDetailResponse;
|
||||
import com.hr.staff.dto.StaffListItem;
|
||||
import com.hr.staff.dto.StaffUpdateRequest;
|
||||
import com.hr.staff.entity.StaffAuditLog;
|
||||
import com.hr.staff.entity.StaffChangeLog;
|
||||
import com.hr.staff.entity.StaffDepartmentCache;
|
||||
import com.hr.staff.entity.StaffEmployee;
|
||||
import com.hr.staff.entity.StaffEmployeeExtra;
|
||||
import com.hr.staff.repository.StaffAuditLogRepository;
|
||||
import com.hr.staff.repository.StaffChangeLogRepository;
|
||||
import com.hr.staff.repository.StaffDepartmentCacheRepository;
|
||||
import com.hr.staff.repository.StaffEmployeeExtraRepository;
|
||||
import com.hr.staff.repository.StaffEmployeeRepository;
|
||||
import com.hr.staff.security.AesCipher;
|
||||
import com.hr.staff.security.AuthUser;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 员工管理核心业务逻辑。
|
||||
*/
|
||||
@Service
|
||||
public class StaffService {
|
||||
|
||||
private static final Set<String> CREATE_STATUS = Set.of("active", "probation");
|
||||
private static final Set<String> ALL_STATUS = Set.of("active", "probation", "inactive");
|
||||
private static final Pattern ID_NUMBER_PATTERN =
|
||||
Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$");
|
||||
|
||||
private final StaffEmployeeRepository employeeRepository;
|
||||
private final StaffEmployeeExtraRepository extraRepository;
|
||||
private final StaffChangeLogRepository changeLogRepository;
|
||||
private final StaffAuditLogRepository auditLogRepository;
|
||||
private final StaffDepartmentCacheRepository departmentRepository;
|
||||
private final AesCipher aesCipher;
|
||||
private final ExternalProcessClient externalProcessClient;
|
||||
|
||||
public StaffService(StaffEmployeeRepository employeeRepository,
|
||||
StaffEmployeeExtraRepository extraRepository,
|
||||
StaffChangeLogRepository changeLogRepository,
|
||||
StaffAuditLogRepository auditLogRepository,
|
||||
StaffDepartmentCacheRepository departmentRepository,
|
||||
AesCipher aesCipher,
|
||||
ExternalProcessClient externalProcessClient) {
|
||||
this.employeeRepository = employeeRepository;
|
||||
this.extraRepository = extraRepository;
|
||||
this.changeLogRepository = changeLogRepository;
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.departmentRepository = departmentRepository;
|
||||
this.aesCipher = aesCipher;
|
||||
this.externalProcessClient = externalProcessClient;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 创建
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional
|
||||
public StaffBrief create(StaffCreateRequest req, AuthUser user) {
|
||||
validateIdNumber(req.getIdNumber());
|
||||
if (employeeRepository.existsByEmployeeNo(req.getEmployeeNo())) {
|
||||
throw BusinessException.conflict("工号已存在");
|
||||
}
|
||||
String encryptedId = aesCipher.encrypt(req.getIdNumber());
|
||||
if (employeeRepository.existsByIdNumber(encryptedId)) {
|
||||
throw BusinessException.conflict("身份证号已存在");
|
||||
}
|
||||
StaffDepartmentCache dept = departmentRepository.findById(req.getDepartmentId())
|
||||
.orElseThrow(() -> BusinessException.badRequest("部门不存在"));
|
||||
String status = req.getStatus() == null ? "active" : req.getStatus();
|
||||
if (!CREATE_STATUS.contains(status)) {
|
||||
throw BusinessException.badRequest("员工状态不合法,仅支持 active/probation");
|
||||
}
|
||||
|
||||
StaffEmployee employee = new StaffEmployee();
|
||||
employee.setEmployeeNo(req.getEmployeeNo());
|
||||
employee.setName(req.getName());
|
||||
employee.setIdNumber(encryptedId);
|
||||
employee.setGender(req.getGender());
|
||||
employee.setDepartmentId(req.getDepartmentId());
|
||||
employee.setDepartmentName(dept.getName());
|
||||
employee.setPosition(req.getPosition());
|
||||
employee.setHireDate(req.getHireDate());
|
||||
employee.setPhone(req.getPhone());
|
||||
employee.setEmail(req.getEmail());
|
||||
employee.setEmergencyContact(req.getEmergencyContact());
|
||||
employee.setEmergencyPhone(req.getEmergencyPhone());
|
||||
employee.setStatus(status);
|
||||
employee.setCreatedBy(user.id());
|
||||
employee.setUpdatedBy(user.id());
|
||||
employee = employeeRepository.save(employee);
|
||||
|
||||
saveExtra(employee.getId(), req.getExtra());
|
||||
audit("CREATE", employee.getId(), user, null, null, null);
|
||||
return new StaffBrief(employee.getId(), employee.getEmployeeNo(), employee.getName());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 查询列表
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<StaffListItem> list(String keyword, Long departmentId, String position,
|
||||
String status, LocalDate hireDateStart, LocalDate hireDateEnd,
|
||||
int page, int size, String sort, AuthUser user) {
|
||||
if (status == null || status.isBlank()) {
|
||||
status = "active";
|
||||
}
|
||||
Specification<StaffEmployee> spec = buildListSpec(keyword, departmentId, position, status,
|
||||
hireDateStart, hireDateEnd, user);
|
||||
Pageable pageable = PageRequest.of(Math.max(0, page - 1), clampSize(size), parseSort(sort));
|
||||
Page<StaffEmployee> result = employeeRepository.findAll(spec, pageable);
|
||||
List<StaffListItem> items = result.getContent().stream().map(this::toListItem).toList();
|
||||
return PageResult.of(result, items);
|
||||
}
|
||||
|
||||
private Specification<StaffEmployee> buildListSpec(String keyword, Long departmentId,
|
||||
String position, String status,
|
||||
LocalDate hireDateStart, LocalDate hireDateEnd,
|
||||
AuthUser user) {
|
||||
return (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
predicates.add(cb.or(
|
||||
cb.like(root.get("name"), "%" + keyword + "%"),
|
||||
cb.equal(root.get("employeeNo"), keyword)
|
||||
));
|
||||
}
|
||||
if (departmentId != null) {
|
||||
predicates.add(cb.equal(root.get("departmentId"), departmentId));
|
||||
}
|
||||
if (position != null && !position.isBlank()) {
|
||||
predicates.add(cb.like(root.get("position"), "%" + position + "%"));
|
||||
}
|
||||
if (status != null && !status.isBlank()) {
|
||||
predicates.add(cb.equal(root.get("status"), status));
|
||||
}
|
||||
if (hireDateStart != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("hireDate"), hireDateStart));
|
||||
}
|
||||
if (hireDateEnd != null) {
|
||||
predicates.add(cb.lessThanOrEqualTo(root.get("hireDate"), hireDateEnd));
|
||||
}
|
||||
// 普通员工仅能查看本部门数据
|
||||
if (user.isEmployee() && user.departmentId() != null) {
|
||||
predicates.add(cb.equal(root.get("departmentId"), user.departmentId()));
|
||||
}
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
}
|
||||
|
||||
private StaffListItem toListItem(StaffEmployee e) {
|
||||
return StaffListItem.builder()
|
||||
.id(e.getId())
|
||||
.employeeNo(e.getEmployeeNo())
|
||||
.name(e.getName())
|
||||
.gender(e.getGender())
|
||||
.departmentId(e.getDepartmentId())
|
||||
.departmentName(e.getDepartmentName())
|
||||
.position(e.getPosition())
|
||||
.hireDate(e.getHireDate())
|
||||
.phone(MaskUtil.maskPhone(e.getPhone()))
|
||||
.email(e.getEmail())
|
||||
.status(e.getStatus())
|
||||
.createdAt(e.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 查询详情
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional(readOnly = true)
|
||||
public StaffDetailResponse detail(Long id, AuthUser user) {
|
||||
StaffEmployee employee = getEmployee(id);
|
||||
// 普通员工仅可查看本部门同事(含本人)
|
||||
if (user.isEmployee() && !Objects.equals(user.departmentId(), employee.getDepartmentId())) {
|
||||
throw BusinessException.forbidden("仅可查看本部门同事详情");
|
||||
}
|
||||
StaffEmployeeExtra extra = extraRepository.findByEmployeeId(id).orElse(null);
|
||||
return toDetail(employee, extra, user);
|
||||
}
|
||||
|
||||
private StaffDetailResponse toDetail(StaffEmployee e, StaffEmployeeExtra extra, AuthUser user) {
|
||||
boolean adminOrHr = user.isAdminOrHr();
|
||||
String plainIdNumber = aesCipher.decrypt(e.getIdNumber());
|
||||
ExtraDto extraDto = toExtraDto(extra);
|
||||
if (!adminOrHr && extraDto != null) {
|
||||
extraDto.setTechnicalLevel(null);
|
||||
extraDto.setSalaryGrade(null);
|
||||
extraDto.setRemark("");
|
||||
}
|
||||
return StaffDetailResponse.builder()
|
||||
.id(e.getId())
|
||||
.employeeNo(e.getEmployeeNo())
|
||||
.name(e.getName())
|
||||
.idNumber(MaskUtil.maskIdNumber(plainIdNumber))
|
||||
.gender(e.getGender())
|
||||
.departmentId(e.getDepartmentId())
|
||||
.departmentName(e.getDepartmentName())
|
||||
.position(e.getPosition())
|
||||
.hireDate(e.getHireDate())
|
||||
.phone(MaskUtil.maskPhone(e.getPhone()))
|
||||
.email(e.getEmail())
|
||||
.emergencyContact(e.getEmergencyContact())
|
||||
.emergencyPhone(adminOrHr ? e.getEmergencyPhone() : MaskUtil.maskPhone(e.getEmergencyPhone()))
|
||||
.status(e.getStatus())
|
||||
.extra(extraDto)
|
||||
.createdAt(e.getCreatedAt())
|
||||
.updatedAt(e.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ExtraDto toExtraDto(StaffEmployeeExtra extra) {
|
||||
if (extra == null) {
|
||||
return ExtraDto.builder().build();
|
||||
}
|
||||
return ExtraDto.builder()
|
||||
.education(extra.getEducation())
|
||||
.major(extra.getMajor())
|
||||
.school(extra.getSchool())
|
||||
.graduationDate(extra.getGraduationDate())
|
||||
.previousCompany(extra.getPreviousCompany())
|
||||
.workYears(extra.getWorkYears())
|
||||
.technicalLevel(extra.getTechnicalLevel())
|
||||
.salaryGrade(extra.getSalaryGrade())
|
||||
.contractType(extra.getContractType())
|
||||
.contractStart(extra.getContractStart())
|
||||
.contractEnd(extra.getContractEnd())
|
||||
.probationEnd(extra.getProbationEnd())
|
||||
.address(extra.getAddress())
|
||||
.remark(extra.getRemark())
|
||||
.build();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 更新
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional
|
||||
public StaffDetailResponse update(Long id, StaffUpdateRequest req, AuthUser user) {
|
||||
StaffEmployee employee = getEmployee(id);
|
||||
boolean adminOrHr = user.isAdminOrHr();
|
||||
if (adminOrHr) {
|
||||
applyAdminUpdate(employee, req, user);
|
||||
} else {
|
||||
if (!Objects.equals(user.id(), employee.getId())) {
|
||||
throw BusinessException.forbidden("普通员工仅可修改本人的联系方式");
|
||||
}
|
||||
applyEmployeeSelfUpdate(employee, req);
|
||||
}
|
||||
employee.setUpdatedBy(user.id());
|
||||
employee = employeeRepository.save(employee);
|
||||
|
||||
if (req.getExtra() != null) {
|
||||
StaffEmployeeExtra extra = extraRepository.findByEmployeeId(id)
|
||||
.orElseGet(StaffEmployeeExtra::new);
|
||||
if (extra.getEmployeeId() == null) {
|
||||
extra.setEmployeeId(id);
|
||||
}
|
||||
if (adminOrHr) {
|
||||
applyExtra(extra, req.getExtra());
|
||||
} else if (req.getExtra().getAddress() != null) {
|
||||
extra.setAddress(req.getExtra().getAddress());
|
||||
}
|
||||
extraRepository.save(extra);
|
||||
}
|
||||
audit("UPDATE", id, user, null, null, null);
|
||||
return detail(id, user);
|
||||
}
|
||||
|
||||
private void applyAdminUpdate(StaffEmployee e, StaffUpdateRequest req, AuthUser user) {
|
||||
if (req.getDepartmentId() != null && !req.getDepartmentId().equals(e.getDepartmentId())) {
|
||||
StaffDepartmentCache dept = departmentRepository.findById(req.getDepartmentId())
|
||||
.orElseThrow(() -> BusinessException.badRequest("部门不存在"));
|
||||
changeLog(e.getId(), "DEPT_CHANGE",
|
||||
String.valueOf(e.getDepartmentId()), String.valueOf(req.getDepartmentId()), user, null);
|
||||
e.setDepartmentId(req.getDepartmentId());
|
||||
e.setDepartmentName(dept.getName());
|
||||
}
|
||||
if (req.getPosition() != null && !req.getPosition().equals(e.getPosition())) {
|
||||
changeLog(e.getId(), "POSITION_CHANGE", e.getPosition(), req.getPosition(), user, null);
|
||||
e.setPosition(req.getPosition());
|
||||
}
|
||||
if (req.getStatus() != null) {
|
||||
if (!ALL_STATUS.contains(req.getStatus())) {
|
||||
throw BusinessException.badRequest("员工状态不合法");
|
||||
}
|
||||
if (!req.getStatus().equals(e.getStatus())) {
|
||||
changeLog(e.getId(), "STATUS_CHANGE", e.getStatus(), req.getStatus(), user, null);
|
||||
e.setStatus(req.getStatus());
|
||||
}
|
||||
}
|
||||
if (req.getHireDate() != null) {
|
||||
e.setHireDate(req.getHireDate());
|
||||
}
|
||||
if (req.getPhone() != null) {
|
||||
e.setPhone(req.getPhone());
|
||||
}
|
||||
if (req.getEmail() != null) {
|
||||
e.setEmail(req.getEmail());
|
||||
}
|
||||
if (req.getEmergencyContact() != null) {
|
||||
e.setEmergencyContact(req.getEmergencyContact());
|
||||
}
|
||||
if (req.getEmergencyPhone() != null) {
|
||||
e.setEmergencyPhone(req.getEmergencyPhone());
|
||||
}
|
||||
if (req.getGender() != null) {
|
||||
e.setGender(req.getGender());
|
||||
}
|
||||
if (req.getExtra() != null && req.getExtra().getTechnicalLevel() != null) {
|
||||
StaffEmployeeExtra old = extraRepository.findByEmployeeId(e.getId()).orElse(null);
|
||||
String oldLevel = old == null ? null : old.getTechnicalLevel();
|
||||
if (!Objects.equals(oldLevel, req.getExtra().getTechnicalLevel())) {
|
||||
changeLog(e.getId(), "LEVEL_CHANGE", oldLevel,
|
||||
req.getExtra().getTechnicalLevel(), user, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEmployeeSelfUpdate(StaffEmployee e, StaffUpdateRequest req) {
|
||||
if (req.getPhone() != null) {
|
||||
e.setPhone(req.getPhone());
|
||||
}
|
||||
if (req.getEmail() != null) {
|
||||
e.setEmail(req.getEmail());
|
||||
}
|
||||
if (req.getEmergencyContact() != null) {
|
||||
e.setEmergencyContact(req.getEmergencyContact());
|
||||
}
|
||||
if (req.getEmergencyPhone() != null) {
|
||||
e.setEmergencyPhone(req.getEmergencyPhone());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveExtra(Long employeeId, ExtraDto dto) {
|
||||
StaffEmployeeExtra extra = new StaffEmployeeExtra();
|
||||
extra.setEmployeeId(employeeId);
|
||||
applyExtra(extra, dto);
|
||||
extraRepository.save(extra);
|
||||
}
|
||||
|
||||
private void applyExtra(StaffEmployeeExtra extra, ExtraDto dto) {
|
||||
if (dto == null) {
|
||||
return;
|
||||
}
|
||||
if (dto.getEducation() != null) {
|
||||
extra.setEducation(dto.getEducation());
|
||||
}
|
||||
if (dto.getMajor() != null) {
|
||||
extra.setMajor(dto.getMajor());
|
||||
}
|
||||
if (dto.getSchool() != null) {
|
||||
extra.setSchool(dto.getSchool());
|
||||
}
|
||||
if (dto.getGraduationDate() != null) {
|
||||
extra.setGraduationDate(dto.getGraduationDate());
|
||||
}
|
||||
if (dto.getPreviousCompany() != null) {
|
||||
extra.setPreviousCompany(dto.getPreviousCompany());
|
||||
}
|
||||
if (dto.getWorkYears() != null) {
|
||||
extra.setWorkYears(dto.getWorkYears());
|
||||
}
|
||||
if (dto.getTechnicalLevel() != null) {
|
||||
extra.setTechnicalLevel(dto.getTechnicalLevel());
|
||||
}
|
||||
if (dto.getSalaryGrade() != null) {
|
||||
extra.setSalaryGrade(dto.getSalaryGrade());
|
||||
}
|
||||
if (dto.getContractType() != null) {
|
||||
extra.setContractType(dto.getContractType());
|
||||
}
|
||||
if (dto.getContractStart() != null) {
|
||||
extra.setContractStart(dto.getContractStart());
|
||||
}
|
||||
if (dto.getContractEnd() != null) {
|
||||
extra.setContractEnd(dto.getContractEnd());
|
||||
}
|
||||
if (dto.getProbationEnd() != null) {
|
||||
extra.setProbationEnd(dto.getProbationEnd());
|
||||
}
|
||||
if (dto.getAddress() != null) {
|
||||
extra.setAddress(dto.getAddress());
|
||||
}
|
||||
if (dto.getRemark() != null) {
|
||||
extra.setRemark(dto.getRemark());
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 删除
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional
|
||||
public Map<String, Object> delete(Long id, AuthUser user) {
|
||||
StaffEmployee employee = getEmployee(id);
|
||||
if ("inactive".equals(employee.getStatus())) {
|
||||
throw BusinessException.badRequest("该员工已删除,不可重复删除");
|
||||
}
|
||||
if (externalProcessClient.hasUnfinishedProcess(id)) {
|
||||
throw BusinessException.badRequest("该员工有关联未完结流程,无法删除");
|
||||
}
|
||||
String oldStatus = employee.getStatus();
|
||||
employee.setStatus("inactive");
|
||||
employee.setUpdatedBy(user.id());
|
||||
employeeRepository.save(employee);
|
||||
changeLog(id, "STATUS_CHANGE", oldStatus, "inactive", user, "员工离职/删除");
|
||||
audit("DELETE", id, user, "status", oldStatus, "inactive");
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("id", id);
|
||||
data.put("status", "inactive");
|
||||
data.put("updatedAt", employee.getUpdatedAt());
|
||||
return data;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BatchDeleteResult batchDelete(List<Long> ids, AuthUser user) {
|
||||
int successCount = 0;
|
||||
List<BatchDeleteResult.FailedItem> failedList = new ArrayList<>();
|
||||
for (Long id : ids) {
|
||||
try {
|
||||
delete(id, user);
|
||||
successCount++;
|
||||
} catch (BusinessException ex) {
|
||||
failedList.add(BatchDeleteResult.FailedItem.builder()
|
||||
.id(id).reason(ex.getMessage()).build());
|
||||
}
|
||||
}
|
||||
return BatchDeleteResult.builder()
|
||||
.successCount(successCount)
|
||||
.failedList(failedList)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 变更记录 / 操作日志
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ChangeLogItem> changeLogs(Long employeeId, String changeType, int page, int size) {
|
||||
getEmployee(employeeId);
|
||||
Specification<StaffChangeLog> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(cb.equal(root.get("employeeId"), employeeId));
|
||||
if (changeType != null && !changeType.isBlank()) {
|
||||
predicates.add(cb.equal(root.get("changeType"), changeType));
|
||||
}
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
Pageable pageable = PageRequest.of(Math.max(0, page - 1), clampSize(size),
|
||||
Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<StaffChangeLog> result = changeLogRepository.findAll(spec, pageable);
|
||||
List<ChangeLogItem> items = result.getContent().stream()
|
||||
.map(this::toChangeLogItem)
|
||||
.toList();
|
||||
return PageResult.of(result, items);
|
||||
}
|
||||
|
||||
private ChangeLogItem toChangeLogItem(StaffChangeLog log) {
|
||||
return ChangeLogItem.builder()
|
||||
.id(log.getId())
|
||||
.changeType(log.getChangeType())
|
||||
.oldValue(log.getOldValue())
|
||||
.newValue(log.getNewValue())
|
||||
.operatorId(log.getOperatorId())
|
||||
.operatorName(log.getOperatorName())
|
||||
.changeReason(log.getChangeReason())
|
||||
.createdAt(log.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<AuditLogItem> auditLogs(String targetType, Long targetId, String operation,
|
||||
LocalDateTime startTime, LocalDateTime endTime,
|
||||
int page, int size) {
|
||||
Specification<StaffAuditLog> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (targetType != null && !targetType.isBlank()) {
|
||||
predicates.add(cb.equal(root.get("targetType"), targetType));
|
||||
}
|
||||
if (targetId != null) {
|
||||
predicates.add(cb.equal(root.get("targetId"), targetId));
|
||||
}
|
||||
if (operation != null && !operation.isBlank()) {
|
||||
predicates.add(cb.equal(root.get("operation"), operation));
|
||||
}
|
||||
if (startTime != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startTime));
|
||||
}
|
||||
if (endTime != null) {
|
||||
predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), endTime));
|
||||
}
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
Pageable pageable = PageRequest.of(Math.max(0, page - 1), clampSize(size),
|
||||
Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<StaffAuditLog> result = auditLogRepository.findAll(spec, pageable);
|
||||
List<AuditLogItem> items = result.getContent().stream()
|
||||
.map(this::toAuditLogItem)
|
||||
.toList();
|
||||
return PageResult.of(result, items);
|
||||
}
|
||||
|
||||
private AuditLogItem toAuditLogItem(StaffAuditLog log) {
|
||||
return AuditLogItem.builder()
|
||||
.id(log.getId())
|
||||
.targetType(log.getTargetType())
|
||||
.targetId(log.getTargetId())
|
||||
.operation(log.getOperation())
|
||||
.operatorId(log.getOperatorId())
|
||||
.operatorName(log.getOperatorName())
|
||||
.fieldName(log.getFieldName())
|
||||
.beforeValue(log.getBeforeValue())
|
||||
.afterValue(log.getAfterValue())
|
||||
.requestIp(log.getRequestIp())
|
||||
.createdAt(log.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 工号查重 / 部门列表
|
||||
// ------------------------------------------------------------------
|
||||
@Transactional(readOnly = true)
|
||||
public CheckEmployeeNoResult checkEmployeeNo(String employeeNo, Long excludeId) {
|
||||
boolean exists = excludeId == null
|
||||
? employeeRepository.existsByEmployeeNo(employeeNo)
|
||||
: employeeRepository.existsByEmployeeNoAndIdNot(employeeNo, excludeId);
|
||||
return CheckEmployeeNoResult.builder().exists(exists).build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DepartmentItem> departments() {
|
||||
return departmentRepository.findAllByOrderByIdAsc().stream()
|
||||
.map(d -> DepartmentItem.builder()
|
||||
.id(d.getId())
|
||||
.name(d.getName())
|
||||
.parentId(d.getParentId())
|
||||
.level(d.getLevel())
|
||||
.employeeCount((int) employeeRepository.countByDepartmentId(d.getId()))
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 私有工具方法
|
||||
// ------------------------------------------------------------------
|
||||
private StaffEmployee getEmployee(Long id) {
|
||||
return employeeRepository.findById(id)
|
||||
.orElseThrow(() -> BusinessException.notFound("员工不存在"));
|
||||
}
|
||||
|
||||
private void validateIdNumber(String idNumber) {
|
||||
if (!ID_NUMBER_PATTERN.matcher(idNumber).matches()) {
|
||||
throw BusinessException.badRequest("身份证号格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private void changeLog(Long employeeId, String type, String oldValue, String newValue,
|
||||
AuthUser user, String reason) {
|
||||
StaffChangeLog log = new StaffChangeLog();
|
||||
log.setEmployeeId(employeeId);
|
||||
log.setChangeType(type);
|
||||
log.setOldValue(oldValue);
|
||||
log.setNewValue(newValue);
|
||||
log.setOperatorId(user.id());
|
||||
log.setOperatorName(user.username());
|
||||
log.setChangeReason(reason);
|
||||
changeLogRepository.save(log);
|
||||
}
|
||||
|
||||
private void audit(String operation, Long targetId, AuthUser user,
|
||||
String fieldName, String beforeValue, String afterValue) {
|
||||
StaffAuditLog log = new StaffAuditLog();
|
||||
log.setTargetType("EMPLOYEE");
|
||||
log.setTargetId(targetId);
|
||||
log.setOperation(operation);
|
||||
log.setOperatorId(user.id());
|
||||
log.setOperatorName(user.username());
|
||||
log.setFieldName(fieldName);
|
||||
log.setBeforeValue(beforeValue);
|
||||
log.setAfterValue(afterValue);
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
log.setRequestIp(attrs != null ? attrs.getRequest().getRemoteAddr() : null);
|
||||
log.setUserAgent(attrs != null ? attrs.getRequest().getHeader("User-Agent") : null);
|
||||
auditLogRepository.save(log);
|
||||
}
|
||||
|
||||
private Sort parseSort(String sort) {
|
||||
if (sort == null || sort.isBlank()) {
|
||||
return Sort.by(Sort.Direction.DESC, "createdAt");
|
||||
}
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (String part : sort.split(";")) {
|
||||
if (part.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String[] kv = part.trim().split(",");
|
||||
String field = kv[0].trim();
|
||||
Sort.Direction direction = kv.length > 1 && "asc".equalsIgnoreCase(kv[1].trim())
|
||||
? Sort.Direction.ASC
|
||||
: Sort.Direction.DESC;
|
||||
orders.add(new Sort.Order(direction, field));
|
||||
}
|
||||
return orders.isEmpty() ? Sort.by(Sort.Direction.DESC, "createdAt") : Sort.by(orders);
|
||||
}
|
||||
|
||||
private int clampSize(int size) {
|
||||
return Math.max(1, Math.min(size, 100));
|
||||
}
|
||||
}
|
||||
CRUD 业务逻辑 + 权限控制 + 数据脱敏 + 逻辑删除
|
||||
@ -1,199 +1 @@
|
||||
package com.hr.staff;
|
||||
|
||||
import com.hr.staff.repository.StaffEmployeeRepository;
|
||||
import com.hr.staff.security.AuthUser;
|
||||
import com.hr.staff.security.JwtService;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("dev")
|
||||
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||
@Transactional
|
||||
class StaffApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JwtService jwtService;
|
||||
|
||||
@Autowired
|
||||
private StaffEmployeeRepository employeeRepository;
|
||||
|
||||
private String adminToken() {
|
||||
return "Bearer " + jwtService.generateToken(new AuthUser(1L, "admin", Set.of("ADMIN"), 1003L));
|
||||
}
|
||||
|
||||
private String hrToken() {
|
||||
return "Bearer " + jwtService.generateToken(new AuthUser(2L, "hr", Set.of("HR"), 1003L));
|
||||
}
|
||||
|
||||
private String employeeToken(Long departmentId) {
|
||||
return "Bearer " + jwtService.generateToken(new AuthUser(10L, "employee", Set.of("EMPLOYEE"), departmentId));
|
||||
}
|
||||
|
||||
private String createBody(String employeeNo, String name, String idNumber, Long departmentId) {
|
||||
return """
|
||||
{
|
||||
"employeeNo": "%s",
|
||||
"name": "%s",
|
||||
"idNumber": "%s",
|
||||
"gender": 1,
|
||||
"departmentId": %d,
|
||||
"position": "Java开发工程师",
|
||||
"hireDate": "2024-01-10",
|
||||
"phone": "13800138000",
|
||||
"email": "zhangsan@example.com",
|
||||
"emergencyContact": "张父",
|
||||
"emergencyPhone": "13900139000",
|
||||
"status": "active",
|
||||
"extra": {
|
||||
"education": "本科",
|
||||
"technicalLevel": "P6",
|
||||
"salaryGrade": "S3",
|
||||
"address": "北京市朝阳区"
|
||||
}
|
||||
}
|
||||
""".formatted(employeeNo, name, idNumber, departmentId);
|
||||
}
|
||||
|
||||
private void createEmployee(String employeeNo, String name, String idNumber, Long departmentId) throws Exception {
|
||||
mockMvc.perform(post("/api/v1/staff")
|
||||
.header("Authorization", adminToken())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(createBody(employeeNo, name, idNumber, departmentId)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.code").value(201));
|
||||
}
|
||||
|
||||
private Long employeeId(String employeeNo) {
|
||||
return employeeRepository.findByEmployeeNo(employeeNo).orElseThrow().getId();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test01_createListAndDetail() throws Exception {
|
||||
createEmployee("EMP20240001", "张三", "110101199001011234", 1001L);
|
||||
mockMvc.perform(get("/api/v1/staff").header("Authorization", adminToken()).param("page", "1").param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.totalElements").value(1))
|
||||
.andExpect(jsonPath("$.data.content[0].employeeNo").value("EMP20240001"))
|
||||
.andExpect(jsonPath("$.data.content[0].phone").value("138****8000"));
|
||||
Long id = employeeId("EMP20240001");
|
||||
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.idNumber").value("110************234"))
|
||||
.andExpect(jsonPath("$.data.phone").value("138****8000"))
|
||||
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000"))
|
||||
.andExpect(jsonPath("$.data.extra.technicalLevel").value("P6"))
|
||||
.andExpect(jsonPath("$.data.extra.salaryGrade").value("S3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test02_duplicateEmployeeNoReturns409() throws Exception {
|
||||
createEmployee("EMP20240002", "李四", "110101199001011235", 1001L);
|
||||
mockMvc.perform(post("/api/v1/staff").header("Authorization", adminToken()).contentType(MediaType.APPLICATION_JSON)
|
||||
.content(createBody("EMP20240002", "王五", "110101199001011236", 1002L)))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value(409));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test03_employeeDataPermissionAndMasking() throws Exception {
|
||||
createEmployee("EMP20240003", "赵六", "110101199001011237", 1001L);
|
||||
Long id = employeeId("EMP20240003");
|
||||
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1001L)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.idNumber").value("110************237"))
|
||||
.andExpect(jsonPath("$.data.phone").value("138****8000"))
|
||||
.andExpect(jsonPath("$.data.emergencyPhone").value("139****9000"))
|
||||
.andExpect(jsonPath("$.data.extra.technicalLevel").doesNotExist());
|
||||
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1002L)))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.code").value(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test04_updateGeneratesChangeLog() throws Exception {
|
||||
createEmployee("EMP20240004", "孙七", "110101199001011238", 1001L);
|
||||
Long id = employeeId("EMP20240004");
|
||||
mockMvc.perform(put("/api/v1/staff/" + id).header("Authorization", adminToken()).contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"position": "高级Java开发工程师", "extra": {"technicalLevel": "P7"}}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.position").value("高级Java开发工程师"));
|
||||
mockMvc.perform(get("/api/v1/staff/" + id + "/change-logs").header("Authorization", adminToken()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.totalElements").value(2))
|
||||
.andExpect(jsonPath("$.data.content[0].changeType").value("LEVEL_CHANGE"))
|
||||
.andExpect(jsonPath("$.data.content[1].changeType").value("POSITION_CHANGE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test05_checkEmployeeNo() throws Exception {
|
||||
createEmployee("EMP20240005", "周八", "110101199001011239", 1001L);
|
||||
mockMvc.perform(get("/api/v1/staff/check/employee-no").header("Authorization", adminToken()).param("employeeNo", "EMP20240005"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.exists").value(true));
|
||||
mockMvc.perform(get("/api/v1/staff/check/employee-no").header("Authorization", adminToken()).param("employeeNo", "NOT_EXIST"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.exists").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test06_logicalDeleteAndRepeatProtection() throws Exception {
|
||||
createEmployee("EMP20240006", "吴九", "110101199001011240", 1001L);
|
||||
Long id = employeeId("EMP20240006");
|
||||
mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("inactive"));
|
||||
mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test07_batchDelete() throws Exception {
|
||||
createEmployee("EMP20240007", "郑十", "110101199001011241", 1001L);
|
||||
createEmployee("EMP20240008", "钱一", "110101199001011242", 1001L);
|
||||
Long id1 = employeeId("EMP20240007");
|
||||
Long id2 = employeeId("EMP20240008");
|
||||
mockMvc.perform(post("/api/v1/staff/batch-delete").header("Authorization", adminToken()).contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"ids\": [" + id1 + ", " + id2 + "]}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.successCount").value(2))
|
||||
.andExpect(jsonPath("$.data.failedList.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test08_auditLogsAndDepartments() throws Exception {
|
||||
createEmployee("EMP20240009", "冯二", "110101199001011243", 1001L);
|
||||
mockMvc.perform(get("/api/v1/staff/audit-logs").header("Authorization", adminToken()).param("operation", "CREATE"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.totalElements").value(1))
|
||||
.andExpect(jsonPath("$.data.content[0].operation").value("CREATE"));
|
||||
mockMvc.perform(get("/api/v1/staff/departments").header("Authorization", adminToken()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.length()").value(3));
|
||||
}
|
||||
}
|
||||
8 个端到端集成测试用例
|
||||
Loading…
x
Reference in New Issue
Block a user