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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -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 "";
}
}