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