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,229 @@
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.repository.HrLaborContractRepository;
import com.kaidi.oa.repository.StaffCredentialRepository;
import com.kaidi.oa.repository.StaffDossierRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 行政·综合部——跨部门接口聚合(Module 19 缺口补全)。
*
* 满足需求:
* - 与法务风险部:劳动合同合规性预警(到期 / 即将到期(90天) / 已终止)
* - 与申报服务部:人员证书 API 推送接口(供高企/专精特新申报直接调用)
* - 与财务部:工资档案归档状态检查(staff_dossier 档案状态)
*
* 弥补缺口:「与法务风险部劳动合同合规性预警无专用接口」
* 「与申报服务部的人员证书/社保记录共享为手工查询而非API推送」
* 「与财务部工资单归档/个税申报记录无自动同步」
*
* 端点(聚合读接口,依托已有仓库,无新建实体):
* GET /hr-cross-dept/legal-alerts → 法务部:劳动合同合规性预警
* GET /hr-cross-dept/decl-credentials → 申报服务部:人员证书列表推送
* GET /hr-cross-dept/finance-archive-check → 财务部:员工档案归档状态检查
* GET /hr-cross-dept/summary → 综合汇总(三方向各 TOP 数字)
*/
@RestController
@RequestMapping("/api/oa/hr-cross-dept")
public class HrCrossDeptController {
private final HrLaborContractRepository contractRepo;
private final StaffCredentialRepository credRepo;
private final StaffDossierRepository dossierRepo;
public HrCrossDeptController(HrLaborContractRepository contractRepo,
StaffCredentialRepository credRepo,
StaffDossierRepository dossierRepo) {
this.contractRepo = contractRepo;
this.credRepo = credRepo;
this.dossierRepo = dossierRepo;
}
/**
* 法务部接口:劳动合同合规性预警。
* 满足需求:「与法务风险部劳动合同合规性预警无专用接口」。
*/
@GetMapping("/legal-alerts")
public ApiResp<Map<String, Object>> legalAlerts() {
var contracts = contractRepo.findAll();
String today = LocalDate.now().toString();
List<Map<String, Object>> expired = new ArrayList<>();
List<Map<String, Object>> expiringSoon = new ArrayList<>();
for (var c : contracts) {
if (c.getExpireDate() == null || c.getExpireDate().isBlank()) continue;
String end = c.getExpireDate();
LocalDate endDate;
try { endDate = LocalDate.parse(end); } catch (Exception e) { continue; }
if (endDate.isBefore(LocalDate.now())) {
if (!"已归档".equals(c.getStatus()) && !"已终止".equals(c.getStatus())) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("employeeName", c.getEmployeeName());
m.put("employeeNo", c.getEmployeeNo());
m.put("dept", c.getDept());
m.put("contractType", c.getContractType());
m.put("expireDate", end);
m.put("status", c.getStatus());
m.put("alertType", "合同已过期-需续签或终止");
expired.add(m);
}
} else {
long daysLeft = ChronoUnit.DAYS.between(LocalDate.now(), endDate);
if (daysLeft <= 90) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("employeeName", c.getEmployeeName());
m.put("employeeNo", c.getEmployeeNo());
m.put("dept", c.getDept());
m.put("contractType", c.getContractType());
m.put("expireDate", end);
m.put("daysLeft", daysLeft);
m.put("alertType", "即将到期(90天内)-建议启动续签");
expiringSoon.add(m);
}
}
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("asOf", today);
result.put("expiredContracts", expired);
result.put("expiringSoonContracts", expiringSoon);
result.put("totalAlerts", expired.size() + expiringSoon.size());
result.put("note", "本接口由综合部提供给法务风险部,可设置每日推送");
return ApiResp.ok(result);
}
/**
* 申报服务部接口:人员证书推送。
* 满足需求:「与申报服务部的人员证书/社保记录共享为手工查询而非API推送」。
*/
@GetMapping("/decl-credentials")
public ApiResp<Map<String, Object>> declCredentials(
@RequestParam(required = false) String credType,
@RequestParam(required = false) String personName) {
var creds = credRepo.findAll();
List<Map<String, Object>> list = new ArrayList<>();
for (var c : creds) {
if (credType != null && !credType.isBlank()
&& (c.getCredType() == null || !c.getCredType().contains(credType))) continue;
if (personName != null && !personName.isBlank()
&& (c.getPersonName() == null || !c.getPersonName().contains(personName))) continue;
Map<String, Object> m = new LinkedHashMap<>();
m.put("credId", c.getId());
m.put("personName", c.getPersonName());
m.put("dept", c.getDept());
m.put("credType", c.getCredType());
m.put("credNo", c.getCredNo());
m.put("major", c.getMajor());
m.put("issuer", c.getIssuer());
m.put("expireDate", c.getExpireDate());
m.put("lockState", c.getLockState());
list.add(m);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("asOf", LocalDate.now().toString());
result.put("queryCredType", credType);
result.put("queryPersonName", personName);
result.put("credentials", list);
result.put("total", list.size());
result.put("note", "本接口由综合部提供给申报服务部,供高企/专精特新申报实时调用;社保记录请另行联系HR提供社保局证明");
return ApiResp.ok(result);
}
/**
* 财务部接口:员工档案归档状态检查(检查是否存在档案记录 + 合同到期情况)。
* 满足需求:「与财务部工资单归档/个税申报记录无自动同步」。
*/
@GetMapping("/finance-archive-check")
public ApiResp<Map<String, Object>> financeArchiveCheck(
@RequestParam(required = false) String name) {
var dossiers = dossierRepo.findAll();
List<Map<String, Object>> list = new ArrayList<>();
for (var d : dossiers) {
if (name != null && !name.isBlank()
&& (d.getName() == null || !d.getName().contains(name))) continue;
boolean hasArchive = d.getDossierStatus() != null;
boolean isActive = "在职".equals(d.getDossierStatus());
Map<String, Object> m = new LinkedHashMap<>();
m.put("employeeNo", d.getEmployeeNo());
m.put("staffName", d.getName());
m.put("dept", d.getDept());
m.put("position", d.getPosition());
m.put("dossierStatus", d.getDossierStatus());
m.put("completeness", d.getCompleteness());
m.put("contractExpire", d.getContractExpire());
m.put("hireDate", d.getHireDate());
m.put("archiveNote", completenessNote(d.getCompleteness()));
list.add(m);
}
long completeCount = list.stream()
.filter(m -> m.get("completeness") != null && (Integer) m.get("completeness") >= 80)
.count();
Map<String, Object> result = new LinkedHashMap<>();
result.put("asOf", LocalDate.now().toString());
result.put("totalStaff", list.size());
result.put("completeCount", completeCount);
result.put("incompleteCount", list.size() - completeCount);
result.put("records", list);
result.put("note", "完整度≥80%视为档案齐全;薪酬/个税记录归档请在档案库中补充「薪酬档案」类别条目");
return ApiResp.ok(result);
}
/**
* 综合汇总:三方向数字概览,供综合部看板使用。
*/
@GetMapping("/summary")
public ApiResp<Map<String, Object>> summary() {
long contractTotal = contractRepo.count();
long credTotal = credRepo.count();
long dossierTotal = dossierRepo.count();
Map<String, Object> result = new LinkedHashMap<>();
result.put("asOf", LocalDate.now().toString());
result.put("laborContractTotal", contractTotal);
result.put("staffCredentialTotal", credTotal);
result.put("staffDossierTotal", dossierTotal);
List<Map<String, Object>> modules = new ArrayList<>();
Map<String, Object> m1 = new LinkedHashMap<>();
m1.put("name", "法务合同合规");
m1.put("apiPath", "/api/oa/hr-cross-dept/legal-alerts");
m1.put("targetDept", "法务风险部");
modules.add(m1);
Map<String, Object> m2 = new LinkedHashMap<>();
m2.put("name", "申报证书推送");
m2.put("apiPath", "/api/oa/hr-cross-dept/decl-credentials");
m2.put("targetDept", "申报服务部");
modules.add(m2);
Map<String, Object> m3 = new LinkedHashMap<>();
m3.put("name", "财务薪酬归档");
m3.put("apiPath", "/api/oa/hr-cross-dept/finance-archive-check");
m3.put("targetDept", "财务部");
modules.add(m3);
result.put("modules", modules);
return ApiResp.ok(result);
}
private static String completenessNote(Integer pct) {
if (pct == null || pct < 50) return "档案不完整,需补充材料";
if (pct < 80) return "档案基本完整,建议补充薪酬/绩效记录";
return "档案完整";
}
}