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>
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
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 "低";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user