package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.domain.ComplianceAlertRule; import com.kaidi.oa.domain.HrNonCompete; import com.kaidi.oa.domain.IpAsset; import com.kaidi.oa.domain.LegalCreditInvest; import com.kaidi.oa.domain.LitigationCase; import com.kaidi.oa.repository.ComplianceAlertRuleRepository; import com.kaidi.oa.repository.HrNonCompeteRepository; import com.kaidi.oa.repository.IpAssetRepository; import com.kaidi.oa.repository.LegalCreditInvestRepository; 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.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.math.BigDecimal; import java.time.Instant; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 法务风险部·与其他部门接口(需求 §9)。 * *

补完三条跨部门联动缺口(Gap low 9): *

    *
  1. 销售合同客户信用风险预警:销售部签约前调用 * {@code GET /api/oa/legal-cross-dept/sales-customer-check?customerName=} 自动聚合: * - 法务专项调查结论(LegalCreditInvest) * - 在案诉讼信息(LitigationCase.opponent 模糊匹配) * - 命中的合规预警规则(ComplianceAlertRule + 预警模拟) * 返回综合风险评级(低/中/高/紧急)及处理建议,无需法务手动查阅三张表。
  2. * *
  3. HR 劳动纠纷案件自动建档:HR 部门对离职员工或劳动纠纷发起快速建档, * 调用 {@code POST /api/oa/legal-cross-dept/hr-labor-dispute-register} 一键在 * LitigationCase 中创建劳动仲裁案件记录,并同步将当事人竞业限制状态推进到「离职-补偿中」 * (若 employeeId 关联的 HrNonCompete 存在且状态为「在职-未触发」)。
  4. * *
  5. 知识产权与法务状态同步看板: * {@code GET /api/oa/legal-cross-dept/ip-status-sync} 聚合 IpAsset 台账当前状态 * 映射到法务关注维度:即将失效专利、未缴年费、授权维持专利数; * 供法务无需进入知识产权部系统即可掌握全局 IP 保护状态。
  6. *
* *

所有读口包含敏感数据(涉诉/信用/专利),已纳入 SENSITIVE_READ_PREFIXES(见 sharedFileSnippets)。 */ @RestController @RequestMapping("/api/oa/legal-cross-dept") public class LegalCrossDeptController { private final LitigationCaseRepository caseRepo; private final LegalCreditInvestRepository investRepo; private final ComplianceAlertRuleRepository ruleRepo; private final HrNonCompeteRepository nonCompeteRepo; private final IpAssetRepository ipAssetRepo; public LegalCrossDeptController(LitigationCaseRepository caseRepo, LegalCreditInvestRepository investRepo, ComplianceAlertRuleRepository ruleRepo, HrNonCompeteRepository nonCompeteRepo, IpAssetRepository ipAssetRepo) { this.caseRepo = caseRepo; this.investRepo = investRepo; this.ruleRepo = ruleRepo; this.nonCompeteRepo = nonCompeteRepo; this.ipAssetRepo = ipAssetRepo; } // ======================================================================== // 1. 销售合同客户信用风险预警(销售部→法务部自动联动,Gap §9-1) // ======================================================================== public record CustomerRiskItem(String category, String level, String content) {} public record CustomerRiskResult( String customerName, String overallRiskLevel, String suggestion, boolean blacklisted, boolean hasActiveLitigation, int litigationCount, BigDecimal litigationTotalAmount, String latestInvestConclusion, List riskItems ) {} /** * 销售签约前客户涉诉/信用风险一键预检。 * 聚合三个数据源:法务调查结论 + 在案诉讼 + 合规预警规则,自动给出综合风险等级。 * 销售部门新签合同前、法务自动推送触发时均可调用,无需手工查询。 */ @GetMapping("/sales-customer-check") public ApiResp salesCustomerCheck( @RequestParam String customerName) { if (customerName == null || customerName.isBlank()) { throw new ApiException(400, "customerName 不能为空"); } String name = customerName.trim(); // 法务调查结论(按客户名模糊) List invests = investRepo.findByCustomerNameContaining(name); String latestConclusion = null; boolean blacklisted = false; for (LegalCreditInvest inv : invests) { if (inv.isBlacklisted()) { blacklisted = true; } if ("已结案".equals(inv.getStatus()) && inv.getConclusion() != null) { latestConclusion = inv.getConclusion(); } } // 在案诉讼(按对方当事人名称模糊匹配) List cases = caseRepo.findByOpponentContaining(name); boolean hasActive = cases.stream().anyMatch(c -> !"已结案".equals(c.getStatus())); BigDecimal totalAmt = BigDecimal.ZERO; for (LitigationCase c : cases) { totalAmt = Money.add(totalAmt, c.getAmount() == null ? BigDecimal.ZERO : c.getAmount()); } // 命中合规预警规则(触发场景=销售) List salesRules = ruleRepo.findByEnabled(Boolean.TRUE).stream() .filter(r -> "销售".equals(r.getTriggerScene()) || "通用".equals(r.getTriggerScene())) .toList(); // 构建风险条目 List items = new ArrayList<>(); String overallLevel = "低"; if (blacklisted) { items.add(new CustomerRiskItem("失信", "紧急", "客户已列入失信被执行人名单,禁止合作")); overallLevel = "紧急"; } if (hasActive) { long activeCnt = cases.stream().filter(c -> !"已结案".equals(c.getStatus())).count(); items.add(new CustomerRiskItem("诉讼", "高", "客户存在 " + activeCnt + " 起未结案诉讼,标的金额合计 " + totalAmt.toPlainString() + " 元")); if (!"紧急".equals(overallLevel)) { overallLevel = "高"; } } else if (!cases.isEmpty()) { items.add(new CustomerRiskItem("诉讼历史", "中", "客户有 " + cases.size() + " 起历史诉讼记录(均已结案),注意合同条款设计")); if ("低".equals(overallLevel)) { overallLevel = "中"; } } if ("驳回".equals(latestConclusion)) { items.add(new CustomerRiskItem("法务调查", "高", "法务专项调查结论为「驳回」,建议暂停合作")); if (!"紧急".equals(overallLevel)) { overallLevel = "高"; } } else if ("需进一步调查".equals(latestConclusion)) { items.add(new CustomerRiskItem("法务调查", "中", "法务专项调查尚需进一步核查,建议延缓签约")); if ("低".equals(overallLevel)) { overallLevel = "中"; } } else if ("通过".equals(latestConclusion)) { items.add(new CustomerRiskItem("法务调查", "低", "法务专项调查结论通过,信用状况良好")); } for (ComplianceAlertRule r : salesRules) { items.add(new CustomerRiskItem("合规规则", r.getAlertLevel(), "触发合规预警规则:" + r.getRuleName() + "(" + r.getCondition() + ")")); if ("紧急".equals(r.getAlertLevel())) { overallLevel = "紧急"; } else if ("高".equals(r.getAlertLevel()) && !"紧急".equals(overallLevel)) { overallLevel = "高"; } } String suggestion; switch (overallLevel) { case "紧急" -> suggestion = "禁止签约。客户存在失信/重大法律风险,须经法务总监和总经理特批后方可进行任何商务往来。"; case "高" -> suggestion = "暂缓签约。需法务部门完成专项调查并出具法律意见书,合同须添加特殊担保/保函条款。"; case "中" -> suggestion = "谨慎签约。建议在合同中加强违约责任条款,设置分批付款里程碑,并持续跟踪诉讼进展。"; default -> suggestion = "可以签约。客户信用状况良好,按标准流程审批即可。"; } return ApiResp.ok(new CustomerRiskResult( name, overallLevel, suggestion, blacklisted, hasActive, cases.size(), totalAmt, latestConclusion == null ? "无记录" : latestConclusion, items )); } // ======================================================================== // 2. HR 劳动纠纷案件快速建档(HR→法务自动联动,Gap §9-2) // ======================================================================== public record LaborDisputeRegisterReq( String employeeId, String employeeName, String dept, String disputeType, String description, Double claimAmount, String tribunal, String filingDate, String lawyer ) {} public record LaborDisputeRegisterResult( Long litigationCaseId, String caseNo, boolean nonCompeteTriggered, String nonCompeteStatus, String message ) {} /** * HR 劳动纠纷快速建档。 * 在 LitigationCase 中创建劳动仲裁案件,并自动检查: * 若 employeeId 关联的 HrNonCompete 存在且状态为「在职-未触发」, * 则自动将其推进到「离职-补偿中」状态(离职触发钩子),补完 Gap §9-2 手动建档问题。 */ @PostMapping("/hr-labor-dispute-register") @Transactional public ApiResp hrLaborDisputeRegister( @RequestBody LaborDisputeRegisterReq req) { if (req.employeeName() == null || req.employeeName().isBlank()) { throw new ApiException(400, "employeeName 不能为空"); } // 创建诉讼案件 LitigationCase kase = new LitigationCase(); kase.setCaseNo("LAB-" + System.currentTimeMillis() % 100000); String disputeType = req.disputeType() == null || req.disputeType().isBlank() ? "劳动合同纠纷" : req.disputeType(); kase.setCaseName(req.employeeName() + disputeType + "案"); kase.setCause(disputeType); kase.setOpponent(req.employeeName() + (req.dept() != null && !req.dept().isBlank() ? "(" + req.dept() + ")" : "")); kase.setOurRole("被申请人"); kase.setTribunal(req.tribunal() == null || req.tribunal().isBlank() ? "广州市天河区劳动人事争议仲裁委员会" : req.tribunal()); kase.setAmount(Money.of(req.claimAmount())); kase.setStage("诉前调解"); kase.setLawyer(req.lawyer() == null || req.lawyer().isBlank() ? "待指派" : req.lawyer()); kase.setFilingDate(req.filingDate() == null || req.filingDate().isBlank() ? java.time.LocalDate.now().toString() : req.filingDate()); kase.setResult("审理中"); kase.setStatus("进行中"); kase.setRemark("由 HR 系统劳动纠纷模块自动创建。当事人:" + req.employeeName() + (req.description() != null ? ";" + req.description() : "")); kase.setCreatedAt(Instant.now()); LitigationCase saved = caseRepo.save(kase); // 自动触发竞业限制(联动钩子) boolean ncTriggered = false; String ncStatus = "无竞业限制记录"; if (req.employeeId() != null && !req.employeeId().isBlank()) { List ncs = nonCompeteRepo.findByEmployeeId(req.employeeId()); for (HrNonCompete nc : ncs) { if ("在职-未触发".equals(nc.getStatus())) { nc.setStatus("离职-补偿中"); String today = java.time.LocalDate.now().toString(); if (nc.getStartDate() == null || nc.getStartDate().isBlank()) { nc.setStartDate(today); if (nc.getRestrictionPeriod() != null && nc.getRestrictionPeriod() > 0) { java.time.LocalDate end = java.time.LocalDate.now() .plusMonths(nc.getRestrictionPeriod()); nc.setEndDate(end.toString()); } } nonCompeteRepo.save(nc); ncTriggered = true; ncStatus = "已自动推进为「离职-补偿中」"; break; } ncStatus = nc.getStatus(); } } String msg = "劳动仲裁案件已创建(案号 " + saved.getCaseNo() + ")" + (ncTriggered ? ",竞业限制已自动触发" : ""); return ApiResp.ok(new LaborDisputeRegisterResult( saved.getId(), saved.getCaseNo(), ncTriggered, ncStatus, msg)); } // ======================================================================== // 3. 知识产权法务状态同步看板(IP部→法务部,Gap §9-3) // ======================================================================== public record IpStatusItem( Long id, String proposalNo, String name, String ipType, String stage, String legalStatus, String grantDate, double totalCost, String agency ) {} public record IpStatusSyncResult( int totalAssets, int authorizedMaintaining, int pendingFee, int expiringSoon, int invalidTerminated, int inApplication, List atRiskAssets, Map byType, Map byLegalStatus ) {} /** * 知识产权法务状态同步看板。 * 从 IpAsset 台账聚合法务关注的核心维度,让法务部门无需进入知识产权部系统即可: * - 掌握全量 IP 资产保护状态分布; * - 快速识别已授权维持/即将失效/年费逾期的高风险资产; * - 与法务诉讼台账(专利侵权案)进行对照分析。 * 补完 Gap §9-3:跨部门自动同步机制(只读聚合,无双写风险)。 */ @GetMapping("/ip-status-sync") public ApiResp ipStatusSync() { List all = ipAssetRepo.findAll(); int total = all.size(); int authorized = 0, pendingFee = 0, expiringSoon = 0, invalid = 0, inApp = 0; Map byType = new LinkedHashMap<>(); Map byLegal = new LinkedHashMap<>(); List atRisk = new ArrayList<>(); String today = java.time.LocalDate.now().toString(); String in90 = java.time.LocalDate.now().plusDays(90).toString(); for (IpAsset a : all) { String ls = a.getLegalStatus() == null ? "未知" : a.getLegalStatus(); String stage = a.getStage() == null ? "未知" : a.getStage(); String ipType = a.getIpType() == null ? "其他" : a.getIpType(); byType.merge(ipType, 1, Integer::sum); byLegal.merge(ls, 1, Integer::sum); switch (ls) { case "授权维持" -> authorized++; case "申请中", "公开", "实审" -> inApp++; case "失效终止", "无效宣告" -> invalid++; } // 到期预警:授权维持且授权日+20年在90天内(发明专利20年保护期近) boolean atRiskFlag = false; if ("授权维持".equals(ls) && a.getGrantDate() != null && !a.getGrantDate().isBlank()) { try { java.time.LocalDate gd = java.time.LocalDate.parse(a.getGrantDate().substring(0, 10)); java.time.LocalDate expiry = gd.plusYears(20); String expiryStr = expiry.toString(); if (expiryStr.compareTo(today) >= 0 && expiryStr.compareTo(in90) <= 0) { expiringSoon++; atRiskFlag = true; } } catch (Exception ignored) {} } if ("无效宣告".equals(ls) || "失效终止".equals(ls)) { atRiskFlag = true; } if (atRiskFlag) { atRisk.add(new IpStatusItem( a.getId(), a.getProposalNo(), a.getName(), a.getIpType(), stage, ls, a.getGrantDate(), a.getTotalCost() == null ? 0.0 : a.getTotalCost().doubleValue(), a.getAgency() )); } } return ApiResp.ok(new IpStatusSyncResult( total, authorized, pendingFee, expiringSoon, invalid, inApp, atRisk, byType, byLegal )); } // ======================================================================== // 4. 综合跨部门联动统计看板(Dashboard 聚合接口) // ======================================================================== public record LegalCrossDeptDashboard( // 诉讼看板 long litigationTotal, long litigationActive, double litigationTotalAmount, // 竞业限制看板 long nonCompeteTotal, long nonCompeteActive, long nonCompeteViolated, // 合规规则看板 long alertRuleTotal, long alertRuleEnabled, // 信用调查看板 long investTotal, long investDriven, // 知识产权看板 long ipTotal, long ipAuthorized ) {} /** * 跨部门联动统计看板:法务部门首页仪表盘所需的综合数字, * 一次调用返回诉讼/竞业/合规规则/信用调查/知识产权五个维度数字。 */ @GetMapping("/dashboard") public ApiResp dashboard() { List cases = caseRepo.findAll(); long litActive = cases.stream().filter(c -> !"已结案".equals(c.getStatus())).count(); BigDecimal litAmt = BigDecimal.ZERO; for (LitigationCase c : cases) { litAmt = Money.add(litAmt, c.getAmount() == null ? BigDecimal.ZERO : c.getAmount()); } List ncs = nonCompeteRepo.findAll(); long ncActive = ncs.stream().filter(nc -> "离职-补偿中".equals(nc.getStatus()) || "在职-未触发".equals(nc.getStatus())).count(); long ncViolated = ncs.stream().filter(nc -> "已违约".equals(nc.getStatus())).count(); List rules = ruleRepo.findAll(); long ruleEnabled = rules.stream().filter(r -> Boolean.TRUE.equals(r.getEnabled())).count(); List invests = investRepo.findAll(); long investDriven = invests.stream().filter(inv -> "驳回".equals(inv.getConclusion())).count(); List ipAssets = ipAssetRepo.findAll(); long ipAuthorized = ipAssets.stream() .filter(a -> "授权维持".equals(a.getLegalStatus())).count(); return ApiResp.ok(new LegalCrossDeptDashboard( cases.size(), litActive, litAmt.doubleValue(), ncs.size(), ncActive, ncViolated, rules.size(), ruleEnabled, invests.size(), investDriven, ipAssets.size(), ipAuthorized )); } }