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

246 lines
10 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.AuditArchive;
import com.kaidi.oa.domain.AuditProject;
import com.kaidi.oa.repository.AuditArchiveRepository;
import com.kaidi.oa.repository.AuditFindingRepository;
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.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.Year;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 审计档案库(内控部·审计监察部·第9模块)。
*
* 解决审计缺口:「无统一审计档案库实体(底稿/报告/证据/整改记录无统一归档入口)/
* 无审计发现库单独聚合视图」。
*
* 核心功能:
* - POST / 新建归档记录(状态=归档中);
* - PATCH /{id}/archive 确认归档(归档中→已归档);
* - GET /?year=&type= 按年度/类型过滤查询;
* - GET /search?q= 简易关键词检索(title/keywords/tags/description);
* - GET /findings-library 审计发现库聚合视图(跨审计项目汇总历史问题);
* - GET /summary 档案按年度/类型统计数量;
* - PUT /{id} 更新档案信息(已归档阶段只允许更新标签/关键词);
* - DELETE /{id} 删除(仅归档中阶段)。
*/
@RestController
@RequestMapping("/api/oa/audit-archives")
public class AuditArchiveController {
private final AuditArchiveRepository archiveRepo;
private final AuditProjectRepository projectRepo;
private final AuditFindingRepository findingRepo;
public AuditArchiveController(AuditArchiveRepository archiveRepo,
AuditProjectRepository projectRepo,
AuditFindingRepository findingRepo) {
this.archiveRepo = archiveRepo;
this.projectRepo = projectRepo;
this.findingRepo = findingRepo;
}
@GetMapping
public ApiResp<List<AuditArchive>> list(
@RequestParam(required = false) String year,
@RequestParam(required = false) String archiveType,
@RequestParam(required = false) Long projectId) {
if (projectId != null) {
return ApiResp.ok(archiveRepo.findByProjectId(projectId));
}
if (year != null && !year.isBlank()) {
return ApiResp.ok(archiveRepo.findByYear(year));
}
if (archiveType != null && !archiveType.isBlank()) {
return ApiResp.ok(archiveRepo.findByArchiveType(archiveType));
}
return ApiResp.ok(archiveRepo.findByOrderByYearDescIdDesc());
}
@GetMapping("/{id}")
public ApiResp<AuditArchive> get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
/**
* 简易关键词检索(title/keywords/tags 含匹配,不依赖 ES)。
*/
@GetMapping("/search")
public ApiResp<List<AuditArchive>> search(@RequestParam String q) {
if (q == null || q.isBlank()) {
return ApiResp.ok(List.of());
}
return ApiResp.ok(
archiveRepo.findByKeywordsContainingOrTitleContainingOrTagsContaining(
q, q, q));
}
/**
* 审计发现库聚合视图:按问题类别/风险等级聚合历史全部审计发现(含整改状态),
* 供新项目检索类似问题参考经验。返回发现列表(含 rootCause/category/severity/status)。
*/
@GetMapping("/findings-library")
public ApiResp<Object> findingsLibrary(
@RequestParam(required = false) String category,
@RequestParam(required = false) String severity) {
var all = findingRepo.findAll();
var filtered = all.stream()
.filter(f -> category == null || category.isBlank()
|| category.equals(f.getCategory()))
.filter(f -> severity == null || severity.isBlank()
|| severity.equals(f.getSeverity()))
.map(f -> Map.of(
"id", f.getId(),
"findingNo", f.getFindingNo() == null ? "" : f.getFindingNo(),
"title", f.getTitle() == null ? "" : f.getTitle(),
"category", f.getCategory() == null ? "" : f.getCategory(),
"severity", f.getSeverity() == null ? "" : f.getSeverity(),
"status", f.getStatus() == null ? "" : f.getStatus(),
"rootCause", f.getRootCause() == null ? "" : f.getRootCause(),
"projectName", f.getProjectName() == null ? "" : f.getProjectName(),
"auditee", f.getAuditee() == null ? "" : f.getAuditee()
))
.toList();
return ApiResp.ok(filtered);
}
/**
* 档案统计摘要:按年度/类型统计数量(辅助知识库全览)。
*/
@GetMapping("/summary")
public ApiResp<Map<String, Object>> summary() {
List<AuditArchive> all = archiveRepo.findAll();
Map<String, Long> byYear = all.stream()
.collect(Collectors.groupingBy(
a -> a.getYear() == null ? "未知" : a.getYear(),
Collectors.counting()));
Map<String, Long> byType = all.stream()
.collect(Collectors.groupingBy(
a -> a.getArchiveType() == null ? "其他" : a.getArchiveType(),
Collectors.counting()));
return ApiResp.ok(Map.of("total", (long) all.size(),
"byYear", byYear, "byType", byType));
}
public record CreateRequest(
Long projectId, String year, String archiveType, String title,
String description, String fileRef, String fileHash,
String tags, String keywords, int retentionYear, String archiver) {
}
@PostMapping
public ApiResp<AuditArchive> create(@RequestBody CreateRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "档案标题不能为空");
}
if (req.archiveType() == null || req.archiveType().isBlank()) {
throw new ApiException(400, "档案类型不能为空");
}
AuditArchive a = new AuditArchive();
a.setArchiveNo(nextArchiveNo());
a.setYear(req.year() != null && !req.year().isBlank()
? req.year() : String.valueOf(Year.now().getValue()));
a.setArchiveType(req.archiveType());
a.setTitle(req.title());
a.setDescription(req.description());
a.setFileRef(req.fileRef());
a.setFileHash(req.fileHash());
a.setTags(req.tags());
a.setKeywords(req.keywords());
a.setRetentionYear(req.retentionYear() <= 0 ? 10 : req.retentionYear());
a.setArchiver(req.archiver());
a.setStatus("归档中");
a.setCreatedAt(Instant.now());
if (req.projectId() != null) {
projectRepo.findById(req.projectId()).ifPresent(p -> {
a.setProjectId(p.getId());
a.setProjectName(p.getName());
a.setAuditee(p.getAuditee());
a.setAuditType(p.getAuditType());
});
}
return ApiResp.ok(archiveRepo.save(a));
}
public record UpdateRequest(
String title, String description, String tags, String keywords,
String fileRef, int retentionYear) {
}
@PutMapping("/{id}")
public ApiResp<AuditArchive> update(@PathVariable Long id, @RequestBody UpdateRequest req) {
AuditArchive a = load(id);
if ("已销毁".equals(a.getStatus())) {
throw new ApiException(409, "已销毁档案不允许修改");
}
// 已归档阶段只允许改标签/关键词/描述(核心字段锁定)。
boolean archived = "已归档".equals(a.getStatus());
if (req.tags() != null) a.setTags(req.tags());
if (req.keywords() != null) a.setKeywords(req.keywords());
if (req.description() != null) a.setDescription(req.description());
if (!archived) {
if (req.title() != null && !req.title().isBlank()) a.setTitle(req.title());
if (req.fileRef() != null) a.setFileRef(req.fileRef());
if (req.retentionYear() > 0) a.setRetentionYear(req.retentionYear());
}
return ApiResp.ok(archiveRepo.save(a));
}
public record ArchiveRequest(String archiver) {
}
/** 确认归档(归档中 → 已归档)。 */
@PatchMapping("/{id}/archive")
public ApiResp<AuditArchive> archive(@PathVariable Long id, @RequestBody ArchiveRequest req) {
AuditArchive a = load(id);
if (!"归档中".equals(a.getStatus())) {
throw new ApiException(409, "只有归档中状态可确认归档(当前:" + a.getStatus() + "");
}
a.setStatus("已归档");
a.setArchivedAt(Instant.now());
if (req.archiver() != null && !req.archiver().isBlank()) a.setArchiver(req.archiver());
return ApiResp.ok(archiveRepo.save(a));
}
/** 删除(仅归档中阶段)。 */
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
AuditArchive a = load(id);
if (!"归档中".equals(a.getStatus())) {
throw new ApiException(409, "只有归档中阶段的档案可以删除(当前:" + a.getStatus() + "");
}
archiveRepo.deleteById(id);
return ApiResp.ok(null);
}
private AuditArchive load(Long id) {
return archiveRepo.findById(id)
.orElseThrow(() -> new NotFoundException("审计档案不存在: " + id));
}
private String nextArchiveNo() {
String year = String.valueOf(Year.now().getValue());
long seq = archiveRepo.count() + 1;
return String.format("AC-%s-%03d", year, seq);
}
}