恢复点(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>
230 lines
9.8 KiB
Java
230 lines
9.8 KiB
Java
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));
|
||
}
|
||
}
|