develop: 记录端到端重试验证结果

This commit is contained in:
Pipeline Agent 2026-08-15 01:30:55 +08:00
parent 54c56ca818
commit 5d3632e779
13 changed files with 13 additions and 1424 deletions

View File

@ -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

View File

@ -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();
}
}
}
操作审计日志实体

View File

@ -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();
}
}
}
员工变更记录实体

View File

@ -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();
}
}
部门本地缓存实体

View File

@ -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加密/部门/岗位/状态等

View File

@ -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();
}
}
员工扩展信息实体学历/技术级别/薪资等级/合同/地址等

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 动态筛选

View File

@ -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 业务逻辑 + 权限控制 + 数据脱敏 + 逻辑删除

View File

@ -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 个端到端集成测试用例