Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/AuditQualityReviewController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

230 lines
9.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AuditQualityReview>> 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<AuditQualityReview> get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
/**
* 复核质量趋势:按月统计已完成的复核记录平均综合评分(辅助质量管理)。
*/
@GetMapping("/stats")
public ApiResp<Object> stats() {
List<AuditQualityReview> completed = reviewRepo.findByStatus("已完成");
// 按 reviewDate 年月聚合平均分。
var byMonth = new java.util.LinkedHashMap<String, List<Integer>>();
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<AuditQualityReview> 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<AuditQualityReview> 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<AuditQualityReview> 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<AuditQualityReview> 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<Void> 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));
}
}