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.AuditProject; import com.kaidi.oa.domain.AuditQualityReview; import com.kaidi.oa.repository.AuditProjectRepository; import com.kaidi.oa.repository.AuditQualityReviewRepository; 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.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.Instant; import java.time.LocalDate; import java.time.Year; import java.util.List; /** * 审计质量复核(内控部·审计监察部·第10模块)。 * * 解决审计缺口:「独立质量复核流程(项目完成后指定独立复核人,质量检查程序执行记录、 * 证据充分性、底稿规范性的结论实体)」。 * * 核心功能: * - POST / 新建质量复核(草稿); * - PATCH /{id}/start-review 启动复核(草稿→复核中); * - PATCH /{id}/complete 填写三维评分+综合结论,完成复核(复核中→已完成); * 综合评分自动加权:程序40%+证据35%+底稿25%; * 结论自动初稿:90+优良/75+合格/60+需改进/否则不合格; * - GET /?projectId= 按审计项目查询复核记录; * - GET /stats 复核质量趋势统计(按月统计平均综合评分); * - PUT /{id} 修改(复核中阶段); * - DELETE /{id} 删除(仅草稿阶段)。 */ @RestController @RequestMapping("/api/oa/audit-quality-reviews") public class AuditQualityReviewController { private final AuditQualityReviewRepository reviewRepo; private final AuditProjectRepository projectRepo; public AuditQualityReviewController(AuditQualityReviewRepository reviewRepo, AuditProjectRepository projectRepo) { this.reviewRepo = reviewRepo; this.projectRepo = projectRepo; } @GetMapping public ApiResp> list( @RequestParam(required = false) Long projectId, @RequestParam(required = false) String status, @RequestParam(required = false) String reviewer) { if (projectId != null) { return ApiResp.ok(reviewRepo.findByProjectId(projectId)); } if (status != null && !status.isBlank()) { return ApiResp.ok(reviewRepo.findByStatus(status)); } if (reviewer != null && !reviewer.isBlank()) { return ApiResp.ok(reviewRepo.findByReviewer(reviewer)); } return ApiResp.ok(reviewRepo.findByOrderByIdDesc()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(load(id)); } /** * 复核质量趋势:按月统计已完成的复核记录平均综合评分(辅助质量管理)。 */ @GetMapping("/stats") public ApiResp stats() { List completed = reviewRepo.findByStatus("已完成"); // 按 reviewDate 年月聚合平均分。 var byMonth = new java.util.LinkedHashMap>(); for (AuditQualityReview r : completed) { String month = r.getReviewDate() == null ? "未知" : r.getReviewDate().substring(0, Math.min(7, r.getReviewDate().length())); byMonth.computeIfAbsent(month, k -> new java.util.ArrayList<>()).add(r.getOverallScore()); } var trend = byMonth.entrySet().stream().map(e -> { double avg = e.getValue().stream().mapToInt(Integer::intValue).average().orElse(0); return java.util.Map.of("month", e.getKey(), "count", e.getValue().size(), "avgScore", Math.round(avg * 10) / 10.0); }).toList(); return ApiResp.ok(java.util.Map.of("trend", trend, "totalCompleted", completed.size())); } public record CreateRequest( Long projectId, String reviewer, String reviewDate, String notes) { } @PostMapping public ApiResp create(@RequestBody CreateRequest req) { if (req.projectId() == null) { throw new ApiException(400, "请选择关联审计项目"); } if (req.reviewer() == null || req.reviewer().isBlank()) { throw new ApiException(400, "复核人不能为空"); } AuditProject proj = projectRepo.findById(req.projectId()) .orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId())); // 独立性校验:复核人不能与项目负责人相同。 if (req.reviewer().equals(proj.getLeadAuditor())) { throw new ApiException(409, "复核人不能与审计项目负责人相同(违反独立性原则)"); } AuditQualityReview r = new AuditQualityReview(); r.setReviewNo(nextReviewNo()); r.setProjectId(proj.getId()); r.setProjectName(proj.getName()); r.setReviewer(req.reviewer()); r.setReviewDate(req.reviewDate() != null ? req.reviewDate() : LocalDate.now().toString()); r.setStatus("草稿"); r.setCreatedAt(Instant.now()); return ApiResp.ok(reviewRepo.save(r)); } /** 启动复核(草稿 → 复核中)。 */ @PatchMapping("/{id}/start-review") public ApiResp startReview(@PathVariable Long id) { AuditQualityReview r = load(id); if (!"草稿".equals(r.getStatus())) { throw new ApiException(409, "只有草稿状态可启动复核(当前:" + r.getStatus() + ")"); } r.setStatus("复核中"); return ApiResp.ok(reviewRepo.save(r)); } public record CompleteRequest( int procedureScore, int evidenceScore, int workpaperScore, String procedureComment, String evidenceComment, String workpaperComment, String overallComment, String overallResult, boolean improvementRequired, String improvementItems) { } /** * 填写三维评分并完成复核(复核中 → 已完成)。 * 综合评分加权:程序执行×40% + 证据充分性×35% + 底稿规范性×25%。 * 结论自动初稿(可被 overallResult 覆盖):90+优良/75+合格/60+需改进/否则不合格。 */ @PatchMapping("/{id}/complete") public ApiResp complete(@PathVariable Long id, @RequestBody CompleteRequest req) { AuditQualityReview r = load(id); if (!"复核中".equals(r.getStatus())) { throw new ApiException(409, "只有复核中状态可提交结论(当前:" + r.getStatus() + ")"); } int ps = clamp(req.procedureScore()); int es = clamp(req.evidenceScore()); int ws = clamp(req.workpaperScore()); r.setProcedureScore(ps); r.setEvidenceScore(es); r.setWorkpaperScore(ws); // 加权综合评分。 int overall = (int) Math.round(ps * 0.4 + es * 0.35 + ws * 0.25); r.setOverallScore(overall); // 自动结论初稿。 String autoResult = overall >= 90 ? "优良" : overall >= 75 ? "合格" : overall >= 60 ? "需改进" : "不合格"; r.setOverallResult(req.overallResult() != null && !req.overallResult().isBlank() ? req.overallResult() : autoResult); r.setProcedureComment(req.procedureComment()); r.setEvidenceComment(req.evidenceComment()); r.setWorkpaperComment(req.workpaperComment()); r.setOverallComment(req.overallComment()); r.setImprovementRequired(req.improvementRequired()); r.setImprovementItems(req.improvementItems()); r.setStatus("已完成"); return ApiResp.ok(reviewRepo.save(r)); } public record UpdateRequest(String reviewer, String reviewDate, String notes) { } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody UpdateRequest req) { AuditQualityReview r = load(id); if ("已完成".equals(r.getStatus())) { throw new ApiException(409, "已完成的复核不允许修改"); } if (req.reviewer() != null && !req.reviewer().isBlank()) r.setReviewer(req.reviewer()); if (req.reviewDate() != null) r.setReviewDate(req.reviewDate()); return ApiResp.ok(reviewRepo.save(r)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { AuditQualityReview r = load(id); if (!"草稿".equals(r.getStatus())) { throw new ApiException(409, "只有草稿阶段的复核可以删除(当前:" + r.getStatus() + ")"); } reviewRepo.deleteById(id); return ApiResp.ok(null); } private AuditQualityReview load(Long id) { return reviewRepo.findById(id) .orElseThrow(() -> new NotFoundException("审计质量复核不存在: " + id)); } private String nextReviewNo() { String year = String.valueOf(Year.now().getValue()); long seq = reviewRepo.count() + 1; return String.format("QR-%s-%03d", year, seq); } private static int clamp(int v) { return Math.min(100, Math.max(0, v)); } }