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,183 @@
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.AuditEvidence;
import com.kaidi.oa.domain.AuditProject;
import com.kaidi.oa.repository.AuditEvidenceRepository;
import com.kaidi.oa.repository.AuditProjectRepository;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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;
/**
* 审计证据管理(内控部·审计监察部·模块2)。
*
* 补全 PARTIAL 缺口:证据分类/标签/全文检索(证据管理),专用证据库而非依赖档案关键词检索。
*
* 功能:
* - POST / 新增证据记录(关联项目+底稿+发现);
* - GET / 按项目/底稿/发现/分类过滤列表;
* - GET /search?q= 关键词检索(title/keywords/tags);
* - PUT /{id} 编辑证据信息;
* - DELETE /{id} 删除证据;
* - GET /categories 证据类别统计。
*
* 路径 /api/oa/audit-evidence 纳入 SENSITIVE_READ_PREFIXES(含审计证据敏感数据)。
*/
@RestController
@RequestMapping("/api/oa/audit-evidence")
public class AuditEvidenceController {
private final AuditEvidenceRepository evidenceRepo;
private final AuditProjectRepository projectRepo;
public AuditEvidenceController(AuditEvidenceRepository evidenceRepo,
AuditProjectRepository projectRepo) {
this.evidenceRepo = evidenceRepo;
this.projectRepo = projectRepo;
}
@GetMapping
public ApiResp<List<AuditEvidence>> list(
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long workPaperId,
@RequestParam(required = false) Long findingId,
@RequestParam(required = false) String category) {
if (projectId != null) {
return ApiResp.ok(evidenceRepo.findByProjectId(projectId));
}
if (workPaperId != null) {
return ApiResp.ok(evidenceRepo.findByWorkPaperId(workPaperId));
}
if (findingId != null) {
return ApiResp.ok(evidenceRepo.findByFindingId(findingId));
}
if (category != null && !category.isBlank()) {
return ApiResp.ok(evidenceRepo.findByCategory(category));
}
return ApiResp.ok(evidenceRepo.findByOrderByIdDesc());
}
@GetMapping("/{id}")
public ApiResp<AuditEvidence> get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
/** 关键词检索(title/keywords/tags)。 */
@GetMapping("/search")
public ApiResp<List<AuditEvidence>> search(@RequestParam String q) {
if (q == null || q.isBlank()) {
return ApiResp.ok(List.of());
}
return ApiResp.ok(
evidenceRepo.findByTitleContainingOrKeywordsContainingOrTagsContaining(q, q, q));
}
/** 证据类别统计(各类别数量)。 */
@GetMapping("/categories")
public ApiResp<Object> categories() {
List<AuditEvidence> all = evidenceRepo.findAll();
java.util.Map<String, Long> byCategory = new java.util.LinkedHashMap<>();
for (AuditEvidence e : all) {
String cat = e.getCategory() == null ? "其他" : e.getCategory();
byCategory.merge(cat, 1L, Long::sum);
}
return ApiResp.ok(byCategory);
}
public record CreateRequest(
Long projectId, Long workPaperId, Long findingId,
String title, String category, String tags, String keywords,
String fileRef, String fileName, String fileType, String fileHash,
Boolean keyEvidence, String watermark, String description,
String collectedBy, String collectDate) {
}
@PostMapping
public ApiResp<AuditEvidence> create(@RequestBody CreateRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "证据标题不能为空");
}
if (req.projectId() == null) {
throw new ApiException(400, "请选择关联审计项目");
}
AuditEvidence e = new AuditEvidence();
e.setEvidenceNo(nextEvidenceNo());
e.setProjectId(req.projectId());
projectRepo.findById(req.projectId()).ifPresent(p -> e.setProjectName(p.getName()));
e.setWorkPaperId(req.workPaperId());
e.setFindingId(req.findingId());
e.setTitle(req.title());
e.setCategory(req.category() == null || req.category().isBlank() ? "其他" : req.category());
e.setTags(req.tags());
e.setKeywords(req.keywords());
e.setFileRef(req.fileRef());
e.setFileName(req.fileName());
e.setFileType(req.fileType());
e.setFileHash(req.fileHash());
e.setKeyEvidence(req.keyEvidence() != null && req.keyEvidence());
e.setWatermark(req.watermark());
e.setDescription(req.description());
e.setCollectedBy(req.collectedBy());
e.setCollectDate(req.collectDate() != null && !req.collectDate().isBlank()
? req.collectDate() : LocalDate.now().toString());
e.setCreatedAt(Instant.now());
return ApiResp.ok(evidenceRepo.save(e));
}
public record UpdateRequest(
String title, String category, String tags, String keywords,
String fileRef, String fileName, String fileType, String fileHash,
Boolean keyEvidence, String watermark, String description) {
}
@PutMapping("/{id}")
public ApiResp<AuditEvidence> update(@PathVariable Long id, @RequestBody UpdateRequest req) {
AuditEvidence e = load(id);
if (req.title() != null && !req.title().isBlank()) e.setTitle(req.title());
if (req.category() != null) e.setCategory(req.category());
if (req.tags() != null) e.setTags(req.tags());
if (req.keywords() != null) e.setKeywords(req.keywords());
if (req.fileRef() != null) e.setFileRef(req.fileRef());
if (req.fileName() != null) e.setFileName(req.fileName());
if (req.fileType() != null) e.setFileType(req.fileType());
if (req.fileHash() != null) e.setFileHash(req.fileHash());
if (req.keyEvidence() != null) e.setKeyEvidence(req.keyEvidence());
if (req.watermark() != null) e.setWatermark(req.watermark());
if (req.description() != null) e.setDescription(req.description());
return ApiResp.ok(evidenceRepo.save(e));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
if (!evidenceRepo.existsById(id)) {
throw new NotFoundException("审计证据不存在: " + id);
}
evidenceRepo.deleteById(id);
return ApiResp.ok(null);
}
private AuditEvidence load(Long id) {
return evidenceRepo.findById(id)
.orElseThrow(() -> new NotFoundException("审计证据不存在: " + id));
}
private String nextEvidenceNo() {
String year = String.valueOf(Year.now().getValue());
long seq = evidenceRepo.count() + 1;
return String.format("AE-%s-%03d", year, seq);
}
}