package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.HrNonCompete; import com.kaidi.oa.repository.HrNonCompeteRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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.Instant; import java.time.LocalDate; import java.util.List; /** * 员工竞业限制管理(法务合规·与其他部门接口)。 * 专项管理员工竞业限制条款:签署登记、离职触发(状态推进到"离职-补偿中")、到期确认、违约记录。 * 含竞业补偿金(BigDecimal),不使用 double 裸算。 * 含个人薪资信息,含 SENSITIVE_READ_PREFIXES 保护。 */ @RestController @RequestMapping("/api/oa/hr-non-compete") public class HrNonCompeteController { private final HrNonCompeteRepository repo; public HrNonCompeteController(HrNonCompeteRepository repo) { this.repo = repo; } @GetMapping public ApiResp> list( @RequestParam(required = false) String status, @RequestParam(required = false) String dept) { if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (dept != null && !dept.isBlank()) { return ApiResp.ok(repo.findByDept(dept)); } return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(repo.findById(id) .orElseThrow(() -> new NotFoundException("竞业限制记录不存在: " + id))); } public record NonCompeteRequest( String employeeId, String employeeName, String dept, String position, Long laborContractId, Integer restrictionPeriod, String startDate, String endDate, Double compensationMonthly, String restrictedIndustries, String status, String violationNote, String signedDate) { } @PostMapping @Transactional public ApiResp create(@RequestBody NonCompeteRequest req) { if (req.employeeName() == null || req.employeeName().isBlank()) { throw new ApiException(400, "员工姓名(employeeName) 不能为空"); } HrNonCompete nc = new HrNonCompete(); nc.setEmployeeId(req.employeeId()); nc.setEmployeeName(req.employeeName()); nc.setDept(req.dept()); nc.setPosition(req.position()); nc.setLaborContractId(req.laborContractId()); nc.setRestrictionPeriod(req.restrictionPeriod()); nc.setStartDate(req.startDate()); nc.setEndDate(req.endDate()); nc.setCompensationMonthly(Money.of(req.compensationMonthly())); nc.setRestrictedIndustries(req.restrictedIndustries()); nc.setStatus(req.status() == null || req.status().isBlank() ? "在职-未触发" : req.status()); nc.setViolationNote(req.violationNote()); nc.setSignedDate(req.signedDate() == null || req.signedDate().isBlank() ? LocalDate.now().toString() : req.signedDate()); nc.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(nc)); } @PatchMapping("/{id}") @Transactional public ApiResp update(@PathVariable Long id, @RequestBody NonCompeteRequest req) { HrNonCompete nc = repo.findById(id) .orElseThrow(() -> new NotFoundException("竞业限制记录不存在: " + id)); if (req.employeeName() != null && !req.employeeName().isBlank()) nc.setEmployeeName(req.employeeName()); if (req.dept() != null) nc.setDept(req.dept()); if (req.position() != null) nc.setPosition(req.position()); if (req.restrictionPeriod() != null) nc.setRestrictionPeriod(req.restrictionPeriod()); if (req.startDate() != null) nc.setStartDate(req.startDate()); if (req.endDate() != null) nc.setEndDate(req.endDate()); if (req.compensationMonthly() != null) nc.setCompensationMonthly(Money.of(req.compensationMonthly())); if (req.restrictedIndustries() != null) nc.setRestrictedIndustries(req.restrictedIndustries()); if (req.status() != null && !req.status().isBlank()) nc.setStatus(req.status()); if (req.violationNote() != null) nc.setViolationNote(req.violationNote()); return ApiResp.ok(repo.save(nc)); } @DeleteMapping("/{id}") @Transactional public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) { throw new NotFoundException("竞业限制记录不存在: " + id); } repo.deleteById(id); return ApiResp.ok(null); } // ---------- 离职触发(状态推进) ---------- /** * 员工离职触发竞业限制:状态从「在职-未触发」推进到「离职-补偿中」, * 并设置限制起始日期。 */ @PostMapping("/{id}/trigger") @Transactional public ApiResp trigger(@PathVariable Long id) { HrNonCompete nc = repo.findById(id) .orElseThrow(() -> new NotFoundException("竞业限制记录不存在: " + id)); if (!"在职-未触发".equals(nc.getStatus())) { throw new ApiException(409, "只有「在职-未触发」状态可触发竞业限制"); } nc.setStatus("离职-补偿中"); String today = LocalDate.now().toString(); if (nc.getStartDate() == null || nc.getStartDate().isBlank()) { nc.setStartDate(today); // 根据限制月数推算到期日 if (nc.getRestrictionPeriod() != null && nc.getRestrictionPeriod() > 0) { LocalDate end = LocalDate.now().plusMonths(nc.getRestrictionPeriod()); nc.setEndDate(end.toString()); } } return ApiResp.ok(repo.save(nc)); } // ---------- 违约记录 ---------- public record ViolationReq(String violationNote) { } @PostMapping("/{id}/violate") @Transactional public ApiResp violate(@PathVariable Long id, @RequestBody ViolationReq req) { HrNonCompete nc = repo.findById(id) .orElseThrow(() -> new NotFoundException("竞业限制记录不存在: " + id)); if (req.violationNote() == null || req.violationNote().isBlank()) { throw new ApiException(400, "违约情况说明(violationNote) 不能为空"); } nc.setViolationNote(req.violationNote()); nc.setStatus("已违约"); return ApiResp.ok(repo.save(nc)); } // ---------- 到期确认 ---------- @PostMapping("/{id}/expire") @Transactional public ApiResp expire(@PathVariable Long id) { HrNonCompete nc = repo.findById(id) .orElseThrow(() -> new NotFoundException("竞业限制记录不存在: " + id)); nc.setStatus("已到期"); return ApiResp.ok(repo.save(nc)); } // ---------- 近期到期预警(30 天内) ---------- @GetMapping("/expiring-soon") public ApiResp> expiringSoon() { String today = LocalDate.now().toString(); String in30 = LocalDate.now().plusDays(30).toString(); return ApiResp.ok(repo.findByStatus("离职-补偿中").stream() .filter(nc -> nc.getEndDate() != null && nc.getEndDate().compareTo(today) >= 0 && nc.getEndDate().compareTo(in30) <= 0) .toList()); } }