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

556 lines
29 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.Archive;
import com.kaidi.oa.domain.ArchiveSubmission;
import com.kaidi.oa.domain.ArchiveVersion;
import com.kaidi.oa.domain.Contract;
import com.kaidi.oa.domain.Declaration;
import com.kaidi.oa.domain.LitigationCase;
import com.kaidi.oa.repository.ArchiveRepository;
import com.kaidi.oa.repository.ArchiveSubmissionRepository;
import com.kaidi.oa.repository.ArchiveVersionRepository;
import com.kaidi.oa.repository.ContractRepository;
import com.kaidi.oa.repository.DeclarationRepository;
import com.kaidi.oa.repository.LitigationCaseRepository;
import org.springframework.transaction.annotation.Transactional;
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.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.util.ArrayList;
import java.util.List;
/**
* 资料室·Module 8「与其它部门的接口要求」缺口补完。
*
* <p>补审计「跨模块接口」六大缺口(跳过 IoT/BIM 等外部系统):
* <ul>
* <li>申报服务部:自动获取档案电子版及归档状态(DeclarationController 未调用 ArchiveRepository)→
* GET /archive-cross/declaration/{declId}/archive-status</li>
* <li>法务风险部:合同/诉讼文书自动归档链(TriggerRuleEngine 仅有 project.toArchive
* 无 contract.toArchive / legal.toArchive)→
* POST /archive-cross/contract/{contractId}/archive(触发合同自动归档)、
* POST /archive-cross/litigation/{caseId}/archive(诉讼文书归档);</li>
* <li>工程监理部:图纸版本归档对接(SupervisionInspectionController 未链接 ArchiveVersion)→
* GET /archive-cross/supervision/drawing-versions(取所有图纸类版本归档状态)、
* POST /archive-cross/supervision/drawing/{archiveId}/new-version(新建图纸版本);</li>
* <li>财务部:会计档案自动归档链 →
* POST /archive-cross/finance/{year}/accounting-archive(财务部按年度触发会计档案归档);</li>
* <li>人力资源部:员工档案自动归档链 →
* POST /archive-cross/hr/employee/{employeeNo}/archiveHR 事件触发员工档案流入资料室);</li>
* <li>知识产权部+质安部:证书/质量记录归档链 →
* POST /archive-cross/ip/cert/archive(知识产权证书归档)、
* POST /archive-cross/quality/record/archive(质量记录归档);</li>
* <li>聚合状态视图:GET /archive-cross/integration-dashboard — 各模块归档状态汇总。</li>
* </ul>
*
* <p>幂等:每次自动触发前检查 archiveRepo.findBySourceTypeAndSourceId(),已有则跳过。
* 写口受 default-deny(ADMIN/APPROVER) 保护;读口纳入 SENSITIVE_READ_PREFIXES。
*/
@RestController
@RequestMapping("/api/oa/archive-cross")
public class ArchiveCrossModuleController {
private final ArchiveRepository archiveRepo;
private final ArchiveSubmissionRepository submissionRepo;
private final ArchiveVersionRepository versionRepo;
private final ContractRepository contractRepo;
private final DeclarationRepository declRepo;
private final LitigationCaseRepository litigationRepo;
public ArchiveCrossModuleController(ArchiveRepository archiveRepo,
ArchiveSubmissionRepository submissionRepo,
ArchiveVersionRepository versionRepo,
ContractRepository contractRepo,
DeclarationRepository declRepo,
LitigationCaseRepository litigationRepo) {
this.archiveRepo = archiveRepo;
this.submissionRepo = submissionRepo;
this.versionRepo = versionRepo;
this.contractRepo = contractRepo;
this.declRepo = declRepo;
this.litigationRepo = litigationRepo;
}
// =====================================================================
// 申报服务部:获取申报项目关联档案电子版及归档状态
// =====================================================================
/**
* 申报服务部接口:自动获取某申报项目关联档案的电子版及归档状态。
* 查 archive 表 sourceType=申报&sourceId=declId(申报→档案自动联动)
* + archive_submission 表按申报部门「申报服务部」过滤。
*/
@GetMapping("/declaration/{declId}/archive-status")
public ApiResp<DeclArchiveStatus> declArchiveStatus(@PathVariable Long declId) {
Declaration decl = declRepo.findById(declId)
.orElseThrow(() -> new NotFoundException("declaration not found: " + declId));
// 检查档案库:sourceType=申报,sourceId=declId
List<Archive> archives = archiveRepo.findBySourceTypeAndSourceId("申报", declId);
// 检查归档申请:来源部门含「申报」
List<ArchiveSubmission> submissions = submissionRepo.findBySourceDept("申报服务部");
List<ArchiveRef> refs = archives.stream()
.map(a -> new ArchiveRef(a.getId(), a.getTitle(), a.getCategory(),
a.getAccessLevel(), a.getArchiveDate(), a.getFileUrl(), "已归档"))
.toList();
String overallStatus = refs.isEmpty() ? "未归档" : "已归档";
long pendingCount = submissions.stream()
.filter(s -> "待审核".equals(s.getStatus()))
.count();
return ApiResp.ok(new DeclArchiveStatus(
declId, decl.getName(), decl.getProgram(), decl.getStatus(),
overallStatus, refs, pendingCount));
}
public record ArchiveRef(Long archiveId, String title, String category,
String accessLevel, String archiveDate, String fileUrl, String status) {}
public record DeclArchiveStatus(Long declId, String declName, String program, String declStatus,
String archiveStatus, List<ArchiveRef> archives, long pendingSubmissions) {}
// =====================================================================
// 法务风险部:合同/诉讼文书自动归档链(contract.toArchive + legal.toArchive
// =====================================================================
/**
* 合同自动归档触发(补 TriggerRuleEngine「无 contract.toArchive」缺口)。
* 仅状态为「履约中」或「已结算」的合同可归档;幂等(已有合同档案则返回现有记录)。
*/
@PostMapping("/contract/{contractId}/archive")
@Transactional
public ApiResp<AutoArchiveResult> archiveContract(@PathVariable Long contractId,
@RequestBody(required = false) OperatorReq req) {
Contract c = contractRepo.findById(contractId)
.orElseThrow(() -> new NotFoundException("contract not found: " + contractId));
// 幂等检查
List<Archive> existing = archiveRepo.findBySourceTypeAndSourceId("合同", contractId);
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), "合同", contractId, c.getName(),
"幂等跳过:该合同已有档案记录 #" + existing.get(0).getId()));
}
if (!"履约中".equals(c.getStatus()) && !"已结算".equals(c.getStatus())) {
throw new ApiException(409, "仅「履约中」或「已结算」合同可归档,当前状态:" + c.getStatus());
}
Archive a = new Archive();
a.setCategory("合同档案");
a.setTitle(c.getName() + "" + nz(c.getType()) + ")合同档案");
a.setSourceType("合同");
a.setSourceId(c.getId());
a.setFileName(c.getName());
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req == null ? "系统" : nz(req.operator()));
a.setArchiveDate(LocalDate.now().toString());
a.setTags("合同编号:" + nz(c.getCode()) + ",甲方:" + nz(c.getPartyA()) + ",乙方:" + nz(c.getPartyB()));
a.setSummary("合同类型:" + nz(c.getType())
+ ";签署日期:" + nz(c.getSignDate())
+ ";状态:" + nz(c.getStatus())
+ ";自动归档(合同→档案联动)");
a.setAccessLevel("内部");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), "合同", contractId, c.getName(),
"合同档案已自动归档,档案ID=" + saved.getId()));
}
/**
* 诉讼文书自动归档触发(补 TriggerRuleEngine「无 legal.toArchive」缺口)。
* 已结案的诉讼文书归档到「合同档案/法律文书」类别;幂等。
*/
@PostMapping("/litigation/{caseId}/archive")
@Transactional
public ApiResp<AutoArchiveResult> archiveLitigation(@PathVariable Long caseId,
@RequestBody(required = false) OperatorReq req) {
LitigationCase lc = litigationRepo.findById(caseId)
.orElseThrow(() -> new NotFoundException("litigation case not found: " + caseId));
List<Archive> existing = archiveRepo.findBySourceTypeAndSourceId("诉讼", caseId);
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), "诉讼", caseId, lc.getCaseName(),
"幂等跳过:该诉讼案件已有档案记录 #" + existing.get(0).getId()));
}
Archive a = new Archive();
a.setCategory("合同档案");
a.setTitle(lc.getCaseName() + " 诉讼文书归档");
a.setSourceType("诉讼");
a.setSourceId(lc.getId());
a.setFileName(lc.getCaseName());
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req == null ? "系统" : nz(req.operator()));
a.setArchiveDate(LocalDate.now().toString());
a.setTags("案号:" + nz(lc.getCaseNo()) + ",阶段:" + nz(lc.getStage()) + ",结果:" + nz(lc.getStatus()));
a.setSummary("诉讼案件「" + lc.getCaseName() + "」文书归档;立案:" + nz(lc.getFilingDate())
+ ";阶段:" + nz(lc.getStage()));
a.setAccessLevel("机密");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), "诉讼", caseId, lc.getCaseName(),
"诉讼文书已自动归档,档案ID=" + saved.getId()));
}
public record AutoArchiveResult(Long archiveId, String sourceType, Long sourceId,
String sourceName, String message) {}
public record OperatorReq(String operator, String remark) {}
// =====================================================================
// 工程监理部:图纸版本归档对接
// =====================================================================
/**
* 图纸版本归档状态聚合(补「SupervisionInspectionController 未链接 ArchiveVersion」缺口)。
* 查所有「图纸档案」类别的 Archive 记录 + 对应的 ArchiveVersion 清单,
* 返回图纸版本归档状态(现行版本/作废版本数量)。
*/
@GetMapping("/supervision/drawing-versions")
public ApiResp<List<DrawingVersionStatus>> drawingVersions() {
List<Archive> drawings = archiveRepo.findByCategory("图纸档案");
List<DrawingVersionStatus> result = new ArrayList<>();
for (Archive a : drawings) {
List<ArchiveVersion> versions = versionRepo.findByArchiveIdOrderByIdDesc(a.getId());
long currentCount = versions.stream().filter(v -> Boolean.TRUE.equals(v.getIsCurrent())).count();
long obsoleteCount = versions.stream().filter(v -> "作废".equals(v.getStatus())).count();
String latestVersionNo = versions.isEmpty() ? null : versions.get(0).getVersionNo();
result.add(new DrawingVersionStatus(a.getId(), a.getTitle(), a.getArchiveDate(),
versions.size(), currentCount, obsoleteCount, latestVersionNo));
}
return ApiResp.ok(result);
}
/**
* 工程监理部为某图纸档案新建版本归档(链 ArchiveVersion,修复「未链接 ArchiveVersion」缺口)。
* 自动将旧现行版置「作废」,新版置「现行」。
*/
@PostMapping("/supervision/drawing/{archiveId}/new-version")
@Transactional
public ApiResp<ArchiveVersion> newDrawingVersion(@PathVariable Long archiveId,
@RequestBody DrawingVersionReq req) {
Archive a = archiveRepo.findById(archiveId)
.orElseThrow(() -> new NotFoundException("archive not found: " + archiveId));
if (req.versionNo() == null || req.versionNo().isBlank()) {
throw new ApiException(400, "版本号(versionNo) 不能为空");
}
// 旧现行版→作废
versionRepo.findByArchiveIdAndIsCurrentTrue(archiveId).ifPresent(old -> {
old.setIsCurrent(Boolean.FALSE);
old.setStatus("作废");
versionRepo.save(old);
});
// 新版现行
ArchiveVersion v = new ArchiveVersion();
v.setArchiveId(archiveId);
v.setArchiveTitle(a.getTitle());
v.setVersionNo(req.versionNo());
v.setIsCurrent(Boolean.TRUE);
v.setStatus("现行");
v.setChangeNote(req.changeNote());
v.setAuthor(req.author());
v.setVersionDate(req.versionDate() == null || req.versionDate().isBlank()
? LocalDate.now().toString() : req.versionDate());
v.setCreatedAt(Instant.now());
return ApiResp.ok(versionRepo.save(v));
}
public record DrawingVersionStatus(Long archiveId, String title, String archiveDate,
int totalVersions, long currentVersions, long obsoleteVersions,
String latestVersionNo) {}
public record DrawingVersionReq(String versionNo, String changeNote, String author, String versionDate) {}
// =====================================================================
// 财务部:会计档案自动归档链
// =====================================================================
/**
* 财务部会计档案年度归档触发(补「财务部会计档案自动归档链缺失」缺口)。
* 按年度创建一条「财务档案」类别的档案记录(幂等:同年度已有则跳过)。
*/
@PostMapping("/finance/{year}/accounting-archive")
@Transactional
public ApiResp<AutoArchiveResult> financeAccountingArchive(@PathVariable int year,
@RequestBody(required = false) OperatorReq req) {
if (year < 2000 || year > 2099) {
throw new ApiException(400, "年度参数不合法:" + year);
}
String title = year + "年度会计档案(凭证/账簿/报表)";
// 幂等:检查是否已有同年度财务档案
List<Archive> existing = archiveRepo.findByCategory("财务档案").stream()
.filter(a -> a.getTitle() != null && a.getTitle().startsWith(String.valueOf(year)))
.toList();
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), "财务", (long) year, title,
"幂等跳过:" + year + " 年度会计档案已归档,档案ID=" + existing.get(0).getId()));
}
Archive a = new Archive();
a.setCategory("财务档案");
a.setTitle(title);
a.setSourceType("财务");
a.setSourceId((long) year);
a.setFileName(title);
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req == null ? "财务部" : nz(req.operator()));
a.setArchiveDate(LocalDate.now().toString());
a.setTags("年度:" + year + ",类型:会计档案,保存期限:30年");
a.setSummary(year + "年度财务档案自动归档,含凭证/账簿/报表,依《会计档案管理办法》保存30年");
a.setAccessLevel("机密");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), "财务", (long) year, title,
"财务会计档案已归档,档案ID=" + saved.getId()));
}
// =====================================================================
// 人力资源部:员工档案流入资料室
// =====================================================================
/**
* HR 事件触发员工档案流入资料室(补「人力资源部员工档案自动归档链缺失」缺口)。
* HrEventArchiveController 是独立人事档案,不流入资料室 Archive 表;
* 本端点将员工重要档案(离职/合同/年度评估)写入 Archive 表(幂等)。
*/
@PostMapping("/hr/employee/{employeeNo}/archive")
@Transactional
public ApiResp<AutoArchiveResult> hrEmployeeArchive(@PathVariable String employeeNo,
@RequestBody HrArchiveReq req) {
if (req.eventType() == null || req.eventType().isBlank()) {
throw new ApiException(400, "人事事件类型(eventType) 不能为空");
}
if (req.employeeName() == null || req.employeeName().isBlank()) {
throw new ApiException(400, "员工姓名(employeeName) 不能为空");
}
String category = "人事档案";
String title = req.employeeName() + "" + employeeNo + "" + req.eventType() + "档案";
String sourceType = "人事-" + req.eventType();
// 幂等:同员工同事件类型只归档一次(用 tags 匹配)
List<Archive> existing = archiveRepo.findByCategory(category).stream()
.filter(a -> a.getTags() != null && a.getTags().contains("员工编号:" + employeeNo)
&& a.getTags().contains("事件:" + req.eventType()))
.toList();
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), sourceType, null, title,
"幂等跳过:该员工" + req.eventType() + "档案已归档"));
}
Archive a = new Archive();
a.setCategory(category);
a.setTitle(title);
a.setSourceType(sourceType);
a.setFileName(title);
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req.operator() == null ? "人力资源部" : req.operator());
a.setArchiveDate(req.eventDate() == null ? LocalDate.now().toString() : req.eventDate());
a.setTags("员工编号:" + employeeNo + ",员工姓名:" + req.employeeName()
+ ",部门:" + nz(req.dept()) + ",事件:" + req.eventType());
a.setSummary("员工「" + req.employeeName() + "」" + req.eventType() + "档案,由 HR 事件联动自动归档");
a.setAccessLevel("机密");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), sourceType, null, title,
"员工档案已自动归档至资料室,档案ID=" + saved.getId()));
}
public record HrArchiveReq(String employeeName, String dept, String eventType,
String eventDate, String operator, String remark) {}
// =====================================================================
// 知识产权部:证书电子档同步归档
// =====================================================================
/**
* 知识产权证书归档(补「知识产权部证书电子档同步链缺失」缺口)。
* 专利/商标/著作权证书获批后自动触发归档至资料室。幂等。
*/
@PostMapping("/ip/cert/archive")
@Transactional
public ApiResp<AutoArchiveResult> ipCertArchive(@RequestBody IpCertReq req) {
if (req.certNo() == null || req.certNo().isBlank()) {
throw new ApiException(400, "证书编号(certNo) 不能为空");
}
if (req.certType() == null || req.certType().isBlank()) {
throw new ApiException(400, "证书类型(certType) 不能为空");
}
String title = req.certType() + "" + nz(req.certName()) + "" + req.certNo() + "";
// 幂等:按证书编号查 tags
List<Archive> existing = archiveRepo.findByCategory("知识产权档案").stream()
.filter(a -> a.getTags() != null && a.getTags().contains("证书编号:" + req.certNo()))
.toList();
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), "知识产权", null, title,
"幂等跳过:该证书已归档,档案ID=" + existing.get(0).getId()));
}
Archive a = new Archive();
a.setCategory("知识产权档案");
a.setTitle(title);
a.setSourceType("知识产权");
a.setFileName(title);
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req.operator() == null ? "知识产权部" : req.operator());
a.setArchiveDate(req.grantDate() == null ? LocalDate.now().toString() : req.grantDate());
a.setTags("证书编号:" + req.certNo() + ",证书类型:" + req.certType()
+ ",权利人:" + nz(req.holder()) + ",有效期至:" + nz(req.validUntil()));
a.setSummary("知识产权证书「" + title + "」电子档自动同步至资料室");
a.setAccessLevel("机密");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), "知识产权", null, title,
"知识产权证书已归档,档案ID=" + saved.getId()));
}
public record IpCertReq(String certNo, String certType, String certName,
String holder, String grantDate, String validUntil, String operator) {}
// =====================================================================
// 质安部:质量记录归档链
// =====================================================================
/**
* 质量记录归档(补「质安部质量记录归档链缺失」缺口)。
* 质量记录(检验报告/体系文件/安全报告/特种设备档案)由质安部触发归档至资料室。幂等。
*/
@PostMapping("/quality/record/archive")
@Transactional
public ApiResp<AutoArchiveResult> qualityRecordArchive(@RequestBody QualityArchiveReq req) {
if (req.recordNo() == null || req.recordNo().isBlank()) {
throw new ApiException(400, "质量记录编号(recordNo) 不能为空");
}
if (req.recordType() == null || req.recordType().isBlank()) {
throw new ApiException(400, "记录类型(recordType) 不能为空");
}
String title = req.recordType() + "" + nz(req.title()) + "" + req.recordNo() + "";
// 幂等
List<Archive> existing = archiveRepo.findByCategory("质量档案").stream()
.filter(a -> a.getTags() != null && a.getTags().contains("记录编号:" + req.recordNo()))
.toList();
if (!existing.isEmpty()) {
return ApiResp.ok(new AutoArchiveResult(
existing.get(0).getId(), "质量", null, title,
"幂等跳过:该质量记录已归档,档案ID=" + existing.get(0).getId()));
}
Archive a = new Archive();
a.setCategory("质量档案");
a.setTitle(title);
a.setSourceType("质量");
a.setFileName(title);
a.setFileType("PDF");
a.setFileSize(0L);
a.setUploader(req.operator() == null ? "质安部" : req.operator());
a.setArchiveDate(req.recordDate() == null ? LocalDate.now().toString() : req.recordDate());
a.setTags("记录编号:" + req.recordNo() + ",记录类型:" + req.recordType()
+ ",项目:" + nz(req.projectName()));
a.setSummary("质安部质量记录「" + title + "」自动归档至资料室");
a.setAccessLevel("内部");
a.setCreatedAt(Instant.now());
Archive saved = archiveRepo.save(a);
return ApiResp.ok(new AutoArchiveResult(
saved.getId(), "质量", null, title,
"质量记录已归档,档案ID=" + saved.getId()));
}
public record QualityArchiveReq(String recordNo, String recordType, String title,
String projectName, String recordDate, String operator) {}
// =====================================================================
// 综合归档状态看板(跨模块集成 Dashboard)
// =====================================================================
/**
* 各模块归档状态汇总:各类别档案数量 + 跨模块归档链覆盖情况。
* 供「跨模块集成」管理页展示全局归档状态。
*/
@GetMapping("/integration-dashboard")
public ApiResp<IntegrationDashboard> integrationDashboard() {
List<Archive> all = archiveRepo.findAll();
long contractArchives = all.stream().filter(a -> "合同".equals(a.getSourceType())).count();
long projectArchives = all.stream().filter(a -> "项目".equals(a.getSourceType())).count();
long litigArchives = all.stream().filter(a -> "诉讼".equals(a.getSourceType())).count();
long hrArchives = all.stream().filter(a -> a.getSourceType() != null
&& a.getSourceType().startsWith("人事")).count();
long financeArchives = all.stream().filter(a -> "财务".equals(a.getSourceType())).count();
long ipArchives = all.stream().filter(a -> "知识产权".equals(a.getSourceType())).count();
long qualityArchives = all.stream().filter(a -> "质量".equals(a.getSourceType())).count();
long drawingArchives = all.stream().filter(a -> "图纸档案".equals(a.getCategory())).count();
long manualArchives = all.stream().filter(a -> "手动".equals(a.getSourceType())).count();
long archiveArchives = all.stream().filter(a -> "归档".equals(a.getSourceType())).count();
List<ModuleArchiveStat> stats = List.of(
new ModuleArchiveStat("合同管理", "contract.toArchive", contractArchives, contractArchives > 0),
new ModuleArchiveStat("项目管理", "project.toArchive", projectArchives, projectArchives > 0),
new ModuleArchiveStat("法务风险(诉讼)", "legal.toArchive", litigArchives, litigArchives > 0),
new ModuleArchiveStat("人力资源", "hr.toArchive", hrArchives, hrArchives > 0),
new ModuleArchiveStat("财务部", "finance.toArchive", financeArchives, financeArchives > 0),
new ModuleArchiveStat("知识产权", "ip.toArchive", ipArchives, ipArchives > 0),
new ModuleArchiveStat("质安部", "quality.toArchive", qualityArchives, qualityArchives > 0),
new ModuleArchiveStat("工程监理(图纸)", "supervision.drawing", drawingArchives, drawingArchives > 0),
new ModuleArchiveStat("在线归档申请", "archive.submission", archiveArchives, archiveArchives > 0),
new ModuleArchiveStat("手动上传", "manual", manualArchives, true)
);
long activeChains = stats.stream().filter(ModuleArchiveStat::linked).count();
// 图纸版本统计
List<Archive> drawings = archiveRepo.findByCategory("图纸档案");
long totalDrawingVersions = 0;
for (Archive d : drawings) {
totalDrawingVersions += versionRepo.findByArchiveIdOrderByIdDesc(d.getId()).size();
}
return ApiResp.ok(new IntegrationDashboard(
all.size(), stats, activeChains, stats.size(),
drawings.size(), totalDrawingVersions,
LocalDate.now().toString()));
}
public record ModuleArchiveStat(String module, String ruleKey, long archiveCount, boolean linked) {}
public record IntegrationDashboard(
long totalArchives,
List<ModuleArchiveStat> moduleStats,
long activeChains,
long totalChains,
long drawingArchiveCount,
long drawingVersionCount,
String reportDate) {}
// ---- helpers ----
private static String nz(String s) { return s == null ? "" : s; }
}