feat: 立项与合同HCS前端接入配套端点——C103-8合同特批发起/B103结果回传、C103-2/5附件代理、C101-1风险与C103-6/12主数据查询,合同推送高风险客商挂起待特批
This commit is contained in:
+17
@@ -238,4 +238,21 @@ public class ContractManage extends TenantEntity {
|
||||
@Schema(description = "合规风险校验时间")
|
||||
private LocalDateTime hcsRiskTime;
|
||||
|
||||
@Schema(description = "合规特批流程ID(C103-8 返回)")
|
||||
private String hcsSpecFdId;
|
||||
|
||||
@Schema(description = "合规特批状态 none/need/processing/approved/rejected")
|
||||
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String hcsSpecStatus;
|
||||
|
||||
@Schema(description = "合规最近特批动作时间")
|
||||
private LocalDateTime hcsSpecTime;
|
||||
|
||||
@Schema(description = "合规特批结果/发起失败原因")
|
||||
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String hcsSpecMsg;
|
||||
|
||||
@Schema(description = "特批申请材料+内部审批附件JSON(含C103-2 合规附件ID)")
|
||||
private String hcsSpecFilesJson;
|
||||
|
||||
}
|
||||
|
||||
+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)
|
||||
*/
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.hcs.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 合同合规系统(HCS)C103-8 合同发起特批流程入参
|
||||
* <p>
|
||||
* TODO 待联调确认(前端接入设计 §6-U1):C103-8 报文 v4 未提供,本 DTO 按 C106-3 结算付款特批的
|
||||
* formData/mechanisms 模式预估,字段名以合规方最新接口说明为准。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class HcsSpecContractSaveDTO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 特批流程ID(更新必填,发起不传)
|
||||
*/
|
||||
private String fdId;
|
||||
/**
|
||||
* 【必填】操作人登录名(§9-U2)
|
||||
*/
|
||||
private String fdLoginName;
|
||||
/**
|
||||
* 【必填】合规合同ID(blade_contract_manage.hcs_fd_id,非名称)
|
||||
*/
|
||||
private String fdContractId;
|
||||
/**
|
||||
* 【必填】合同编号
|
||||
*/
|
||||
private String fdContractNo;
|
||||
/**
|
||||
* 【必填】单据主题:"合同特批-" + 合同编号
|
||||
*/
|
||||
private String fdDocSubject;
|
||||
/**
|
||||
* 特批事由(合同名称/特批原因,非必传)
|
||||
*/
|
||||
private String fdReason;
|
||||
/**
|
||||
* 特批附件(C103-2 转传返回的合规附件ID 引用)
|
||||
*/
|
||||
private HcsMechanismsDTO mechanisms;
|
||||
}
|
||||
+15
@@ -54,6 +54,7 @@ import org.springblade.hcs.dto.HcsReceiptBillSaveDTO;
|
||||
import org.springblade.hcs.dto.HcsResult;
|
||||
import org.springblade.hcs.dto.HcsRiskQueryDTO;
|
||||
import org.springblade.hcs.dto.HcsRiskVO;
|
||||
import org.springblade.hcs.dto.HcsSpecContractSaveDTO;
|
||||
import org.springblade.hcs.dto.HcsSpecPaySaveDTO;
|
||||
import org.springblade.hcs.dto.HcsTemplateVO;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
@@ -97,6 +98,7 @@ public interface IHcsClient {
|
||||
String CREDIT_CHECK_URL = "${thirdParty.hcs.creditCheckUrl:/openapi/eop-cdraft/lcode/ext-cdraft/extEopCdraftMainOpen/contractCreditCheck}";
|
||||
String SPEC_PAY_ADD_URL = "${thirdParty.hcs.specPayAddUrl:/openapi/sys-modeling/lcode/ext-pay-receive/extPayReceiveOpenService/addSpecApplyPay}";
|
||||
String SPEC_PAY_UPDATE_URL = "${thirdParty.hcs.specPayUpdateUrl:/openapi/sys-modeling/lcode/ext-pay-receive/extPayReceiveOpenService/updateSpecApplyPay}";
|
||||
String SPEC_CONTRACT_ADD_URL = "${thirdParty.hcs.specContractAddUrl:/openapi/sys-modeling/lcode/ext-cdraft/extEopCdraftMainOpen/addSpecApplyContract}";
|
||||
String PERF_RECORD_ADD_URL = "${thirdParty.hcs.perfRecordAddUrl:/openapi/sys-modeling/apis/data/km-pay-receive/V1/dataSync/add}";
|
||||
String PERF_RECORD_UPDATE_URL = "${thirdParty.hcs.perfRecordUpdateUrl:/openapi/sys-modeling/apis/data/km-pay-receive/V1/dataSync/update}";
|
||||
String RECEIPT_BILL_ADD_URL = "${thirdParty.hcs.receiptBillAddUrl:/openapi/sys-modeling/lcode/ext-invoice-receive/extInvoiceReceiveOpenService/addReceiveBill}";
|
||||
@@ -264,6 +266,19 @@ public interface IHcsClient {
|
||||
@PostMapping(SPEC_PAY_UPDATE_URL)
|
||||
HcsResult<HcsResult<Void>> updateSpecApplyPay(@RequestBody HcsSpecPaySaveDTO param);
|
||||
|
||||
/**
|
||||
* C103-8 合同发起特批流程(高风险客商签约特批)
|
||||
* <p>
|
||||
* TODO 待联调确认(前端接入设计 §6-U1):C103-8 报文与路径均未在 v4 接口说明中定义,
|
||||
* 本端点按 C106-3 结算付款特批的报文结构预估(双层嵌套 data.data 承接返回),
|
||||
* 路径可经 thirdParty.hcs.specContractAddUrl 覆盖,字段以合规方最新接口说明为准。
|
||||
*
|
||||
* @param param 请求参数
|
||||
* @return data.data.{fdId, fdXFormId, docViewUrl}(fdId 为特批流程ID)
|
||||
*/
|
||||
@PostMapping(SPEC_CONTRACT_ADD_URL)
|
||||
HcsResult<HcsResult<HcsFormSaveVO>> addSpecContract(@RequestBody HcsSpecContractSaveDTO param);
|
||||
|
||||
/**
|
||||
* C108-1 履约记录新建
|
||||
*
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# 项目立项与合同 HCS 前端接入设计(tms-erp-web)
|
||||
|
||||
> **日期**:2026-10-09
|
||||
> **依据**:《桂物合同集成相关接口说明-v4.docx》、[TMS业财融合方案.docx](../TMS业财融合方案.docx)、[2026-09-27-桂物合同合规系统HCS接入技术设计.md](./2026-09-27-桂物合同合规系统HCS接入技术设计.md)、[2026-09-28-HCS接入实施状态与确认清单.md](./2026-09-28-HCS接入实施状态与确认清单.md)
|
||||
> **涉及工程**:`tms-api`(后端补前端代理端点)、`tms-erp-web`(前端页面与 API 模块)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体原则
|
||||
|
||||
| # | 原则 |
|
||||
|---|---|
|
||||
| P1 | **前端不直连 HCS**。所有 C 系列接口由 tms-api 代理:`Authorization`/`Key-Auth` 静态双头只存在于服务端,浏览器不可见,也无跨域问题 |
|
||||
| P2 | **自动推送不动**。C102-1/2、C103-1/11 仍走"审批通过 → 置 unsynced → 扫描任务推送"链路(零埋点,ADR-0001),前端只补**状态展示 + 手动重推 + 对账回查** |
|
||||
| P3 | **附件主存储仍在 TMS OSS**。表单上传逻辑不改;HCS 转传由后端推送链路自动完成(HcsAttachmentSupport 已实现)。前端新增的是**手动转传**(特批材料、确定性上传)与**HCS 附件回下载** |
|
||||
| P4 | **前端结算币种/合同分类不改**。两个下拉继续用本地字典(`currency_type`/`contractCategory`),存量值与交互零变化;C103-12 币种、C103-7 分类只在后端推送侧映射(`currency-name-mapping`/`template-mapping`,已实现),前端无触点 |
|
||||
| P5 | UI 照**"同步金蝶"按钮模式**(参考 `customer-archive.vue`、`formal-settlement-table-panel.vue` 的 kingdeeSyncStatus 列 + 推送按钮),HCS 用 `hcsSyncStatus` 等同构字段 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口 → 前端触点映射总表
|
||||
|
||||
| HCS 接口 | 后端现状 | 面向前端端点 | 前端触点 |
|
||||
|---|---|---|---|
|
||||
| C101-1 客商风险等级 | Feign 已有,合同推送/付款提交时服务端调用 | **`GET /hcs/risk`(新增)** | 合同表单选甲乙方后展示风险标签;高风险+黑名单给"发起特批"入口 |
|
||||
| C102-1 项目立项新增 | 扫描任务自动推送 | — | 无直接触点(列表展示同步结果) |
|
||||
| C102-2 项目立项更新 | 变更审批通过自动推送 | — | 同上 |
|
||||
| C102-3 项目立项查询 | `GET /hcs/recon/project` 已有 | 已有 | 立项列表"合规对账"弹窗 |
|
||||
| C103-12 币种列表 | Feign + Redis 缓存已有(推送侧映射用) | **移出前端范围**(2026-10-09 确认:前端结算币种沿用本地字典,无触点) | — |
|
||||
| C103-6 模板列表 | Feign + Redis 缓存已有 | **`GET /hcs/master-data/template`(新增)** | 管理端"合规主数据/映射"查看页(表单不选模板,template-mapping 在 Nacos) |
|
||||
| ~~C103-7 分类列表~~ | Feign + 缓存已有 | **移出前端范围**(2026-10-09 确认:分类为推送侧 Nacos 映射的前置,前端无界面触点) | — |
|
||||
| C103-1 合同同步 | 审批通过自动推送 | `POST /hcs/sync/contract/{id}` 已有 | 合同列表"推送合规"按钮 |
|
||||
| C103-11 合同信息更新 | 变更审批通过自动推送 | 同上(重推入口) | 同上 |
|
||||
| C103-2 附件上传 | Feign 已有,推送链路自动转传 | **`POST /hcs/attachment/upload`(新增)** | 合同/特批材料"上传到合同系统"手动操作,返回 fdId 落库 |
|
||||
| C103-3 合同列表查询 | `GET /hcs/recon/contract` 已有 | 已有 | 合同列表"合规对账"弹窗 |
|
||||
| C103-4 合同详情查询 | `GET /hcs/recon/contract/detail` 已有 | 已有 | 对账弹窗"查看合规详情" |
|
||||
| C103-5 合同附件下载 | 仅 Feign 端点 | **`GET /hcs/attachment/download`(新增)** | 对账弹窗/合同详情中下载 HCS 侧附件 |
|
||||
| C103-8 合同发起特批 | **本期正式接入**(业务闭环见集成流程梳理:高风险客商签约必走;B103 结果回传) | **`POST /hcs/contract/spec-apply`(新增)+ `POST /hcs/callback/spec-result`(B103 接收,新增)** | 合同详情"发起特批"、特批状态展示、回传后自动恢复推送 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端补充端点设计(tms-api)
|
||||
|
||||
全部挂在 `HcsSyncController`(`/hcs` 前缀,与既有端点同文件),复用 `HcsMasterDataSupport`、`HcsAttachmentSupport`、`IHcsClient`,不新建 Support。
|
||||
|
||||
### 3.1 主数据查询(走 Redis 缓存,未命中自动拉取)
|
||||
|
||||
```
|
||||
GET /hcs/master-data/template
|
||||
返回 R<List<HcsTemplateVO>>:直接序列化
|
||||
```
|
||||
|
||||
仅模板一个端点;币种/分类已在 `HcsMasterDataSupport` 内部缓存供推送映射,不暴露前端。
|
||||
|
||||
### 3.2 附件代理
|
||||
|
||||
```
|
||||
POST /hcs/attachment/upload (multipart:fdFile + bizType 可选)
|
||||
→ 服务端包装 fdLoginName=当前登录名、fdAnonymous=false,调 C103-2
|
||||
→ 返回 R<{ fdId }>;同时写 HcsAttachmentSupport 的 Redis 映射缓存(attachmentKey → fdId),
|
||||
使后续 C103-1 推送链路命中缓存不再重传
|
||||
→ 文件限制与 TMS OSS 现行一致:pdf/bmp/jpeg/png/jpg/doc/docx/ppt/pptx/xlsx/xls/eml/msg/zip,≤50M
|
||||
|
||||
GET /hcs/attachment/download?fdId=...
|
||||
→ 调 C103-5,文件流透传(Content-Disposition attachment,文件名从附件元数据或 fdId 兜底)
|
||||
→ axios 需 responseType: 'blob'
|
||||
```
|
||||
|
||||
### 3.3 客商风险查询(C101-1)
|
||||
|
||||
```
|
||||
GET /hcs/risk?customerCode=...
|
||||
→ 调 IHcsClient.relativeRisk(C101-1)
|
||||
→ 返回 R<HcsRiskVO>:{ fdId, fdNumber, fdBlackStatus, fdRiskLevel }
|
||||
→ 结果写 blade_hcs_check_log(biz_type=risk,留痕与合同推送链路同表)
|
||||
```
|
||||
|
||||
> 前端仅用于**展示与提示**,不作为安全边界:强制校验仍在合同推送链路(`risk.block-blacklist`)与付款提交事务内。
|
||||
|
||||
### 3.4 合同发起特批(C103-8)+ 结果回传(B103)——本期正式接入
|
||||
|
||||
**业务闭环**(依据《TMS业财融合-集成流程图文字版与业务流程梳理.md》§3.3/§8):
|
||||
|
||||
```
|
||||
合同审批通过 → C101-1 查甲乙方风险
|
||||
├─ 非高风险 → 常规链路:C103-2 附件转传 → C103-1 推送(现状不变)
|
||||
└─ 高风险 → 合同链路挂起(hcs_spec_status=need)
|
||||
→ TMS 内部特批审批(按原审批流)
|
||||
→ 前端补特批申请材料 + 内部审批流程附件 → C103-8 发起特批(hcs_spec_status=processing)
|
||||
→ 合同系统集团审批(流程在合同系统预配置)
|
||||
→ B103 特批结果回传 TMS(hcs_spec_status=approved/rejected)
|
||||
→ approved → 自动恢复 C103-1 推送 → 完成签订;rejected → 前端提示,重新发起
|
||||
```
|
||||
|
||||
**数据模型**:`blade_contract_manage` 追加特批字段组(照付款申请 `hcs_spec_*` 惯例),SQL 落 `doc/sql/transport/`:
|
||||
|
||||
```sql
|
||||
hcs_spec_fd_id varchar(64) COMMENT '合规特批流程ID(C103-8 返回)',
|
||||
hcs_spec_status varchar(16) DEFAULT 'none' COMMENT '特批状态 none/need/processing/approved/rejected',
|
||||
hcs_spec_time datetime COMMENT '最近特批动作时间',
|
||||
hcs_spec_msg varchar(512) COMMENT '特批结果/失败原因',
|
||||
hcs_spec_files_json text COMMENT '特批申请材料+内部审批附件(含C103-2 fdId)'
|
||||
```
|
||||
|
||||
**端点设计**:
|
||||
|
||||
```
|
||||
POST /hcs/contract/spec-apply (body:{ contractId, attachments: [{fdId, name}] })
|
||||
校验:合同审批通过 && hcs_spec_status ∈ {need, rejected} && 特批材料非空
|
||||
组装报文调 C103-8(报文模式预估照 C106-3:fd_contract_id=hcs_fd_id、fd_contract_no、
|
||||
fd_doc_subject="合同特批-"+contractNo、特批附件 mechanisms.attachment 带 fdBindType=ADD)
|
||||
→ 返回 data.fdId → hcs_spec_fd_id,状态置 processing;失败置 need 并记 hcs_spec_msg
|
||||
→ C103-8 v4 未含报文,联调前按上述预估实现并加 TODO 标注(§6-U1)
|
||||
|
||||
POST /hcs/callback/spec-result (B103 合规→TMS 回调接收端点)
|
||||
公开端点(@PreAuth(AuthConstant.PERMIT_ALL) + 报文验签字段待合规方口径,先记录原文留痕)
|
||||
body:{ 合同标识/特批流程fdId, 审批结果, 审批意见 } ← 字段名待 B103 报文(§6-U1)
|
||||
处理:定位合同 → 回写 hcs_spec_status=approved/rejected + hcs_spec_msg
|
||||
→ approved 时把合同 hcs_sync_status 重置 unsynced,由既有扫描任务自动走 C103-1(零新增链路)
|
||||
→ B103 报文未到位前端点先落库留痕不置状态,联调时接线
|
||||
```
|
||||
|
||||
**状态展示与操作权限**:
|
||||
|
||||
| hcs_spec_status | 前端表现 |
|
||||
|---|---|
|
||||
| none | 无特批相关 UI |
|
||||
| need(高风险待特批) | 合同详情警告条 +【发起特批】按钮可用;推送合规按钮禁用并提示"高风险客商,需先完成特批" |
|
||||
| processing | "特批中"标签(橙);发起按钮禁用 |
|
||||
| approved | "特批通过"标签(绿);推送恢复 |
|
||||
| rejected | "特批驳回"标签(红)+ 驳回原因;可重新发起 |
|
||||
|
||||
### 3.5 权限
|
||||
|
||||
`/hcs/**` 补 `@PreAuth`(菜单权限码,随菜单 SQL 一起落 `doc/sql/transport/`):
|
||||
|
||||
| 权限码 | 控制点 |
|
||||
|---|---|
|
||||
| `hcs_sync_push` | 推送合规/重推按钮 |
|
||||
| `hcs_sync_status` | 合规状态列/筛选(登录可见即可时不建) |
|
||||
| `hcs_recon` | 合规对账弹窗 |
|
||||
| `hcs_attachment` | 上传到合同系统 / 合规附件下载 |
|
||||
| `hcs_master_data` | 主数据/映射查看页 |
|
||||
| `hcs_spec_apply` | 发起特批按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端设计(tms-erp-web)
|
||||
|
||||
### 4.1 新增 API 模块 `src/api/business/hcs.js`
|
||||
|
||||
集中一个文件,不拆散到各业务 api(对齐 `api/payment/paymentApplication.js` 的超时覆盖惯例):
|
||||
|
||||
```js
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/hcs';
|
||||
|
||||
// 合规推送涉及 HCS 远程多次调用(风险校验+附件转传+起草),可达 20s+,覆盖全局 10s 超时
|
||||
export const repush = (bizType, id) =>
|
||||
request({ url: `${baseUrl}/sync/${bizType}/${id}`, method: 'post', timeout: 60000 });
|
||||
|
||||
export const getTemplates = () =>
|
||||
request({ url: `${baseUrl}/master-data/template`, method: 'get' });
|
||||
|
||||
export const getRisk = customerCode =>
|
||||
request({ url: `${baseUrl}/risk`, method: 'get', params: { customerCode } });
|
||||
|
||||
export const uploadAttachment = formData =>
|
||||
request({ url: `${baseUrl}/attachment/upload`, method: 'post', data: formData, timeout: 60000 });
|
||||
|
||||
export const downloadAttachment = fdId =>
|
||||
request({ url: `${baseUrl}/attachment/download`, method: 'get', params: { fdId }, responseType: 'blob' });
|
||||
|
||||
export const reconProject = projectCode =>
|
||||
request({ url: `${baseUrl}/recon/project`, method: 'get', params: { projectCode } });
|
||||
export const reconContract = contractNo =>
|
||||
request({ url: `${baseUrl}/recon/contract`, method: 'get', params: { contractNo } });
|
||||
export const reconContractDetail = params =>
|
||||
request({ url: `${baseUrl}/recon/contract/detail`, method: 'get', params });
|
||||
|
||||
export const applyContractSpec = data =>
|
||||
request({ url: `${baseUrl}/contract/spec-apply`, method: 'post', data });
|
||||
```
|
||||
|
||||
### 4.2 结算币种(C103-12)——前端不改
|
||||
|
||||
前端"结算币种"下拉继续用本地字典 `currency_type`(`loadSettlementDictionaries()` 现状不动),存储值、编辑回显、下游运单/结算取值全部维持现状。C103-12 币种列表仅在后端推送 C103-1 时经 `currency-name-mapping`(`RMB→人民币`)映射,已实现,无前端工作。
|
||||
|
||||
### 4.3 合同模板(C103-6)
|
||||
|
||||
- **表单"合同类别"保留本地字典 `contractCategory`**:它是 TMS 业务主数据,推送侧靠 Nacos `template-mapping` 转模板ID。C103-7 已移出前端范围(2026-10-09 确认)。
|
||||
- 新增**管理端"合规主数据"查看页**(`views/business/hcs-master-data.vue`,模板列表 + "刷新缓存"按钮,`POST /hcs/master-data/refresh` 已有)。菜单挂系统管理或合规管理目录下。
|
||||
|
||||
### 4.4 客商风险等级(C101-1)
|
||||
|
||||
合同表单(新增/变更)选择甲方/乙方后:
|
||||
|
||||
- 调 `getRisk(customerCode)`,在甲乙方字段旁展示风险标签:无风险/低/中/高 + 黑名单红标(`fdBlackStatus === 'yes'`)。
|
||||
- 高风险或黑名单时,表单顶部警告条:"该客商为高风险/黑名单客商,按集团规定不可签约,如需签约请发起特批",并提供【发起特批】按钮(走 §4.7)。
|
||||
- 项目立项表单不做强制校验(业务流程:风险校验在合同签订与付款环节),立项页不加。
|
||||
- 失败静默(HCS 不可达不阻塞填写,强校验在后端推送/付款链路)。
|
||||
|
||||
### 4.5 附件上传到合同系统(C103-2)
|
||||
|
||||
- **表单附件流程不变**:仍传 TMS OSS,推送时后端自动转传(已实现,含 7 天 fdId 映射缓存)。
|
||||
- 新增**手动转传入口**:合同详情页附件区(`contract-attachment-section.vue`)加"上传到合同系统"操作(仅已审批通过的合同显示):
|
||||
1. 选中附件行 → `uploadAttachment(formData)`(前端从 OSS url 拉文件或直接复用原始 File);
|
||||
2. 返回 `fdId` 写入附件行 `hcsFdId` 字段(后端 `update-attachments` 已有,行结构加一字段即可);
|
||||
3. 已转传的行显示"已上传合规"标记。
|
||||
- 特批材料(C103-8 占位)也走此端点先取得 fdId。
|
||||
|
||||
### 4.6 合同附件下载(C103-5)
|
||||
|
||||
- 对账弹窗(§4.7)中展示 HCS 侧附件列表(来自 C103-4 详情),点击下载调 `downloadAttachment(fdId)`,blob 落地文件。
|
||||
- `contract-attachment-section.vue` 的行内"下载"仍走 TMS OSS url,不改。
|
||||
|
||||
### 4.7 列表页合规状态 + 推送/对账(照"同步金蝶"模式)
|
||||
|
||||
`project-apply.vue` 与 `contract-manage.vue` 各加:
|
||||
|
||||
1. **状态列**:`合规状态`(prop `hcsSyncStatus`)——`unsynced 待推送`(灰)/`synced 已推送`(绿)/`failed 推送失败`(红);失败时 tooltip 显示 `hcsSyncMsg`;已推送的可附 `hcsFdId`。
|
||||
- 列配置加在 `src/option/business/project-apply.js`、`contract-manage.js`(或页面内 column 定义,与两页现状一致)。
|
||||
- 筛选区加"合规状态"下拉(unsynced/synced/failed,复用 settlement 模块 kingdeeSyncStatusOptions 的写法)。
|
||||
2. **操作按钮**:行操作"推送合规"(`repush(bizType, id)`,成功后刷新行;按钮文案照 customer-archive 的"推送中…"防重态);权限码 `hcs_sync_push`。
|
||||
3. **对账弹窗**:行操作/工具栏"合规对账"——立项按 `projectCode` 调 `reconProject`,合同按 `contractNo` 调 `reconContract`;弹窗内 JSON 树/描述列表展示 HCS 返回,附"查看合规详情"(C103-4,合同页专属)与 HCS 附件下载(§4.6)。权限码 `hcs_recon`。
|
||||
|
||||
### 4.8 合同发起特批(C103-8,本期接入)
|
||||
|
||||
- **入口**:合同详情页【发起特批】按钮,仅 `hcsSpecStatus ∈ {need, rejected}` 时可用;权限码 `hcs_spec_apply`。
|
||||
- **交互**:弹窗选择/确认特批申请材料(先经 §4.5 `uploadAttachment` 取 fdId,弹窗内表格展示已传材料,含内部审批流程附件)→ 确认调 `applyContractSpec({ contractId, attachments })` → 成功后详情刷新为"特批中"。
|
||||
- **状态展示**:合同列表与详情按 §3.4 状态表展示特批标签(need/processing/approved/rejected);`need` 时列表"推送合规"按钮禁用并 tooltip 提示。
|
||||
- **结果回传**:B103 回调由后端接收并自动恢复推送,前端无需轮询;详情页打开时实时拉取最新状态即可,`rejected` 时展示 `hcsSpecMsg` 驳回原因。
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施拆分
|
||||
|
||||
| 阶段 | tms-api | tms-erp-web | 依赖 |
|
||||
|---|---|---|---|
|
||||
| F1 基础 | `GET /hcs/master-data/template` + `@PreAuth`/菜单 SQL | `api/business/hcs.js`;管理端主数据页 | 无 |
|
||||
| F2 状态与推送 | 无(端点已有) | 两列表页:状态列/筛选 + 推送合规按钮 | F1(hcs.js) |
|
||||
| F3 对账与附件 | `/hcs/risk`、`/hcs/attachment/upload`、`/hcs/attachment/download` | 对账弹窗 + 合规附件下载 + 手动转传 + 附件行 `hcsFdId` | F2 |
|
||||
| F4 特批 | 合同特批字段组 SQL;`POST /hcs/contract/spec-apply` + `POST /hcs/callback/spec-result`(B103);合同推送链路加 need 挂起判断 | 发起特批弹窗 + 特批状态标签 + 按钮联动 | C103-8/B103 报文(§6-U1,可先按预估模式实现联调替换) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 未决事项
|
||||
|
||||
| # | 事项 | 影响 | 当前处理 |
|
||||
|---|---|---|---|
|
||||
| U1 | **C103-8 / B103 报文 v4 未提供**(业务闭环已在集成流程梳理中明确) | 特批发起与结果回传的报文字段 | 按 C106-3 模式预估实现 + 回调端点先落库留痕;联调拿到报文后替换(代码已留 TODO) |
|
||||
| U2 | **C103-5 返回流是否带文件名**(v4 仅写"文件流") | 下载文件命名 | 后端以附件元数据/`fdId` 兜底命名,联调校准 |
|
||||
| U3 | 合规状态列/菜单权限是否需要按租户差异化 | 菜单 SQL | 先按统一菜单落地 |
|
||||
@@ -0,0 +1,41 @@
|
||||
-- 桂物合同合规系统(HCS)接入:合同特批字段组(C103-8 发起特批 / B103 结果回传,前端接入设计 §3.4)。
|
||||
-- blade_contract_manage 追加特批流程ID/状态/时间/原因/材料字段;可重复执行。
|
||||
SET @db_name = DATABASE();
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_fd_id'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_fd_id varchar(64) DEFAULT NULL COMMENT ''合规特批流程ID(C103-8 返回)'''
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_status'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_status varchar(16) DEFAULT ''none'' COMMENT ''合规特批状态 none/need/processing/approved/rejected'''
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_time'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_time datetime DEFAULT NULL COMMENT ''合规最近特批动作时间'''
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_msg'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_msg varchar(512) DEFAULT NULL COMMENT ''合规特批结果/发起失败原因'''
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_files_json'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_files_json text COMMENT ''特批申请材料+内部审批附件JSON(含C103-2 合规附件ID)'''
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 存量合同的特批状态初始化为 none(新增列带 DEFAULT 'none' 已覆盖;此处兜底历史执行场景)
|
||||
UPDATE blade_contract_manage SET hcs_spec_status = 'none' WHERE hcs_spec_status IS NULL AND is_deleted = 0;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 桂物合同合规系统(HCS)前端接入:合规主数据菜单(前端接入设计 §4.3,照 AGENTS §15 菜单 SQL 规范)。
|
||||
-- 合规主数据页(模板列表 + 刷新缓存);字符集 utf8mb4_general_ci。ID 为雪花段占位,如与现有数据冲突请调整。
|
||||
-- 注意:/hcs/** 端点当前登录即可访问(照 HcsSyncController 现状),菜单落地后可按需补 @PreAuth(menu) 注解。
|
||||
|
||||
DELETE FROM blade_menu WHERE code IN ('hcs_master_data', 'hcs_master_data_refresh');
|
||||
|
||||
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
|
||||
VALUES (2075432100100000001, 1164733399668962201, 'hcs_master_data', '合规主数据', 'hcs_master_data', '/business/hcs-master-data', 'iconfont icon-caidan', 40, 1, 0, 1, '', '', 0);
|
||||
|
||||
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
|
||||
VALUES (2075432100100000002, 2075432100100000001, 'hcs_master_data_refresh', '刷新缓存', 'hcs_master_data_refresh', '', '', 1, 2, 0, 1, '', '', 0);
|
||||
Reference in New Issue
Block a user