Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/AuditRiskAuditController.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

329 lines
15 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.AuditFinding;
import com.kaidi.oa.domain.AuditProject;
import com.kaidi.oa.domain.AuditRiskAssessment;
import com.kaidi.oa.domain.RiskAssessment;
import com.kaidi.oa.repository.AuditFindingRepository;
import com.kaidi.oa.repository.AuditProjectRepository;
import com.kaidi.oa.repository.AuditRiskAssessmentRepository;
import com.kaidi.oa.repository.RiskAssessmentRepository;
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.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;
/**
* 风险管理有效性审计(内控部·审计监察部·模块4)。
*
* 补全 PARTIAL 缺口:
* 1) 风险管理有效性审计:审计人员评估风险应对措施有效性的专用端点;
* 2) 发现风险管理缺陷时自动生成 AuditFinding(无效 → 联动创建问题,双向回填 findingId);
* 3) 从风险库批量导入待评估风险(PATCH /import-from-risk-library);
* 4) 有效性统计摘要(GET /summary)。
*
* 端点:
* - POST / 新建评估记录(待评估);
* - PATCH /import-from-risk-library 从 RiskAssessment 风险库导入高/重大风险批量创建待评估记录;
* - PUT /{id} 编辑评估内容(待评估/评估中状态);
* - PATCH /{id}/assess 提交评估结论(有效/部分有效/无效),无效时自动派生 AuditFinding
* - DELETE /{id} 删除(待评估状态);
* - GET / 列表(按项目 id/状态/有效性过滤);
* - GET /summary 有效性统计(有效/部分有效/无效各占比)。
*/
@RestController
@RequestMapping("/api/oa/audit-risk-assessments")
public class AuditRiskAuditController {
private final AuditRiskAssessmentRepository assessRepo;
private final AuditProjectRepository projectRepo;
private final RiskAssessmentRepository riskRepo;
private final AuditFindingRepository findingRepo;
public AuditRiskAuditController(AuditRiskAssessmentRepository assessRepo,
AuditProjectRepository projectRepo,
RiskAssessmentRepository riskRepo,
AuditFindingRepository findingRepo) {
this.assessRepo = assessRepo;
this.projectRepo = projectRepo;
this.riskRepo = riskRepo;
this.findingRepo = findingRepo;
}
@GetMapping
public ApiResp<List<AuditRiskAssessment>> list(
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String effectiveness) {
if (projectId != null) {
return ApiResp.ok(assessRepo.findByProjectId(projectId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(assessRepo.findByStatus(status));
}
if (effectiveness != null && !effectiveness.isBlank()) {
return ApiResp.ok(assessRepo.findByEffectiveness(effectiveness));
}
return ApiResp.ok(assessRepo.findByOrderByIdDesc());
}
@GetMapping("/{id}")
public ApiResp<AuditRiskAssessment> get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
/** 有效性统计(有效/部分有效/无效各占比,按项目统计)。 */
@GetMapping("/summary")
public ApiResp<Object> summary(@RequestParam(required = false) Long projectId) {
List<AuditRiskAssessment> all = projectId != null
? assessRepo.findByProjectId(projectId)
: assessRepo.findAll();
long effective = all.stream().filter(a -> "有效".equals(a.getEffectiveness())).count();
long partial = all.stream().filter(a -> "部分有效".equals(a.getEffectiveness())).count();
long ineffective = all.stream().filter(a -> "无效".equals(a.getEffectiveness())).count();
long assessed = all.stream().filter(a -> a.getEffectiveness() != null && !a.getEffectiveness().isBlank()).count();
long pending = all.stream().filter(a -> a.getEffectiveness() == null || a.getEffectiveness().isBlank()).count();
java.util.Map<String, Object> result = new java.util.LinkedHashMap<>();
result.put("total", all.size());
result.put("assessed", assessed);
result.put("pending", pending);
result.put("effective", effective);
result.put("partial", partial);
result.put("ineffective", ineffective);
result.put("defectRate", assessed <= 0 ? 0.0
: Math.round((ineffective + partial) * 10000d / assessed) / 100d);
return ApiResp.ok(result);
}
public record CreateRequest(
Long projectId, Long riskId, String riskName, String riskCategory,
String riskLevel, String responsibleDept, String originalMeasure,
String assessor) {
}
/** 新建风险管理有效性评估记录(待评估)。 */
@PostMapping
@Transactional
public ApiResp<AuditRiskAssessment> create(@RequestBody CreateRequest req) {
if (req.projectId() == null) {
throw new ApiException(400, "请选择关联审计项目");
}
if ((req.riskName() == null || req.riskName().isBlank()) && req.riskId() == null) {
throw new ApiException(400, "请填写风险名称或选择风险库中的风险");
}
AuditProject project = projectRepo.findById(req.projectId())
.orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId()));
AuditRiskAssessment a = new AuditRiskAssessment();
a.setAssessNo(nextAssessNo());
a.setProjectId(project.getId());
a.setProjectName(project.getName());
a.setRiskId(req.riskId());
// 如果关联风险库,从风险库补充信息。
if (req.riskId() != null) {
riskRepo.findById(req.riskId()).ifPresent(r -> {
a.setRiskName(r.getRiskName());
a.setRiskCategory(r.getCategory());
a.setRiskLevel(r.getLevel());
a.setResponsibleDept(r.getResponsibleDept());
a.setOriginalMeasure(r.getMeasure());
});
}
// 手动覆盖优先。
if (req.riskName() != null && !req.riskName().isBlank()) a.setRiskName(req.riskName());
if (req.riskCategory() != null) a.setRiskCategory(req.riskCategory());
if (req.riskLevel() != null) a.setRiskLevel(req.riskLevel());
if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept());
if (req.originalMeasure() != null) a.setOriginalMeasure(req.originalMeasure());
a.setAssessor(req.assessor());
a.setStatus("待评估");
a.setCreatedAt(Instant.now());
return ApiResp.ok(assessRepo.save(a));
}
public record ImportRequest(Long projectId, String assessor, String minRiskLevel) {
}
/**
* 从风险库批量导入高/重大风险,创建待评估记录(去重:已有 riskId 相同的不重复创建)。
*/
@PostMapping("/import-from-risk-library")
@Transactional
public ApiResp<Object> importFromRiskLibrary(@RequestBody ImportRequest req) {
if (req.projectId() == null) {
throw new ApiException(400, "请指定审计项目");
}
AuditProject project = projectRepo.findById(req.projectId())
.orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId()));
String minLevel = req.minRiskLevel() == null || req.minRiskLevel().isBlank()
? "高" : req.minRiskLevel();
List<RiskAssessment> risks = riskRepo.findAll();
// 过滤满足最低风险等级。
List<String> targetLevels = levelAtLeast(minLevel);
List<AuditRiskAssessment> existing = assessRepo.findByProjectId(req.projectId());
java.util.Set<Long> existingRiskIds = new java.util.HashSet<>();
for (AuditRiskAssessment e : existing) {
if (e.getRiskId() != null) existingRiskIds.add(e.getRiskId());
}
int created = 0;
int skipped = 0;
for (RiskAssessment r : risks) {
if (!targetLevels.contains(r.getLevel())) { skipped++; continue; }
if (existingRiskIds.contains(r.getId())) { skipped++; continue; }
AuditRiskAssessment a = new AuditRiskAssessment();
a.setAssessNo(nextAssessNo());
a.setProjectId(project.getId());
a.setProjectName(project.getName());
a.setRiskId(r.getId());
a.setRiskName(r.getRiskName());
a.setRiskCategory(r.getCategory());
a.setRiskLevel(r.getLevel());
a.setResponsibleDept(r.getResponsibleDept());
a.setOriginalMeasure(r.getMeasure());
a.setAssessor(req.assessor());
a.setStatus("待评估");
a.setCreatedAt(Instant.now());
assessRepo.save(a);
existingRiskIds.add(r.getId());
created++;
}
return ApiResp.ok(java.util.Map.of("created", created, "skipped", skipped));
}
public record UpdateRequest(
String riskName, String riskCategory, String riskLevel,
String responsibleDept, String originalMeasure,
String assessmentDetail, String recommendation, String assessor) {
}
@PutMapping("/{id}")
public ApiResp<AuditRiskAssessment> update(@PathVariable Long id, @RequestBody UpdateRequest req) {
AuditRiskAssessment a = load(id);
if ("有效".equals(a.getStatus()) || "部分有效".equals(a.getStatus()) || "无效".equals(a.getStatus())) {
throw new ApiException(409, "已评估完成的记录不可修改,请联系管理员");
}
if (req.riskName() != null && !req.riskName().isBlank()) a.setRiskName(req.riskName());
if (req.riskCategory() != null) a.setRiskCategory(req.riskCategory());
if (req.riskLevel() != null) a.setRiskLevel(req.riskLevel());
if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept());
if (req.originalMeasure() != null) a.setOriginalMeasure(req.originalMeasure());
if (req.assessmentDetail() != null) a.setAssessmentDetail(req.assessmentDetail());
if (req.recommendation() != null) a.setRecommendation(req.recommendation());
if (req.assessor() != null) a.setAssessor(req.assessor());
a.setStatus("评估中");
return ApiResp.ok(assessRepo.save(a));
}
public record AssessRequest(
String effectiveness, String assessmentDetail,
String recommendation, String assessor,
Long projectId) {
}
/**
* 提交评估结论。effectiveness=无效时,自动在关联审计项目下派生 AuditFinding。
*/
@PatchMapping("/{id}/assess")
@Transactional
public ApiResp<AuditRiskAssessment> assess(@PathVariable Long id, @RequestBody AssessRequest req) {
AuditRiskAssessment a = load(id);
if (req.effectiveness() == null || req.effectiveness().isBlank()) {
throw new ApiException(400, "评估结论不能为空(有效/部分有效/无效)");
}
if (!List.of("有效", "部分有效", "无效").contains(req.effectiveness())) {
throw new ApiException(400, "评估结论须为:有效 / 部分有效 / 无效");
}
a.setEffectiveness(req.effectiveness());
if (req.assessmentDetail() != null) a.setAssessmentDetail(req.assessmentDetail());
if (req.recommendation() != null) a.setRecommendation(req.recommendation());
if (req.assessor() != null) a.setAssessor(req.assessor());
a.setAssessDate(LocalDate.now().toString());
a.setStatus(req.effectiveness());
// 无效 → 自动派生 AuditFinding。
if ("无效".equals(req.effectiveness()) && a.getFindingId() == null) {
Long linkedProjectId = a.getProjectId() != null ? a.getProjectId()
: (req.projectId() != null ? req.projectId() : null);
if (linkedProjectId != null) {
AuditFinding finding = new AuditFinding();
finding.setProjectId(linkedProjectId);
projectRepo.findById(linkedProjectId).ifPresent(p -> {
finding.setProjectName(p.getName());
finding.setAuditee(p.getAuditee());
});
finding.setTitle("风险管理缺陷:" + (a.getRiskName() == null ? "" : a.getRiskName()));
finding.setFactBasis("风险管理有效性审计评估编号 " + a.getAssessNo() + "" +
(a.getAssessmentDetail() == null ? "应对措施无效" : a.getAssessmentDetail()));
finding.setViolatedRule("风险管理制度");
finding.setSeverity(mapRiskLevelToSeverity(a.getRiskLevel()));
finding.setCategory("内控缺陷");
finding.setStatus("待沟通");
finding.setCreatedAt(Instant.now());
// 生成问题编号。
String year = String.valueOf(Year.now().getValue());
long seq = findingRepo.count() + 1;
finding.setFindingNo(String.format("AF-%s-%03d", year, seq));
AuditFinding saved = findingRepo.save(finding);
a.setFindingId(saved.getId());
a.setFindingNo(saved.getFindingNo());
}
}
return ApiResp.ok(assessRepo.save(a));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
AuditRiskAssessment a = load(id);
if (!"待评估".equals(a.getStatus())) {
throw new ApiException(409, "只有待评估状态的记录可以删除");
}
assessRepo.deleteById(id);
return ApiResp.ok(null);
}
private AuditRiskAssessment load(Long id) {
return assessRepo.findById(id)
.orElseThrow(() -> new NotFoundException("风险审计评估记录不存在: " + id));
}
private String nextAssessNo() {
String year = String.valueOf(Year.now().getValue());
long seq = assessRepo.count() + 1;
return String.format("ARA-%s-%03d", year, seq);
}
private static List<String> levelAtLeast(String minLevel) {
return switch (minLevel) {
case "重大" -> List.of("重大");
case "高" -> List.of("高", "重大");
case "中" -> List.of("中", "高", "重大");
default -> List.of("低", "中", "高", "重大");
};
}
private static String mapRiskLevelToSeverity(String riskLevel) {
if ("重大".equals(riskLevel)) return "高";
if ("高".equals(riskLevel)) return "高";
if ("中".equals(riskLevel)) return "中";
return "低";
}
}