feat: HCS履约规则C103-9行级同步(随合同链路执行/行级增删改聚合/履约计划ID与阶段fdId回填持久化)
This commit is contained in:
+16
-9
@@ -70,7 +70,7 @@ import java.util.Objects;
|
||||
* <p>
|
||||
* 扫描任务按序执行:① 客商风险校验(可配开关,黑名单阻断)→ ② 附件转传(挂载点待 §9-U8 确认,先转传缓存)→
|
||||
* ③ C103-3 判重(§9-U4 查询能力联调确认)→ ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→
|
||||
* ⑤ 履约规则 C103-9(P2 接线)。
|
||||
* ⑤ 履约规则 C103-9 行级同步({@link HcsExeStageSyncSupport})。
|
||||
* 结果留痕:风险校验写 blade_hcs_check_log 与合同 hcs_risk_* 字段;推送结果回填 hcs_fd_id/hcs_sync_status。
|
||||
*
|
||||
* @author Chill
|
||||
@@ -97,6 +97,7 @@ public class HcsContractSyncSupport {
|
||||
private final CustomerArchiveMapper customerArchiveMapper;
|
||||
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
|
||||
private final HcsAttachmentSupport hcsAttachmentSupport;
|
||||
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
|
||||
|
||||
/**
|
||||
* 推送合同到 HCS:风险校验 → 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
|
||||
@@ -112,20 +113,26 @@ public class HcsContractSyncSupport {
|
||||
// ② 附件转传(先转传并缓存合规附件ID,C103-1 挂载方式待 §9-U8 确认后接线)
|
||||
transferAttachmentsIfEnabled(contractManage);
|
||||
// ③④ 首推判重后起草(C103-1);已有合规ID(变更单)走更新(C103-11)
|
||||
String resultMessage;
|
||||
if (StringUtil.isBlank(contractManage.getHcsFdId())) {
|
||||
String existFdId = findExistFdId(contractManage);
|
||||
if (StringUtil.isNotBlank(existFdId)) {
|
||||
markSynced(contractManage, existFdId, "判重命中,回填已有合规合同ID");
|
||||
return "判重命中,回填已有合规合同ID";
|
||||
resultMessage = "判重命中,回填已有合规合同ID";
|
||||
} else {
|
||||
String fdContractId = draftContract(contractManage);
|
||||
// 先持久化合规合同ID(调用方必须保存),履约规则同步失败时下轮凭其走更新补偿
|
||||
markSynced(contractManage, fdContractId, "C103-1 合同发起成功");
|
||||
resultMessage = "C103-1 合同发起成功";
|
||||
}
|
||||
String fdContractId = draftContract(contractManage);
|
||||
// TODO 待联调(P2):履约规则 C103-9 行级同步随合同链路执行(§5.3,依赖 blade_contract_exe_stage 表)
|
||||
markSynced(contractManage, fdContractId, "C103-1 合同发起成功");
|
||||
return "C103-1 合同发起成功";
|
||||
} else {
|
||||
updateContract(contractManage);
|
||||
markSynced(contractManage, contractManage.getHcsFdId(), "C103-11 合同更新成功");
|
||||
resultMessage = "C103-11 合同更新成功";
|
||||
}
|
||||
updateContract(contractManage);
|
||||
markSynced(contractManage, contractManage.getHcsFdId(), "C103-11 合同更新成功");
|
||||
return "C103-11 合同更新成功";
|
||||
// ⑤ 履约规则 C103-9 行级同步(随合同链路,§5.3;失败抛出由任务落 failed,下轮凭 hcs_fd_id 走更新补偿)
|
||||
String exeStageResult = hcsExeStageSyncSupport.syncExeStages(contractManage);
|
||||
return resultMessage + ";" + exeStageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* 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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.dto.HcsExeplanStageDTO;
|
||||
import org.springblade.hcs.dto.HcsExeplanStageVO;
|
||||
import org.springblade.hcs.dto.HcsExeplanSyncDTO;
|
||||
import org.springblade.hcs.dto.HcsExeplanVO;
|
||||
import org.springblade.hcs.dto.HcsResult;
|
||||
import org.springblade.hcs.exception.HcsException;
|
||||
import org.springblade.hcs.feign.IHcsClient;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.HcsContractExeStageMapper;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.HcsContractExeStage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* HCS 履约规则同步支撑组件(C103-9 新版行级同步,设计文档 §5.3,ADR-0002)
|
||||
* <p>
|
||||
* 随合同推送链路执行(合同须已同步、hcs_fd_id 存在):聚合 row_op 非 none 的规则行
|
||||
* (add 行不带 fdId,update/delete 行必须带合规阶段ID),整体 fdEopExeplanOperateType
|
||||
* 首次为 add、后续为 update;返回的 fdExeplanId 回填 blade_contract_manage.hcs_exeplan_id,
|
||||
* 变更阶段 fdId 按行回填 hcs_stage_fd_id(合规返回的 fdId 必须持久化,后续行更新/删除全靠它)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HcsExeStageSyncSupport {
|
||||
|
||||
/**
|
||||
* 行级扩展内容固定空 JSON
|
||||
*/
|
||||
private static final String EXTEND_CONTENT_EMPTY = "{}";
|
||||
|
||||
private final IHcsClient hcsClient;
|
||||
private final HcsProperties hcsProperties;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
private final HcsContractExeStageMapper hcsContractExeStageMapper;
|
||||
|
||||
/**
|
||||
* 同步合同履约规则到 HCS(C103-9),成功后回填 fdExeplanId 与各阶段 fdId 并清除行级操作标记
|
||||
*
|
||||
* @param contractManage 合同(须已同步,hcs_fd_id 非空)
|
||||
* @return 同步结果描述(无待同步行时返回提示)
|
||||
*/
|
||||
public String syncExeStages(ContractManage contractManage) {
|
||||
if (StringUtil.isBlank(contractManage.getHcsFdId())) {
|
||||
throw new HcsException("C103-9 履约规则同步失败,合同尚未推送合规(contractNo=" + contractManage.getContractNo() + ")");
|
||||
}
|
||||
// 含软删行:delete 行已逻辑删除但须凭合规阶段ID 上报(原生 SQL 绕过逻辑删除过滤)
|
||||
List<HcsContractExeStage> pendingRows = hcsContractExeStageMapper.selectSyncRows(contractManage.getId());
|
||||
if (pendingRows.isEmpty()) {
|
||||
return "履约规则无待同步行";
|
||||
}
|
||||
HcsExeplanSyncDTO param = buildSyncParam(contractManage, pendingRows);
|
||||
log.info("HCS履约规则同步开始:contractNo={},整体操作 {},{} 行", contractManage.getContractNo(),
|
||||
param.getFdEopExeplanOperateType(), pendingRows.size());
|
||||
HcsResult<HcsExeplanVO> result = hcsClient.exeplanAdd(param);
|
||||
if (result == null || !result.isSuccess() || result.getData() == null) {
|
||||
log.error("HCS履约规则同步失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
|
||||
throw new HcsException("C103-9 履约规则同步失败:" + (result == null ? "响应为空" : result.getMsg()));
|
||||
}
|
||||
HcsExeplanVO exeplan = result.getData();
|
||||
backfill(contractManage, pendingRows, param.getFdStages(), exeplan);
|
||||
return String.format("C103-9 履约规则同步成功(%s,%d 行)", param.getFdEopExeplanOperateType(), pendingRows.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 C103-9 入参(字段标注照 §3.4 规范,映射见设计文档 §5.3)
|
||||
*/
|
||||
private HcsExeplanSyncDTO buildSyncParam(ContractManage contractManage, List<HcsContractExeStage> pendingRows) {
|
||||
HcsExeplanSyncDTO param = new HcsExeplanSyncDTO();
|
||||
// 【必填】合同ID:HCS 合同ID(blade_contract_manage.hcs_fd_id,非 TMS 主键)
|
||||
param.setFdContractId(contractManage.getHcsFdId());
|
||||
// 【必填】外部系统标识:fd_sources,配置 thirdParty.hcs.src-system(D14)
|
||||
param.setFdOuterSystem(hcsProperties.getSrcSystem());
|
||||
// 【必填】操作人登录名:合同承办人登录名(§9-U2)
|
||||
param.setFdLoginName(requireLoginName(contractManage));
|
||||
// 【必填】计划整体操作类型:首次同步 add,已同步过(有履约计划ID)update
|
||||
param.setFdEopExeplanOperateType(StringUtil.isBlank(contractManage.getHcsExeplanId()) ? "add" : "update");
|
||||
// 履约计划ID:已同步过则传(回填自首次同步返回)
|
||||
param.setFdExeplanId(contractManage.getHcsExeplanId());
|
||||
List<HcsExeplanStageDTO> stages = new ArrayList<>();
|
||||
for (HcsContractExeStage row : pendingRows) {
|
||||
HcsExeplanStageDTO stage = new HcsExeplanStageDTO();
|
||||
// 阶段项ID:行级 update/delete 必传(取 hcs_stage_fd_id),add 行不带
|
||||
if (!"add".equals(row.getRowOp())) {
|
||||
stage.setFdId(row.getHcsStageFdId());
|
||||
}
|
||||
// 【必填】行级操作:add / update / delete
|
||||
stage.setFdEopExeplanOperateType(row.getRowOp());
|
||||
// 【必填】履约类型:payment=付款 / collection=收款
|
||||
stage.setFdPerformType(row.getPerformType());
|
||||
// 款项性质:prepayment/progress/acceptance/retention
|
||||
stage.setFdFundNature(row.getFundNature());
|
||||
// 计划金额(元)
|
||||
stage.setFdFundAmount(row.getFundAmount());
|
||||
// 资金日期(毫秒时间戳)
|
||||
stage.setFdFundDate(toMillis(row.getFundDate()));
|
||||
// 开票规则(如 payFirst)
|
||||
stage.setFdInvoiceRule(row.getInvoiceRule());
|
||||
// 账期
|
||||
stage.setFdInvoiceCycle(row.getInvoiceCycle());
|
||||
// 金额类型:行维护值,空取配置 thirdParty.hcs.exe-stage.fund-type(默认 byAmount)
|
||||
stage.setFdFundType(StringUtil.isBlank(row.getFundType()) ? hcsProperties.getExeStage().getFundType() : row.getFundType());
|
||||
// 是否完成
|
||||
stage.setFdIsFinish(row.getIsFinish() != null && row.getIsFinish() > 0);
|
||||
// 扩展内容 JSON 字符串:固定 "{}"
|
||||
stage.setFdExtendContent(EXTEND_CONTENT_EMPTY);
|
||||
stages.add(stage);
|
||||
}
|
||||
param.setFdStages(stages);
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填:fdExeplanId → 合同 hcs_exeplan_id;变更阶段 fdId 按行回填 hcs_stage_fd_id 并清除行级操作标记
|
||||
* <p>
|
||||
* TODO 待联调确认:fdChangeStages 与请求行的对应关系 v4 未明确,优先按请求行序对应(首次全 add 场景),
|
||||
* 行数不一致时按业务属性组合键匹配,仍匹配不到的 add 行保持待同步(下轮重试)。
|
||||
*/
|
||||
private void backfill(ContractManage contractManage, List<HcsContractExeStage> pendingRows,
|
||||
List<HcsExeplanStageDTO> requestStages, HcsExeplanVO exeplan) {
|
||||
// 履约计划ID 回填(后续行更新/删除依赖,必须持久化)
|
||||
if (StringUtil.isNotBlank(exeplan.getFdExeplanId())
|
||||
&& !exeplan.getFdExeplanId().equals(contractManage.getHcsExeplanId())) {
|
||||
contractManage.setHcsExeplanId(exeplan.getFdExeplanId());
|
||||
}
|
||||
backfillExeplanId(contractManage);
|
||||
List<HcsExeplanStageVO> changeStages = exeplan.getFdChangeStages() == null ? List.of() : exeplan.getFdChangeStages();
|
||||
// 行数一致时按请求行序一一对应;不一致时按业务属性组合键匹配
|
||||
boolean positional = changeStages.size() == requestStages.size();
|
||||
Map<String, HcsExeplanStageVO> byAttribute = positional ? Map.of() : mapByAttribute(changeStages);
|
||||
int addRowCount = 0;
|
||||
int unmatchedCount = 0;
|
||||
for (int index = 0; index < pendingRows.size(); index++) {
|
||||
HcsContractExeStage row = pendingRows.get(index);
|
||||
String stageFdId = row.getHcsStageFdId();
|
||||
if ("add".equals(row.getRowOp())) {
|
||||
addRowCount++;
|
||||
HcsExeplanStageVO matched = positional ? changeStages.get(index) : byAttribute.get(attributeKey(requestStages.get(index)));
|
||||
stageFdId = matched == null ? null : matched.getFdId();
|
||||
if (StringUtil.isBlank(stageFdId)) {
|
||||
unmatchedCount++;
|
||||
}
|
||||
}
|
||||
markRowSynced(row, stageFdId);
|
||||
}
|
||||
log.info("HCS履约规则阶段ID回填完成:contractNo={},add {} 行,未匹配 {} 行(模式:{})",
|
||||
contractManage.getContractNo(), addRowCount, unmatchedCount, positional ? "行序对应" : "属性匹配");
|
||||
}
|
||||
|
||||
/**
|
||||
* 行同步成功回写:回填合规阶段ID、清除行级操作标记并置 synced
|
||||
*/
|
||||
private void markRowSynced(HcsContractExeStage row, String stageFdId) {
|
||||
if (StringUtil.isNotBlank(stageFdId)) {
|
||||
row.setHcsStageFdId(stageFdId);
|
||||
} else if ("add".equals(row.getRowOp())) {
|
||||
// add 行未取得阶段ID:保持待同步,下轮重试(ADR-0002:阶段ID 必须持久化)
|
||||
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
|
||||
row.setHcsSyncMsg("C103-9 返回阶段ID未能匹配,待重试");
|
||||
hcsContractExeStageMapper.updateById(row);
|
||||
log.warn("HCS履约规则 add 行未匹配到返回阶段ID:rowId={},contractId={}", row.getId(), row.getContractId());
|
||||
return;
|
||||
}
|
||||
row.setRowOp("none");
|
||||
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
|
||||
row.setHcsSyncMsg("C103-9 同步成功");
|
||||
hcsContractExeStageMapper.updateById(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务属性组合键索引返回阶段(行数不一致时的兜底匹配)
|
||||
*/
|
||||
private Map<String, HcsExeplanStageVO> mapByAttribute(List<HcsExeplanStageVO> changeStages) {
|
||||
Map<String, HcsExeplanStageVO> byAttribute = new HashMap<>();
|
||||
for (HcsExeplanStageVO stage : changeStages) {
|
||||
byAttribute.putIfAbsent(String.join("|", Objects.toString(stage.getFdPerformType(), ""),
|
||||
Objects.toString(stage.getFdFundNature(), ""),
|
||||
Objects.toString(stage.getFdFundAmount(), ""),
|
||||
Objects.toString(stage.getFdFundDate(), "")), stage);
|
||||
}
|
||||
return byAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求行的业务属性组合键
|
||||
*/
|
||||
private String attributeKey(HcsExeplanStageDTO stage) {
|
||||
return String.join("|", Objects.toString(stage.getFdPerformType(), ""),
|
||||
Objects.toString(stage.getFdFundNature(), ""),
|
||||
Objects.toString(stage.getFdFundAmount(), ""),
|
||||
Objects.toString(stage.getFdFundDate(), ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同履约计划ID 回写(fdExeplanId 必须持久化;失败仅告警不阻断行回填,下轮同步会再次携带)
|
||||
*/
|
||||
private void backfillExeplanId(ContractManage contractManage) {
|
||||
try {
|
||||
contractManageMapper.updateById(contractManage);
|
||||
log.info("HCS履约计划ID已回填:contractNo={},fdExeplanId={}", contractManage.getContractNo(), contractManage.getHcsExeplanId());
|
||||
} catch (Exception exception) {
|
||||
log.error("HCS履约计划ID回写失败:contractNo={}", contractManage.getContractNo(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同承办人登录名(fdLoginName 必填)
|
||||
*/
|
||||
private String requireLoginName(ContractManage contractManage) {
|
||||
if (contractManage.getHandlerUserId() == null) {
|
||||
throw new HcsException("C103-9 履约规则同步失败,合同承办人未维护(contractNo=" + contractManage.getContractNo() + ")");
|
||||
}
|
||||
User user = UserCache.getUser(contractManage.getHandlerUserId());
|
||||
if (user == null || StringUtil.isBlank(user.getAccount())) {
|
||||
throw new HcsException("C103-9 履约规则同步失败,承办人登录名解析不到(§9-U2)");
|
||||
}
|
||||
return user.getAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime → 毫秒时间戳
|
||||
*/
|
||||
private Long toMillis(LocalDateTime localDateTime) {
|
||||
return localDateTime == null ? null : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -51,7 +51,7 @@ import java.util.UUID;
|
||||
* <p>
|
||||
* 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
|
||||
* 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
|
||||
* 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 P2 接线)。
|
||||
* 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步)。
|
||||
* 多实例部署时以 Redis 分布式锁防重扫(照既有 BladeRedis 用法,单实例部署行为与既有金蝶任务一致)。
|
||||
* 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts,供手动端点(HcsSyncController)复用。
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user