package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.LegalRiskMeasure; import com.kaidi.oa.repository.LegalRiskMeasureRepository; 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.LinkedHashMap; import java.util.List; import java.util.Map; /** * 风险应对措施执行跟踪(法务合规·风险控制与评估)。 * 针对中高风险制定的具体应对措施清单,分配责任人/部门/完成时限,跟踪执行状态。 * 提供逾期预警接口(/overdue),列出 dueDate < today 且 status 非已完成/已取消 的超期措施。 * 状态流转:待执行→执行中→已完成(或随时取消)。 * * 读口含责任人/措施描述等内部治理信息,需认证。 */ @RestController @RequestMapping("/api/oa/legal-risk-measures") public class LegalRiskMeasureController { private static final List CLOSED_STATUSES = List.of("已完成", "已取消"); private final LegalRiskMeasureRepository repo; public LegalRiskMeasureController(LegalRiskMeasureRepository repo) { this.repo = repo; } @GetMapping public ApiResp> list( @RequestParam(required = false) String status, @RequestParam(required = false) Long riskAssessId, @RequestParam(required = false) String owner, @RequestParam(required = false) String ownerDept) { if (riskAssessId != null) { return ApiResp.ok(repo.findByRiskAssessId(riskAssessId)); } if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (owner != null && !owner.isBlank()) { return ApiResp.ok(repo.findByOwner(owner)); } if (ownerDept != null && !ownerDept.isBlank()) { return ApiResp.ok(repo.findByOwnerDept(ownerDept)); } 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 MeasureRequest( Long riskAssessId, String measureNo, String title, String description, String owner, String ownerDept, String dueDate, String status, String completedDate, String result) { } @PostMapping @Transactional public ApiResp create(@RequestBody MeasureRequest req) { if (req.title() == null || req.title().isBlank()) { throw new ApiException(400, "措施标题(title) 不能为空"); } if (req.dueDate() == null || req.dueDate().isBlank()) { throw new ApiException(400, "完成时限(dueDate) 不能为空"); } LegalRiskMeasure m = new LegalRiskMeasure(); m.setRiskAssessId(req.riskAssessId()); m.setMeasureNo(req.measureNo() == null || req.measureNo().isBlank() ? "FXCS-" + (repo.count() + 1) : req.measureNo()); m.setTitle(req.title()); m.setDescription(req.description()); m.setOwner(req.owner()); m.setOwnerDept(req.ownerDept()); m.setDueDate(req.dueDate()); m.setStatus(req.status() == null || req.status().isBlank() ? "待执行" : req.status()); m.setCompletedDate(req.completedDate()); m.setResult(req.result()); m.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(m)); } @PatchMapping("/{id}") @Transactional public ApiResp update(@PathVariable Long id, @RequestBody MeasureRequest req) { LegalRiskMeasure m = repo.findById(id) .orElseThrow(() -> new NotFoundException("风险应对措施不存在: " + id)); if (req.title() != null && !req.title().isBlank()) m.setTitle(req.title()); if (req.description() != null) m.setDescription(req.description()); if (req.owner() != null) m.setOwner(req.owner()); if (req.ownerDept() != null) m.setOwnerDept(req.ownerDept()); if (req.dueDate() != null && !req.dueDate().isBlank()) m.setDueDate(req.dueDate()); if (req.status() != null && !req.status().isBlank()) m.setStatus(req.status()); if (req.completedDate() != null) m.setCompletedDate(req.completedDate()); if (req.result() != null) m.setResult(req.result()); return ApiResp.ok(repo.save(m)); } @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}/start") @Transactional public ApiResp start(@PathVariable Long id) { LegalRiskMeasure m = repo.findById(id) .orElseThrow(() -> new NotFoundException("风险应对措施不存在: " + id)); if (!"待执行".equals(m.getStatus())) { throw new ApiException(409, "只有「待执行」状态的措施可以启动"); } m.setStatus("执行中"); return ApiResp.ok(repo.save(m)); } public record CompleteRequest(String result) { } @PostMapping("/{id}/complete") @Transactional public ApiResp complete(@PathVariable Long id, @RequestBody CompleteRequest req) { LegalRiskMeasure m = repo.findById(id) .orElseThrow(() -> new NotFoundException("风险应对措施不存在: " + id)); if ("已完成".equals(m.getStatus()) || "已取消".equals(m.getStatus())) { throw new ApiException(409, "该措施已处于终态,无法再次标记完成"); } m.setStatus("已完成"); m.setCompletedDate(LocalDate.now().toString()); if (req.result() != null && !req.result().isBlank()) { m.setResult(req.result()); } return ApiResp.ok(repo.save(m)); } @PostMapping("/{id}/cancel") @Transactional public ApiResp cancel(@PathVariable Long id) { LegalRiskMeasure m = repo.findById(id) .orElseThrow(() -> new NotFoundException("风险应对措施不存在: " + id)); if ("已完成".equals(m.getStatus())) { throw new ApiException(409, "已完成的措施不可取消"); } m.setStatus("已取消"); return ApiResp.ok(repo.save(m)); } // ---------- 逾期预警 ---------- /** * 返回 dueDate < 今日 且 status 不是「已完成」/「已取消」 的超期措施。 * 供法务每日检查应对措施跟踪进度。 */ @GetMapping("/overdue") public ApiResp> overdue() { String today = LocalDate.now().toString(); List all = repo.findAll(); return ApiResp.ok(all.stream() .filter(m -> m.getDueDate() != null && m.getDueDate().compareTo(today) < 0 && !CLOSED_STATUSES.contains(m.getStatus())) .toList()); } // ---------- 统计 ---------- public record MeasureStats(int total, int pending, int inProgress, int completed, int cancelled, int overdueCount, Map byDept) { } @GetMapping("/stats") public ApiResp stats() { String today = LocalDate.now().toString(); List all = repo.findAll(); int pending = 0, inProgress = 0, completed = 0, cancelled = 0, overdueCount = 0; Map byDept = new LinkedHashMap<>(); for (LegalRiskMeasure m : all) { String s = m.getStatus() == null ? "待执行" : m.getStatus(); if ("待执行".equals(s)) pending++; else if ("执行中".equals(s)) inProgress++; else if ("已完成".equals(s)) completed++; else if ("已取消".equals(s)) cancelled++; if (m.getDueDate() != null && m.getDueDate().compareTo(today) < 0 && !CLOSED_STATUSES.contains(s)) { overdueCount++; } byDept.merge(m.getOwnerDept() == null ? "未分配" : m.getOwnerDept(), 1, Integer::sum); } return ApiResp.ok(new MeasureStats(all.size(), pending, inProgress, completed, cancelled, overdueCount, byDept)); } }