:feat: 审批通过自动创建履约计划行,C103-9同步前置至特批挂起之前

This commit is contained in:
刘泉佳
2026-10-14 14:11:31 +08:00
parent 433015fde0
commit 076145d7f6
6 changed files with 209 additions and 33 deletions
@@ -212,6 +212,43 @@ public final class HcsSyncConstant {
*/
public static final String SPEC_REVIEW_NODE = "特批审核";
// ==================== 履约计划行(blade_contract_exe_stage,C103-9) ====================
/**
* 行级同步操作:无(同步成功后清除标记)
*/
public static final String ROW_OP_NONE = "none";
/**
* 行级同步操作:新增
*/
public static final String ROW_OP_ADD = "add";
/**
* 行级同步操作:更新
*/
public static final String ROW_OP_UPDATE = "update";
/**
* 行级同步操作:删除
*/
public static final String ROW_OP_DELETE = "delete";
/**
* 履约类型:付款
*/
public static final String PERFORM_TYPE_PAYMENT = "payment";
/**
* 履约类型:收款
*/
public static final String PERFORM_TYPE_COLLECTION = "collection";
/**
* 款项性质:进度款
*/
public static final String FUND_NATURE_PROGRESS = "progress";
// ==================== Redis 缓存 key(照 BFI blade:third: 前缀惯例) ====================
/**
@@ -26,6 +26,7 @@
package org.springblade.transport.service;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import java.util.List;
@@ -45,6 +46,14 @@ public interface IContractExeStageService extends BaseService<HcsContractExeStag
*/
List<HcsContractExeStage> listByContractId(Long contractId);
/**
* 审批通过自动创建默认履约计划行(合同下无规则行时):承运商合同=付款进度款(收票规则取合同结算模式),
* 客户合同=收款进度款;金额/资金日期留空,随合同链路推 C103-9
*
* @param contractManage 合同单(审批通过)
*/
void createDefaultRowOnApprove(ContractManage contractManage);
/**
* 保存合同履约规则行(按行差异置同步标记:新行/未同步行 add,已同步行 update)
*
@@ -57,11 +57,6 @@ import java.util.List;
@RequiredArgsConstructor
public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeStageMapper, HcsContractExeStage> implements IContractExeStageService {
/**
* 行级同步操作:无
*/
private static final String ROW_OP_NONE = "none";
private final ContractManageMapper contractManageMapper;
private final HcsProperties hcsProperties;
@@ -87,7 +82,7 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
row.setContractId(contractId);
if (row.getId() == null) {
// 新行:待随合同链路首推(row_op=add)
row.setRowOp("add");
row.setRowOp(HcsSyncConstant.ROW_OP_ADD);
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
row.setHcsSyncMsg("待随合同链路同步(C103-9)");
row.setFundType(StringUtil.isBlank(row.getFundType()) ? hcsProperties.getExeStage().getFundType() : row.getFundType());
@@ -99,7 +94,7 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
throw new ServiceException("履约规则行不属于该合同:" + row.getId());
}
// 已同步行编辑 → update;未同步行保持 add
String rowOp = StringUtil.isBlank(existing.getHcsStageFdId()) ? "add" : "update";
String rowOp = StringUtil.isBlank(existing.getHcsStageFdId()) ? HcsSyncConstant.ROW_OP_ADD : HcsSyncConstant.ROW_OP_UPDATE;
existing.setPerformType(row.getPerformType());
existing.setFundNature(row.getFundNature());
existing.setFundAmount(row.getFundAmount());
@@ -127,7 +122,7 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
}
if (StringUtil.isNotBlank(row.getHcsStageFdId())) {
// 已同步行:软删+标记 delete,由 C103-9 凭合规阶段ID 上报删除(ADR-0002,不可物理删除)
row.setRowOp("delete");
row.setRowOp(HcsSyncConstant.ROW_OP_DELETE);
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
row.setHcsSyncMsg("待随合同链路同步(C103-9 delete)");
updateById(row);
@@ -137,6 +132,48 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
return true;
}
@Override
public void createDefaultRowOnApprove(ContractManage contractManage) {
Long contractId = contractManage.getId();
if (count(Wrappers.<HcsContractExeStage>lambdaQuery().eq(HcsContractExeStage::getContractId, contractId)) > 0) {
return;
}
HcsContractExeStage row = new HcsContractExeStage();
row.setContractId(contractId);
if ("承运商合同".equals(contractManage.getContractCategory())) {
row.setPerformType(HcsSyncConstant.PERFORM_TYPE_PAYMENT);
row.setInvoiceRule(mapInvoiceRule(contractManage.getSettlementMode()));
} else if ("客户合同".equals(contractManage.getContractCategory())) {
row.setPerformType(HcsSyncConstant.PERFORM_TYPE_COLLECTION);
} else {
log.info("合同类别非承运商/客户,不自动创建履约计划行:contractNo={},contractCategory={}", contractManage.getContractNo(), contractManage.getContractCategory());
return;
}
// 默认进度款计划行:金额/资金日期留空,随合同链路推 C103-9
row.setFundNature(HcsSyncConstant.FUND_NATURE_PROGRESS);
row.setFundType(hcsProperties.getExeStage().getFundType());
row.setIsFinish(0);
row.setRowOp(HcsSyncConstant.ROW_OP_ADD);
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
row.setHcsSyncMsg("审批通过自动创建,待随合同链路同步(C103-9)");
save(row);
log.info("审批通过自动创建履约计划行:contractNo={},contractCategory={},performType={},invoiceRule={}",
contractManage.getContractNo(), contractManage.getContractCategory(), row.getPerformType(), row.getInvoiceRule());
}
/**
* 结算模式文本映射 C103-9 开票规则:先款后票=payFirst、先票后款=invoiceFirst,其余(月结/现结等)不传
*/
private String mapInvoiceRule(String settlementMode) {
if ("先款后票".equals(settlementMode)) {
return "payFirst";
}
if ("先票后款".equals(settlementMode)) {
return "invoiceFirst";
}
return null;
}
/**
* 行字段校验
*/
@@ -144,7 +181,7 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
if (StringUtil.isBlank(row.getPerformType())) {
throw new ServiceException("履约类型不能为空(payment/collection)");
}
if (!List.of("payment", "collection").contains(row.getPerformType())) {
if (!List.of(HcsSyncConstant.PERFORM_TYPE_PAYMENT, HcsSyncConstant.PERFORM_TYPE_COLLECTION).contains(row.getPerformType())) {
throw new ServiceException("履约类型非法:" + row.getPerformType());
}
if (row.getFundAmount() != null && row.getFundAmount().signum() < 0) {
@@ -42,6 +42,7 @@ import org.springblade.transport.mapper.HcsContractExeStageMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
@@ -76,6 +77,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
private final HcsContractExeStageMapper hcsContractExeStageMapper;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
private final HcsCreditCheckSupport hcsCreditCheckSupport;
private final IContractExeStageService contractExeStageService;
private static final String STAGE_DRAFT = "draft";
private static final String STAGE_TEMPORARY = "temporary";
@@ -236,6 +238,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
// HCS 推送:审批通过(含变更通过)置待同步状态,事务提交后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
// 合同审批通过:履约规则未同步行统一置待同步标记,随合同链路调 C103-9(ADR-0002)
// 审批通过自动创建默认履约计划行(承运商=付款进度款/客户=收款进度款),已有规则行时不重复创建
contractExeStageService.createDefaultRowOnApprove(contractManage);
hcsContractExeStageMapper.update(null, Wrappers.<HcsContractExeStage>lambdaUpdate()
.eq(HcsContractExeStage::getContractId, contractManage.getId())
.isNull(HcsContractExeStage::getHcsStageFdId)
@@ -72,9 +72,9 @@ import java.util.Objects;
* <p>
* 扫描任务按序执行:⓪ 特批挂起门禁 → ① 客商风险校验(可配开关,仅留痕,高风险不再阻断)→
* ② 附件转传(v4 挂载点 fdFieldJson.attachment,随 C103-1 挂载)→ ③ C103-3 判重(§9-U4 查询能力联调确认)→
* ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→ ⑤ 高风险/黑名单挂起"特批审核"
* (外部客商管控:报文随 fdSepcailContract=true 在合规侧走特批,{@link HcsContractSpecSyncSupport} 接收结果)→
* ⑥ 履约规则 C103-9 行级同步({@link HcsExeStageSyncSupport})。
* ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→ ⑤ 履约计划 C103-9 行级同步({@link HcsExeStageSyncSupport},
* 仅依赖 hcs_fd_id,先于特批挂起执行,失败补偿不被特批门禁阻塞)→ ⑥ 高风险/黑名单挂起"特批审核"
* (外部客商管控:报文随 fdSepcailContract=true 在合规侧走特批,{@link HcsContractSpecSyncSupport} 接收结果)。
* 结果留痕:风险校验写 blade_hcs_check_log 与合同 hcs_risk_* 字段;推送结果回填 hcs_fd_id/hcs_sync_status。
*
* @author Chill
@@ -94,6 +94,11 @@ public class HcsContractSyncSupport {
*/
private static final String BLACK_STATUS_YES = "yes";
/**
* signer 相对方单位类型:暂统一传 customer(客户,2026-10-14 联调确认,§9-U6 后续细化)
*/
private static final String SIGNER_FD_TYPE_CUSTOMER = "customer";
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ContractManageMapper contractManageMapper;
@@ -105,7 +110,7 @@ public class HcsContractSyncSupport {
/**
* 推送合同到 HCS:特批挂起门禁 → 风险校验(仅留痕,高风险不再阻断)→ 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
* → 高风险/黑名单客商合同挂起"特批审核"等待合规特批结果(报文随 fdSepcailContract=true,外部客商管控)
* → 履约计划行级同步(C103-9,先于特批挂起)→ 高风险/黑名单客商合同挂起"特批审核"等待合规特批结果(报文随 fdSepcailContract=true,外部客商管控)
*
* @param contractManage 合同单(approvalStatus 须为 approved/change_approved)
* @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
@@ -145,17 +150,17 @@ public class HcsContractSyncSupport {
markSynced(contractManage, contractManage.getHcsFdId(), "C103-11 合同更新成功");
resultMessage = "C103-11 合同更新成功";
}
// ⑤ 高风险/黑名单客商(外部客商管控):起草/更新报文已随 fdSepcailContract=true 在合规侧发起特批流程,
// ⑤ 履约规则 C103-9 行级同步(随合同链路,§5.3;仅依赖 hcs_fd_id 与特批无关,先于特批挂起执行——
// 失败抛出落 failed 时特批未挂起,修复后可直接重推补偿,不被特批门禁阻塞)
if (hcsProperties.getExeStage().isSyncEnabled()) {
resultMessage = resultMessage + ";" + hcsExeStageSyncSupport.syncExeStages(contractManage);
}
// ⑥ 高风险/黑名单客商(外部客商管控):起草/更新报文已随 fdSepcailContract=true 在合规侧发起特批流程,
// 此处挂起"特批审核"等待结果(合规推送 contractLegal/result 或定时 C103-4 查询联动恢复/驳回)
if (risky) {
hangSpecReview(contractManage);
}
// ⑥ 履约规则 C103-9 行级同步(随合同链路,§5.3;一期暂不对接默认关,打开后失败抛出由任务落 failed,下轮凭 hcs_fd_id 走更新补偿)
if (!hcsProperties.getExeStage().isSyncEnabled()) {
return resultMessage;
}
String exeStageResult = hcsExeStageSyncSupport.syncExeStages(contractManage);
return resultMessage + ";" + exeStageResult;
return resultMessage;
}
/**
@@ -361,7 +366,10 @@ public class HcsContractSyncSupport {
settlementType = HcsContractFieldConstant.SETTLEMENT_TYPE_COLLECTION;
}
if (StringUtil.isNotBlank(settlementType)) {
fields.add(field(HcsContractFieldConstant.FIELD_FD_SETTLEMENT_TYPE, "合同结算类型", settlementType));
HcsXformFieldDTO settlementField = field(HcsContractFieldConstant.FIELD_FD_SETTLEMENT_TYPE, "合同结算类型", settlementType);
// 字典类字段需带 fdIsSystem=true(合规侧按系统字典值解析,2026-10-14 联调确认)
settlementField.setFdIsSystem("true");
fields.add(settlementField);
} else {
log.warn("HCS合同结算类型无法确定:contractCategory=[{}] 非承运商合同/客户合同,fdSettlementType 不传", contractManage.getContractCategory());
}
@@ -409,7 +417,7 @@ public class HcsContractSyncSupport {
}
/**
* signer 相对方(签署方):甲/乙双方,fdType(inner/外部)判定规则待联调(§9-U6)暂不传
* signer 相对方(签署方):甲/乙双方,fdType 暂统一传 customer(客户,2026-10-14 联调确认)
*/
private List<HcsSignerDTO> buildSigners(ContractManage contractManage) {
List<HcsSignerDTO> signers = new ArrayList<>();
@@ -431,6 +439,8 @@ public class HcsContractSyncSupport {
signer.setFdName(partyName);
// 相对方编号:按名称反查 CustomerArchive 客商编码,反查不到留空(§9-U6)
signer.setFdNumber(queryCustomerCode(partyName));
// 单位类型:暂统一传 customer(客户),inner/外部判定规则后续联调细化(§9-U6)
signer.setFdType(SIGNER_FD_TYPE_CUSTOMER);
return signer;
}
@@ -1,7 +1,7 @@
# 新建合同推送 HCS(合同合规系统)流程梳理
> 依据代码现状整理,核心类:`HcsContractSyncSupport`(blade-service/blade-transport/.../support/hcs)。
> 对接接口:C101-1 风险校验、C103-2 附件上传、C103-3 合同判重、C103-1 合同发起、C103-11 合同更新、C103-8 特批、C103-9 履约规则;特批结果接收 `/hcs/contractLegal/result`(合规推送,替代未建设的 B103,见 §9.4)。
> 对接接口:C101-1 风险校验、C103-2 附件上传、C103-3 合同判重、C103-1 合同发起、C103-11 合同更新、C103-8 特批、C103-9 履约计划、C108-1/C108-2 履约记录;特批结果接收 `/hcs/contractLegal/result`(合规推送,替代未建设的 B103,见 §9.4)。
> "必填" 标注来源:代码注释中标注的 v4 接口文档要求(HCS 侧),以及 TMS 推送前强校验(缺失直接失败)。
---
@@ -34,13 +34,13 @@
④ 合同发起/更新
首推且未判重命中 → C103-1 发起(返回 fdContractId,必须持久化)
已有 hcsFdId(变更单)→ C103-11 更新(顶层 fdId 定位)
⑤ 自动特批(C103-8,高风险/黑名单 且 hcsSpecStatus ≠ approved 时)
置 hcsSpecStatus=need + currentNode="特批审核"(TMS 流程挂起展示)
→ 筛选「特批说明」附件(attachmentsJson 中 attachmentType=spec_explanation)转传 C103-2
→ 自动发起 C103-8 → hcsSpecStatus=processing
发起失败保持 need 仅留痕,可人工经 /hcs/contract/spec-apply 重发
⑥ 履约规则同步(C103-9,开关 exe-stage.sync-enabled,一期默认关闭)
随合同链路行级同步,失败抛出走下轮凭 hcs_fd_id 更新补偿
⑤ 履约计划同步(C103-9,开关 exe-stage.sync-enabled,一期默认关闭,详见 §十)
仅依赖 hcs_fd_id,先于特批挂起执行(2026-10-14 调整);
失败抛出走下轮凭 hcs_fd_id 更新补偿,不被特批门禁阻塞
⑥ 自动特批挂起(高风险/黑名单 且 hcsSpecStatus ≠ approved 时)
报文随 fdSepcailContract=true 在合规侧发起特批流程(2026-10-13 起,无独立 C103-8 发起接口)
→ 置 hcsSpecStatus=processing + currentNode="特批审核"(TMS 流程挂起展示,
等待合规推送 contractLegal/result 或 C103-4 定时查询联动;发起异常仅留痕)
```
结果回填:
@@ -139,7 +139,11 @@
| `contract.require-risk-check` | 推送前客商风险校验开关 | — |
| `risk.block-blacklist` | 黑名单阻断付款提交(付款链路);合同链路不再阻断推送,高风险/黑名单转自动特批(§九) | — |
| `attachment.transfer-enabled` | 附件转传开关 | 关闭时不挂附件 |
| `exe-stage.sync-enabled` | C103-9 履约规则同步开关(一期默认关) | — |
| `exe-stage.sync-enabled` | C103-9 履约计划同步开关(一期默认关) | — |
| `exe-stage.fund-type` | C103-9 行金额类型缺省值(默认 byAmount) | — |
| `operator-login-name` | C103-9 报文操作人登录名(默认 srvadmin,非承办人) | — |
| `perf-record.sync-enabled` | C108-1/2 履约记录同步开关(一期默认关) | — |
| `perf-record.xform-id` | 履约记录表单模板ID(待合规方提供) | 未配置不传 fd_xform_id |
---
@@ -148,6 +152,7 @@
| 字段 | 含义 |
| --- | --- |
| `hcs_fd_id` | 合规合同ID(C103-1 返回的 fdContractId,判重命中也回填) |
| `hcs_exeplan_id` | 履约计划ID(C103-9 返回的 fdExeplanId,整体 update 定位用) |
| `hcs_sync_status` | unsynced / synced / failed |
| `hcs_retry_count` | 推送重试次数 |
| `hcs_sync_time` / `hcs_sync_msg` | 最近推送时间 / 结果描述 |
@@ -193,8 +198,8 @@ TMS 审批通过(approved / change_approved)
→ 即时推送 / 扫描兜底 → pushContract
→ ① 风险校验留痕(risky 标志,不阻断)
→ ④ C103-1 起草成功(hcs_sync_status=synced,回填 hcs_fd_id)
→ ⑤ 置 hcs_spec_status=need + currentNode="特批审核"(TMS 流程挂起)
→ 筛「特批说明」附件转传 C103-2 → 自动发起 C103-8 → hcs_spec_status=processing
→ ⑤ C103-9 履约计划同步(先于特批挂起;失败单据落 failed,修复后重推不被特批门禁阻塞)
→ ⑥ 置 hcs_spec_status=processing + currentNode="特批审核"(TMS 流程挂起,合规侧随报文 fdSepcailContract=true 走特批)
```
- 原设计"黑名单挂起不推送、人工发起特批"存在死锁(挂起时无 `hcs_fd_id`,C103-8 报文必填合规合同ID 永远失败),本次变更为 **先 C103-1 起草、后 C103-8 特批**,特批通过即合规侧放行签约,无需恢复推送;
@@ -218,3 +223,77 @@ TMS 审批通过(approved / change_approved)
| rejected | `approvalStatus=rejected` + `currentNode="已驳回"`,走重新编辑 → 提交 → 审批 → 重新特批闭环(重新推送时 C103-11 更新后再次自动特批),reason 随原文见 `hcs_spec_msg` |
> 等效兜底:页面人工确认(`POST /hcs/contract/spec/confirm`,合同)与(`POST /hcs/spec-pay/confirm`,付款)与合规推送等效,二者均以先到为准。原推测性接收端点 `POST /hcs/callback/spec-result`(B103 候选键解析)已删除。
---
## 十、履约计划(C103-9)与履约记录(C108-1/C108-2)同步
> 业务定位:涉及收付款的合同须在合规侧建立履约计划,付款履约记录凭计划阶段ID(`hcs_stage_fd_id`)关联阶段。
> 一期 `exe-stage.sync-enabled=false`、`perf-record.sync-enabled=false` 暂不上报 HCS(2026-10-09 范围调整),但 `blade_contract_exe_stage` 表、维护端点与审批标记逻辑均在运行。
### 10.1 数据模型与维护入口
`blade_contract_exe_stage` 是 TMS 侧的**合同履约计划行项目表**:合同的收付款资金计划逐行存储(如"预付款 30 万 / 进度款 50 万 / 质保金 20 万"各一行),是 C103-9 推送 HCS 的数据源。单独建表而非挂合同主表 JSON 的原因:
- **行级同步**:C103-9 是行级 add/update/delete 操作,每行需独立 `row_op` 标记、独立回填合规阶段ID、独立同步状态
- **删除不可逆**:已同步行必须软删后凭 `hcs_stage_fd_id` 上报 delete,直接物理删会在合规侧留孤儿数据
- **重试粒度**:单行失败(如 add 未匹配到返回阶段ID)可单独保持 failed 下轮重试,不拖累整单
- **一期攒数据**:开关关闭不上报,但表与维护端点照常运行,打开后可凭已有数据补推
行字段(`HcsContractExeStage`):
| 字段 | 说明 |
| --- | --- |
| `perform_type` | 履约类型 payment=付款 / collection=收款 |
| `fund_nature` | 款项性质 prepayment=预付 / progress=进度 / acceptance=验收 / retention=质保 |
| `fund_amount` / `fund_date` | 计划金额(元)/ 计划资金日期 |
| `invoice_rule` / `invoice_cycle` | 开票规则 payFirst=先款后票 / invoiceFirst=先票后款;账期(天) |
| `fund_type` / `is_finish` | 金额类型(空取配置 `exe-stage.fund-type`)/ 是否完成 |
| `row_op` | 行级同步标记 none / add / update / delete |
| `hcs_stage_fd_id` | 合规阶段ID(C103-9 返回,行 update/delete 全靠它,必须持久化) |
| `hcs_sync_status` / `hcs_sync_msg` | 行级同步状态与结果 |
> `fund_type`(金额类型)是 HCS 报文要求的**金额口径声明**(byAmount=按固定金额),一期三级兜底恒为 byAmount(建表默认 → 行值为空取 `exe-stage.fund-type` → 推送组装时再兜底);其他口径(如按比例)值域 HCS 文档未给出,待联调后放开,代码不做枚举校验。
维护端点 `ContractExeStageController`(列表 / 批量提交 / 删除,`ContractExeStageServiceImpl`):
- 校验:`performType` 必填且仅 payment/collection,计划金额非负
- 新行置 `row_op=add` 待随合同链路首推;已同步行(有 `hcs_stage_fd_id`)编辑置 update、未同步行保持 add
- 删除:已同步行软删 + `row_op=delete`(须凭合规阶段ID 上报删除,不可物理删除);未同步行直接物理删除
### 10.2 同步时机
- 合同审批通过(`ContractManageServiceImpl#approve`):未同步行(`hcs_stage_fd_id is null`)统一置 `row_op=add`
- 执行位置:`pushContract` 第⑤步(先于特批挂起,2026-10-14 调整);前置强约束:合同须已有 `hcs_fd_id`(先 C103-1 起草、后 C103-9);开关关闭时直接跳过
### 10.3 C103-9 报文与回填(`HcsExeStageSyncSupport`)
顶层(`HcsExeplanSyncDTO`):`fdContractId`=`hcs_fd_id`(合规合同ID,非 TMS 主键)、`fdOuterSystem`=`src-system`、`fdLoginName`=配置 `operator-login-name`(默认 srvadmin)、`fdEopExeplanOperateType` 首次 add / 已同步 update(看 `hcs_exeplan_id`,已同步则同时传 `fdExeplanId`)。
行级 `fdStages[]`(对应待同步行):
| HCS 字段 | TMS 取值 | 备注 |
| --- | --- | --- |
| `fdId` | `hcs_stage_fd_id` | add 行不传;update/delete 必传 |
| `fdEopExeplanOperateType` | `row_op` | add / update / delete |
| `fdPerformType` | `perform_type` | 必填 |
| `fdFundNature` / `fdFundAmount` / `fdFundDate` | 同名字段 | 资金日期转毫秒时间戳 |
| `fdInvoiceRule` / `fdInvoiceCycle` | 同名字段 | 账期(天) |
| `fdFundType` | `fund_type`,空取 `exe-stage.fund-type` | 默认 byAmount |
| `fdIsFinish` | `is_finish` | boolean |
| `fdExtendContent` | 固定 `"{}"` | |
取数与回填:
- 取数 `selectSyncRows`:`row_op <> 'none'` 的行,**含软删的 delete 行**(原生 SQL 绕过逻辑删除过滤)
- 成功回填三件事:`fdExeplanId` → `blade_contract_manage.hcs_exeplan_id`;`fdChangeStages[]` 各行合规 fdId → `hcs_stage_fd_id`;成功行清 `row_op=none` 并置行级 synced
- `fdChangeStages` 与请求行对应关系 v4 未明确(待联调):行数一致按请求行序对应;不一致按 履约类型|款项性质|金额|资金日期 组合键匹配;add 行匹配不到保持 failed 待下轮重试
- 失败抛异常走合同 `markFailed`,下轮凭 `hcs_fd_id` 整体重推补偿(§二⑥)
### 10.4 履约记录(付款链路,C108-1/C108-2)
`HcsPerfRecordSyncSupport`,随付款推送链路执行(`HcsPaymentSyncSupport`,开关 `perf-record.sync-enabled`,一期默认关):
- 付款审批通过 → C108-1 新建,返回 fdId 回填付款申请 `hcs_perf_fd_id`(立即持久化)
- 实际付款推进(`paidAmount` 变化置待同步)→ C108-2 更新,`formData.fdId`=`hcs_perf_fd_id` 必填(响应无 data,success=true 即成功)
- 关联履约阶段:按合同 + 付款类型匹配**已同步**的计划行(`hcs_stage_fd_id` 非空),无关联不传 fd_id —— 履约计划同步是履约记录关联的前置