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> legalAlerts() { var contracts = contractRepo.findAll(); String today = LocalDate.now().toString(); List> expired = new ArrayList<>(); List> 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 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 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 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> declCredentials( @RequestParam(required = false) String credType, @RequestParam(required = false) String personName) { var creds = credRepo.findAll(); List> 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 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 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> financeArchiveCheck( @RequestParam(required = false) String name) { var dossiers = dossierRepo.findAll(); List> 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 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 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> summary() { long contractTotal = contractRepo.count(); long credTotal = credRepo.count(); long dossierTotal = dossierRepo.count(); Map result = new LinkedHashMap<>(); result.put("asOf", LocalDate.now().toString()); result.put("laborContractTotal", contractTotal); result.put("staffCredentialTotal", credTotal); result.put("staffDossierTotal", dossierTotal); List> modules = new ArrayList<>(); Map m1 = new LinkedHashMap<>(); m1.put("name", "法务合同合规"); m1.put("apiPath", "/api/oa/hr-cross-dept/legal-alerts"); m1.put("targetDept", "法务风险部"); modules.add(m1); Map m2 = new LinkedHashMap<>(); m2.put("name", "申报证书推送"); m2.put("apiPath", "/api/oa/hr-cross-dept/decl-credentials"); m2.put("targetDept", "申报服务部"); modules.add(m2); Map 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 "档案完整"; } }