恢复点(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>
230 lines
10 KiB
Java
230 lines
10 KiB
Java
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 "档案完整";
|
|
}
|
|
}
|