package com.kaidi.oa.web; import com.fasterxml.jackson.databind.ObjectMapper; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.Contract; import com.kaidi.oa.domain.FormInstance; import com.kaidi.oa.domain.LegalAuthLimit; import com.kaidi.oa.repository.ContractRepository; import com.kaidi.oa.repository.FormInstanceRepository; import com.kaidi.oa.repository.LegalAuthLimitRepository; import com.kaidi.oa.service.WorkflowService; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 合同审批流接入 OA 审批引擎(内控部 / 法务风险部 · 合同全生命周期管理)。 * * 补齐缺口:合同审批流未接入 OA 审批引擎(ApprovalInstance/WorkflowEngine), * 差异化审批流程靠 status 字段手动推进,无引擎驱动联动。 * * 本控制器是 Contract 与 WorkflowService(FormInstance/FlowTask)之间的桥接层: * - POST /legal-contract-flow/{contractId}/submit:为合同发起「合同法务审批单」 * (templateId=legal-contract-review) 流程实例,返回 FormInstance id; * - POST /legal-contract-flow/{contractId}/advance:推进流程节点(同意/退回/转交); * - GET /legal-contract-flow/{contractId}/status:查询合同对应的审批实例状态; * - GET /legal-contract-flow/pending:查询待当前用户审批的合同流程; * - GET /legal-contract-flow/stats:合同审批流统计(各状态数量)。 * * 合同状态机联动: * - 提交流程时:Contract.status → 「审批中」; * - 流程全部通过(已办结)时:Contract.status → 「履约中」(由 TriggerRuleEngine 驱动); * - 任一节点退回时:Contract.status → 「草稿」(可重新修改再提交)。 * * 读口含合同金额敏感信息,前端通过 http 标准请求头携带 token;写口受 default-deny 保护。 */ @RestController @RequestMapping("/api/oa/legal-contract-flow") public class LegalContractFlowController { /** OA 审批模板 ID,需在 TemplateSeedData 中已注册(种子数据已添加)。 */ private static final String TEMPLATE_ID = "legal-contract-review"; private final ContractRepository contractRepo; private final FormInstanceRepository instanceRepo; private final WorkflowService workflowService; private final ObjectMapper objectMapper; private final LegalAuthLimitRepository authLimitRepo; public LegalContractFlowController(ContractRepository contractRepo, FormInstanceRepository instanceRepo, WorkflowService workflowService, ObjectMapper objectMapper, LegalAuthLimitRepository authLimitRepo) { this.contractRepo = contractRepo; this.instanceRepo = instanceRepo; this.workflowService = workflowService; this.objectMapper = objectMapper; this.authLimitRepo = authLimitRepo; } // ==================== 提交审批流 ==================== public record SubmitRequest( String applicant, String applyDept, String riskLevel, String riskPoints, String remark) { } /** * 为合同发起 OA 审批引擎流程(合同法务审批单)。 * 调用此接口后,审批任务将在 OA 待办中体现,法务/财务/高管可在流程中心审批。 * 合同 status 自动变「审批中」。 */ @PostMapping("/{contractId}/submit") @Transactional public ApiResp> submitReview(@PathVariable Long contractId, @RequestBody SubmitRequest req) { Contract c = contractRepo.findById(contractId) .orElseThrow(() -> new NotFoundException("合同不存在: " + contractId)); if ("审批中".equals(c.getStatus())) { throw new ApiException(409, "合同 [" + c.getCode() + "] 已有进行中的审批流,请勿重复提交"); } String applicant = req.applicant() == null || req.applicant().isBlank() ? "发起人" : req.applicant(); // ---- 授权前置校验(补齐缺口8:业务流程主动调用 authLimit/check 做超授权硬拦截)---- // 查询申请人是否具有有效的「合同签署」授权且金额上限覆盖本合同金额。 // 若授权记录不存在(未录入),仅作 riskLevel 提升,不阻断(允许法务在审批流中再审)。 // 若授权记录存在但金额超限,则硬拒绝(409),要求先申请超限授权。 BigDecimal contractAmount = Money.nz(c.getAmount()); List authLimits = authLimitRepo.findByGrantee(applicant).stream() .filter(a -> "有效".equals(a.getStatus()) && "合同签署".equals(a.getAuthType())) .toList(); if (!authLimits.isEmpty() && contractAmount.compareTo(BigDecimal.ZERO) > 0) { BigDecimal maxLimit = authLimits.stream() .map(LegalAuthLimit::getAmountLimit) .max(BigDecimal::compareTo) .orElse(BigDecimal.ZERO); // amountLimit=0 表示无金额上限授权 if (maxLimit.compareTo(BigDecimal.ZERO) > 0 && contractAmount.compareTo(maxLimit) > 0) { throw new ApiException(409, "超授权拦截:申请人 [" + applicant + "] 合同签署授权上限为 " + maxLimit + " 元,合同金额 " + contractAmount + " 元已超限," + "请先向授权管理台账申请提额后再发起审批。"); } } // ---- 授权前置校验结束 ---- // 构建表单 dataJson Map data = new LinkedHashMap<>(); data.put("applicant", applicant); data.put("applyDept", req.applyDept() == null ? "" : req.applyDept()); data.put("contractName", c.getName() != null ? c.getName() : ""); data.put("counterparty", c.getPartyB() != null ? c.getPartyB() : ""); data.put("amount", c.getAmount() != null ? c.getAmount().toPlainString() : "0"); data.put("signDate", c.getSignDate() != null ? c.getSignDate() : ""); data.put("contractType", c.getType() != null ? c.getType() : "其他"); data.put("riskLevel", req.riskLevel() == null ? "中" : req.riskLevel()); data.put("riskPoints", req.riskPoints() == null ? "" : req.riskPoints()); data.put("remark", req.remark() == null ? "" : req.remark()); String dataJson; try { dataJson = objectMapper.writeValueAsString(data); } catch (Exception e) { dataJson = "{}"; } String title = "合同法务审批 - " + (c.getCode() != null ? c.getCode() : "") + " - " + (c.getName() != null ? c.getName() : ""); FormInstance inst = workflowService.submit(TEMPLATE_ID, dataJson, title, applicant); // 更新合同状态为「审批中」 c.setStatus("审批中"); contractRepo.save(c); // 若申请人无授权记录,在响应中标记 authWarning 提示 boolean noAuthRecord = authLimits.isEmpty(); Map m = new LinkedHashMap<>(); m.put("contractId", contractId); m.put("contractCode", c.getCode()); m.put("instanceId", inst.getId()); m.put("instanceStatus", inst.getStatus()); m.put("contractStatus", c.getStatus()); m.put("authWarning", noAuthRecord ? "申请人 [" + applicant + "] 无合同签署授权记录,已提交审批,请法务节点重点审核授权合规性" : null); return ApiResp.ok(m); } // ==================== 推进节点 ==================== public record AdvanceRequest(String action, String opinion, String user, String targetUser) { } /** * 推进合同审批节点(同意/退回/转交/加签/知会)。 * 办结时合同状态自动变「履约中」;退回时变「草稿」。 */ @PostMapping("/{contractId}/advance") @Transactional public ApiResp> advance(@PathVariable Long contractId, @RequestBody AdvanceRequest req) { Contract c = contractRepo.findById(contractId) .orElseThrow(() -> new NotFoundException("合同不存在: " + contractId)); FormInstance inst = findActiveInstance(contractId); if (inst == null) { throw new ApiException(409, "合同 [" + c.getCode() + "] 当前无进行中的审批流"); } String user = req.user() == null || req.user().isBlank() ? req.opinion() : req.user(); FormInstance updated; if (req.targetUser() != null && !req.targetUser().isBlank()) { updated = workflowService.advance(inst.getId(), req.action(), req.opinion(), user, req.targetUser()); } else { updated = workflowService.advance(inst.getId(), req.action(), req.opinion(), user); } // 联动合同状态 if ("已办结".equals(updated.getStatus())) { c.setStatus("履约中"); contractRepo.save(c); } else if ("已退回".equals(updated.getStatus())) { c.setStatus("草稿"); contractRepo.save(c); } Map m = new LinkedHashMap<>(); m.put("contractId", contractId); m.put("instanceId", updated.getId()); m.put("instanceStatus", updated.getStatus()); m.put("contractStatus", c.getStatus()); return ApiResp.ok(m); } // ==================== 查询合同审批状态 ==================== /** * 查询某合同的当前审批流实例状态。 */ @GetMapping("/{contractId}/status") public ApiResp> status(@PathVariable Long contractId) { Contract c = contractRepo.findById(contractId) .orElseThrow(() -> new NotFoundException("合同不存在: " + contractId)); FormInstance inst = findActiveInstance(contractId); Map m = new LinkedHashMap<>(); m.put("contractId", contractId); m.put("contractCode", c.getCode()); m.put("contractName", c.getName()); m.put("contractStatus", c.getStatus()); m.put("instanceId", inst != null ? inst.getId() : null); m.put("instanceStatus", inst != null ? inst.getStatus() : "无流程"); m.put("currentNode", inst != null ? inst.getCurrentNode() : null); return ApiResp.ok(m); } /** 查询所有待某用户审批的合同流程。 */ @GetMapping("/pending") public ApiResp> pending(@RequestParam String user) { return ApiResp.ok(instanceRepo.findByStatus("待办").stream() .filter(i -> TEMPLATE_ID.equals(i.getTemplateId())) .toList()); } /** 合同审批流整体统计。 */ @GetMapping("/stats") public ApiResp> stats() { List all = instanceRepo.findAll().stream() .filter(i -> TEMPLATE_ID.equals(i.getTemplateId())).toList(); Map m = new LinkedHashMap<>(); m.put("total", all.size()); m.put("pending", all.stream().filter(i -> "待办".equals(i.getStatus())).count()); m.put("done", all.stream().filter(i -> "已办结".equals(i.getStatus())).count()); m.put("returned", all.stream().filter(i -> "已退回".equals(i.getStatus())).count()); return ApiResp.ok(m); } // ---------- helpers ---------- /** 找合同对应的最新进行中的审批实例(按 title 前缀匹配 contractCode)。 */ private FormInstance findActiveInstance(Long contractId) { Contract c = contractRepo.findById(contractId).orElse(null); if (c == null) return null; String prefix = "合同法务审批 - " + (c.getCode() != null ? c.getCode() : ""); return instanceRepo.findByStatusIn(List.of("待办", "办理中")).stream() .filter(i -> TEMPLATE_ID.equals(i.getTemplateId()) && i.getTitle() != null && i.getTitle().startsWith(prefix)) .findFirst().orElse(null); } }