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,555 @@
|
||||
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}/archive(HR 事件触发员工档案流入资料室);</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; }
|
||||
}
|
||||
Reference in New Issue
Block a user