feat: 立项与合同HCS前端接入配套端点——C103-8合同特批发起/B103结果回传、C103-2/5附件代理、C101-1风险与C103-6/12主数据查询,合同推送高风险客商挂起待特批
This commit is contained in:
+5
@@ -84,6 +84,11 @@ public final class HcsSyncConstant {
|
||||
*/
|
||||
public static final String CHECK_TYPE_CREDIT = "credit";
|
||||
|
||||
/**
|
||||
* 校验类型:合同特批(C103-8 发起 / B103 结果回传留痕)
|
||||
*/
|
||||
public static final String CHECK_TYPE_SPEC = "spec";
|
||||
|
||||
/**
|
||||
* 校验结果:通过
|
||||
*/
|
||||
|
||||
+140
@@ -26,14 +26,27 @@
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import feign.Response;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
|
||||
import org.springblade.hcs.dto.HcsCurrencyVO;
|
||||
import org.springblade.hcs.dto.HcsRiskVO;
|
||||
import org.springblade.hcs.dto.HcsTemplateVO;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.support.hcs.HcsAttachmentSupport;
|
||||
import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
|
||||
import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport;
|
||||
@@ -41,12 +54,22 @@ import org.springblade.transport.support.hcs.HcsReconSupport;
|
||||
import org.springblade.transport.support.hcs.HcsMasterDataSupport;
|
||||
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
|
||||
import org.springblade.transport.task.HcsPushTask;
|
||||
import org.springframework.http.MediaType;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HCS 合规推送端点(设计文档 §5.0 手动入口)
|
||||
@@ -67,6 +90,9 @@ public class HcsSyncController {
|
||||
private final HcsPushTask hcsPushTask;
|
||||
private final HcsProjectSyncSupport hcsProjectSyncSupport;
|
||||
private final HcsContractSyncSupport hcsContractSyncSupport;
|
||||
private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport;
|
||||
private final HcsCreditCheckSupport hcsCreditCheckSupport;
|
||||
private final HcsAttachmentSupport hcsAttachmentSupport;
|
||||
private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
|
||||
private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
|
||||
private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
|
||||
@@ -171,4 +197,118 @@ public class HcsSyncController {
|
||||
public R<?> reconInvoiceRegistration(@Parameter(description = "开票申请单号", required = true) @RequestParam String applicationNo) {
|
||||
return R.data(hcsReconSupport.reconInvoiceRegistration(applicationNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规模板列表(C103-6,Redis 缓存优先,前端接入设计 §3.1)
|
||||
*/
|
||||
@GetMapping("/master-data/template")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "合规模板列表查询", description = "C103-6 模板列表,走服务端 Redis 缓存,未命中自动全量拉取")
|
||||
public R<List<HcsTemplateVO>> templates() {
|
||||
return R.data(hcsMasterDataSupport.getTemplates());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规币种列表(C103-12,Redis 缓存优先,前端接入设计 §3.1)
|
||||
*/
|
||||
@GetMapping("/master-data/currency")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "合规币种列表查询", description = "C103-12 币种列表,走服务端 Redis 缓存,未命中自动全量拉取")
|
||||
public R<List<HcsCurrencyVO>> currencies() {
|
||||
return R.data(hcsMasterDataSupport.getCurrencies());
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路)
|
||||
*/
|
||||
@GetMapping("/risk")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),结果留痕")
|
||||
public R<HcsRiskVO> risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode) {
|
||||
return R.data(hcsCreditCheckSupport.queryRiskByCustomerCode(customerCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件上传到合同系统(C103-2 代理,前端接入设计 §3.2),返回合规附件ID
|
||||
*/
|
||||
@PostMapping(value = "/attachment/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "附件上传到合同系统", description = "C103-2 代理:单文件上传至合规,返回合规附件ID(fdId)")
|
||||
public R<String> uploadAttachment(@Parameter(description = "附件文件", required = true) @RequestParam("fdFile") MultipartFile fdFile) {
|
||||
return R.data(hcsAttachmentSupport.uploadOne(fdFile, AuthUtil.getUserAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同附件下载(C103-5 代理,前端接入设计 §3.2):文件流透传浏览器
|
||||
*/
|
||||
@GetMapping("/attachment/download")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名")
|
||||
public void downloadAttachment(@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
|
||||
@Parameter(description = "下载文件名(可选)") @RequestParam(required = false) String name,
|
||||
HttpServletResponse response) throws Exception {
|
||||
String fileName = StringUtil.isBlank(name) ? "hcs-attachment-" + fdId : name;
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"));
|
||||
try (Response hcsResponse = hcsAttachmentSupport.downloadRaw(fdId);
|
||||
InputStream inputStream = hcsResponse.body().asInputStream();
|
||||
OutputStream outputStream = response.getOutputStream()) {
|
||||
inputStream.transferTo(outputStream);
|
||||
outputStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同发起特批(C103-8,前端接入设计 §3.4):body = { contractId, attachments: [{fdId, name}] }
|
||||
*/
|
||||
@PostMapping("/contract/spec-apply")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "合同发起特批", description = "C103-8:高风险客商合同特批发起,attachments 为经 /hcs/attachment/upload 上传的合规附件引用")
|
||||
@SuppressWarnings("unchecked")
|
||||
public R<String> applyContractSpec(@RequestBody Map<String, Object> body) {
|
||||
Object contractIdValue = body == null ? null : body.get("contractId");
|
||||
if (contractIdValue == null || StringUtil.isBlank(String.valueOf(contractIdValue))) {
|
||||
return R.fail("contractId 不能为空");
|
||||
}
|
||||
Long contractId = Long.valueOf(String.valueOf(contractIdValue));
|
||||
Object attachmentsValue = body == null ? null : body.get("attachments");
|
||||
List<HcsAttachmentRefDTO> attachments = (attachmentsValue instanceof List<?> list ? list : List.<Object>of()).stream()
|
||||
.filter(Map.class::isInstance)
|
||||
.map(item -> {
|
||||
Map<String, Object> attachment = (Map<String, Object>) item;
|
||||
HcsAttachmentRefDTO ref = new HcsAttachmentRefDTO();
|
||||
// 附件ID:C103-2 上传返回的合规附件ID
|
||||
ref.setFdId(String.valueOf(attachment.get("fdId")));
|
||||
// 附件名
|
||||
ref.setFdFileName(attachment.get("name") == null ? null : String.valueOf(attachment.get("name")));
|
||||
return ref;
|
||||
})
|
||||
.toList();
|
||||
return R.data(hcsContractSpecSyncSupport.applySpec(contractId, attachments, AuthUtil.getUserAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同特批结果人工确认(§6-U1 过渡方案:B103 报文未到位前与回调等效)
|
||||
*/
|
||||
@PostMapping("/contract/spec/confirm")
|
||||
@ApiOperationSupport(order = 14)
|
||||
@Operation(summary = "合同特批结果人工确认", description = "传入合同ID 与结果(approved/rejected),仅特批中(processing)允许确认")
|
||||
public R<String> confirmContractSpec(@Parameter(description = "合同ID", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "确认结果 approved/rejected", required = true) @RequestParam String result) {
|
||||
return R.data(hcsContractSpecSyncSupport.confirmSpecResult(id, result));
|
||||
}
|
||||
|
||||
/**
|
||||
* B103 特批结果回传接收(合规系统→TMS 回调,公开端点 + 原文留痕,前端接入设计 §3.4)
|
||||
* <p>
|
||||
* TODO 待联调(§6-U1):B103 报文未定,当前按候选字段(contractNo/fdId/result 等)解析,
|
||||
* 解析失败仅留痕不置状态,报文口径确认后完善解析。
|
||||
*/
|
||||
@PostMapping("/callback/spec-result")
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "B103特批结果回传", description = "合规系统回调:合同特批结果回传接收,原文留痕并回写合同特批状态")
|
||||
public R<String> specResultCallback(@RequestBody(required = false) Map<String, Object> body) {
|
||||
return R.data(hcsContractSpecSyncSupport.handleSpecCallback(body));
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -33,6 +33,7 @@ import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.constant.HcsConstant;
|
||||
import org.springblade.hcs.dto.HcsAttachmentDownloadDTO;
|
||||
import org.springblade.hcs.dto.HcsAttachmentVO;
|
||||
import org.springblade.hcs.dto.HcsResult;
|
||||
import org.springblade.hcs.exception.HcsException;
|
||||
@@ -123,6 +124,44 @@ public class HcsAttachmentSupport {
|
||||
return attachmentFdIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端单文件直传合规(C103-2 代理,前端接入设计 §3.2):返回合规附件ID;
|
||||
* 不写映射缓存(无 TMS 附件元数据键),调用方自行持久化 fdId(如特批材料 hcs_spec_files_json)
|
||||
*
|
||||
* @param file 上传文件
|
||||
* @param loginName 操作人登录名(fdLoginName)
|
||||
* @return 合规附件ID
|
||||
*/
|
||||
public String uploadOne(MultipartFile file, String loginName) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new HcsException("HCS附件上传失败:文件为空");
|
||||
}
|
||||
HcsResult<HcsAttachmentVO> result = hcsClient.uploadAttachment(file, "false", loginName);
|
||||
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
|
||||
log.error("HCS附件上传失败响应:{}", result == null ? "无响应" : String.format("code=%s, msg=%s, traceId=%s",
|
||||
result.getCode(), result.getMsg(), result.getTraceId()));
|
||||
throw new HcsException("HCS附件上传失败:" + (result == null ? "响应为空" : result.getMsg()));
|
||||
}
|
||||
return result.getData().getFdId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规附件下载(C103-5 代理,前端接入设计 §3.2):返回原始文件流响应,由控制器透传浏览器
|
||||
*
|
||||
* @param fdId 合规附件ID
|
||||
* @return Feign 文件流响应(调用方负责关闭)
|
||||
*/
|
||||
public feign.Response downloadRaw(String fdId) {
|
||||
HcsAttachmentDownloadDTO param = new HcsAttachmentDownloadDTO();
|
||||
// 【必填】附件ID:上传时返回的 fdId
|
||||
param.setFdId(fdId);
|
||||
feign.Response response = hcsClient.downloadAttachment(param);
|
||||
if (response == null || response.body() == null) {
|
||||
throw new HcsException("HCS附件下载失败:响应为空(fdId=" + fdId + ")");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转传单个附件:映射缓存命中直接返回,未命中下载上传后写缓存
|
||||
*/
|
||||
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.support.hcs;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
|
||||
import org.springblade.hcs.dto.HcsMechanismsDTO;
|
||||
import org.springblade.hcs.dto.HcsResult;
|
||||
import org.springblade.hcs.dto.HcsSpecContractSaveDTO;
|
||||
import org.springblade.hcs.dto.HcsFormSaveVO;
|
||||
import org.springblade.hcs.exception.HcsException;
|
||||
import org.springblade.hcs.feign.IHcsClient;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.HcsContractCheckLogMapper;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.HcsContractCheckLog;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HCS 合同特批同步支撑组件(C103-8 发起特批 / B103 特批结果回传,前端接入设计 §3.4)
|
||||
* <p>
|
||||
* 业务闭环:合同推送时 C101-1 风险校验发现黑名单 → 置 hcs_spec_status=need 挂起(推送链路门禁跳过)→
|
||||
* 内部特批审批通过后,前端上传特批材料(C103-2 取合规附件ID)并发起 C103-8 → 状态 processing(流程挂起)→
|
||||
* 集团审批结果经 B103 回传 TMS → approved 后既有扫描任务自动恢复 C103-1 推送,rejected 可重新发起。
|
||||
* <p>
|
||||
* TODO 待联调确认(前端接入设计 §6-U1):C103-8 与 B103 报文 v4 均未提供,
|
||||
* C103-8 按 C106-3 模式预估(双层嵌套返回),B103 回调按候选字段解析、原文留痕 blade_hcs_check_log。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HcsContractSpecSyncSupport {
|
||||
|
||||
/**
|
||||
* 特批附件字段标识(照 C106-3 fd_spc_approval_attach 惯例预估,§6-U1 以合规方报文为准)
|
||||
*/
|
||||
private static final String SPEC_ATTACH_ENTITY_KEY = "fd_spc_approval_attach";
|
||||
|
||||
/**
|
||||
* B103 原文留痕最大长度(blade_hcs_check_log.msg 为 varchar(512))
|
||||
*/
|
||||
private static final int CALLBACK_MSG_MAX_LENGTH = 500;
|
||||
|
||||
private final IHcsClient hcsClient;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
|
||||
|
||||
/**
|
||||
* 发起合同特批(C103-8):校验合同与特批状态 → 组装预估报文 → 回填特批流程ID,状态置 processing
|
||||
*
|
||||
* @param contractId 合同ID
|
||||
* @param attachments 特批附件引用(fdId=C103-2 合规附件ID,fdFileName=文件名)
|
||||
* @param loginName 当前操作人登录名
|
||||
* @return 结果描述
|
||||
*/
|
||||
public String applySpec(Long contractId, List<HcsAttachmentRefDTO> attachments, String loginName) {
|
||||
ContractManage contractManage = requireAppliableContract(contractId);
|
||||
if (attachments == null || attachments.isEmpty()) {
|
||||
throw new ServiceException("请先上传特批申请材料(含内部审批流程附件)");
|
||||
}
|
||||
HcsSpecContractSaveDTO param = new HcsSpecContractSaveDTO();
|
||||
// 【必填】操作人登录名:当前登录人(§9-U2)
|
||||
param.setFdLoginName(loginName);
|
||||
// 【必填】合规合同ID:blade_contract_manage.hcs_fd_id(非名称,照 C106-3 fd_contract_name 惯例)
|
||||
param.setFdContractId(requireContractFdId(contractManage));
|
||||
// 【必填】合同编号
|
||||
param.setFdContractNo(contractManage.getContractNo());
|
||||
// 【必填】单据主题:"合同特批-" + 合同编号
|
||||
param.setFdDocSubject("合同特批-" + contractManage.getContractNo());
|
||||
// 特批事由:合同名称(非必传)
|
||||
param.setFdReason(contractManage.getContractName());
|
||||
// 特批附件:发起带 fdBindType=ADD(entityKey 预估照 C106-3,§6-U1)
|
||||
param.setMechanisms(buildMechanisms(attachments));
|
||||
// 特批材料留痕(含合规附件ID,供重新发起与排查)
|
||||
contractManage.setHcsSpecFilesJson(JSONUtil.toJsonStr(attachments));
|
||||
try {
|
||||
log.info("HCS合同特批发起开始:contractNo={},附件 {} 个", contractManage.getContractNo(), attachments.size());
|
||||
// TODO 待联调(§6-U1):报文按 C106-3 模式预估,双层嵌套 data.data.fdId
|
||||
HcsResult<HcsResult<HcsFormSaveVO>> result = hcsClient.addSpecContract(param);
|
||||
String specFdId = extractNestedFdId(contractManage, result);
|
||||
contractManage.setHcsSpecFdId(specFdId);
|
||||
markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_PROCESSING, "C103-8 合同特批发起成功");
|
||||
writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_PASS, "C103-8 特批发起成功,特批流程ID=" + specFdId, result.getTraceId());
|
||||
log.info("HCS合同特批发起成功:contractNo={},specFdId={}", contractManage.getContractNo(), specFdId);
|
||||
return "特批发起成功,等待集团审批";
|
||||
} catch (Exception exception) {
|
||||
String message = StringUtil.isBlank(exception.getMessage()) ? "特批发起异常" : exception.getMessage();
|
||||
// 失败保持待特批(need),允许重试发起
|
||||
markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_NEED, "C103-8 特批发起失败:" + message);
|
||||
writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_FAIL, "C103-8 特批发起失败:" + message, null);
|
||||
throw exception instanceof ServiceException serviceException ? serviceException
|
||||
: new ServiceException("合同特批发起失败:" + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 特批结果人工确认(§6-U1 过渡方案:B103 报文未到位前,页面确认与回调等效)
|
||||
*
|
||||
* @param id 合同ID
|
||||
* @param result approved/rejected
|
||||
* @return 结果描述
|
||||
*/
|
||||
public String confirmSpecResult(Long id, String result) {
|
||||
ContractManage contractManage = contractManageMapper.selectById(id);
|
||||
if (contractManage == null) {
|
||||
throw new ServiceException("合同单不存在:" + id);
|
||||
}
|
||||
if (!List.of(HcsSyncConstant.SPEC_STATUS_APPROVED, HcsSyncConstant.SPEC_STATUS_REJECTED).contains(result)) {
|
||||
throw new ServiceException("特批结果仅支持 approved/rejected");
|
||||
}
|
||||
if (!HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())) {
|
||||
throw new ServiceException("仅特批中的合同允许确认结果(当前状态:" + contractManage.getHcsSpecStatus() + ")");
|
||||
}
|
||||
markSpec(contractManage, result, "特批结果人工确认:" + result);
|
||||
writeCheckLog(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)
|
||||
? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL, "特批结果人工确认:" + result, null);
|
||||
log.info("HCS合同特批结果人工确认:contractNo={},specFdId={},结果={}", contractManage.getContractNo(), contractManage.getHcsSpecFdId(), result);
|
||||
return "特批结果已确认:" + result;
|
||||
}
|
||||
|
||||
/**
|
||||
* B103 特批结果回传处理(回调端点入口):候选字段解析 + 原文留痕,
|
||||
* approved 时合同保持待同步(unsynced),由既有扫描任务自动恢复 C103-1 推送
|
||||
*
|
||||
* @param body B103 回调报文(结构待联调,原文全量留痕)
|
||||
* @return 处理结果描述
|
||||
*/
|
||||
public String handleSpecCallback(Map<String, Object> body) {
|
||||
String raw = body == null ? "" : JSONUtil.toJsonStr(body);
|
||||
String contractNo = firstNonBlank(body, "fdContractNo", "contractNo", "fd_contract_no", "fdNumber");
|
||||
String specFdId = firstNonBlank(body, "fdId", "fdSpecId", "fd_spec_id", "specFdId");
|
||||
String resultText = firstNonBlank(body, "result", "fdResult", "fd_result", "approvalResult", "status");
|
||||
ContractManage contractManage = locateContract(contractNo, specFdId);
|
||||
if (contractManage == null) {
|
||||
log.warn("B103特批结果回传未定位到合同:contractNo={},specFdId={},报文已留痕", contractNo, specFdId);
|
||||
writeCallbackLog(null, HcsSyncConstant.CHECK_RESULT_SKIP, "B103 回传未定位到合同:" + raw);
|
||||
return "已留痕:未定位到合同(contractNo=" + contractNo + ")";
|
||||
}
|
||||
Boolean approved = parseSpecResult(resultText);
|
||||
if (approved == null) {
|
||||
log.warn("B103特批结果字段未识别:contractNo={},resultText={},报文已留痕", contractManage.getContractNo(), resultText);
|
||||
writeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP, "B103 回传结果字段未识别:" + raw);
|
||||
return "已留痕:结果字段未识别(contractNo=" + contractManage.getContractNo() + ")";
|
||||
}
|
||||
String status = approved ? HcsSyncConstant.SPEC_STATUS_APPROVED : HcsSyncConstant.SPEC_STATUS_REJECTED;
|
||||
markSpec(contractManage, status, "B103 特批结果回传:" + raw);
|
||||
writeCallbackLog(contractManage, approved ? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL,
|
||||
"B103 特批结果回传:" + raw);
|
||||
log.info("B103特批结果回传处理完成:contractNo={},结果={}", contractManage.getContractNo(), status);
|
||||
return "特批结果已处理:" + status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验可发起特批的合同:存在、审批通过、特批状态为待特批/已驳回
|
||||
*/
|
||||
private ContractManage requireAppliableContract(Long contractId) {
|
||||
ContractManage contractManage = contractManageMapper.selectById(contractId);
|
||||
if (contractManage == null) {
|
||||
throw new ServiceException("合同单不存在:" + contractId);
|
||||
}
|
||||
if (!List.of("approved", "change_approved").contains(contractManage.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的合同可发起特批(当前状态:" + contractManage.getApprovalStatus() + ")");
|
||||
}
|
||||
if (!List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_REJECTED).contains(contractManage.getHcsSpecStatus())) {
|
||||
throw new ServiceException("仅待特批/已驳回的合同可发起特批(当前状态:" + contractManage.getHcsSpecStatus() + ")");
|
||||
}
|
||||
return contractManage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规合同ID(合同须已推送合规回填 hcs_fd_id)
|
||||
*/
|
||||
private String requireContractFdId(ContractManage contractManage) {
|
||||
if (StringUtil.isBlank(contractManage.getHcsFdId())) {
|
||||
throw new ServiceException("合同尚未推送合规,无法发起特批(contractNo=" + contractManage.getContractNo() + ")");
|
||||
}
|
||||
return contractManage.getHcsFdId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 特批附件挂载(发起带 fdBindType=ADD)
|
||||
*/
|
||||
private HcsMechanismsDTO buildMechanisms(List<HcsAttachmentRefDTO> attachments) {
|
||||
HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
|
||||
mechanisms.setAttachment(attachments.stream().map(ref -> {
|
||||
HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO();
|
||||
// 附件ID:C103-2 转传返回的合规附件ID
|
||||
attachment.setFdId(ref.getFdId());
|
||||
// 附件名(预估字段,§6-U1)
|
||||
attachment.setFdFileName(ref.getFdFileName());
|
||||
// 特批附件字段标识
|
||||
attachment.setFdEntityKey(SPEC_ATTACH_ENTITY_KEY);
|
||||
// 绑定类型:发起传 ADD
|
||||
attachment.setFdBindType("ADD");
|
||||
return attachment;
|
||||
}).toList());
|
||||
return mechanisms;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 C103-8 双层嵌套返回 data.data.fdId(照 C106-3 模式预估)
|
||||
*/
|
||||
private String extractNestedFdId(ContractManage contractManage, HcsResult<HcsResult<HcsFormSaveVO>> result) {
|
||||
if (result == null || !result.isSuccess() || result.getData() == null || !result.getData().isSuccess()
|
||||
|| result.getData().getData() == null || StringUtil.isBlank(result.getData().getData().getFdId())) {
|
||||
log.error("HCS合同特批发起失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
|
||||
throw new HcsException("C103-8 合同特批发起失败:" + (result == null ? "响应为空" : result.getMsg()));
|
||||
}
|
||||
return result.getData().getData().getFdId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按合同编号/特批流程ID 定位合同(B103 报文候选键)
|
||||
*/
|
||||
private ContractManage locateContract(String contractNo, String specFdId) {
|
||||
if (StringUtil.isNotBlank(contractNo)) {
|
||||
ContractManage contractManage = contractManageMapper.selectOne(Wrappers.<ContractManage>lambdaQuery()
|
||||
.eq(ContractManage::getContractNo, contractNo)
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.last("limit 1"));
|
||||
if (contractManage != null) {
|
||||
return contractManage;
|
||||
}
|
||||
}
|
||||
if (StringUtil.isNotBlank(specFdId)) {
|
||||
return contractManageMapper.selectOne(Wrappers.<ContractManage>lambdaQuery()
|
||||
.eq(ContractManage::getHcsSpecFdId, specFdId)
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.last("limit 1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析特批结果文本:approved/通过/pass/true → true;rejected/驳回/refuse/false → false;未识别 → null
|
||||
*/
|
||||
private Boolean parseSpecResult(String resultText) {
|
||||
if (StringUtil.isBlank(resultText)) {
|
||||
return null;
|
||||
}
|
||||
String text = resultText.trim().toLowerCase();
|
||||
if (List.of("approved", "pass", "true", "通过", "审批通过", "同意").contains(text)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
if (List.of("rejected", "refuse", "refused", "false", "驳回", "不通过", "不同意").contains(text)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从候选字段名依次取首个非空字符串值
|
||||
*/
|
||||
private String firstNonBlank(Map<String, Object> body, String... keys) {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
Object value = body.get(key);
|
||||
if (value != null && StringUtil.isNotBlank(String.valueOf(value))) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回写特批状态与动作时间
|
||||
*/
|
||||
private void markSpec(ContractManage contractManage, String status, String message) {
|
||||
contractManage.setHcsSpecStatus(status);
|
||||
contractManage.setHcsSpecTime(LocalDateTime.now());
|
||||
contractManage.setHcsSpecMsg(StringUtil.left(message, 500));
|
||||
contractManageMapper.updateById(contractManage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 特批留痕(blade_hcs_check_log,biz_type=contract,check_type=spec,异常不影响主流程)
|
||||
*/
|
||||
private void writeCheckLog(ContractManage contractManage, String result, String msg, String traceId) {
|
||||
try {
|
||||
HcsContractCheckLog checkLog = new HcsContractCheckLog();
|
||||
checkLog.setTenantId(contractManage.getTenantId());
|
||||
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
|
||||
checkLog.setBizId(contractManage.getId());
|
||||
checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_SPEC);
|
||||
checkLog.setResult(result);
|
||||
checkLog.setMsg(StringUtil.left(msg, CALLBACK_MSG_MAX_LENGTH));
|
||||
checkLog.setTraceId(traceId);
|
||||
checkLog.setCreateTime(LocalDateTime.now());
|
||||
hcsContractCheckLogMapper.insert(checkLog);
|
||||
} catch (Exception exception) {
|
||||
log.error("HCS合同特批留痕写入失败:contractNo={}", contractManage.getContractNo(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* B103 回传留痕(合同未定位到时 bizId 为空,原文留痕便于联调解析)
|
||||
*/
|
||||
private void writeCallbackLog(ContractManage contractManage, String result, String msg) {
|
||||
if (contractManage == null) {
|
||||
try {
|
||||
HcsContractCheckLog checkLog = new HcsContractCheckLog();
|
||||
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
|
||||
checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_SPEC);
|
||||
checkLog.setResult(result);
|
||||
checkLog.setMsg(StringUtil.left(msg, CALLBACK_MSG_MAX_LENGTH));
|
||||
checkLog.setCreateTime(LocalDateTime.now());
|
||||
hcsContractCheckLogMapper.insert(checkLog);
|
||||
} catch (Exception exception) {
|
||||
log.error("B103回传留痕写入失败", exception);
|
||||
}
|
||||
return;
|
||||
}
|
||||
writeCheckLog(contractManage, result, msg, null);
|
||||
}
|
||||
}
|
||||
+29
-9
@@ -100,15 +100,22 @@ public class HcsContractSyncSupport {
|
||||
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
|
||||
|
||||
/**
|
||||
* 推送合同到 HCS:风险校验 → 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
|
||||
* 推送合同到 HCS:特批挂起门禁 → 风险校验(黑名单挂起待特批)→ 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
|
||||
*
|
||||
* @param contractManage 合同单(approvalStatus 须为 approved/change_approved)
|
||||
* @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
|
||||
*/
|
||||
public String pushContract(ContractManage contractManage) {
|
||||
// ① 客商风险校验(可配开关),黑名单且 block-blacklist=true 时阻断推送
|
||||
// ⓪ 特批挂起门禁:待特批/特批中的合同跳过推送,等待 B103 特批结果回传后恢复(C103-8,前端接入设计 §3.4)
|
||||
if (List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(contractManage.getHcsSpecStatus())) {
|
||||
log.info("HCS合同推送跳过(特批挂起中):contractNo={},hcsSpecStatus={}", contractManage.getContractNo(), contractManage.getHcsSpecStatus());
|
||||
return "合同特批挂起中(" + contractManage.getHcsSpecStatus() + "),跳过推送";
|
||||
}
|
||||
// ① 客商风险校验(可配开关),黑名单且 block-blacklist=true 时挂起待特批(不再直接失败)
|
||||
if (hcsProperties.getContract().isRequireRiskCheck()) {
|
||||
checkRelativeRisk(contractManage);
|
||||
if (checkRelativeRisk(contractManage)) {
|
||||
return "高风险客商,合同已挂起待特批(C103-8)";
|
||||
}
|
||||
}
|
||||
// ② 附件转传(先转传并缓存合规附件ID,C103-1 挂载方式待 §9-U8 确认后接线)
|
||||
transferAttachmentsIfEnabled(contractManage);
|
||||
@@ -139,9 +146,13 @@ public class HcsContractSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* C101-1 客商风险校验:甲乙双方各查一次,留痕 check_log 与合同 hcs_risk_* 字段,黑名单阻断
|
||||
* C101-1 客商风险校验:甲乙双方各查一次,留痕 check_log 与合同 hcs_risk_* 字段;
|
||||
* 黑名单且 block-blacklist=true 时不再直接失败,而是置 hcs_spec_status=need 挂起待特批(C103-8);
|
||||
* 特批已通过(approved,单次有效)的合同不重复挂起,保证 B103 回传后推送链路可走通
|
||||
*
|
||||
* @return true=黑名单挂起待特批(调用方跳过本次推送)
|
||||
*/
|
||||
private void checkRelativeRisk(ContractManage contractManage) {
|
||||
private boolean checkRelativeRisk(ContractManage contractManage) {
|
||||
RiskResult riskPartyA = queryRisk(contractManage, "甲方", contractManage.getPartyA());
|
||||
RiskResult riskPartyB = queryRisk(contractManage, "乙方", contractManage.getPartyB());
|
||||
// 风险等级:甲乙双方取先查到的非空值(无法量化比较,留痕双方原始值,联调后细化)
|
||||
@@ -155,11 +166,17 @@ public class HcsContractSyncSupport {
|
||||
log.info("HCS合同风险校验完成:contractNo={},甲方[{}={}/{}],乙方[{}={}/{}]",
|
||||
contractManage.getContractNo(), contractManage.getPartyA(), riskPartyA.blackStatus(), riskPartyA.riskLevel(),
|
||||
contractManage.getPartyB(), riskPartyB.blackStatus(), riskPartyB.riskLevel());
|
||||
if (blacklist && hcsProperties.getRisk().isBlockBlacklist()) {
|
||||
String message = "客商黑名单,禁止推送(甲方[" + riskPartyA.blackStatus() + "]乙方[" + riskPartyB.blackStatus() + "])";
|
||||
log.error("HCS合同推送被风险校验阻断:contractNo={},{}", contractManage.getContractNo(), message);
|
||||
throw new HcsException(message);
|
||||
boolean specApproved = HcsSyncConstant.SPEC_STATUS_APPROVED.equals(contractManage.getHcsSpecStatus());
|
||||
if (blacklist && hcsProperties.getRisk().isBlockBlacklist() && !specApproved) {
|
||||
String message = "高风险客商(甲方[" + riskPartyA.blackStatus() + "]乙方[" + riskPartyB.blackStatus() + "]),合同挂起待特批";
|
||||
contractManage.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
|
||||
contractManage.setHcsSpecTime(LocalDateTime.now());
|
||||
contractManage.setHcsSpecMsg(message);
|
||||
contractManageMapper.updateById(contractManage);
|
||||
log.warn("HCS合同推送因高风险客商挂起待特批:contractNo={},{}", contractManage.getContractNo(), message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,6 +384,9 @@ public class HcsContractSyncSupport {
|
||||
if (!List.of("approved", "change_approved").contains(contractManage.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的合同可推送合规(当前状态:" + contractManage.getApprovalStatus() + ")");
|
||||
}
|
||||
if (List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(contractManage.getHcsSpecStatus())) {
|
||||
throw new ServiceException("高风险客商合同需先完成特批(C103-8,当前特批状态:" + contractManage.getHcsSpecStatus() + ")");
|
||||
}
|
||||
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
|
||||
contractManage.setHcsRetryCount(0);
|
||||
contractManageMapper.updateById(contractManage);
|
||||
|
||||
+38
@@ -170,6 +170,44 @@ public class HcsCreditCheckSupport {
|
||||
log.warn("HCS付款提交额度超额(转特批):paymentNo={},{}", payment.getPaymentNo(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端客商风险查询(C101-1,展示用,前端接入设计 §3.3):
|
||||
* 按客商编号查询风险等级与黑名单状态并留痕(biz_id 为空);仅用于展示提示,不作为安全边界
|
||||
*
|
||||
* @param customerCode 客商编号
|
||||
* @return 风险信息(fdBlackStatus/fdRiskLevel)
|
||||
*/
|
||||
public HcsRiskVO queryRiskByCustomerCode(String customerCode) {
|
||||
if (StringUtil.isBlank(customerCode)) {
|
||||
throw new ServiceException("客商编号不能为空");
|
||||
}
|
||||
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
|
||||
// 【必填】客商编号(用哪个编码待联调,§9-U6)
|
||||
param.setFdNumber(customerCode);
|
||||
HcsResult<HcsRiskVO> result = hcsClient.getRelativeRisk(param);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
log.error("HCS客商风险查询失败响应:{},fdNumber={}",
|
||||
result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), customerCode);
|
||||
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
|
||||
}
|
||||
HcsRiskVO risk = result.getData();
|
||||
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
|
||||
boolean blacklist = BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
|
||||
try {
|
||||
HcsContractCheckLog checkLog = new HcsContractCheckLog();
|
||||
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
|
||||
checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
|
||||
checkLog.setResult(blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
|
||||
checkLog.setMsg("页面风险查询[" + customerCode + "]黑名单=" + blackStatus + ",风险等级=" + (risk == null ? null : risk.getFdRiskLevel()));
|
||||
checkLog.setTraceId(result.getTraceId());
|
||||
checkLog.setCreateTime(LocalDateTime.now());
|
||||
hcsContractCheckLogMapper.insert(checkLog);
|
||||
} catch (Exception exception) {
|
||||
log.error("HCS页面风险查询留痕写入失败:fdNumber={}", customerCode, exception);
|
||||
}
|
||||
return risk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user