invoiceTypeMapping = new HashMap<>();
+ }
+
+ @Data
+ public static class Attachment {
+ /**
+ * 附件转传总开关
+ */
+ private boolean transferEnabled = true;
+ /**
+ * TMS 文件服务下载基址(拉取附件流用)
+ */
+ private String fileDownloadBase = "";
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/ThirdPartyHcsAutoConfiguration.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/ThirdPartyHcsAutoConfiguration.java
new file mode 100644
index 00000000..1331c36a
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/ThirdPartyHcsAutoConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 第三方API 合同合规系统(HCS)配置类
+ *
+ * @author Chill
+ */
+@ComponentScan("org.springblade.hcs")
+@EnableConfigurationProperties(HcsProperties.class)
+@Configuration
+public class ThirdPartyHcsAutoConfiguration {
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/constant/HcsConstant.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/constant/HcsConstant.java
new file mode 100644
index 00000000..a8fd786c
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/constant/HcsConstant.java
@@ -0,0 +1,79 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.constant;
+
+/**
+ * 合同合规系统(HCS)常量类
+ *
+ * @author Chill
+ */
+public class HcsConstant {
+
+ /**
+ * 接口调用成功 messageKey(统一出参 code,业务成败以 success 字段为准)
+ */
+ public static final String SUCCESS_CODE = "return.optSuccess";
+
+ /**
+ * 认证方式:OAuth2(默认,URL 追加 access_token)
+ */
+ public static final String AUTH_TYPE_OAUTH2 = "oauth2";
+
+ /**
+ * 认证方式:Basic(Authorization 请求头)
+ */
+ public static final String AUTH_TYPE_BASIC = "basic";
+
+ /**
+ * 网关认证请求头名称
+ */
+ public static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * Basic 认证请求头前缀
+ */
+ public static final String HEADER_BASIC_PREFIX = "Basic ";
+
+ /**
+ * OAuth2 access_token 追加到业务请求 URL 的参数名
+ */
+ public static final String QUERY_ACCESS_TOKEN = "access_token";
+
+ /**
+ * HCS access_token 缓存 key(照 BFI 令牌缓存命名惯例)
+ */
+ public static final String HCS_TOKEN_CACHE_KEY = "blade:third:hcs:token";
+
+ /**
+ * token 提前失效缓冲时长(毫秒),防止刚好过期(照 BFI 120s 缓冲)
+ */
+ public static final long TOKEN_EXPIRE_BUFFER_MILLIS = 120_000L;
+
+ /**
+ * getToken 接口默认路径片段,拦截器据此跳过令牌追加(避免给取令牌请求再带 access_token)
+ */
+ public static final String GET_TOKEN_PATH = "/authapi/getToken";
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentDownloadDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentDownloadDTO.java
new file mode 100644
index 00000000..95c56a68
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentDownloadDTO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-5 附件下载入参(对账/排查备用)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsAttachmentDownloadDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 附件ID
+ */
+ private String fdId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentRefDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentRefDTO.java
new file mode 100644
index 00000000..65a5d05e
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentRefDTO.java
@@ -0,0 +1,70 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-2 上传取得 fdId,再经 mechanisms.attachment[] 引用)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsAttachmentRefDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 附件ID(附件上传接口返回)
+ */
+ private String fdId;
+ /**
+ * 附件名称(不含扩展名)
+ */
+ private String fdFileName;
+ /**
+ * 附件扩展名
+ */
+ private String fdFileExtName;
+ /**
+ * 附件完整文件名
+ */
+ private String fullName;
+ /**
+ * 附件绑定的表单字段 key:特批 fd_spc_approval_attach、履约记录 fd_plan_att、收票单 fd_attach
+ */
+ private String fdEntityKey;
+ /**
+ * 绑定类型:仅发起类接口传 ADD,更新类接口不传
+ */
+ private String fdBindType;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentVO.java
new file mode 100644
index 00000000..6b8db430
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsAttachmentVO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-2 附件上传出参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsAttachmentVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 附件ID(调用方必须持久化/缓存映射,供各业务接口引用)
+ */
+ private String fdId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsBpmOperationDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsBpmOperationDTO.java
new file mode 100644
index 00000000..50512898
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsBpmOperationDTO.java
@@ -0,0 +1,54 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C102-1 项目立项新建 流程操作入参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsBpmOperationDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 操作类型:固定 drafter_submit(以起草人身份直接提交流程)
+ */
+ private String operationType;
+ /**
+ * 立项表单模板编码(环境相关值,配置 thirdParty.hcs.project.template-code,§9-U9)
+ */
+ private String templateCode;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCategoryVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCategoryVO.java
new file mode 100644
index 00000000..a477400e
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCategoryVO.java
@@ -0,0 +1,62 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-7 合同分类列表出参行(fdParent 可组装分类树)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsCategoryVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 分类ID
+ */
+ private String fdId;
+ /**
+ * 分类名称(查询条件 fdName.$contains)
+ */
+ private String fdName;
+ /**
+ * 上级分类
+ */
+ private HcsRefVO fdParent;
+ /**
+ * 状态:enable / disable
+ */
+ private String fdStatus;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDetailQueryDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDetailQueryDTO.java
new file mode 100644
index 00000000..6075cf10
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDetailQueryDTO.java
@@ -0,0 +1,56 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-4 合同详情入参(fdId 与 fdNumber 二选一)
+ *
+ * TODO 待联调确认:出参 data 结构 v4 未定义,对账功能联调后以实际返回为准(设计文档 §5.9)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsContractDetailQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同ID
+ */
+ private String fdId;
+ /**
+ * 合同编号
+ */
+ private String fdNumber;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftDTO.java
new file mode 100644
index 00000000..8f0f98a6
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftDTO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-1 合同发起入参(C103-11 合同更新入参结构相同,复用本 DTO)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsContractDraftDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同模板ID(C103-6 模板列表获取,配置 template-mapping,§9-U7)
+ */
+ private String fdTemplateId;
+ /**
+ * 外部系统标识(fd_sources,约定值 TMS,配置 thirdParty.hcs.src-system)
+ */
+ private String fdOuterSystem;
+ /**
+ * 操作人登录名
+ */
+ private String fdLoginName;
+ /**
+ * 保存类型:submit(提交)
+ */
+ private String saveType;
+ /**
+ * 合同字段(模板字段 + 签署方)
+ */
+ private HcsContractFieldJsonDTO fdFieldJson;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftVO.java
new file mode 100644
index 00000000..60d815e9
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftVO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-1 合同发起出参(fdContractId 必须持久化,用于合同更新、履约规则同步、额度校验等)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsContractDraftVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同ID(回填 TMS blade_contract_manage.hcs_fd_id)
+ */
+ private String fdContractId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractFieldJsonDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractFieldJsonDTO.java
new file mode 100644
index 00000000..702bcbea
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractFieldJsonDTO.java
@@ -0,0 +1,57 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)C103-1 合同发起 fdFieldJson 节点(C103-1 新建 / C103-11 更新共用)
+ *
+ * §9-U8 待联调确认:v4 未定义合同附件传递字段,附件挂载方式待合规方确认后补充。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsContractFieldJsonDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 模板字段值列表
+ */
+ private List xformFields;
+ /**
+ * 签署方(相对方)列表
+ */
+ private List signer;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractVO.java
new file mode 100644
index 00000000..f3285515
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractVO.java
@@ -0,0 +1,62 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-3 合同信息列表出参行(判重与对账回查,按 columns 返回)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsContractVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同ID
+ */
+ private String fdId;
+ /**
+ * 合同标题(判重条件 fdNumber 关联核对)
+ */
+ private String fdSubject;
+ /**
+ * 合同编号(判重条件,§9-U4 查询能力联调确认)
+ */
+ private String fdNumber;
+ /**
+ * 合同分类层级ID
+ */
+ private String fdCateHierarchyId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckDTO.java
new file mode 100644
index 00000000..c014ed49
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckDTO.java
@@ -0,0 +1,55 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C106-1 项目/合同额度校验入参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsCreditCheckDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同ID(必填,HCS 合同ID blade_contract_manage.hcs_fd_id)
+ */
+ private String fdContractId;
+ /**
+ * 申请金额(必填,元)
+ */
+ private BigDecimal fdApplyAmount;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckVO.java
new file mode 100644
index 00000000..51f2e437
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCreditCheckVO.java
@@ -0,0 +1,69 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C106-1 项目/合同额度校验出参
+ *
+ * 注意:超额时外层 success 仍为 true,必须以 fdIsCredit 为准;超额不拦截提交,转入特批链路(D15/R2-Q2)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsCreditCheckVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 额度是否通过:true 通过 / false 超额(必看字段)
+ */
+ private Boolean fdIsCredit;
+ /**
+ * 合同ID
+ */
+ private String fdContractId;
+ /**
+ * 合同金额
+ */
+ private BigDecimal fdContractAmount;
+ /**
+ * 已付金额
+ */
+ private BigDecimal fdPayedAmount;
+ /**
+ * 超额原因说明(回填 hcs_credit_msg)
+ */
+ private String fdMessage;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCurrencyVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCurrencyVO.java
new file mode 100644
index 00000000..09d2a3fe
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCurrencyVO.java
@@ -0,0 +1,78 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-12 合同币种列表出参行(开票登记 fdCurrencyId、履约计划币种取自本接口)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsCurrencyVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 币种ID(currency-id-mapping 目标值)
+ */
+ private String fdId;
+ /**
+ * 币种中文名(查询条件 fdCName.$contains)
+ */
+ private String fdCName;
+ /**
+ * 币种简称
+ */
+ private String fdSName;
+ /**
+ * 币种英文名(如 CNY)
+ */
+ private String fdEName;
+ /**
+ * 币种编码
+ */
+ private String fdNumber;
+ /**
+ * 币种符号
+ */
+ private String fdSymbol;
+ /**
+ * 状态:enable / disable
+ */
+ private String fdStatus;
+ /**
+ * 是否本位币
+ */
+ private Boolean fdIsStandardCurrency;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java
new file mode 100644
index 00000000..1f6246c3
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java
@@ -0,0 +1,93 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C103-9(新) 履约规则同步 行级操作项(数据源 blade_contract_exe_stage,ADR-0002)
+ *
+ * 新增行可不带 fdId;更新/删除行必须带 fdId(合规阶段ID,回填自首次同步返回)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsExeplanStageDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 阶段项ID(行级 update/delete 必传,取 blade_contract_exe_stage.hcs_stage_fd_id)
+ */
+ private String fdId;
+ /**
+ * 行级操作:add / update / delete
+ */
+ private String fdEopExeplanOperateType;
+ /**
+ * 履约类型:payment=付款 / collection=收款
+ */
+ private String fdPerformType;
+ /**
+ * 款项性质:prepayment=预付款 / progress=进度款 / acceptance=验收款 / retention=质保金
+ */
+ private String fdFundNature;
+ /**
+ * 开票规则(如 payFirst)
+ */
+ private String fdInvoiceRule;
+ /**
+ * 账期
+ */
+ private Integer fdInvoiceCycle;
+ /**
+ * 计划金额(元)
+ */
+ private BigDecimal fdFundAmount;
+ /**
+ * 资金日期(毫秒时间戳)
+ */
+ private Long fdFundDate;
+ /**
+ * 金额类型(默认 byAmount 按金额,配置 thirdParty.hcs.exe-stage.fund-type)
+ */
+ private String fdFundType;
+ /**
+ * 是否完成
+ */
+ private Boolean fdIsFinish;
+ /**
+ * 扩展内容 JSON 字符串(固定 "{}")
+ */
+ private String fdExtendContent;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageVO.java
new file mode 100644
index 00000000..325b6081
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageVO.java
@@ -0,0 +1,97 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C103-9(新) 履约规则同步出参-变更阶段行
+ *
+ * 合规返回的阶段 fdId 必须持久化(回填 blade_contract_exe_stage.hcs_stage_fd_id),后续行更新/删除全靠它。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsExeplanStageVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 阶段项ID(必须持久化)
+ */
+ private String fdId;
+ /**
+ * 阶段唯一标识
+ */
+ private String fdUniqueId;
+ /**
+ * 计划金额
+ */
+ private BigDecimal fdPlanFundAmount;
+ /**
+ * 实际金额
+ */
+ private BigDecimal fdActualFundAmount;
+ /**
+ * 履约类型:payment / collection
+ */
+ private String fdPerformType;
+ /**
+ * 款项性质
+ */
+ private String fdFundNature;
+ /**
+ * 开票规则
+ */
+ private String fdInvoiceRule;
+ /**
+ * 账期
+ */
+ private Integer fdInvoiceCycle;
+ /**
+ * 计划金额
+ */
+ private BigDecimal fdFundAmount;
+ /**
+ * 资金日期(毫秒时间戳)
+ */
+ private Long fdFundDate;
+ /**
+ * 金额类型
+ */
+ private String fdFundType;
+ /**
+ * 扩展内容 JSON 字符串
+ */
+ private String fdExtendContent;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanSyncDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanSyncDTO.java
new file mode 100644
index 00000000..d8946967
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanSyncDTO.java
@@ -0,0 +1,74 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)C103-9(新) 合同履约规则同步入参(支持整批增删改)
+ *
+ * fdContractId 传 HCS 合同ID(blade_contract_manage.hcs_fd_id,非 TMS 主键);
+ * fdExeplanId 已同步过则传(首次同步返回后持久化到 blade_contract_manage.hcs_exeplan_id)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsExeplanSyncDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同ID(HCS 合同ID)
+ */
+ private String fdContractId;
+ /**
+ * 外部系统标识(fd_sources,约定值 TMS)
+ */
+ private String fdOuterSystem;
+ /**
+ * 操作人登录名
+ */
+ private String fdLoginName;
+ /**
+ * 计划整体操作类型(如 add,随场景)
+ */
+ private String fdEopExeplanOperateType;
+ /**
+ * 履约计划ID(已同步过则传)
+ */
+ private String fdExeplanId;
+ /**
+ * 行级操作列表
+ */
+ private List fdStages;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java
new file mode 100644
index 00000000..973ff964
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java
@@ -0,0 +1,55 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)C103-9(新) 履约规则同步出参(fdExeplanId 与变更阶段 fdId 均须持久化)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsExeplanVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 履约计划ID(回填 blade_contract_manage.hcs_exeplan_id)
+ */
+ private String fdExeplanId;
+ /**
+ * 本次变更的阶段列表(按行回填 hcs_stage_fd_id)
+ */
+ private List fdChangeStages;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsFormSaveVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsFormSaveVO.java
new file mode 100644
index 00000000..ae17ae93
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsFormSaveVO.java
@@ -0,0 +1,58 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)流程单据新建返回结果(C102-1 项目立项、C106-3 特批发起、C108-1 履约记录新建、C109-1 收票单新建出参)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsFormSaveVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 单据主键ID(调用方必须持久化,供后续更新使用)
+ */
+ private String fdId;
+ /**
+ * 表单模板ID
+ */
+ private String fdXFormId;
+ /**
+ * 单据查看页面路径
+ */
+ private String docViewUrl;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsIdVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsIdVO.java
new file mode 100644
index 00000000..1f51c6aa
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsIdVO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)单据ID返回结果(新建类接口出参 data 仅含 fdId 的场景:C103-2/C111-2/C111-3 等)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsIdVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 单据ID(调用方必须持久化,供后续更新/删除/关联使用)
+ */
+ private String fdId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegAddDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegAddDTO.java
new file mode 100644
index 00000000..e1d90bf1
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegAddDTO.java
@@ -0,0 +1,103 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)C111-2 新增开票登记入参
+ *
+ * 校验:fdContractNumber(须在合规本地存在)与 fdCurrencyId 必填;
+ * 附件通过 fdAttIds(C103-2 附件上传返回的ID数组)引用。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsInvoiceRegAddDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 单据编号(开票申请单号,同租户唯一,判重候选键)
+ */
+ private String fdNumber;
+ /**
+ * 登记日期(毫秒时间戳)
+ */
+ private Long fdRegisterDate;
+ /**
+ * 合同编号(必填,经应收结算链路回溯,§9-U10)
+ */
+ private String fdContractNumber;
+ /**
+ * 发票类型(字典映射 invoice-type-mapping,§9-U3)
+ */
+ private String fdInvoiceType;
+ /**
+ * 结算币种ID(必填,currency-id-mapping,§9-U3)
+ */
+ private String fdCurrencyId;
+ /**
+ * 开票方(销方 issuerName)
+ */
+ private String fdInvoiceParty;
+ /**
+ * 款项性质(默认 progress,配置 invoice.fund-nature-default)
+ */
+ private String fdFundNature;
+ /**
+ * 开票金额(含税)
+ */
+ private BigDecimal fdInvoiceAmount;
+ /**
+ * 备注
+ */
+ private String fdRemark;
+ /**
+ * 状态:0=已开票,1=已作废(新增固定 0)
+ */
+ private Integer fdStatus;
+ /**
+ * 第三方创建人编码(申请人工号)
+ */
+ private String fdThirdCreatorCode;
+ /**
+ * 第三方创建部门编码(申请部门编码)
+ */
+ private String fdThirdCreatorDeptCode;
+ /**
+ * 附件ID列表(C103-2 附件上传返回)
+ */
+ private List fdAttIds;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java
new file mode 100644
index 00000000..44465dfb
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java
@@ -0,0 +1,53 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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 lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 合同合规系统(HCS)C111-3 更新开票登记入参(金蝶开票完成后回填票据日期/状态)
+ *
+ * fdId 必填(C111-2 新增返回,持久化于 hcs_fd_id),其余可选;作废传 fdStatus=1。
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsInvoiceRegUpdateDTO extends HcsInvoiceRegAddDTO {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 开票登记ID(必填,C111-2 新增返回,持久化于 hcs_fd_id)
+ */
+ private String fdId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegVO.java
new file mode 100644
index 00000000..202e8fc9
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegVO.java
@@ -0,0 +1,127 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C111-1 查询开票登记列表出参行(判重条件 fdNumber,§9-U4 查询能力联调确认)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsInvoiceRegVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键ID
+ */
+ private String fdId;
+ /**
+ * 单据编号(TMS 开票申请单号)
+ */
+ private String fdNumber;
+ /**
+ * 登记日期(毫秒时间戳)
+ */
+ private Long fdRegisterDate;
+ /**
+ * 票据日期(毫秒时间戳,金蝶开票完成后回填)
+ */
+ private Long fdBillDate;
+ /**
+ * 合同
+ */
+ private HcsRefVO fdContract;
+ /**
+ * 合同编号
+ */
+ private String fdContractNumber;
+ /**
+ * 发票类型
+ */
+ private String fdInvoiceType;
+ /**
+ * 结算币种(币种ID)
+ */
+ private String fdCurrencyId;
+ /**
+ * 开票方
+ */
+ private String fdInvoiceParty;
+ /**
+ * 款项性质(prepayment/progress/acceptance/retention)
+ */
+ private String fdFundNature;
+ /**
+ * 开票金额(含税)
+ */
+ private BigDecimal fdInvoiceAmount;
+ /**
+ * 备注
+ */
+ private String fdRemark;
+ /**
+ * 状态:0=已开票,1=已作废
+ */
+ private Integer fdStatus;
+ /**
+ * 数据来源(如 erp)
+ */
+ private String fdDataSource;
+ /**
+ * 创建人
+ */
+ private HcsRefVO fdCreator;
+ /**
+ * 创建人部门
+ */
+ private HcsRefVO fdCreatorDept;
+ /**
+ * 创建时间(毫秒时间戳)
+ */
+ private Long fdCreateTime;
+ /**
+ * 第三方创建人编码
+ */
+ private String fdThirdCreatorCode;
+ /**
+ * 第三方创建部门编码
+ */
+ private String fdThirdCreatorDeptCode;
+ /**
+ * 发起主体(合同关联的发起主体名称)
+ */
+ private String fdInitiator;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsMechanismsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsMechanismsDTO.java
new file mode 100644
index 00000000..b2c1fc8c
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsMechanismsDTO.java
@@ -0,0 +1,51 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)附件挂载容器(C106 特批 / C108 履约记录 / C109 收票单的 mechanisms 节点)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsMechanismsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 附件列表(fdId 来自 C103-2 附件上传接口)
+ */
+ private List attachment;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageQueryDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageQueryDTO.java
new file mode 100644
index 00000000..15a2a201
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageQueryDTO.java
@@ -0,0 +1,75 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 合同合规系统(HCS)通用分页查询入参(§1.5 分页与查询约定,各查询接口通用)
+ *
+ * 注意:C102-3 项目立项查询的 columns/sorts 用下划线字段名(fd_id/fd_num/fd_pro_name/fd_create_time),
+ * 其余查询接口出入参均为驼峰;出参 content 均为驼峰字段。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsPageQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 查询返回的字段列表(视接口必填)
+ */
+ private List columns;
+ /**
+ * 查询条件,空对象表示无条件;支持操作符,如 {"fdName": {"$contains": "xx"}}(包含匹配)
+ */
+ private Map conditions;
+ /**
+ * 分页偏移量(必填,从 0 开始)
+ */
+ private Integer offset;
+ /**
+ * 每页条数(必填)
+ */
+ private Integer pageSize;
+ /**
+ * 排序条件,如 {"fdCreateTime": "desc"}
+ */
+ private Map sorts;
+ /**
+ * 当前操作人(C102-3 项目立项查询需携带,其余查询接口按需)
+ */
+ private HcsUserRefVO curUser;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageVO.java
new file mode 100644
index 00000000..14b4cbbb
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPageVO.java
@@ -0,0 +1,62 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)分页查询返回结果(位于统一出参 data 内)
+ *
+ * @param 数据行类型
+ * @author Chill
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsPageVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 数据列表
+ */
+ private List content;
+ /**
+ * 分页偏移量(从 0 开始)
+ */
+ private Integer offset;
+ /**
+ * 每页条数
+ */
+ private Integer pageSize;
+ /**
+ * 总条数
+ */
+ private Integer totalSize;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordFormDataDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordFormDataDTO.java
new file mode 100644
index 00000000..16f41bbe
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordFormDataDTO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C108-1/C108-2 履约记录 formData 节点
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsPerfRecordFormDataDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 履约记录ID(C108-2 更新必填,取 C108-1 新建返回的 data.fdId)
+ */
+ private String fdId;
+ /**
+ * 履约记录业务字段
+ */
+ private HcsPerfRecordPropsDTO dynamicProps;
+ /**
+ * 单据主题
+ */
+ private String fdDocSubject;
+ /**
+ * 单据状态(示例 10)
+ */
+ private String fdDocStatus;
+ /**
+ * 表单模板ID(环境值,§9-U9)
+ */
+ private String fdXformId;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordPropsDTO.java
new file mode 100644
index 00000000..799bb389
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordPropsDTO.java
@@ -0,0 +1,110 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 合同合规系统(HCS)C108-1/C108-2 履约记录 dynamicProps 业务字段(字段名按接口文档下划线原样传值)
+ *
+ * 注意:fd_paln_type 为接口文档原文拼写(plan 的笔误,必须沿用原拼写,否则字段无法识别);
+ * 金额字段为字符串传值。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsPerfRecordPropsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 申请金额(字符串传值)
+ */
+ @JsonProperty("fd_apply_amount")
+ private String fdApplyAmount;
+ /**
+ * 实际金额(字符串传值,实际付款推进时更新)
+ */
+ @JsonProperty("fd_fact_amount")
+ private String fdFactAmount;
+ /**
+ * 申请编号
+ */
+ @JsonProperty("fd_apply_no")
+ private String fdApplyNo;
+ /**
+ * 计划日期(毫秒时间戳)
+ */
+ @JsonProperty("fd_plan_date")
+ private Long fdPlanDate;
+ /**
+ * 计划类型(原文拼写 fd_paln_type,paymentType)
+ */
+ @JsonProperty("fd_paln_type")
+ private String fdPalnType;
+ /**
+ * 性质(款项性质,取关联履约规则行/结算性质)
+ */
+ @JsonProperty("fd_property")
+ private String fdProperty;
+ /**
+ * 版本ID
+ */
+ @JsonProperty("fd_version")
+ private String fdVersion;
+ /**
+ * 表单模板ID(环境值 fd_xform_id,§9-U9)
+ */
+ @JsonProperty("fd_xform_id")
+ private String fdXformId;
+ /**
+ * 来源履约计划项ID(关联履约阶段 hcs_stage_fd_id,无关联则不传)
+ */
+ @JsonProperty("fd_id")
+ private String fdId;
+ /**
+ * 合同ID(HCS 合同ID)
+ */
+ @JsonProperty("fd_contract_id")
+ private String fdContractId;
+ /**
+ * 附件不可下载
+ */
+ @JsonProperty("fd_att_no_download")
+ private Boolean fdAttNoDownload;
+ /**
+ * 草稿不可编辑打印
+ */
+ @JsonProperty("fd_draft_no_edit_prints")
+ private Boolean fdDraftNoEditPrints;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java
new file mode 100644
index 00000000..362d8539
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java
@@ -0,0 +1,61 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C108-1 履约记录新建入参(C108-2 更新入参结构相同,复用本 DTO)
+ *
+ * 更新时 formData.fdId 必填(新增返回的ID,持久化于 hcs_perf_fd_id),另可传 dynamicProps.fd_contract_id 指定合同;
+ * 更新响应无 data,以 success 判断结果。附件经 mechanisms.attachment[] 绑定,fdEntityKey=fd_plan_att。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsPerfRecordSaveDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 当前操作人 {fdLoginName}(必填)
+ */
+ private HcsUserRefVO curUser;
+ /**
+ * 履约记录表单数据
+ */
+ private HcsPerfRecordFormDataDTO formData;
+ /**
+ * 附件挂载
+ */
+ private HcsMechanismsDTO mechanisms;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectAddDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectAddDTO.java
new file mode 100644
index 00000000..090c421c
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectAddDTO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C102-1 项目立项新建入参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsProjectAddDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 流程操作(operationType=drafter_submit、templateCode 环境值)
+ */
+ private HcsBpmOperationDTO bpmOperation;
+ /**
+ * 是否校验权限(接口调用传 false)
+ */
+ private Boolean checkAuth;
+ /**
+ * 当前操作人 {fdLoginName}
+ */
+ private HcsUserRefVO curUser;
+ /**
+ * 项目表单数据
+ */
+ private HcsProjectFormDataDTO formData;
+ /**
+ * 保存选项(saveDraft=false 直接提交)
+ */
+ private HcsSaveOptionDTO saveOption;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectFormDataDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectFormDataDTO.java
new file mode 100644
index 00000000..b635d2f8
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectFormDataDTO.java
@@ -0,0 +1,54 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)项目立项 formData 节点(C102-1 新建 / C102-2 更新共用;更新时 fdId 必填)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsProjectFormDataDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 项目立项单ID(C102-1 新建返回,C102-2 更新必填)
+ */
+ private String fdId;
+ /**
+ * 项目业务字段
+ */
+ private HcsProjectPropsDTO dynamicProps;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectPropsDTO.java
new file mode 100644
index 00000000..e63ed1b4
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectPropsDTO.java
@@ -0,0 +1,149 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合同合规系统(HCS)项目立项 dynamicProps 业务字段(C102-1 新建 / C102-2 更新共用,字段名按接口文档下划线原样传值)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsProjectPropsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 项目名称
+ */
+ @JsonProperty("fd_pro_name")
+ private String fdProName;
+ /**
+ * 项目简称
+ */
+ @JsonProperty("fd_pro_simple_name")
+ private String fdProSimpleName;
+ /**
+ * 创建单位 {fdId, fdName}(HCS 组织 fdId 与 TMS 不同源,先传名称,§9-U2)
+ */
+ @JsonProperty("fd_creator_unit")
+ private HcsRefVO fdCreatorUnit;
+ /**
+ * 项目负责人 {fdLoginName}
+ */
+ @JsonProperty("fd_head")
+ private HcsUserRefVO fdHead;
+ /**
+ * 结算类型(字典值,映射配置待联调,§9-U3)
+ */
+ @JsonProperty("fd_settlement_type")
+ private String fdSettlementType;
+ /**
+ * 计划保证金(TMS 万元 × 10000 转元,变更映射语义联调确认)
+ */
+ @JsonProperty("fd_pro_plan_deposit")
+ private BigDecimal fdProPlanDeposit;
+ /**
+ * 是否自营项目(一期默认 "1",联调确认)
+ */
+ @JsonProperty("fd_is_self_pro")
+ private String fdIsSelfPro;
+ /**
+ * 项目收入目标(一期不传,联调确认)
+ */
+ @JsonProperty("fd_pro_income_goal")
+ private BigDecimal fdProIncomeGoal;
+ /**
+ * 承接部门 {fdId, fdName}(先传名称,§9-U2)
+ */
+ @JsonProperty("fd_taker_dept")
+ private HcsRefVO fdTakerDept;
+ /**
+ * 承接人 {fdLoginName}
+ */
+ @JsonProperty("fd_taker")
+ private HcsUserRefVO fdTaker;
+ /**
+ * 付款额度(TMS 万元 × 10000 转元)
+ */
+ @JsonProperty("fd_pay_limit")
+ private BigDecimal fdPayLimit;
+ /**
+ * 开票额度(TMS 万元 × 10000 转元)
+ */
+ @JsonProperty("fd_out_invoice_limit")
+ private BigDecimal fdOutInvoiceLimit;
+ /**
+ * 项目建设内容
+ */
+ @JsonProperty("fd_pro_content")
+ private String fdProContent;
+ /**
+ * 是否可用(固定 true)
+ */
+ @JsonProperty("fd_is_available")
+ private Boolean fdIsAvailable;
+ /**
+ * 项目编号(必填,判重候选键)
+ */
+ @JsonProperty("fd_num")
+ private String fdNum;
+ /**
+ * 可用单位列表 [{fdId, fdName}](同创建单位先传名称)
+ */
+ @JsonProperty("fd_avail_units")
+ private List fdAvailUnits;
+ /**
+ * 金蝶同步状态(固定 "1")
+ */
+ @JsonProperty("fd_kingdee_sync_status")
+ private String fdKingdeeSyncStatus;
+ /**
+ * 来源标识(fd_sources,约定值 TMS,配置 thirdParty.hcs.src-system,D14)
+ */
+ @JsonProperty("fd_sources")
+ private String fdSources;
+ /**
+ * 来源系统主键 ID(存 TMS 业务单据主键,D14)
+ */
+ @JsonProperty("fd_kingdee_id")
+ private String fdKingdeeId;
+ /**
+ * 创建人 {fdLoginName}
+ */
+ @JsonProperty("fd_creator")
+ private HcsUserRefVO fdCreator;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectUpdateDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectUpdateDTO.java
new file mode 100644
index 00000000..f830a2e6
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectUpdateDTO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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 lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 合同合规系统(HCS)C102-2 项目立项更新入参
+ *
+ * 入参结构与新建一致(去掉 bpmOperation/saveOption),formData.fdId 必填(新建返回的项目ID);
+ * 响应无 data,以 success 判断结果。
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsProjectUpdateDTO extends HcsProjectAddDTO {
+ @Serial
+ private static final long serialVersionUID = 1L;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectVO.java
new file mode 100644
index 00000000..edfc4f41
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectVO.java
@@ -0,0 +1,60 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C102-3 项目立项查询出参行
+ *
+ * 注意:本接口入参 columns/sorts 用下划线字段名(fd_id/fd_num/fd_pro_name),出参 content 为驼峰。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsProjectVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 项目立项单ID
+ */
+ private String fdId;
+ /**
+ * 项目编号(判重条件 fd_num)
+ */
+ private String fdNum;
+ /**
+ * 项目名称
+ */
+ private String fdProName;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillFormDataDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillFormDataDTO.java
new file mode 100644
index 00000000..50fea9bd
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillFormDataDTO.java
@@ -0,0 +1,54 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C109-1/C109-2 收票单 formData 节点(更新时文档状态无需传递)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsReceiptBillFormDataDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 收票单业务字段
+ */
+ private HcsReceiptBillPropsDTO dynamicProps;
+ /**
+ * 单据主题(更新接口不传)
+ */
+ private String fdDocSubject;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillPropsDTO.java
new file mode 100644
index 00000000..2f5c6256
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillPropsDTO.java
@@ -0,0 +1,105 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C109-1/C109-2 收票单 dynamicProps 业务字段(字段名按接口文档下划线原样传值)
+ *
+ * §9-U11 待联调确认:fd_ap_bill_src_subject 关联结算付款单的编号体系(应付单号键)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsReceiptBillPropsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 发票号码
+ */
+ @JsonProperty("fd_invoice_code")
+ private String fdInvoiceCode;
+ /**
+ * 开票/收票日期(YYYY-MM-DD 字符串,非毫秒时间戳)
+ */
+ @JsonProperty("fd_bill_date")
+ private String fdBillDate;
+ /**
+ * 开票方(销方,供应商名称或编号)
+ */
+ @JsonProperty("fd_invoicer")
+ private String fdInvoicer;
+ /**
+ * 收票方(购方,收票单位名称或编号)
+ */
+ @JsonProperty("fd_buyer")
+ private String fdBuyer;
+ /**
+ * 价税合计金额
+ */
+ @JsonProperty("fd_invoice_amt_tax_incl")
+ private BigDecimal fdInvoiceAmtTaxIncl;
+ /**
+ * 税率(小数,如 0.13,TMS 13 ÷ 100)
+ */
+ @JsonProperty("fd_tax_rate")
+ private BigDecimal fdTaxRate;
+ /**
+ * 发票类型(字典值,配置 receipt-bill.invoice-type-mapping,§9-U3)
+ */
+ @JsonProperty("fd_invoice_type")
+ private String fdInvoiceType;
+ /**
+ * 合同主题(或合同编号/ID,应付结算链路回溯或人工补录,D13)
+ */
+ @JsonProperty("fd_contract_subject")
+ private String fdContractSubject;
+ /**
+ * 合同编号(应付结算链路回溯或人工补录,D13)
+ */
+ @JsonProperty("fd_contract_no")
+ private String fdContractNo;
+ /**
+ * 关联结算付款单主题(或编号/ID,收票认领应付单号,§9-U11)
+ */
+ @JsonProperty("fd_ap_bill_src_subject")
+ private String fdApBillSrcSubject;
+ /**
+ * 备注
+ */
+ @JsonProperty("fd_remark")
+ private String fdRemark;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillSaveDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillSaveDTO.java
new file mode 100644
index 00000000..830b7edf
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsReceiptBillSaveDTO.java
@@ -0,0 +1,67 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C109-1 收票单新建入参(C109-2 更新入参结构相同,复用本 DTO)
+ *
+ * fdId 为收票单主键:新建不传;更新必填(新建返回 data.data.fdId,持久化于 hcs_fd_id),
+ * 更新一般只传变更字段、文档状态无需传递。
+ * 新建成功返回双层嵌套 data.data.{fdId, fdXFormId, docViewUrl}。
+ * 附件(发票扫描件)经 mechanisms.attachment[] 绑定:fdEntityKey=fd_attach、发起带 fdBindType=ADD。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsReceiptBillSaveDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 收票单主键(新建不传,更新必填)
+ */
+ private String fdId;
+ /**
+ * 操作人登录名(必填)
+ */
+ private String fdLoginName;
+ /**
+ * 收票单表单数据
+ */
+ private HcsReceiptBillFormDataDTO formData;
+ /**
+ * 附件挂载
+ */
+ private HcsMechanismsDTO mechanisms;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRefVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRefVO.java
new file mode 100644
index 00000000..92cd5185
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRefVO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)引用对象(组织/部门/合同等统一 {fdId, fdName} 引用)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsRefVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 引用记录ID
+ */
+ private String fdId;
+ /**
+ * 引用记录名称
+ */
+ private String fdName;
+
+ /**
+ * 仅按名称引用(HCS 组织 fdId 与 TMS 不同源,§9-U2,先传名称)
+ *
+ * @param fdName 名称
+ * @return 引用对象
+ */
+ public static HcsRefVO ofName(String fdName) {
+ HcsRefVO ref = new HcsRefVO();
+ ref.setFdName(fdName);
+ return ref;
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsResult.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsResult.java
new file mode 100644
index 00000000..50645b9a
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsResult.java
@@ -0,0 +1,78 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 合同合规系统(HCS)统一返回结果
+ *
+ * 业务成败以 success 字段为准(code 仅为成功/错误 messageKey)。
+ * 结算付款特批(C106-3/4)与收票单(C109-1/2)的 data 内层又包一层同结构包装,
+ * 取业务数据用 data.data,即 {@code HcsResult>}。
+ *
+ * @param 响应数据类型
+ * @author Chill
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsResult implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 是否成功(必看字段,业务成败以此为准)
+ */
+ private Boolean success;
+ /**
+ * 成功/错误的 messageKey,成功为 return.optSuccess
+ */
+ private String code;
+ /**
+ * 成功/错误的提示信息
+ */
+ private String msg;
+ /**
+ * 返回数据对象,结构以各接口为准
+ */
+ private T data;
+ /**
+ * 链路追踪 ID,排查问题时提供给合同合规系统
+ */
+ private String traceId;
+
+ /**
+ * 是否调用成功
+ *
+ * @return true-成功
+ */
+ public boolean isSuccess() {
+ return Boolean.TRUE.equals(success);
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskQueryDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskQueryDTO.java
new file mode 100644
index 00000000..d5ed92c2
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskQueryDTO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C101-1 客商获取相对方风险等级入参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsRiskQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 客商编号(必填;用客商哪个编码待联调确认,§9-U6)
+ */
+ private String fdNumber;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskVO.java
new file mode 100644
index 00000000..8a88b2c7
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsRiskVO.java
@@ -0,0 +1,62 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C101-1 客商获取相对方风险等级出参(结果留痕 blade_hcs_check_log 与合同 hcs_risk_* 字段)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsRiskVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 相对方ID
+ */
+ private String fdId;
+ /**
+ * 客商编号
+ */
+ private String fdNumber;
+ /**
+ * 黑名单状态(示例 no)
+ */
+ private String fdBlackStatus;
+ /**
+ * 风险等级(示例 "1")
+ */
+ private String fdRiskLevel;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSaveOptionDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSaveOptionDTO.java
new file mode 100644
index 00000000..f39bbaba
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSaveOptionDTO.java
@@ -0,0 +1,50 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C102-1 项目立项新建 保存选项入参
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsSaveOptionDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * false 直接提交,true 仅存草稿(TMS 推送固定 false)
+ */
+ private Boolean saveDraft;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSignerDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSignerDTO.java
new file mode 100644
index 00000000..4badd703
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSignerDTO.java
@@ -0,0 +1,64 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-1 合同发起 signer 签署方(相对方)入参项
+ *
+ * §9-U6 待联调确认:fdNumber(相对方编号体系)与 fdType(inner/外部)判定规则。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsSignerDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 相对方名称(TMS 甲乙方名称)
+ */
+ private String fdName;
+ /**
+ * 相对方编号(按名称反查 CustomerArchive 编码,反查不到留空,§9-U6)
+ */
+ private String fdNumber;
+ /**
+ * 单位类型:inner 表示内部单位(判定规则联调确认,§9-U6)
+ */
+ private String fdType;
+ /**
+ * 签署地位:甲 / 乙
+ */
+ private String fdStanding;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayFormDataDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayFormDataDTO.java
new file mode 100644
index 00000000..74415c31
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayFormDataDTO.java
@@ -0,0 +1,58 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C106-3/C106-4 结算付款特批 formData 节点
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsSpecPayFormDataDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 特批业务字段
+ */
+ private HcsSpecPayPropsDTO dynamicProps;
+ /**
+ * 表单层单据主题
+ */
+ private String fdDocSubject;
+ /**
+ * 表单层单据状态(配置 spec-pay.doc-status,示例 10)
+ */
+ private String fdDocStatus;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
new file mode 100644
index 00000000..d2ddbce8
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
@@ -0,0 +1,148 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 合同合规系统(HCS)C106-3/C106-4 结算付款特批 dynamicProps 业务字段(字段名按接口文档下划线原样传值)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsSpecPayPropsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 合同(必填,传 HCS 合同ID,非名称)
+ */
+ @JsonProperty("fd_contract_name")
+ private String fdContractName;
+ /**
+ * 合同编号
+ */
+ @JsonProperty("fd_contract_no")
+ private String fdContractNo;
+ /**
+ * 收款账户名
+ */
+ @JsonProperty("fd_receive_account_name")
+ private String fdReceiveAccountName;
+ /**
+ * 收款账号
+ */
+ @JsonProperty("fd_receive_account")
+ private String fdReceiveAccount;
+ /**
+ * 开户行
+ */
+ @JsonProperty("fd_bank_name")
+ private String fdBankName;
+ /**
+ * 收款人
+ */
+ @JsonProperty("fd_payee")
+ private String fdPayee;
+ /**
+ * 付款人
+ */
+ @JsonProperty("fd_payer")
+ private String fdPayer;
+ /**
+ * 付款金额
+ */
+ @JsonProperty("fd_pay_amount")
+ private BigDecimal fdPayAmount;
+ /**
+ * 合计金额
+ */
+ @JsonProperty("fd_total_amount")
+ private BigDecimal fdTotalAmount;
+ /**
+ * 结算币种(名称,如"人民币",映射配置 currency-name-mapping)
+ */
+ @JsonProperty("fd_settle_currency")
+ private String fdSettleCurrency;
+ /**
+ * 付款用途(字典值,配置 spec-pay.payment-purpose,§9-U3)
+ */
+ @JsonProperty("fd_payment_purpose")
+ private String fdPaymentPurpose;
+ /**
+ * 付款性质(字典值,配置 spec-pay.payment-nature,§9-U3)
+ */
+ @JsonProperty("fd_payment_nature")
+ private String fdPaymentNature;
+ /**
+ * 付款类型(字典值,配置 spec-pay.payment-type,§9-U3)
+ */
+ @JsonProperty("fd_payment_type")
+ private String fdPaymentType;
+ /**
+ * 单据编号(TMS 付款单号,来源付款单标识)
+ */
+ @JsonProperty("fd_doc_no")
+ private String fdDocNo;
+ /**
+ * 单据主题("结算付款特批-" + 付款单号)
+ */
+ @JsonProperty("fd_doc_subject")
+ private String fdDocSubject;
+ /**
+ * 单据状态(配置 spec-pay.doc-status,示例 10)
+ */
+ @JsonProperty("fd_doc_status")
+ private String fdDocStatus;
+ /**
+ * 事由
+ */
+ @JsonProperty("fd_reason")
+ private String fdReason;
+ /**
+ * 补充说明
+ */
+ @JsonProperty("fd_supplement_note")
+ private String fdSupplementNote;
+ /**
+ * 来源付款单ID(存 TMS 付款申请主键,D14)
+ */
+ @JsonProperty("fd_id")
+ private String fdId;
+ /**
+ * 特批附件字段标识(固定 fd_spc_approval_attach)
+ */
+ @JsonProperty("fd_spc_approval_attach")
+ private String fdSpcApprovalAttach;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPaySaveDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPaySaveDTO.java
new file mode 100644
index 00000000..a184785d
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPaySaveDTO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)C106-3 结算付款特批流程发起入参(C106-4 更新入参结构相同,复用本 DTO)
+ *
+ * fdId 为结算付款流程ID:发起不传;更新必填(发起返回 data.data.fdId,持久化于 hcs_spec_fd_id)。
+ * 附件经 mechanisms.attachment[] 绑定:发起时带 fdBindType=ADD,更新时不带。
+ * 发起成功返回双层嵌套 data.data.{fdId, fdXFormId, docViewUrl};更新内层无业务数据,以内层 success 判断。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsSpecPaySaveDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 结算付款流程ID(发起不传,更新必填)
+ */
+ private String fdId;
+ /**
+ * 操作人登录名(必填)
+ */
+ private String fdLoginName;
+ /**
+ * 特批表单数据
+ */
+ private HcsSpecPayFormDataDTO formData;
+ /**
+ * 附件挂载
+ */
+ private HcsMechanismsDTO mechanisms;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTemplateVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTemplateVO.java
new file mode 100644
index 00000000..c0270741
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTemplateVO.java
@@ -0,0 +1,93 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-6 合同模板列表出参行(fdTemplateId 供 C103-1 合同发起使用)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsTemplateVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 模板ID
+ */
+ private String fdId;
+ /**
+ * 模板名称
+ */
+ private String fdName;
+ /**
+ * 模板状态(如 draft)
+ */
+ private String fdStatus;
+ /**
+ * 合同性质(如 standard)
+ */
+ private String fdContractCharacter;
+ /**
+ * 创建时间(毫秒时间戳)
+ */
+ private Long fdCreateTime;
+ /**
+ * 创建人
+ */
+ private HcsRefVO fdCreator;
+ /**
+ * 创建部门
+ */
+ private HcsRefVO fdCreatorDept;
+ /**
+ * 模板扩展属性(含中文名)
+ */
+ private HcsTemplatePropsVO dynamicProps;
+
+ /**
+ * 合同模板扩展属性
+ */
+ @Data
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class HcsTemplatePropsVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 模板中文名(template-mapping 校验用)
+ */
+ private String fdNameCn;
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTokenVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTokenVO.java
new file mode 100644
index 00000000..211bd6df
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTokenVO.java
@@ -0,0 +1,63 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 合同合规系统(HCS)OAuth2 取令牌返回结果(GET /authapi/getToken)
+ *
+ * TODO 待联调确认:v4 文档未给出本接口响应字段示例,字段名按通用 OAuth2 惯例预置,
+ * 联调时以实际返回为准(设计文档 §3.2)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsTokenVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 访问令牌,调用业务接口时追加到 URL 参数 access_token
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+ /**
+ * 令牌类型
+ */
+ @JsonProperty("token_type")
+ private String tokenType;
+ /**
+ * 令牌有效期(毫秒)
+ */
+ @JsonProperty("expires_in")
+ private Long expiresIn;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsUserRefVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsUserRefVO.java
new file mode 100644
index 00000000..ffb482a2
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsUserRefVO.java
@@ -0,0 +1,66 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)人员引用对象(人员引用可只传 {fdLoginName},§9-U2 登录名同源性待联调确认)
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsUserRefVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 用户名(可为空)
+ */
+ private String fdName;
+ /**
+ * 登录名
+ */
+ private String fdLoginName;
+
+ /**
+ * 仅按登录名引用
+ *
+ * @param fdLoginName 登录名
+ * @return 人员引用对象
+ */
+ public static HcsUserRefVO ofLoginName(String fdLoginName) {
+ HcsUserRefVO ref = new HcsUserRefVO();
+ ref.setFdLoginName(fdLoginName);
+ return ref;
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsXformFieldDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsXformFieldDTO.java
new file mode 100644
index 00000000..6f0d71c2
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsXformFieldDTO.java
@@ -0,0 +1,64 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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-1 合同发起 xformFields 模板字段项(按"字段名+值"逐项传模板字段)
+ *
+ * 金额/日期等值均以字符串传递;时间字段需带 fdType=timestamp(如 SIGN_DATE、fdEstimateStartTime)。
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HcsXformFieldDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 模板字段名(如 fdNumber、fdSubject、fdAmount、SIGN_DATE)
+ */
+ private String name;
+ /**
+ * 模板字段标签(字段中文名,如"合同编号")
+ */
+ private String label;
+ /**
+ * 字段值(字符串传值)
+ */
+ private String value;
+ /**
+ * 字段类型标记:时间字段传 timestamp
+ */
+ private String fdType;
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/exception/HcsException.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/exception/HcsException.java
new file mode 100644
index 00000000..03091db3
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/exception/HcsException.java
@@ -0,0 +1,42 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.exception;
+
+/**
+ * 合同合规系统(HCS)接口异常
+ *
+ * @author Chill
+ */
+public class HcsException extends RuntimeException {
+
+ public HcsException(String message) {
+ super(message);
+ }
+
+ public HcsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
new file mode 100644
index 00000000..7b4a0eb7
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
@@ -0,0 +1,338 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.feign;
+
+import feign.Response;
+import org.springblade.hcs.config.HcsFeignClientConfig;
+import org.springblade.hcs.dto.HcsAttachmentDownloadDTO;
+import org.springblade.hcs.dto.HcsAttachmentVO;
+import org.springblade.hcs.dto.HcsCategoryVO;
+import org.springblade.hcs.dto.HcsContractDetailQueryDTO;
+import org.springblade.hcs.dto.HcsContractDraftDTO;
+import org.springblade.hcs.dto.HcsContractDraftVO;
+import org.springblade.hcs.dto.HcsContractVO;
+import org.springblade.hcs.dto.HcsCreditCheckDTO;
+import org.springblade.hcs.dto.HcsCreditCheckVO;
+import org.springblade.hcs.dto.HcsCurrencyVO;
+import org.springblade.hcs.dto.HcsExeplanSyncDTO;
+import org.springblade.hcs.dto.HcsExeplanVO;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsIdVO;
+import org.springblade.hcs.dto.HcsInvoiceRegAddDTO;
+import org.springblade.hcs.dto.HcsInvoiceRegUpdateDTO;
+import org.springblade.hcs.dto.HcsInvoiceRegVO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsPageVO;
+import org.springblade.hcs.dto.HcsPerfRecordSaveDTO;
+import org.springblade.hcs.dto.HcsProjectAddDTO;
+import org.springblade.hcs.dto.HcsProjectUpdateDTO;
+import org.springblade.hcs.dto.HcsProjectVO;
+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.HcsSpecPaySaveDTO;
+import org.springblade.hcs.dto.HcsTemplateVO;
+import org.springblade.hcs.dto.HcsTokenVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestPart;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 合同合规系统(HCS)接口(一期 24 个业务接口 + OAuth2 取令牌,D4)
+ *
+ * 除附件上传(multipart/form-data)、附件下载(文件流)外均为 POST + application/json(UTF-8)。
+ * 业务成败以统一出参 {@link HcsResult#isSuccess()} 为准;
+ * C106-3/4、C109-1/2 的业务数据在双层嵌套 data.data 中。
+ *
+ * @author Chill
+ */
+@FeignClient(name = "HCS", url = "${thirdParty.hcs.baseUrl}", configuration = HcsFeignClientConfig.class)
+public interface IHcsClient {
+
+ String GET_TOKEN_URL = "${thirdParty.hcs.getTokenUrl:/authapi/getToken}";
+ String PROJECT_ADD_URL = "${thirdParty.hcs.projectAddUrl:/openapi/sys-modeling/apis/data/km-project1/V1/projectApproval/add}";
+ String PROJECT_UPDATE_URL = "${thirdParty.hcs.projectUpdateUrl:/openapi/sys-modeling/apis/data/km-project1/V1/projectApproval/update}";
+ String PROJECT_LIST_URL = "${thirdParty.hcs.projectListUrl:/openapi/sys-modeling/apis/data/km-project1/V1/projectApproval/list}";
+ String RELATIVE_RISK_URL = "${thirdParty.hcs.relativeRiskUrl:/openapi/eop-base/eopBaseRelativeOpen/getRelativeRisk}";
+ String CONTRACT_DRAFT_URL = "${thirdParty.hcs.contractDraftUrl:/openapi/eop-cdraft/eopCdraftMainOpen/draft}";
+ String CONTRACT_UPDATE_URL = "${thirdParty.hcs.contractUpdateUrl:/openapi/eop-cdraft/eopCdraftMainOpen/updateDraft}";
+ String CONTRACT_LIST_URL = "${thirdParty.hcs.contractListUrl:/openapi/eop-data/eopDataContractOpen/list}";
+ String CONTRACT_DETAIL_URL = "${thirdParty.hcs.contractDetailUrl:/openapi/eop-data/eopDataContractOpen/detail}";
+ String EXEPLAN_ADD_URL = "${thirdParty.hcs.exeplanAddUrl:/openapi/eop-exeplan/eopExeplanMainOpen/add}";
+ String TEMPLATE_LIST_URL = "${thirdParty.hcs.templateListUrl:/openapi/eop-cdraft/eopCdraftTemplateOpen/list}";
+ String CATEGORY_LIST_URL = "${thirdParty.hcs.categoryListUrl:/openapi/eop-base/eopBaseContractCategoryOpen/list}";
+ String CURRENCY_LIST_URL = "${thirdParty.hcs.currencyListUrl:/openapi/eop-base/eopBaseCurrencyOpen/list}";
+ String ATTACHMENT_UPLOAD_URL = "${thirdParty.hcs.attachmentUploadUrl:/openapi/sys-attach/fileStream/upload}";
+ String ATTACHMENT_DOWNLOAD_URL = "${thirdParty.hcs.attachmentDownloadUrl:/openapi/sys-attach/fileStream/download}";
+ 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 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}";
+ String RECEIPT_BILL_UPDATE_URL = "${thirdParty.hcs.receiptBillUpdateUrl:/openapi/sys-modeling/lcode/ext-invoice-receive/extInvoiceReceiveOpenService/updateReceiveBill}";
+ String INVOICE_REG_LIST_URL = "${thirdParty.hcs.invoiceRegListUrl:/openapi/eop-data/eopDataInvoiceRegistrationOpen/list}";
+ String INVOICE_REG_ADD_URL = "${thirdParty.hcs.invoiceRegAddUrl:/openapi/eop-data/eopDataInvoiceRegistrationOpen/add}";
+ String INVOICE_REG_UPDATE_URL = "${thirdParty.hcs.invoiceRegUpdateUrl:/openapi/eop-data/eopDataInvoiceRegistrationOpen/update}";
+
+ /**
+ * OAuth2 获取令牌(appKey/appSecret 走 URL 参数,D2)
+ *
+ * @param appKey 应用 appKey
+ * @param appSecret 应用 appSecret
+ * @return 令牌信息
+ */
+ @GetMapping(GET_TOKEN_URL)
+ HcsResult getToken(@RequestParam("appKey") String appKey, @RequestParam("appSecret") String appSecret);
+
+ /**
+ * C102-1 项目立项新建
+ *
+ * @param param 请求参数
+ * @return 项目 fdId / fdXFormId / docViewUrl
+ */
+ @PostMapping(PROJECT_ADD_URL)
+ HcsResult projectAdd(@RequestBody HcsProjectAddDTO param);
+
+ /**
+ * C102-2 项目立项更新(响应无 data,以 success 判断)
+ *
+ * @param param 请求参数(formData.fdId 必填)
+ * @return 统一出参
+ */
+ @PostMapping(PROJECT_UPDATE_URL)
+ HcsResult projectUpdate(@RequestBody HcsProjectUpdateDTO param);
+
+ /**
+ * C102-3 项目立项查询(columns/sorts 用下划线字段名,出参驼峰)
+ *
+ * @param param 分页查询参数
+ * @return 项目立项分页数据
+ */
+ @PostMapping(PROJECT_LIST_URL)
+ HcsResult> projectList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C101-1 客商获取相对方风险等级
+ *
+ * @param param 请求参数
+ * @return 相对方风险信息
+ */
+ @PostMapping(RELATIVE_RISK_URL)
+ HcsResult getRelativeRisk(@RequestBody HcsRiskQueryDTO param);
+
+ /**
+ * C103-1 合同发起传输(合同新建)
+ *
+ * @param param 请求参数
+ * @return 合同 fdContractId(必须持久化)
+ */
+ @PostMapping(CONTRACT_DRAFT_URL)
+ HcsResult contractDraft(@RequestBody HcsContractDraftDTO param);
+
+ /**
+ * C103-11 合同信息更新(入参同发起,响应无 data,§9-U5 定位已有合同方式待联调)
+ *
+ * @param param 请求参数
+ * @return 统一出参
+ */
+ @PostMapping(CONTRACT_UPDATE_URL)
+ HcsResult contractUpdate(@RequestBody HcsContractDraftDTO param);
+
+ /**
+ * C103-3 合同信息列表(判重与对账回查,§9-U4 查询能力联调确认)
+ *
+ * @param param 分页查询参数
+ * @return 合同分页数据
+ */
+ @PostMapping(CONTRACT_LIST_URL)
+ HcsResult> contractList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C103-4 合同详情(fdId 与 fdNumber 二选一)
+ *
+ * TODO 待联调确认:出参 data 结构 v4 未定义,暂以对象承接,联调后补充(设计文档 §5.9)
+ *
+ * @param param 请求参数
+ * @return 合同详情(结构待联调确认)
+ */
+ @PostMapping(CONTRACT_DETAIL_URL)
+ HcsResult contractDetail(@RequestBody HcsContractDetailQueryDTO param);
+
+ /**
+ * C103-9(新) 合同履约规则同步(行级增删改,与废弃旧接口同路径不同入参,务必用本新版结构)
+ *
+ * @param param 请求参数
+ * @return fdExeplanId 与变更阶段 fdId(均须持久化)
+ */
+ @PostMapping(EXEPLAN_ADD_URL)
+ HcsResult exeplanAdd(@RequestBody HcsExeplanSyncDTO param);
+
+ /**
+ * C103-6 合同模板列表获取(fdTemplateId 供合同发起使用)
+ *
+ * @param param 分页查询参数
+ * @return 合同模板分页数据
+ */
+ @PostMapping(TEMPLATE_LIST_URL)
+ HcsResult> templateList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C103-7 合同分类列表查询
+ *
+ * @param param 分页查询参数
+ * @return 合同分类分页数据
+ */
+ @PostMapping(CATEGORY_LIST_URL)
+ HcsResult> categoryList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C103-12 合同币种列表查询(开票登记 fdCurrencyId、履约计划币种取自本接口)
+ *
+ * @param param 分页查询参数
+ * @return 币种分页数据
+ */
+ @PostMapping(CURRENCY_LIST_URL)
+ HcsResult> currencyList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C103-2 合同附件上传(multipart/form-data,返回附件 fdId 供各业务接口引用)
+ *
+ * @param fdFile 附件文件
+ * @param fdAnonymous 是否匿名(true/false)
+ * @param fdLoginName 操作人登录名
+ * @return 附件 fdId
+ */
+ @PostMapping(value = ATTACHMENT_UPLOAD_URL, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ HcsResult uploadAttachment(@RequestPart("fdFile") MultipartFile fdFile,
+ @RequestParam(value = "fdAnonymous", required = false) String fdAnonymous,
+ @RequestParam(value = "fdLoginName", required = false) String fdLoginName);
+
+ /**
+ * C103-5 合同附件下载(对账/排查备用,返回文件流,非 JSON 包装)
+ *
+ * @param param 请求参数
+ * @return 文件流响应
+ */
+ @PostMapping(ATTACHMENT_DOWNLOAD_URL)
+ Response downloadAttachment(@RequestBody HcsAttachmentDownloadDTO param);
+
+ /**
+ * C106-1 项目/合同额度校验(超额时 success 仍为 true,必须以 data.fdIsCredit 为准)
+ *
+ * @param param 请求参数
+ * @return 额度校验结果
+ */
+ @PostMapping(CREDIT_CHECK_URL)
+ HcsResult contractCreditCheck(@RequestBody HcsCreditCheckDTO param);
+
+ /**
+ * C106-3 结算付款特批流程发起(业务数据在双层嵌套 data.data)
+ *
+ * @param param 请求参数
+ * @return data.data.{fdId, fdXFormId, docViewUrl}(fdId 为特批流程ID,更新必传)
+ */
+ @PostMapping(SPEC_PAY_ADD_URL)
+ HcsResult> addSpecApplyPay(@RequestBody HcsSpecPaySaveDTO param);
+
+ /**
+ * C106-4 结算付款特批流程更新(顶层 fdId 必填,内层无业务数据,以内层 success 判断)
+ *
+ * @param param 请求参数
+ * @return 双层出参
+ */
+ @PostMapping(SPEC_PAY_UPDATE_URL)
+ HcsResult> updateSpecApplyPay(@RequestBody HcsSpecPaySaveDTO param);
+
+ /**
+ * C108-1 履约记录新建
+ *
+ * @param param 请求参数
+ * @return 履约记录 fdId(必须持久化)
+ */
+ @PostMapping(PERF_RECORD_ADD_URL)
+ HcsResult perfRecordAdd(@RequestBody HcsPerfRecordSaveDTO param);
+
+ /**
+ * C108-2 履约记录更新(formData.fdId 必填,响应无 data,success=true 即成功)
+ *
+ * @param param 请求参数
+ * @return 统一出参
+ */
+ @PostMapping(PERF_RECORD_UPDATE_URL)
+ HcsResult perfRecordUpdate(@RequestBody HcsPerfRecordSaveDTO param);
+
+ /**
+ * C109-1 收票单新建(业务数据在双层嵌套 data.data)
+ *
+ * @param param 请求参数
+ * @return data.data.{fdId, fdXFormId, docViewUrl}(fdId 为收票单主键,更新必传)
+ */
+ @PostMapping(RECEIPT_BILL_ADD_URL)
+ HcsResult> addReceiveBill(@RequestBody HcsReceiptBillSaveDTO param);
+
+ /**
+ * C109-2 收票单更新(顶层 fdId 必填,只传变更字段,文档状态无需传递)
+ *
+ * @param param 请求参数
+ * @return 双层出参
+ */
+ @PostMapping(RECEIPT_BILL_UPDATE_URL)
+ HcsResult> updateReceiveBill(@RequestBody HcsReceiptBillSaveDTO param);
+
+ /**
+ * C111-1 查询开票登记列表(判重与对账回查,§9-U4 查询能力联调确认)
+ *
+ * @param param 分页查询参数
+ * @return 开票登记分页数据
+ */
+ @PostMapping(INVOICE_REG_LIST_URL)
+ HcsResult> invoiceRegistrationList(@RequestBody HcsPageQueryDTO param);
+
+ /**
+ * C111-2 新增开票登记
+ *
+ * @param param 请求参数
+ * @return 开票登记 fdId(必须持久化)
+ */
+ @PostMapping(INVOICE_REG_ADD_URL)
+ HcsResult invoiceRegistrationAdd(@RequestBody HcsInvoiceRegAddDTO param);
+
+ /**
+ * C111-3 更新开票登记(fdId 必填,金蝶开票完成后回填票据日期/状态)
+ *
+ * @param param 请求参数
+ * @return 开票登记 fdId
+ */
+ @PostMapping(INVOICE_REG_UPDATE_URL)
+ HcsResult invoiceRegistrationUpdate(@RequestBody HcsInvoiceRegUpdateDTO param);
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/interceptor/HcsAuthInterceptor.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/interceptor/HcsAuthInterceptor.java
new file mode 100644
index 00000000..ade05035
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/interceptor/HcsAuthInterceptor.java
@@ -0,0 +1,99 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.interceptor;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+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.constant.HcsConstant;
+import org.springblade.hcs.token.HcsTokenService;
+import org.springframework.beans.factory.ObjectProvider;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * 合同合规系统(HCS)请求拦截器,按配置的认证方式注入凭据(D2,两种实现并存、切换不改代码)
+ *
+ * oauth2(默认):GET /authapi/getToken 取令牌后,把 access_token 追加到业务请求 URL;
+ * basic:注入 Authorization: Basic base64(user:password) 请求头。
+ *
+ * 取令牌请求本身不追加 access_token(路径守卫 + 空令牌守卫双重防护)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class HcsAuthInterceptor implements RequestInterceptor {
+
+ private final HcsProperties properties;
+
+ /**
+ * 惰性持有令牌服务,打破 Feign 客户端与令牌服务的构造期循环依赖
+ */
+ private final ObjectProvider tokenServiceProvider;
+
+ @Override
+ public void apply(RequestTemplate template) {
+ if (HcsConstant.AUTH_TYPE_BASIC.equals(properties.getAuthType())) {
+ applyBasicAuth(template);
+ return;
+ }
+ applyOAuth2Token(template);
+ }
+
+ /**
+ * Basic 认证:注入 Authorization 请求头(凭据走 Nacos + 环境变量注入)
+ */
+ private void applyBasicAuth(RequestTemplate template) {
+ if (StringUtil.isBlank(properties.getBasicUsername()) || StringUtil.isBlank(properties.getBasicPassword())) {
+ log.warn("HCS Basic 认证凭据未配置(thirdParty.hcs.basic-username/basic-password),请求 {} 将无认证头", template.path());
+ return;
+ }
+ String credentials = properties.getBasicUsername() + ":" + properties.getBasicPassword();
+ String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ template.header(HcsConstant.HEADER_AUTHORIZATION, HcsConstant.HEADER_BASIC_PREFIX + encoded);
+ }
+
+ /**
+ * OAuth2 认证:业务请求 URL 追加 access_token 参数
+ */
+ private void applyOAuth2Token(RequestTemplate template) {
+ // 取令牌请求本身不再追加 access_token
+ if (template.path() != null && template.path().contains(HcsConstant.GET_TOKEN_PATH)) {
+ return;
+ }
+ String accessToken = tokenServiceProvider.getObject().getAccessToken();
+ if (StringUtil.isBlank(accessToken)) {
+ log.warn("HCS access_token 为空,请求 {} 未追加令牌参数", template.path());
+ return;
+ }
+ template.query(HcsConstant.QUERY_ACCESS_TOKEN, accessToken);
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/token/HcsTokenService.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/token/HcsTokenService.java
new file mode 100644
index 00000000..fddfca29
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/token/HcsTokenService.java
@@ -0,0 +1,140 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.token;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.constant.HcsConstant;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsTokenVO;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * 合同合规系统(HCS)OAuth2 令牌服务(D2,照 BfiServiceImpl 惯例)
+ *
+ * GET /authapi/getToken(appKey/appSecret)→ access_token 存 Redis
+ * (key {@link HcsConstant#HCS_TOKEN_CACHE_KEY},过期前留 120s 缓冲)。
+ * 业务调用统一经 {@link #getAccessToken()} 取缓存令牌;
+ * 401 时调用 {@link #refreshAccessToken()} 强制刷新一次后重放(D7,重试职责归扫描任务,此处仅刷新令牌)。
+ * auth-type=basic 时不走本服务,由拦截器注入 Basic 认证头。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class HcsTokenService {
+
+ private final HcsProperties properties;
+ private final IHcsClient client;
+ private final BladeRedis bladeRedis;
+
+ /**
+ * 获取访问令牌:Redis 命中直接返回,未命中远端取令牌并缓存
+ *
+ * @return access_token
+ */
+ public String getAccessToken() {
+ String token = bladeRedis.get(HcsConstant.HCS_TOKEN_CACHE_KEY);
+ if (StringUtil.isNotBlank(token)) {
+ return token;
+ }
+ return requestToken();
+ }
+
+ /**
+ * 强制刷新访问令牌(401 时调用一次并重放请求)
+ *
+ * @return 新 access_token
+ */
+ public String refreshAccessToken() {
+ log.info("强制刷新HCS令牌,清除本地缓存");
+ bladeRedis.del(HcsConstant.HCS_TOKEN_CACHE_KEY);
+ return requestToken();
+ }
+
+ /**
+ * 远端取令牌并缓存
+ *
+ * @return access_token
+ */
+ private String requestToken() {
+ if (StringUtil.isBlank(properties.getAppKey()) || StringUtil.isBlank(properties.getAppSecret())) {
+ throw new HcsException("获取HCS令牌失败,appKey/appSecret 未配置(thirdParty.hcs.app-key/app-secret)");
+ }
+ long start = System.currentTimeMillis();
+ log.info("调用HCS接口 获取令牌开始");
+ HcsResult result = client.getToken(properties.getAppKey(), properties.getAppSecret());
+ log.info("调用HCS接口 获取令牌结束 耗时:{}ms,success:{}", System.currentTimeMillis() - start, result.isSuccess());
+ if (result == null || !result.isSuccess()) {
+ 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()));
+ }
+ HcsTokenVO tokenVO = result.getData();
+ if (tokenVO == null || StringUtil.isBlank(tokenVO.getAccessToken())) {
+ log.error("调用HCS接口 获取令牌返回数据为空");
+ throw new HcsException("调用HCS接口获取令牌异常,返回数据为空");
+ }
+ cacheToken(tokenVO);
+ return tokenVO.getAccessToken();
+ }
+
+ /**
+ * 缓存令牌,按 expires_in 毫秒数设置提前失效时间
+ *
+ * TODO 待联调确认:v4 未给出 getToken 响应字段示例,expires_in 字段名与单位以实际返回为准(设计文档 §3.2)
+ *
+ * @param tokenVO 令牌信息
+ */
+ private void cacheToken(HcsTokenVO tokenVO) {
+ if (tokenVO.getExpiresIn() == null || tokenVO.getExpiresIn() <= HcsConstant.TOKEN_EXPIRE_BUFFER_MILLIS) {
+ log.warn("HCS令牌未返回有效期或有效期不足缓冲时长,本次不缓存");
+ return;
+ }
+ // 过期时间 - 缓冲时长,防止刚好过期
+ long expireMillis = tokenVO.getExpiresIn() - HcsConstant.TOKEN_EXPIRE_BUFFER_MILLIS;
+ bladeRedis.getValueOps().setIfAbsent(HcsConstant.HCS_TOKEN_CACHE_KEY, tokenVO.getAccessToken(), Duration.ofMillis(expireMillis));
+ log.info("HCS令牌已缓存,有效期 {}ms(含 {}ms 提前失效缓冲)", expireMillis, HcsConstant.TOKEN_EXPIRE_BUFFER_MILLIS);
+ }
+
+ /**
+ * 令牌缓存是否命中(诊断用)
+ *
+ * @return true-缓存存在
+ */
+ public boolean isTokenCached() {
+ return StringUtil.isNotBlank(Objects.toString(bladeRedis.get(HcsConstant.HCS_TOKEN_CACHE_KEY), null));
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/util/HcsPageUtil.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/util/HcsPageUtil.java
new file mode 100644
index 00000000..7039edeb
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/util/HcsPageUtil.java
@@ -0,0 +1,102 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.util;
+
+import org.springblade.core.tool.utils.Func;
+import org.springblade.hcs.dto.HcsPageVO;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.IntFunction;
+
+/**
+ * 合同合规系统(HCS)分页查询通用翻页工具(照 BfiUtil 惯例)
+ *
+ * HCS 分页以 offset(从 0 开始)+ pageSize 位移,适用于所有返回 {@link HcsPageVO} 的查询接口
+ * (C102-3 项目立项查询、C103-3 合同列表、C103-6/7/12 主数据列表、C111-1 开票登记列表等):
+ *
{@code
+ * List rows = HcsPageUtil.queryAllPages(offset -> {
+ * query.setOffset(offset);
+ * query.setPageSize(pageSize);
+ * return hcsClient.projectList(query).getData();
+ * });
+ * }
+ *
+ * @author Chill
+ */
+public class HcsPageUtil {
+
+ /**
+ * 分页查询默认最大页数保护,防止 HCS 侧 totalSize 异常时死循环
+ */
+ public static final int DEFAULT_MAX_PAGES = 50;
+
+ private HcsPageUtil() {
+ }
+
+ /**
+ * HCS 分页查询通用翻页,自动翻页汇总全量数据行(默认最大页数保护)
+ *
+ * @param pageFetcher 偏移量 -> 分页结果,lambda 内负责设置 offset(必要时含页大小)并调用接口
+ * @param 数据行类型
+ * @return 全量数据行
+ */
+ public static List queryAllPages(IntFunction> pageFetcher) {
+ return queryAllPages(pageFetcher, DEFAULT_MAX_PAGES);
+ }
+
+ /**
+ * HCS 分页查询通用翻页,自动翻页汇总全量数据行
+ *
+ * 终止条件:返回页为空、数据行为空、数据行不满一页(按返回 pageSize 判断)、已取满 totalSize、超出最大页数保护
+ *
+ * @param pageFetcher 偏移量 -> 分页结果,lambda 内负责设置 offset(必要时含页大小)并调用接口
+ * @param maxPages 最大页数保护
+ * @param 数据行类型
+ * @return 全量数据行
+ */
+ public static List queryAllPages(IntFunction> pageFetcher, int maxPages) {
+ List rows = new ArrayList<>();
+ int offset = 0;
+ for (int pageNo = 1; pageNo <= maxPages; pageNo++) {
+ HcsPageVO page = pageFetcher.apply(offset);
+ if (page == null || Func.isEmpty(page.getContent())) {
+ break;
+ }
+ rows.addAll(page.getContent());
+ // 已取满总数时无需继续翻页
+ if (page.getTotalSize() != null && rows.size() >= page.getTotalSize()) {
+ break;
+ }
+ // 末页数据不满一页时无需继续翻页(返回 pageSize 为空时依赖 totalSize 与最大页数保护)
+ if (page.getPageSize() != null && page.getContent().size() < page.getPageSize()) {
+ break;
+ }
+ offset += page.getContent().size();
+ }
+ return rows;
+ }
+}
diff --git a/blade-third-party-api/pom.xml b/blade-third-party-api/pom.xml
index d0a0b444..4b2ed3c0 100644
--- a/blade-third-party-api/pom.xml
+++ b/blade-third-party-api/pom.xml
@@ -20,6 +20,7 @@
blade-track-api
blade-wechat-api
blade-bfi-api
+ blade-hcs-api
pom
BladeX 第三方API集合
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index d5f1b4de..2345de68 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -64,3 +64,83 @@ thirdParty:
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
+ hcs:
+ # 合同合规系统(HCS)开放接口地址,直连可配(网关/iPaaS 部署时改此处,代码不感知)
+ base-url: ${HCS_BASE_URL:http://127.0.0.1:8050}
+ # 认证方式:oauth2(默认,URL 追加 access_token)| basic(Authorization 请求头),切换不改代码
+ auth-type: ${HCS_AUTH_TYPE:oauth2}
+ # OAuth2 应用 appKey
+ app-key: ${HCS_APP_KEY:}
+ # OAuth2 应用 appSecret,敏感值通过环境变量注入
+ app-secret: ${HCS_APP_SECRET:}
+ # Basic 认证用户名/密码(auth-type=basic 时生效)
+ basic-username: ${HCS_BASIC_USER:}
+ basic-password: ${HCS_BASIC_PASSWORD:}
+ # 来源系统标识(fd_sources,约定值 TMS)
+ src-system: ${HCS_SRC_SYSTEM:TMS}
+ # 推送任务(扫描首推/补偿,P1 接线)
+ push:
+ # 总开关(默认关,灰度打开)
+ sync-enabled: ${HCS_PUSH_SYNC_ENABLED:false}
+ # 扫描调度 cron,默认每5分钟
+ cron: ${HCS_PUSH_CRON:0 */5 * * * ?}
+ # 每类单据单轮扫描批量
+ batch-size: ${HCS_PUSH_BATCH_SIZE:50}
+ # 推送失败自动重试上限,超限仅人工重推
+ max-retry: ${HCS_PUSH_MAX_RETRY:5}
+ # 项目立项(C102)
+ project:
+ # 立项 bpmOperation.templateCode(环境相关值,§9-U9 待合规方提供)
+ template-code: ${HCS_PROJECT_TEMPLATE_CODE:}
+ # 合同(C103-1/11)
+ contract:
+ # TMS 合同类别 -> HCS 模板ID(§9-U7 待合规方提供模板清单)
+ template-mapping: {}
+ # TMS 结算方式 -> 付款|收款(§9-U3 字典映射待联调)
+ settlement-type-mapping: {}
+ # TMS 币种码 -> HCS 币种名称(照金蝶 RMB->CNY 映射惯例)
+ currency-name-mapping: {"RMB": "人民币"}
+ # 合同 fdSepcailContract 固定 false(D15,配置项预留)
+ special-contract-enabled: ${HCS_CONTRACT_SPECIAL_ENABLED:false}
+ # 合同推送前是否强制客商风险校验
+ require-risk-check: ${HCS_CONTRACT_REQUIRE_RISK_CHECK:true}
+ # 履约规则(C103-9)
+ exe-stage:
+ # 金额类型默认值(fdFundType)
+ fund-type: ${HCS_EXE_STAGE_FUND_TYPE:byAmount}
+ # 客商风险校验(C101-1)
+ risk:
+ check-on-contract: ${HCS_RISK_CHECK_ON_CONTRACT:true}
+ check-on-payment: ${HCS_RISK_CHECK_ON_PAYMENT:true}
+ # 黑名单是否阻断(合同置 failed / 付款提交拦截)
+ block-blacklist: ${HCS_RISK_BLOCK_BLACKLIST:true}
+ # 额度校验(C106-1)
+ credit:
+ # 付款提交校验是否要求合同已推送合规
+ require-contract-synced: ${HCS_CREDIT_REQUIRE_CONTRACT_SYNCED:true}
+ # 结算付款特批(C106-3/4)
+ spec-pay:
+ # 单据状态(示例 10)
+ doc-status: ${HCS_SPEC_PAY_DOC_STATUS:10}
+ # 付款用途/性质/类型字典值(§9-U3 待联调后配置)
+ payment-purpose: ${HCS_SPEC_PAY_PAYMENT_PURPOSE:}
+ payment-nature: ${HCS_SPEC_PAY_PAYMENT_NATURE:}
+ payment-type: ${HCS_SPEC_PAY_PAYMENT_TYPE:}
+ # 开票登记(C111)
+ invoice:
+ # TMS 发票类型 -> HCS 发票类型字典值(§9-U3 待联调后配置)
+ invoice-type-mapping: {"electronic_normal": "", "electronic_special": ""}
+ # TMS 币种码 -> HCS 币种 fdId(经 C103-12 币种表缓存取值,§9-U3 待联调后配置)
+ currency-id-mapping: {"RMB": ""}
+ # 款项性质默认值(fdFundNature)
+ fund-nature-default: ${HCS_INVOICE_FUND_NATURE_DEFAULT:progress}
+ # 收票单(C109)
+ receipt-bill:
+ # TMS 发票类型 -> HCS 收票发票类型字典值(§9-U3 待联调后配置)
+ invoice-type-mapping: {}
+ # 附件转传(C103-2)
+ attachment:
+ # 附件转传总开关
+ transfer-enabled: ${HCS_ATTACHMENT_TRANSFER_ENABLED:true}
+ # TMS 文件服务下载基址(拉取附件流用)
+ file-download-base: ${HCS_ATTACHMENT_FILE_DOWNLOAD_BASE:}
diff --git a/doc/sql/transport/blade_hcs_sync_fields_20260927.sql b/doc/sql/transport/blade_hcs_sync_fields_20260927.sql
new file mode 100644
index 00000000..02d53bd9
--- /dev/null
+++ b/doc/sql/transport/blade_hcs_sync_fields_20260927.sql
@@ -0,0 +1,191 @@
+-- 桂物合同合规系统(HCS)接入:业务表统一集成字段组(设计文档 §4,P0)。
+-- blade_project_apply / blade_contract_manage / blade_payment_application /
+-- blade_invoice_application / blade_invoice_receipt 统一追加 hcs_* 五列;
+-- 后续阶段按 §8 排期再追加各场景扩展字段(履约计划ID、风险留痕、特批字段组等)。
+-- 通过 information_schema 判断字段是否存在,可重复执行。
+SET @db_name = DATABASE();
+
+-- ==================== blade_project_apply(C102 立项回填) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_project_apply' AND column_name = 'hcs_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_project_apply ADD COLUMN hcs_fd_id varchar(64) DEFAULT NULL COMMENT ''合规系统单据ID(fdId/fdContractId)'''
+);
+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_project_apply' AND column_name = 'hcs_sync_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_project_apply ADD COLUMN hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'''
+);
+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_project_apply' AND column_name = 'hcs_retry_count'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_project_apply ADD COLUMN hcs_retry_count int DEFAULT 0 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_project_apply' AND column_name = 'hcs_sync_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_project_apply ADD COLUMN hcs_sync_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_project_apply' AND column_name = 'hcs_sync_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_project_apply ADD COLUMN hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''最近同步结果信息/失败原因'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== blade_contract_manage(C103 合同回填) ====================
+
+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_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_fd_id varchar(64) DEFAULT NULL COMMENT ''合规系统单据ID(fdId/fdContractId)'''
+);
+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_sync_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'''
+);
+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_retry_count'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_retry_count int DEFAULT 0 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_sync_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_sync_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_sync_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''最近同步结果信息/失败原因'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== blade_payment_application(C106 额度/特批、C108 履约记录) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_payment_application' AND column_name = 'hcs_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_fd_id varchar(64) DEFAULT NULL COMMENT ''合规系统单据ID(fdId/fdContractId)'''
+);
+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_payment_application' AND column_name = 'hcs_sync_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'''
+);
+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_payment_application' AND column_name = 'hcs_retry_count'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_retry_count int DEFAULT 0 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_payment_application' AND column_name = 'hcs_sync_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_sync_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_payment_application' AND column_name = 'hcs_sync_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''最近同步结果信息/失败原因'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== blade_invoice_application(C111 开票登记回填) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_invoice_application' AND column_name = 'hcs_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_application ADD COLUMN hcs_fd_id varchar(64) DEFAULT NULL COMMENT ''合规系统单据ID(fdId/fdContractId)'''
+);
+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_invoice_application' AND column_name = 'hcs_sync_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_application ADD COLUMN hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'''
+);
+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_invoice_application' AND column_name = 'hcs_retry_count'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_application ADD COLUMN hcs_retry_count int DEFAULT 0 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_invoice_application' AND column_name = 'hcs_sync_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_application ADD COLUMN hcs_sync_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_invoice_application' AND column_name = 'hcs_sync_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_application ADD COLUMN hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''最近同步结果信息/失败原因'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== blade_invoice_receipt(C109 收票单回填) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_invoice_receipt' AND column_name = 'hcs_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_fd_id varchar(64) DEFAULT NULL COMMENT ''合规系统单据ID(fdId/fdContractId)'''
+);
+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_invoice_receipt' AND column_name = 'hcs_sync_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'''
+);
+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_invoice_receipt' AND column_name = 'hcs_retry_count'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_retry_count int DEFAULT 0 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_invoice_receipt' AND column_name = 'hcs_sync_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_sync_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_invoice_receipt' AND column_name = 'hcs_sync_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''最近同步结果信息/失败原因'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
From cc9e01cff498367b62a78b3189e15239f549094f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 22:41:04 +0800
Subject: [PATCH 002/124] =?UTF-8?q?feat:=20HCS=E7=AB=8B=E9=A1=B9=E4=B8=8E?=
=?UTF-8?q?=E5=90=88=E5=90=8C=E6=95=B0=E6=8D=AE=E5=B1=82=EF=BC=88=E5=AE=9E?=
=?UTF-8?q?=E4=BD=93=E5=90=88=E8=A7=84=E5=AD=97=E6=AE=B5=E7=BB=84/?=
=?UTF-8?q?=E5=90=88=E5=90=8C=E9=A3=8E=E9=99=A9=E7=95=99=E7=97=95=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5/=E6=A0=A1=E9=AA=8C=E7=95=99=E7=97=95=E8=A1=A8/transpo?=
=?UTF-8?q?rt=E6=8E=A5=E5=85=A5hcs-api=E4=BE=9D=E8=B5=96=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/entity/ContractManage.java | 29 +++++
.../pojo/entity/HcsContractCheckLog.java | 79 ++++++++++++
.../transport/pojo/entity/ProjectApply.java | 17 +++
blade-service/blade-transport/pom.xml | 4 +
.../transport/constant/HcsSyncConstant.java | 121 ++++++++++++++++++
.../mapper/HcsContractCheckLogMapper.java | 40 ++++++
.../blade_hcs_contract_fields_20260927.sql | 55 ++++++++
pom.xml | 5 +
8 files changed, 350 insertions(+)
create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractCheckLog.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractCheckLogMapper.java
create mode 100644 doc/sql/transport/blade_hcs_contract_fields_20260927.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
index d56fec74..7398b13f 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
@@ -209,4 +209,33 @@ public class ContractManage extends TenantEntity {
@Schema(description = "备注")
private String remark;
+ @Schema(description = "合规系统单据ID(fdContractId)")
+ private String hcsFdId;
+
+ @Schema(description = "合规同步状态 unsynced/synced/failed")
+ private String hcsSyncStatus;
+
+ @Schema(description = "合规推送失败重试次数")
+ @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
+ private Integer hcsRetryCount;
+
+ @Schema(description = "合规最近同步时间")
+ private LocalDateTime hcsSyncTime;
+
+ @Schema(description = "合规最近同步结果信息/失败原因")
+ @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
+ private String hcsSyncMsg;
+
+ @Schema(description = "合规履约计划ID(C103-9 返回 fdExeplanId)")
+ private String hcsExeplanId;
+
+ @Schema(description = "合规客商风险等级(C101-1,甲方/乙方取高)")
+ private String hcsRiskLevel;
+
+ @Schema(description = "合规客商黑名单状态(C101-1,甲方/乙方取是)")
+ private String hcsBlackStatus;
+
+ @Schema(description = "合规风险校验时间")
+ private LocalDateTime hcsRiskTime;
+
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractCheckLog.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractCheckLog.java
new file mode 100644
index 00000000..f7815ad9
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractCheckLog.java
@@ -0,0 +1,79 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * HCS 风险/额度校验留痕实体类(照 ops 日志惯例,仅插入不改)
+ *
+ * @author Chill
+ */
+@Data
+@TableName("blade_hcs_check_log")
+@Schema(description = "HCS 风险/额度校验留痕")
+public class HcsContractCheckLog implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ @Schema(description = "主键")
+ private Long id;
+
+ @Schema(description = "租户ID")
+ private String tenantId;
+
+ @Schema(description = "业务类型 project/contract/payment")
+ private String bizType;
+
+ @Schema(description = "业务单据ID")
+ private Long bizId;
+
+ @Schema(description = "校验类型 risk=客商风险/credit=额度")
+ private String checkType;
+
+ @Schema(description = "校验结果 pass=通过/fail=不通过/skip=跳过")
+ private String result;
+
+ @Schema(description = "校验说明")
+ private String msg;
+
+ @Schema(description = "合规系统链路追踪ID")
+ private String traceId;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
index 5526da8e..410884ef 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
@@ -198,4 +198,21 @@ public class ProjectApply extends TenantEntity {
@Schema(description = "作废原因")
private String voidReason;
+ @Schema(description = "合规系统单据ID(fdId)")
+ private String hcsFdId;
+
+ @Schema(description = "合规同步状态 unsynced/synced/failed")
+ private String hcsSyncStatus;
+
+ @Schema(description = "合规推送失败重试次数")
+ @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
+ private Integer hcsRetryCount;
+
+ @Schema(description = "合规最近同步时间")
+ private LocalDateTime hcsSyncTime;
+
+ @Schema(description = "合规最近同步结果信息/失败原因")
+ @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
+ private String hcsSyncMsg;
+
}
diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml
index b9ed58c8..2443ce7a 100644
--- a/blade-service/blade-transport/pom.xml
+++ b/blade-service/blade-transport/pom.xml
@@ -51,6 +51,10 @@
org.springblade
blade-bfi-api
+
+ org.springblade
+ blade-hcs-api
+
org.springframework.boot
spring-boot-starter-amqp
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
new file mode 100644
index 00000000..ad7238fb
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -0,0 +1,121 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.constant;
+
+/**
+ * HCS(合同合规系统)同步常量(TMS 业务侧)
+ *
+ * 命名照 {@link KingdeeBfiConstant} 惯例;与客户端模块 org.springblade.hcs.constant.HcsConstant 区分。
+ *
+ * @author Chill
+ */
+public final class HcsSyncConstant {
+
+ /**
+ * 同步状态:待同步
+ */
+ public static final String HCS_STATUS_UNSYNCED = "unsynced";
+
+ /**
+ * 同步状态:已同步
+ */
+ public static final String HCS_STATUS_SYNCED = "synced";
+
+ /**
+ * 同步状态:同步失败
+ */
+ public static final String HCS_STATUS_FAILED = "failed";
+
+ /**
+ * 推送业务类型:项目立项(C102)
+ */
+ public static final String BIZ_TYPE_PROJECT = "project";
+
+ /**
+ * 推送业务类型:合同(C103)
+ */
+ public static final String BIZ_TYPE_CONTRACT = "contract";
+
+ /**
+ * 校验类型:客商风险(C101-1)
+ */
+ public static final String CHECK_TYPE_RISK = "risk";
+
+ /**
+ * 校验类型:额度(C106-1,P3 付款链使用)
+ */
+ public static final String CHECK_TYPE_CREDIT = "credit";
+
+ /**
+ * 校验结果:通过
+ */
+ public static final String CHECK_RESULT_PASS = "pass";
+
+ /**
+ * 校验结果:不通过(黑名单)
+ */
+ public static final String CHECK_RESULT_FAIL = "fail";
+
+ /**
+ * 校验结果:跳过(客商编号反查不到等)
+ */
+ public static final String CHECK_RESULT_SKIP = "skip";
+
+ // ==================== Redis 缓存 key(照 BFI blade:third: 前缀惯例) ====================
+
+ /**
+ * 主数据缓存:合同模板列表
+ */
+ public static final String MASTER_CACHE_TEMPLATES = "blade:third:hcs:master:templates";
+
+ /**
+ * 主数据缓存:合同分类列表
+ */
+ public static final String MASTER_CACHE_CATEGORIES = "blade:third:hcs:master:categories";
+
+ /**
+ * 主数据缓存:币种列表
+ */
+ public static final String MASTER_CACHE_CURRENCIES = "blade:third:hcs:master:currencies";
+
+ /**
+ * 附件映射缓存 key 前缀(hcs:att:{attachmentKey},TMS 附件元数据键 → 合规附件ID)
+ */
+ public static final String ATTACHMENT_CACHE_KEY_PREFIX = "blade:third:hcs:att:";
+
+ /**
+ * 附件映射缓存 TTL(天),重复上传仅浪费存储无业务风险,故不做持久化映射表(设计文档 §6)
+ */
+ public static final long ATTACHMENT_CACHE_TTL_DAYS = 7L;
+
+ /**
+ * 推送任务分布式锁 key(多实例部署防重扫)
+ */
+ public static final String PUSH_TASK_LOCK_KEY = "blade:third:hcs:push:task:lock";
+
+ private HcsSyncConstant() {
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractCheckLogMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractCheckLogMapper.java
new file mode 100644
index 00000000..74bca5c8
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractCheckLogMapper.java
@@ -0,0 +1,40 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.springblade.transport.pojo.entity.HcsContractCheckLog;
+
+/**
+ * HCS 风险/额度校验留痕 Mapper
+ *
+ * @author Chill
+ */
+@Mapper
+public interface HcsContractCheckLogMapper extends BaseMapper {
+
+}
diff --git a/doc/sql/transport/blade_hcs_contract_fields_20260927.sql b/doc/sql/transport/blade_hcs_contract_fields_20260927.sql
new file mode 100644
index 00000000..ec6854b5
--- /dev/null
+++ b/doc/sql/transport/blade_hcs_contract_fields_20260927.sql
@@ -0,0 +1,55 @@
+-- 桂物合同合规系统(HCS)接入:合同扩展字段与校验留痕表(设计文档 §4,P1)。
+-- blade_contract_manage 追加履约计划ID与风险留痕字段;新建 blade_hcs_check_log 校验留痕表。
+-- 通过 information_schema 判断字段/表是否存在,可重复执行。
+SET @db_name = DATABASE();
+
+-- ==================== blade_contract_manage 扩展字段(C103 回填、履约计划ID、风险留痕) ====================
+
+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_exeplan_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_exeplan_id varchar(64) DEFAULT NULL COMMENT ''合规履约计划ID(C103-9 返回 fdExeplanId)'''
+);
+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_risk_level'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_risk_level varchar(8) DEFAULT NULL COMMENT ''合规客商风险等级(C101-1,甲方/乙方取高)'''
+);
+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_black_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_black_status varchar(8) DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,甲方/乙方取是)'''
+);
+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_risk_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_risk_time datetime DEFAULT NULL COMMENT ''合规风险校验时间'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ==================== blade_hcs_check_log 校验留痕表(照 ops 日志惯例) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = @db_name AND table_name = 'blade_hcs_check_log'),
+ 'SELECT 1',
+ 'CREATE TABLE blade_hcs_check_log (
+ id bigint NOT NULL COMMENT ''主键'',
+ tenant_id varchar(12) DEFAULT ''000000'' COMMENT ''租户ID'',
+ biz_type varchar(32) DEFAULT NULL COMMENT ''业务类型 project/contract/payment'',
+ biz_id bigint DEFAULT NULL COMMENT ''业务单据ID'',
+ check_type varchar(16) DEFAULT NULL COMMENT ''校验类型 risk=客商风险/credit=额度'',
+ result varchar(16) DEFAULT NULL COMMENT ''校验结果 pass=通过/fail=不通过/skip=跳过'',
+ msg varchar(512) DEFAULT NULL COMMENT ''校验说明'',
+ trace_id varchar(64) DEFAULT NULL COMMENT ''合规系统链路追踪ID'',
+ create_time datetime DEFAULT NULL COMMENT ''创建时间'',
+ PRIMARY KEY (id),
+ KEY idx_hcs_check_log_biz (biz_type, biz_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''HCS 风险/额度校验留痕'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
diff --git a/pom.xml b/pom.xml
index f064dd95..650240cc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -159,6 +159,11 @@
blade-bfi-api
${revision}
+
+ org.springblade
+ blade-hcs-api
+ ${revision}
+
org.springblade
blade-wechat-api
From 2cf71afeb48a70dba3bdc601633772494197a3d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 22:43:48 +0800
Subject: [PATCH 003/124] =?UTF-8?q?feat:=20HCS=E6=8E=A8=E9=80=81=E9=9B=B6?=
=?UTF-8?q?=E5=9F=8B=E7=82=B9=E6=8E=A5=E7=BA=BF=EF=BC=88=E7=AB=8B=E9=A1=B9?=
=?UTF-8?q?/=E5=90=88=E5=90=8C=E5=AE=A1=E6=89=B9=E9=80=9A=E8=BF=87?=
=?UTF-8?q?=E4=B8=8EMK=E5=9B=9E=E8=B0=83=E7=BB=9F=E4=B8=80=E7=BD=AEhcs=5Fs?=
=?UTF-8?q?ync=5Fstatus=3Dunsynced=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/transport/mk/MkProcessBizHandlers.java | 5 +++++
.../transport/service/impl/ContractManageServiceImpl.java | 3 +++
.../transport/service/impl/ProjectApplyServiceImpl.java | 3 +++
3 files changed, 11 insertions(+)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
index b5456cf4..6c9bce09 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
@@ -14,6 +14,7 @@ import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springframework.stereotype.Component;
@@ -112,6 +113,8 @@ public final class MkProcessBizHandlers {
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
+ // HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
+ entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
}
@@ -180,6 +183,8 @@ public final class MkProcessBizHandlers {
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
+ // HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
+ entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index aa08bff9..b6cf8840 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -33,6 +33,7 @@ import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.ContractManageExcel;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.pojo.entity.ContractManage;
@@ -214,6 +215,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl
Date: Sun, 27 Sep 2026 22:47:40 +0800
Subject: [PATCH 004/124] =?UTF-8?q?feat:=20HCS=E4=B8=BB=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E7=BC=93=E5=AD=98=E4=B8=8E=E9=99=84=E4=BB=B6=E8=BD=AC=E4=BC=A0?=
=?UTF-8?q?=E6=94=AF=E6=92=91=EF=BC=88C103-6/7/12=E6=A8=A1=E6=9D=BF?=
=?UTF-8?q?=E5=88=86=E7=B1=BB=E5=B8=81=E7=A7=8D=E7=BC=93=E5=AD=98=E3=80=81?=
=?UTF-8?q?C103-2=E9=99=84=E4=BB=B6=E8=BD=AC=E4=BC=A0=E4=B8=8E=E5=90=88?=
=?UTF-8?q?=E8=A7=84=E9=99=84=E4=BB=B6ID=E6=98=A0=E5=B0=84=E7=BC=93?=
=?UTF-8?q?=E5=AD=98=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsAttachmentSupport.java | 290 ++++++++++++++++++
.../support/hcs/HcsMasterDataSupport.java | 206 +++++++++++++
.../springblade/hcs/config/HcsProperties.java | 12 +
doc/nacos/third-party-api.yaml | 4 +
4 files changed, 512 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsMasterDataSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
new file mode 100644
index 00000000..e505f910
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
@@ -0,0 +1,290 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+import cn.hutool.http.HttpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.redis.cache.BladeRedis;
+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.HcsAttachmentVO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * HCS 附件转传支撑组件(C103-2 multipart 上传,设计文档 §6)
+ *
+ * 流程:解析单据附件 JSON 元素(去重键照 ContractManage#attachmentKey 惯例)→
+ * 按元数据 URL 从文件服务拉取流(基址可配 attachment.file-download-base)→
+ * C103-2 上传取得合规附件ID → Redis 映射缓存(blade:third:hcs:att:{attachmentKey},TTL 7 天),
+ * 未命中重传(重复上传仅浪费存储,无业务风险,故不做持久化映射表)。
+ * 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试。
+ *
+ * TODO 待联调确认(§9-U8):v4 的 C103-1 入参未定义附件字段,合同附件向 HCS 的挂载方式待合规方确认,
+ * 本组件先建设通用转传能力并将合规附件ID 缓存备用,确认后按其要求挂载。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsAttachmentSupport {
+
+ /**
+ * 附件元数据去重键候选字段(照 ContractManage#attachmentKey 既有惯例)
+ */
+ private static final List KEY_FIELDS = List.of("url", "link", "fileUrl", "downloadUrl", "uid", "originalName", "name");
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final BladeRedis bladeRedis;
+
+ /**
+ * 解析单据附件 JSON 并转传,返回合规附件ID 列表(保持行序)
+ *
+ * @param attachmentsJson 附件元数据 JSON 数组
+ * @param operatorLoginName 操作人登录名(C103-2 fdLoginName)
+ * @return 合规附件ID 列表
+ */
+ public List transferAttachments(String attachmentsJson, String operatorLoginName) {
+ if (StringUtil.isBlank(attachmentsJson)) {
+ return List.of();
+ }
+ List> attachments = parseJsonArray(attachmentsJson);
+ List attachmentFdIds = new ArrayList<>();
+ for (Map, ?> attachment : attachments) {
+ attachmentFdIds.add(transferOne(attachment, operatorLoginName));
+ }
+ return attachmentFdIds;
+ }
+
+ /**
+ * 解析合同主文件 JSON(仅 approved=true 的文件)与其它附件 JSON 并转传
+ *
+ * @param contractFileJson 合同主文件 JSON
+ * @param attachmentsJson 其它附件 JSON
+ * @param operatorLoginName 操作人登录名
+ * @return 合规附件ID 列表
+ */
+ public List transferContractAttachments(String contractFileJson, String attachmentsJson, String operatorLoginName) {
+ List> attachments = new ArrayList<>();
+ for (Map, ?> file : parseJsonArray(contractFileJson)) {
+ if (isApproved(file)) {
+ attachments.add(file);
+ }
+ }
+ attachments.addAll(parseJsonArray(attachmentsJson));
+ List attachmentFdIds = new ArrayList<>();
+ for (Map, ?> attachment : attachments) {
+ attachmentFdIds.add(transferOne(attachment, operatorLoginName));
+ }
+ return attachmentFdIds;
+ }
+
+ /**
+ * 转传单个附件:映射缓存命中直接返回,未命中下载上传后写缓存
+ */
+ private String transferOne(Map, ?> attachment, String operatorLoginName) {
+ String attachmentKey = attachmentKey(attachment);
+ String cacheKey = HcsSyncConstant.ATTACHMENT_CACHE_KEY_PREFIX + attachmentKey;
+ String cachedFdId = bladeRedis.get(cacheKey);
+ if (StringUtil.isNotBlank(cachedFdId)) {
+ return cachedFdId;
+ }
+ String url = resolveUrl(attachment);
+ if (StringUtil.isBlank(url)) {
+ throw new HcsException("HCS附件转传失败,附件元数据缺少下载地址(attachmentKey=" + attachmentKey + ")");
+ }
+ byte[] fileBytes = downloadFile(url);
+ String fileName = resolveFileName(attachment, url);
+ log.info("HCS附件转传开始:{},大小 {} 字节", fileName, fileBytes.length);
+ HcsResult result = hcsClient.uploadAttachment(
+ new InMemoryMultipartFile("fdFile", fileName, fileBytes), "false", operatorLoginName);
+ 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()));
+ }
+ String attachmentFdId = result.getData().getFdId();
+ bladeRedis.getValueOps().setIfAbsent(cacheKey, attachmentFdId, Duration.ofDays(HcsSyncConstant.ATTACHMENT_CACHE_TTL_DAYS));
+ log.info("HCS附件转传完成:{} → 合规附件ID {}", fileName, attachmentFdId);
+ return attachmentFdId;
+ }
+
+ /**
+ * 按元数据 URL 从文件服务拉取流:完整 URL 直接下载,相对路径拼可配基址
+ */
+ private byte[] downloadFile(String url) {
+ String target = url;
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ String base = hcsProperties.getAttachment().getFileDownloadBase();
+ if (StringUtil.isBlank(base)) {
+ throw new HcsException("HCS附件转传失败,文件服务下载基址未配置(thirdParty.hcs.attachment.file-download-base)");
+ }
+ target = base + url;
+ }
+ try {
+ return HttpUtil.downloadBytes(target);
+ } catch (Exception exception) {
+ log.error("HCS附件转传下载失败:{}", target, exception);
+ throw new HcsException("HCS附件转传下载失败:" + target, exception);
+ }
+ }
+
+ /**
+ * 附件元数据去重键(url|link|fileUrl|downloadUrl|uid|originalName|name 依次取首个非空)
+ */
+ private String attachmentKey(Map, ?> attachment) {
+ for (String field : KEY_FIELDS) {
+ Object value = attachment.get(field);
+ if (value != null && !String.valueOf(value).isBlank()) {
+ return field + ":" + value;
+ }
+ }
+ return String.valueOf(attachment);
+ }
+
+ /**
+ * 提取附件下载地址
+ */
+ private String resolveUrl(Map, ?> attachment) {
+ for (String field : List.of("url", "link", "fileUrl", "downloadUrl", "path")) {
+ Object value = attachment.get(field);
+ if (value != null && !String.valueOf(value).isBlank()) {
+ return String.valueOf(value).trim();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 提取附件文件名(name/originalName 优先,缺失时取 URL 末段)
+ */
+ private String resolveFileName(Map, ?> attachment, String url) {
+ for (String field : List.of("name", "originalName", "fileName")) {
+ Object value = attachment.get(field);
+ if (value != null && !String.valueOf(value).isBlank()) {
+ return String.valueOf(value);
+ }
+ }
+ String lastSegment = url.substring(url.lastIndexOf('/') + 1);
+ return StringUtil.isBlank(lastSegment) ? "hcs-attachment" : lastSegment;
+ }
+
+ /**
+ * 主文件审批标记判定(照 ContractManage#isAttachmentApproved 惯例)
+ */
+ private boolean isApproved(Map, ?> attachment) {
+ Object approved = attachment.get("approved");
+ if (approved == null) {
+ return false;
+ }
+ return Boolean.TRUE.equals(approved) || Objects.equals(approved, 1)
+ || "1".equals(String.valueOf(approved)) || "true".equalsIgnoreCase(String.valueOf(approved));
+ }
+
+ /**
+ * 解析 JSON 数组(照 ContractManage#parseJsonArray 惯例,异常返回空列表)
+ */
+ private List> parseJsonArray(String json) {
+ if (StringUtil.isBlank(json)) {
+ return List.of();
+ }
+ try {
+ return JsonUtil.parse(json, List.class);
+ } catch (Exception exception) {
+ log.warn("HCS附件元数据解析失败,按无附件处理:{}", json);
+ return List.of();
+ }
+ }
+
+ /**
+ * 内存 MultipartFile(下载字节流转传 C103-2 用)
+ */
+ private record InMemoryMultipartFile(String name, String originalFilename, byte[] bytes) implements MultipartFile {
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return originalFilename;
+ }
+
+ @Override
+ public String getContentType() {
+ return MediaType.APPLICATION_OCTET_STREAM_VALUE;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return bytes == null || bytes.length == 0;
+ }
+
+ @Override
+ public long getSize() {
+ return bytes == null ? 0 : bytes.length;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public void transferTo(File dest) throws IOException {
+ try (FileOutputStream outputStream = new FileOutputStream(dest)) {
+ outputStream.write(bytes);
+ }
+ }
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsMasterDataSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsMasterDataSupport.java
new file mode 100644
index 00000000..d09c6836
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsMasterDataSupport.java
@@ -0,0 +1,206 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.dto.HcsCategoryVO;
+import org.springblade.hcs.dto.HcsCurrencyVO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsPageVO;
+import org.springblade.hcs.dto.HcsTemplateVO;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.hcs.util.HcsPageUtil;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HCS 主数据缓存支撑组件(C103-6 模板 / C103-7 分类 / C103-12 币种,设计文档 §5.9)
+ *
+ * 启动/首次使用时拉取并缓存(Redis,照 BFI token 缓存惯例,TTL 可配 master-data.ttl-minutes):
+ * 模板列表供 template-mapping 校验,币种表供 currency-id-mapping 取值。
+ * 提供管理端"刷新缓存"入口(HcsSyncController)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsMasterDataSupport {
+
+ /**
+ * 主数据分页拉取页大小
+ */
+ private static final int PAGE_SIZE = 200;
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final BladeRedis bladeRedis;
+
+ /**
+ * 获取合同模板列表(缓存优先,未命中全量拉取)
+ */
+ public List getTemplates() {
+ List cached = readCache(HcsSyncConstant.MASTER_CACHE_TEMPLATES, HcsTemplateVO.class);
+ if (cached != null) {
+ return cached;
+ }
+ return loadTemplates();
+ }
+
+ /**
+ * 获取合同分类列表(缓存优先,未命中全量拉取)
+ */
+ public List getCategories() {
+ List cached = readCache(HcsSyncConstant.MASTER_CACHE_CATEGORIES, HcsCategoryVO.class);
+ if (cached != null) {
+ return cached;
+ }
+ return loadCategories();
+ }
+
+ /**
+ * 获取币种列表(缓存优先,未命中全量拉取)
+ */
+ public List getCurrencies() {
+ List cached = readCache(HcsSyncConstant.MASTER_CACHE_CURRENCIES, HcsCurrencyVO.class);
+ if (cached != null) {
+ return cached;
+ }
+ return loadCurrencies();
+ }
+
+ /**
+ * 强制刷新三类主数据缓存(管理端入口),并对 template-mapping 做存在性校验
+ *
+ * @return 刷新结果汇总
+ */
+ public String refreshAll() {
+ int templateCount = loadTemplates().size();
+ int categoryCount = loadCategories().size();
+ int currencyCount = loadCurrencies().size();
+ validateTemplateMapping();
+ String summary = String.format("HCS主数据缓存已刷新:模板 %d 条、分类 %d 条、币种 %d 条", templateCount, categoryCount, currencyCount);
+ log.info(summary);
+ return summary;
+ }
+
+ /**
+ * 全量拉取合同模板并缓存
+ */
+ private List loadTemplates() {
+ HcsPageQueryDTO query = baseQuery();
+ List rows = HcsPageUtil.queryAllPages(offset -> {
+ query.setOffset(offset);
+ return hcsClient.templateList(query).getData();
+ });
+ writeCache(HcsSyncConstant.MASTER_CACHE_TEMPLATES, rows);
+ log.info("HCS合同模板列表已缓存,共 {} 条", rows.size());
+ return rows;
+ }
+
+ /**
+ * 全量拉取合同分类并缓存
+ */
+ private List loadCategories() {
+ HcsPageQueryDTO query = baseQuery();
+ List rows = HcsPageUtil.queryAllPages(offset -> {
+ query.setOffset(offset);
+ return hcsClient.categoryList(query).getData();
+ });
+ writeCache(HcsSyncConstant.MASTER_CACHE_CATEGORIES, rows);
+ log.info("HCS合同分类列表已缓存,共 {} 条", rows.size());
+ return rows;
+ }
+
+ /**
+ * 全量拉取币种并缓存
+ */
+ private List loadCurrencies() {
+ HcsPageQueryDTO query = baseQuery();
+ List rows = HcsPageUtil.queryAllPages(offset -> {
+ query.setOffset(offset);
+ return hcsClient.currencyList(query).getData();
+ });
+ writeCache(HcsSyncConstant.MASTER_CACHE_CURRENCIES, rows);
+ log.info("HCS币种列表已缓存,共 {} 条", rows.size());
+ return rows;
+ }
+
+ /**
+ * 校验 contract.template-mapping 中的模板ID是否存在于 HCS 模板列表(§9-U7,仅告警不阻断)
+ */
+ private void validateTemplateMapping() {
+ Map mapping = hcsProperties.getContract().getTemplateMapping();
+ if (mapping.isEmpty()) {
+ log.warn("HCS合同模板映射(thirdParty.hcs.contract.template-mapping)未配置,合同推送将失败(§9-U7 待合规方提供模板ID清单)");
+ return;
+ }
+ List templateIds = getTemplates().stream().map(HcsTemplateVO::getFdId).toList();
+ mapping.forEach((category, templateId) -> {
+ if (StringUtil.isNotBlank(templateId) && !templateIds.contains(templateId)) {
+ log.warn("HCS合同模板映射校验告警:合同类别 [{}] 映射的模板ID [{}] 不在合规模板列表中", category, templateId);
+ }
+ });
+ }
+
+ /**
+ * 分页查询基础参数(columns 必填)
+ */
+ private HcsPageQueryDTO baseQuery() {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ query.setPageSize(PAGE_SIZE);
+ return query;
+ }
+
+ /**
+ * 读缓存(JSON 字符串反序列化)
+ */
+ private List readCache(String cacheKey, Class itemType) {
+ String json = bladeRedis.get(cacheKey);
+ if (StringUtil.isBlank(json)) {
+ return null;
+ }
+ return JSONUtil.toList(json, itemType);
+ }
+
+ /**
+ * 写缓存(JSON 字符串 + 可配 TTL)
+ */
+ private void writeCache(String cacheKey, List> rows) {
+ bladeRedis.getValueOps().setIfAbsent(cacheKey, JSONUtil.toJsonStr(rows),
+ Duration.ofMinutes(hcsProperties.getMasterData().getTtlMinutes()));
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
index a753f6b4..93f2fefb 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
@@ -112,6 +112,10 @@ public class HcsProperties {
* 附件转传(C103-2)配置
*/
private Attachment attachment = new Attachment();
+ /**
+ * 主数据缓存(C103-6/7/12)配置
+ */
+ private MasterData masterData = new MasterData();
@Data
public static class Push {
@@ -252,4 +256,12 @@ public class HcsProperties {
*/
private String fileDownloadBase = "";
}
+
+ @Data
+ public static class MasterData {
+ /**
+ * 模板/分类/币种列表缓存时长(分钟),启动/首次使用时拉取并缓存(设计文档 §5.9)
+ */
+ private int ttlMinutes = 120;
+ }
}
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index 2345de68..db3628d9 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -144,3 +144,7 @@ thirdParty:
transfer-enabled: ${HCS_ATTACHMENT_TRANSFER_ENABLED:true}
# TMS 文件服务下载基址(拉取附件流用)
file-download-base: ${HCS_ATTACHMENT_FILE_DOWNLOAD_BASE:}
+ # 主数据缓存(C103-6/7/12,启动/首次使用时拉取并缓存)
+ master-data:
+ # 缓存时长(分钟)
+ ttl-minutes: ${HCS_MASTER_DATA_TTL_MINUTES:120}
From c34a5b5bc53fd82e3505a34059487a27a3ba1075 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 22:51:42 +0800
Subject: [PATCH 005/124] =?UTF-8?q?feat:=20HCS=E9=A1=B9=E7=9B=AE=E7=AB=8B?=
=?UTF-8?q?=E9=A1=B9=E5=90=8C=E6=AD=A5=E6=94=AF=E6=92=91=EF=BC=88C102-3?=
=?UTF-8?q?=E5=88=A4=E9=87=8D/C102-1=E6=96=B0=E5=BB=BA/C102-2=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0/=E5=90=88=E8=A7=84fdId=E5=9B=9E=E5=A1=AB=E4=B8=8E?=
=?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=A1=A5=E5=81=BF=E5=9B=9E=E5=86=99=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsProjectSyncSupport.java | 309 ++++++++++++++++++
1 file changed, 309 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
new file mode 100644
index 00000000..6a162c95
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
@@ -0,0 +1,309 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.HcsBpmOperationDTO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsPageVO;
+import org.springblade.hcs.dto.HcsProjectAddDTO;
+import org.springblade.hcs.dto.HcsProjectFormDataDTO;
+import org.springblade.hcs.dto.HcsProjectPropsDTO;
+import org.springblade.hcs.dto.HcsProjectUpdateDTO;
+import org.springblade.hcs.dto.HcsProjectVO;
+import org.springblade.hcs.dto.HcsRefVO;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsSaveOptionDTO;
+import org.springblade.hcs.dto.HcsUserRefVO;
+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.ProjectApplyMapper;
+import org.springblade.transport.pojo.entity.ProjectApply;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * HCS 项目立项(C102)同步支撑组件(设计文档 §5.1)
+ *
+ * 触发与链路:审批通过(approved)→ C102-1 新建;变更审批通过(change_approved,hcs_fd_id 已存在)→ C102-2 更新。
+ * 推送前判重:C102-3 conditions={"fd_num": projectCode},命中则直接回填 fdId 置 synced(跳过新建,§9-U4 查询能力联调确认)。
+ * 失败不在此回写,由调用方(HcsPushTask/手动端点)统一 {@link #markFailed} 落 failed 状态进补偿重试。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsProjectSyncSupport {
+
+ /**
+ * TMS 万元 → HCS 元换算倍率
+ */
+ private static final BigDecimal WAN_TO_YUAN = BigDecimal.valueOf(10000);
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final ProjectApplyMapper projectApplyMapper;
+
+ /**
+ * 推送项目立项到 HCS:判重 → 新建(C102-1)/ 更新(C102-2)→ 回填合规 fdId 置 synced
+ *
+ * @param projectApply 项目立项单(approvalStatus 须为 approved/change_approved)
+ * @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
+ */
+ public String pushProject(ProjectApply projectApply) {
+ // 首推先判重(C102-3),命中直接回填跳过新建;已有合规ID(变更单)走更新
+ if (StringUtil.isBlank(projectApply.getHcsFdId())) {
+ String existFdId = findExistFdId(projectApply);
+ if (StringUtil.isNotBlank(existFdId)) {
+ markSynced(projectApply, existFdId, "判重命中,回填已有合规项目ID");
+ return "判重命中,回填已有合规项目ID";
+ }
+ String fdId = addProject(projectApply);
+ markSynced(projectApply, fdId, "C102-1 项目立项新建成功");
+ return "C102-1 项目立项新建成功";
+ }
+ updateProject(projectApply);
+ markSynced(projectApply, projectApply.getHcsFdId(), "C102-2 项目立项更新成功");
+ return "C102-2 项目立项更新成功";
+ }
+
+ /**
+ * C102-1 项目立项新建,返回合规项目 fdId
+ */
+ private String addProject(ProjectApply projectApply) {
+ String operatorLoginName = requireLoginName(projectApply, "C102-1");
+ HcsProjectAddDTO param = new HcsProjectAddDTO();
+
+ HcsBpmOperationDTO bpmOperation = new HcsBpmOperationDTO();
+ // 操作类型:固定 drafter_submit(以起草人身份直接提交流程)
+ bpmOperation.setOperationType("drafter_submit");
+ // 【必填】立项表单模板编码:配置 thirdParty.hcs.project.template-code(环境值,§9-U9)
+ bpmOperation.setTemplateCode(requireTemplateCode());
+ param.setBpmOperation(bpmOperation);
+ // 是否校验权限:接口调用固定 false
+ param.setCheckAuth(false);
+ // 【必填】当前操作人:项目经办人登录名(§9-U2 登录名同源性联调确认)
+ param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
+ param.setFormData(buildFormData(projectApply, null));
+ HcsSaveOptionDTO saveOption = new HcsSaveOptionDTO();
+ // saveDraft=false 直接提交
+ saveOption.setSaveDraft(false);
+ param.setSaveOption(saveOption);
+
+ log.info("HCS项目立项推送开始:{}(C102-1 新建)", projectApply.getProjectCode());
+ HcsResult result = hcsClient.projectAdd(param);
+ if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
+ log.error("HCS项目立项新建失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ throw new HcsException("C102-1 项目立项新建失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result.getData().getFdId();
+ }
+
+ /**
+ * C102-2 项目立项更新(入参同新建,去掉 bpmOperation/saveOption,formData.fdId 必填)
+ */
+ private void updateProject(ProjectApply projectApply) {
+ String operatorLoginName = requireLoginName(projectApply, "C102-2");
+ HcsProjectUpdateDTO param = new HcsProjectUpdateDTO();
+ // 是否校验权限:接口调用固定 false
+ param.setCheckAuth(false);
+ // 【必填】当前操作人:项目经办人登录名
+ param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
+ // 更新必须携带合规项目 fdId
+ param.setFormData(buildFormData(projectApply, projectApply.getHcsFdId()));
+
+ log.info("HCS项目立项推送开始:{}(C102-2 更新)", projectApply.getProjectCode());
+ HcsResult result = hcsClient.projectUpdate(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS项目立项更新失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ throw new HcsException("C102-2 项目立项更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ }
+
+ /**
+ * 组装项目立项 formData(新建/更新共用,字段标注照 §3.4 规范,映射表见设计文档 §5.1)
+ */
+ private HcsProjectFormDataDTO buildFormData(ProjectApply projectApply, String fdId) {
+ HcsProjectFormDataDTO formData = new HcsProjectFormDataDTO();
+ // 合规项目 fdId(C102-2 更新必填,C102-1 不传)
+ formData.setFdId(fdId);
+ HcsProjectPropsDTO props = new HcsProjectPropsDTO();
+ // 【必填】项目名称:ProjectApply.projectName
+ props.setFdProName(projectApply.getProjectName());
+ // 项目简称:ProjectApply.projectShortName
+ props.setFdProSimpleName(projectApply.getProjectShortName());
+ // 【必填】项目编号:ProjectApply.projectCode(判重候选键)
+ props.setFdNum(projectApply.getProjectCode());
+ // 创建单位:TMS 开题部门名称(HCS 组织 fdId 与 TMS 不同源,先传名称,§9-U2)
+ props.setFdCreatorUnit(HcsRefVO.ofName(projectApply.getBusinessDeptName()));
+ // 项目负责人:负责人登录名 {fdLoginName}(§9-U2)
+ props.setFdHead(resolveUserRef(projectApply.getPrincipalUserId()));
+ // 承接部门:TMS 承接部门名称(同 fd_creator_unit 先传名称)
+ props.setFdTakerDept(HcsRefVO.ofName(projectApply.getUndertakeDeptName()));
+ // 承接人:项目经办人登录名 {fdLoginName}
+ props.setFdTaker(resolveUserRef(projectApply.getHandlerUserId()));
+ // 付款额度:TMS fundLimit(万元)× 10000 转 HCS 元
+ props.setFdPayLimit(toYuan(projectApply.getFundLimit()));
+ // 开票额度:TMS receivableLimit(万元)× 10000 转 HCS 元
+ props.setFdOutInvoiceLimit(toYuan(projectApply.getReceivableLimit()));
+ // 项目建设内容:ProjectApply.situationRemark
+ props.setFdProContent(projectApply.getSituationRemark());
+ // 是否可用:固定 true
+ props.setFdIsAvailable(true);
+ // 可用单位列表:与创建单位一致,先传名称
+ props.setFdAvailUnits(List.of(HcsRefVO.ofName(projectApply.getBusinessDeptName())));
+ // 结算类型:TMS settlementMode 原值(字典映射待联调,§9-U3)
+ props.setFdSettlementType(projectApply.getSettlementMode());
+ // 计划保证金:TMS fundDemand(万元)× 10000 转 HCS 元(变更映射曾名"履约保证金",语义联调确认)
+ props.setFdProPlanDeposit(toYuan(projectApply.getFundDemand()));
+ // 是否自营项目:一期固定 "1"(联调确认)
+ props.setFdIsSelfPro("1");
+ // TODO 待确认:项目收入目标 fd_pro_income_goal,TMS 无直接对应字段,一期不传(§5.1)
+ // 金蝶同步状态:固定 "1"
+ props.setFdKingdeeSyncStatus("1");
+ // 来源标识:fd_sources,配置 thirdParty.hcs.src-system(约定值 TMS,D14)
+ props.setFdSources(hcsProperties.getSrcSystem());
+ // 来源系统主键 ID:TMS 项目立项单主键(D14,对账/幂等候选键)
+ props.setFdKingdeeId(String.valueOf(projectApply.getId()));
+ // 创建人:项目经办人登录名(扫描推送无登录上下文,取单据承办人,§9-U2)
+ props.setFdCreator(resolveUserRef(projectApply.getHandlerUserId()));
+ formData.setDynamicProps(props);
+ return formData;
+ }
+
+ /**
+ * C102-3 项目立项查询判重(conditions={"fd_num": projectCode},§9-U4 查询能力联调确认)
+ */
+ private String findExistFdId(ProjectApply projectApply) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ // C102-3 入参 columns 用下划线字段名
+ query.setColumns(List.of("fd_id", "fd_num"));
+ // 判重条件:项目编号精确匹配
+ query.setConditions(Map.of("fd_num", Objects.toString(projectApply.getProjectCode(), "")));
+ query.setOffset(0);
+ query.setPageSize(10);
+ query.setSorts(Map.of("fd_create_time", "desc"));
+ // 查询需携带当前操作人(复用单据承办人登录名)
+ query.setCurUser(HcsUserRefVO.ofLoginName(requireLoginName(projectApply, "C102-3")));
+ HcsResult> result = hcsClient.projectList(query);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS项目立项判重查询失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ throw new HcsException("C102-3 项目立项判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ HcsPageVO page = result.getData();
+ if (page == null || page.getContent() == null || page.getContent().isEmpty()) {
+ return null;
+ }
+ return page.getContent().get(0).getFdId();
+ }
+
+ /**
+ * 回填合规 fdId 并置 synced
+ */
+ public void markSynced(ProjectApply projectApply, String fdId, String message) {
+ projectApply.setHcsFdId(fdId);
+ projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
+ projectApply.setHcsRetryCount(0);
+ projectApply.setHcsSyncTime(LocalDateTime.now());
+ projectApply.setHcsSyncMsg(message);
+ projectApplyMapper.updateById(projectApply);
+ log.info("HCS项目立项推送成功:projectCode={},fdId={},{}", projectApply.getProjectCode(), fdId, message);
+ }
+
+ /**
+ * 推送失败回写:failed + 重试次数+1 + 失败原因(供扫描任务按 hcs_retry_count < max-retry 自动补偿)
+ */
+ public void markFailed(ProjectApply projectApply, String message) {
+ projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
+ projectApply.setHcsRetryCount(projectApply.getHcsRetryCount() == null ? 1 : projectApply.getHcsRetryCount() + 1);
+ projectApply.setHcsSyncTime(LocalDateTime.now());
+ projectApply.setHcsSyncMsg(message);
+ projectApplyMapper.updateById(projectApply);
+ log.error("HCS项目立项推送失败:projectCode={},retryCount={},{}", projectApply.getProjectCode(), projectApply.getHcsRetryCount(), message);
+ }
+
+ /**
+ * 立项表单模板编码(未配置直接失败,提示配置项,§9-U9)
+ */
+ private String requireTemplateCode() {
+ String templateCode = hcsProperties.getProject().getTemplateCode();
+ if (StringUtil.isBlank(templateCode)) {
+ throw new HcsException("HCS项目立项推送失败,立项表单模板编码未配置(thirdParty.hcs.project.template-code,§9-U9)");
+ }
+ return templateCode;
+ }
+
+ /**
+ * 项目经办人登录名(curUser 必填,解析不到直接失败并提示)
+ */
+ private String requireLoginName(ProjectApply projectApply, String scene) {
+ String loginName = resolveLoginName(projectApply.getHandlerUserId());
+ if (StringUtil.isBlank(loginName)) {
+ throw new HcsException("HCS项目立项推送失败(" + scene + "),经办人登录名解析不到(handlerUserId=" + projectApply.getHandlerUserId() + ",§9-U2)");
+ }
+ return loginName;
+ }
+
+ /**
+ * 按用户ID 解析登录名引用(查不到返回 null,调用方按字段必填性兜底)
+ */
+ private HcsUserRefVO resolveUserRef(Long userId) {
+ String loginName = resolveLoginName(userId);
+ return StringUtil.isBlank(loginName) ? null : HcsUserRefVO.ofLoginName(loginName);
+ }
+
+ /**
+ * 按用户ID 解析登录名(UserCache 惯例)
+ */
+ private String resolveLoginName(Long userId) {
+ if (userId == null) {
+ return null;
+ }
+ User user = UserCache.getUser(userId);
+ return user == null ? null : user.getAccount();
+ }
+
+ /**
+ * TMS 万元 → HCS 元
+ */
+ private BigDecimal toYuan(BigDecimal wanAmount) {
+ return wanAmount == null ? null : wanAmount.multiply(WAN_TO_YUAN);
+ }
+}
From 75bbd829c7068450e475163c2b6aafaee53fee69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 22:54:45 +0800
Subject: [PATCH 006/124] =?UTF-8?q?feat:=20HCS=E5=90=88=E5=90=8C=E5=90=8C?=
=?UTF-8?q?=E6=AD=A5=E6=94=AF=E6=92=91=EF=BC=88C101-1=E9=A3=8E=E9=99=A9?=
=?UTF-8?q?=E6=A0=A1=E9=AA=8C=E7=95=99=E7=97=95/C103-3=E5=88=A4=E9=87=8D/C?=
=?UTF-8?q?103-2=E9=99=84=E4=BB=B6=E8=BD=AC=E4=BC=A0/C103-1=E5=8F=91?=
=?UTF-8?q?=E8=B5=B7=E4=B8=8EC103-11=E6=9B=B4=E6=96=B0=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsContractSyncSupport.java | 487 ++++++++++++++++++
1 file changed, 487 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
new file mode 100644
index 00000000..734f0e3d
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -0,0 +1,487 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.HcsContractDraftDTO;
+import org.springblade.hcs.dto.HcsContractDraftVO;
+import org.springblade.hcs.dto.HcsContractFieldJsonDTO;
+import org.springblade.hcs.dto.HcsContractVO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsPageVO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsRiskQueryDTO;
+import org.springblade.hcs.dto.HcsRiskVO;
+import org.springblade.hcs.dto.HcsSignerDTO;
+import org.springblade.hcs.dto.HcsUserRefVO;
+import org.springblade.hcs.dto.HcsXformFieldDTO;
+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.CustomerArchiveMapper;
+import org.springblade.transport.mapper.HcsContractCheckLogMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.CustomerArchive;
+import org.springblade.transport.pojo.entity.HcsContractCheckLog;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * HCS 合同同步支撑组件(C103-1 发起 / C103-11 更新,设计文档 §5.2)
+ *
+ * 扫描任务按序执行:① 客商风险校验(可配开关,黑名单阻断)→ ② 附件转传(挂载点待 §9-U8 确认,先转传缓存)→
+ * ③ C103-3 判重(§9-U4 查询能力联调确认)→ ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→
+ * ⑤ 履约规则 C103-9(P2 接线)。
+ * 结果留痕:风险校验写 blade_hcs_check_log 与合同 hcs_risk_* 字段;推送结果回填 hcs_fd_id/hcs_sync_status。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsContractSyncSupport {
+
+ /**
+ * 合同金额等 xformFields 值一律字符串传值
+ */
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ /**
+ * C101-1 黑名单状态取值(示例 no 表示非黑名单,§9-U3 字典值联调确认)
+ */
+ private static final String BLACK_STATUS_YES = "yes";
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final ContractManageMapper contractManageMapper;
+ private final CustomerArchiveMapper customerArchiveMapper;
+ private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
+
+ /**
+ * 推送合同到 HCS:风险校验 → 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
+ *
+ * @param contractManage 合同单(approvalStatus 须为 approved/change_approved)
+ * @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
+ */
+ public String pushContract(ContractManage contractManage) {
+ // ① 客商风险校验(可配开关),黑名单且 block-blacklist=true 时阻断推送
+ if (hcsProperties.getContract().isRequireRiskCheck()) {
+ checkRelativeRisk(contractManage);
+ }
+ // ② 附件转传(先转传并缓存合规附件ID,C103-1 挂载方式待 §9-U8 确认后接线)
+ transferAttachmentsIfEnabled(contractManage);
+ // ③④ 首推判重后起草(C103-1);已有合规ID(变更单)走更新(C103-11)
+ if (StringUtil.isBlank(contractManage.getHcsFdId())) {
+ String existFdId = findExistFdId(contractManage);
+ if (StringUtil.isNotBlank(existFdId)) {
+ markSynced(contractManage, existFdId, "判重命中,回填已有合规合同ID");
+ return "判重命中,回填已有合规合同ID";
+ }
+ String fdContractId = draftContract(contractManage);
+ // TODO 待联调(P2):履约规则 C103-9 行级同步随合同链路执行(§5.3,依赖 blade_contract_exe_stage 表)
+ markSynced(contractManage, fdContractId, "C103-1 合同发起成功");
+ return "C103-1 合同发起成功";
+ }
+ updateContract(contractManage);
+ markSynced(contractManage, contractManage.getHcsFdId(), "C103-11 合同更新成功");
+ return "C103-11 合同更新成功";
+ }
+
+ /**
+ * C101-1 客商风险校验:甲乙双方各查一次,留痕 check_log 与合同 hcs_risk_* 字段,黑名单阻断
+ */
+ private void checkRelativeRisk(ContractManage contractManage) {
+ RiskResult riskPartyA = queryRisk(contractManage, "甲方", contractManage.getPartyA());
+ RiskResult riskPartyB = queryRisk(contractManage, "乙方", contractManage.getPartyB());
+ // 风险等级:甲乙双方取先查到的非空值(无法量化比较,留痕双方原始值,联调后细化)
+ String riskLevel = StringUtil.isNotBlank(riskPartyA.riskLevel()) ? riskPartyA.riskLevel() : riskPartyB.riskLevel();
+ // 黑名单状态:任一方黑名单即为是
+ boolean blacklist = riskPartyA.blacklist() || riskPartyB.blacklist();
+ contractManage.setHcsRiskLevel(riskLevel);
+ contractManage.setHcsBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
+ contractManage.setHcsRiskTime(LocalDateTime.now());
+ contractManageMapper.updateById(contractManage);
+ 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);
+ }
+ }
+
+ /**
+ * 查询单方客商风险:按名称反查客商编号(§9-U6 编码体系联调确认),反查不到记 skip 留痕不阻断
+ */
+ private RiskResult queryRisk(ContractManage contractManage, String standing, String partyName) {
+ String customerCode = queryCustomerCode(partyName);
+ if (StringUtil.isBlank(customerCode)) {
+ writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP,
+ standing + "方[" + partyName + "]反查不到客商编号,跳过风险校验(§9-U6)", null);
+ return new RiskResult(null, null, false);
+ }
+ HcsRiskQueryDTO param = new HcsRiskQueryDTO();
+ // 【必填】客商编号:按甲乙方名称反查 CustomerArchive 客商编码(§9-U6)
+ param.setFdNumber(customerCode);
+ HcsResult result = hcsClient.getRelativeRisk(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS客商风险查询失败响应:{},contractNo={},standing={},fdNumber={}",
+ result == null ? "无响应" : result.getMsg(), contractManage.getContractNo(), standing, customerCode);
+ throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ HcsRiskVO risk = result.getData();
+ String blackStatus = risk == null ? null : risk.getFdBlackStatus();
+ String riskLevel = risk == null ? null : risk.getFdRiskLevel();
+ boolean blacklist = BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
+ writeCheckLog(contractManage, blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
+ standing + "方[" + partyName + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel, result.getTraceId());
+ return new RiskResult(blackStatus, riskLevel, blacklist);
+ }
+
+ /**
+ * C103-3 合同列表判重(conditions={"fdNumber": contractNo},§9-U4 查询能力联调确认)
+ */
+ private String findExistFdId(ContractManage contractManage) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ query.setColumns(List.of("fdId", "fdNumber"));
+ // 判重条件:合同编号精确匹配
+ query.setConditions(Map.of("fdNumber", Objects.toString(contractManage.getContractNo(), "")));
+ query.setOffset(0);
+ query.setPageSize(10);
+ HcsResult> result = hcsClient.contractList(query);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS合同判重查询失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ throw new HcsException("C103-3 合同判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ HcsPageVO page = result.getData();
+ if (page == null || page.getContent() == null || page.getContent().isEmpty()) {
+ return null;
+ }
+ return page.getContent().get(0).getFdId();
+ }
+
+ /**
+ * C103-1 合同发起,返回合规合同 fdContractId
+ */
+ private String draftContract(ContractManage contractManage) {
+ HcsContractDraftDTO param = buildDraftParam(contractManage);
+ log.info("HCS合同推送开始:{}(C103-1 发起)", contractManage.getContractNo());
+ HcsResult result = hcsClient.contractDraft(param);
+ return validateDraftResult(contractManage, result);
+ }
+
+ /**
+ * C103-11 合同更新(入参同发起;TODO 待联调(§9-U5):HCS 侧如何定位已有合同 v4 未说明)
+ */
+ private void updateContract(ContractManage contractManage) {
+ HcsContractDraftDTO param = buildDraftParam(contractManage);
+ log.info("HCS合同推送开始:{}(C103-11 更新,§9-U5 定位方式待联调)", contractManage.getContractNo());
+ HcsResult result = hcsClient.contractUpdate(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS合同更新失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ throw new HcsException("C103-11 合同更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ }
+
+ /**
+ * 组装 C103-1/C103-11 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.2)
+ */
+ private HcsContractDraftDTO buildDraftParam(ContractManage contractManage) {
+ HcsContractDraftDTO param = new HcsContractDraftDTO();
+ // 【必填】合同模板ID:template-mapping[contractCategory](§9-U7 待合规方提供模板ID清单)
+ param.setFdTemplateId(requireTemplateId(contractManage));
+ // 【必填】外部系统标识:fd_sources,配置 thirdParty.hcs.src-system(D14)
+ param.setFdOuterSystem(hcsProperties.getSrcSystem());
+ // 【必填】操作人登录名:合同承办人登录名(§9-U2)
+ param.setFdLoginName(requireLoginName(contractManage));
+ // 【必填】保存类型:submit(提交)
+ param.setSaveType("submit");
+ HcsContractFieldJsonDTO fieldJson = new HcsContractFieldJsonDTO();
+ fieldJson.setXformFields(buildXformFields(contractManage));
+ // 相对方(签署方):甲方/乙方,编号按名称反查 CustomerArchive(§9-U6)
+ fieldJson.setSigner(buildSigners(contractManage));
+ param.setFdFieldJson(fieldJson);
+ return param;
+ }
+
+ /**
+ * xformFields 模板字段(字段名/中文名取自《桂物合同集成接口逻辑梳理》§5.1 合同字段说明)
+ */
+ private List buildXformFields(ContractManage contractManage) {
+ List fields = new ArrayList<>();
+ // 【必填】合同编号:ContractManage.contractNo
+ fields.add(field("fdNumber", "合同编号", contractManage.getContractNo()));
+ fields.add(field("CONTRACT_NO", "合同编号", contractManage.getContractNo()));
+ // 【必填】合同标题:ContractManage.contractName
+ fields.add(field("fdSubject", "合同标题", contractManage.getContractName()));
+ fields.add(field("CONTRACT_NAME", "合同名称", contractManage.getContractName()));
+ // 【必填】合同金额:contractAmount(字符串传值)
+ fields.add(field("fdAmount", "合同金额", toPlainString(contractManage.getContractAmount())));
+ fields.add(field("AMOUNT", "合同金额", toPlainString(contractManage.getContractAmount())));
+ // 币种:TMS 结算币种经 currency-name-mapping 映射(默认 RMB→人民币,照金蝶惯例)
+ fields.add(field("fdCurrencyId", "币种", mapCurrencyName(contractManage.getSettlementCurrency())));
+ fields.add(field("CURRENCY", "币种", mapCurrencyName(contractManage.getSettlementCurrency())));
+ // 合同结算类型:settlement-type-mapping[settlementMode](映射到"付款/收款"待联调,§9-U3,未配置不传)
+ String settlementType = hcsProperties.getContract().getSettlementTypeMapping().get(contractManage.getSettlementMode());
+ if (StringUtil.isNotBlank(settlementType)) {
+ fields.add(field("fdSettlementType", "合同结算类型", settlementType));
+ } else {
+ log.warn("HCS合同结算类型映射缺失:settlementMode=[{}] 不在 contract.settlement-type-mapping,fdSettlementType 不传(§9-U3)", contractManage.getSettlementMode());
+ }
+ // 特批流程:fdSepcailContract 固定 false(原文拼写笔误必须沿用,D15,配置项预留)
+ fields.add(field("fdSepcailContract", "特批流程", hcsProperties.getContract().isSpecialContractEnabled() ? "true" : "false"));
+ // 合同类型:ContractManage.contractCategory
+ fields.add(field("CONTRACT_TYPE", "合同类型", contractManage.getContractCategory()));
+ // 甲方名称/乙方名称:TMS 存名称字符串
+ fields.add(field("PARTY_A", "甲方名称", contractManage.getPartyA()));
+ fields.add(field("PARTY_B", "乙方名称", contractManage.getPartyB()));
+ // 【必填】签订日期:signDate(fdType=timestamp)
+ fields.add(timestampField("SIGN_DATE", "签订日期", contractManage.getSignDate()));
+ // 预计开始/结束时间:startDate/endDate(fdType=timestamp)
+ fields.add(timestampField("fdEstimateStartTime", "预计开始时间", contractManage.getStartDate()));
+ fields.add(timestampField("fdEstimateEndTime", "预计结束时间", contractManage.getEndDate()));
+ // 创建时间:createTime(fdType=timestamp)
+ fields.add(timestampField("CREATE_TIME", "创建时间", contractManage.getCreateTime()));
+ fields.add(timestampField("fdCreateTime", "创建时间", contractManage.getCreateTime()));
+ // 同步时间:推送时刻(fdType=timestamp)
+ fields.add(timestampField("fd_syn_time", "同步时间", LocalDateTime.now()));
+ // 承办人名称:handlerUserName
+ fields.add(field("CREATE_USER_NAME", "承办人名称", contractManage.getHandlerUserName()));
+ // TODO 待确认:合同状态 CONTRACT_STATUS,TMS 无直接对应,是否必传联调确认(§9-U3),一期不传
+ return fields;
+ }
+
+ /**
+ * signer 相对方(签署方):甲/乙双方,fdType(inner/外部)判定规则待联调(§9-U6)暂不传
+ */
+ private List buildSigners(ContractManage contractManage) {
+ List signers = new ArrayList<>();
+ // 甲方相对方:fdStanding=甲,编号按名称反查(反查不到留空,§9-U6)
+ signers.add(signer("甲", contractManage.getPartyA()));
+ // 乙方相对方:fdStanding=乙
+ signers.add(signer("乙", contractManage.getPartyB()));
+ return signers;
+ }
+
+ /**
+ * 单个相对方
+ */
+ private HcsSignerDTO signer(String fdStanding, String partyName) {
+ HcsSignerDTO signer = new HcsSignerDTO();
+ // 签署地位:甲 / 乙
+ signer.setFdStanding(fdStanding);
+ // 相对方名称:TMS 甲乙方名称
+ signer.setFdName(partyName);
+ // 相对方编号:按名称反查 CustomerArchive 客商编码,反查不到留空(§9-U6)
+ signer.setFdNumber(queryCustomerCode(partyName));
+ return signer;
+ }
+
+ /**
+ * 附件转传(可配开关):合同主文件 approved=true + 其它附件,合规附件ID 走映射缓存
+ */
+ private void transferAttachmentsIfEnabled(ContractManage contractManage) {
+ if (!hcsProperties.getAttachment().isTransferEnabled()) {
+ return;
+ }
+ List attachmentFdIds = hcsAttachmentSupport.transferContractAttachments(
+ contractManage.getContractFileJson(), contractManage.getAttachmentsJson(), requireLoginName(contractManage));
+ // TODO 待联调(§9-U8):C103-1 未定义附件字段,转传后的合规附件ID 暂存映射缓存,挂载方式确认后接线
+ log.info("HCS合同附件转传完成:contractNo={},共 {} 个附件(挂载点待 §9-U8 确认)", contractManage.getContractNo(), attachmentFdIds.size());
+ }
+
+ /**
+ * 校验 C103-1 返回并取合同 fdContractId(必须持久化)
+ */
+ private String validateDraftResult(ContractManage contractManage, HcsResult result) {
+ if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdContractId())) {
+ log.error("HCS合同发起失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ throw new HcsException("C103-1 合同发起失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result.getData().getFdContractId();
+ }
+
+ /**
+ * 回填合规 fdId 并置 synced
+ */
+ public void markSynced(ContractManage contractManage, String fdId, String message) {
+ contractManage.setHcsFdId(fdId);
+ contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
+ contractManage.setHcsRetryCount(0);
+ contractManage.setHcsSyncTime(LocalDateTime.now());
+ contractManage.setHcsSyncMsg(message);
+ contractManageMapper.updateById(contractManage);
+ log.info("HCS合同推送成功:contractNo={},fdContractId={},{}", contractManage.getContractNo(), fdId, message);
+ }
+
+ /**
+ * 推送失败回写:failed + 重试次数+1 + 失败原因
+ */
+ public void markFailed(ContractManage contractManage, String message) {
+ contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
+ contractManage.setHcsRetryCount(contractManage.getHcsRetryCount() == null ? 1 : contractManage.getHcsRetryCount() + 1);
+ contractManage.setHcsSyncTime(LocalDateTime.now());
+ contractManage.setHcsSyncMsg(message);
+ contractManageMapper.updateById(contractManage);
+ log.error("HCS合同推送失败:contractNo={},retryCount={},{}", contractManage.getContractNo(), contractManage.getHcsRetryCount(), message);
+ }
+
+ /**
+ * 模板映射(template-mapping[contractCategory],未配置/无映射直接失败提示,§9-U7)
+ */
+ private String requireTemplateId(ContractManage contractManage) {
+ Map mapping = hcsProperties.getContract().getTemplateMapping();
+ String templateId = mapping.get(contractManage.getContractCategory());
+ if (StringUtil.isBlank(templateId)) {
+ throw new HcsException("HCS合同推送失败,合同类别 [" + contractManage.getContractCategory()
+ + "] 未配置合规模板ID(thirdParty.hcs.contract.template-mapping,§9-U7)");
+ }
+ return templateId;
+ }
+
+ /**
+ * 合同承办人登录名(fdLoginName 必填)
+ */
+ private String requireLoginName(ContractManage contractManage) {
+ if (contractManage.getHandlerUserId() == null) {
+ throw new HcsException("HCS合同推送失败,合同承办人未维护(contractNo=" + contractManage.getContractNo() + ")");
+ }
+ User user = UserCache.getUser(contractManage.getHandlerUserId());
+ if (user == null || StringUtil.isBlank(user.getAccount())) {
+ throw new HcsException("HCS合同推送失败,承办人登录名解析不到(handlerUserId=" + contractManage.getHandlerUserId() + ",§9-U2)");
+ }
+ return user.getAccount();
+ }
+
+ /**
+ * 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
+ */
+ private String queryCustomerCode(String partyName) {
+ if (StringUtil.isBlank(partyName)) {
+ return null;
+ }
+ CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.lambdaQuery()
+ .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyName).or().eq(CustomerArchive::getShortName, partyName))
+ .eq(CustomerArchive::getIsDeleted, 0)
+ .last("limit 1"));
+ return customerArchive == null ? null : customerArchive.getCustomerCode();
+ }
+
+ /**
+ * 币种名称映射:currency-name-mapping 命中取映射值,未命中传原值并告警(§9-U3)
+ */
+ private String mapCurrencyName(String currency) {
+ if (StringUtil.isBlank(currency)) {
+ return null;
+ }
+ return hcsProperties.getContract().getCurrencyNameMapping().getOrDefault(currency, currency);
+ }
+
+ /**
+ * 普通模板字段(字符串传值)
+ */
+ private HcsXformFieldDTO field(String name, String label, String value) {
+ HcsXformFieldDTO field = new HcsXformFieldDTO();
+ field.setName(name);
+ field.setLabel(label);
+ field.setValue(value);
+ return field;
+ }
+
+ /**
+ * 时间模板字段(fdType=timestamp,日期传 yyyy-MM-dd,时间传 yyyy-MM-dd HH:mm:ss)
+ */
+ private HcsXformFieldDTO timestampField(String name, String label, Object value) {
+ if (value == null) {
+ return field(name, label, null);
+ }
+ String text;
+ if (value instanceof LocalDate localDate) {
+ text = localDate.format(DATE_FORMATTER);
+ } else if (value instanceof LocalDateTime localDateTime) {
+ text = localDateTime.format(DATETIME_FORMATTER);
+ } else {
+ // BaseEntity.createTime 为 Date 类型
+ text = DATETIME_FORMATTER.format(((java.util.Date) value).toInstant()
+ .atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
+ }
+ HcsXformFieldDTO field = field(name, label, text);
+ // 时间字段需带 fdType=timestamp
+ field.setFdType("timestamp");
+ return field;
+ }
+
+ /**
+ * 金额字符串(空值安全)
+ */
+ private String toPlainString(BigDecimal amount) {
+ return amount == null ? null : amount.toPlainString();
+ }
+
+ /**
+ * 风险校验留痕(blade_hcs_check_log,异常不影响主流程)
+ */
+ 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_RISK);
+ checkLog.setResult(result);
+ checkLog.setMsg(msg);
+ checkLog.setTraceId(traceId);
+ checkLog.setCreateTime(LocalDateTime.now());
+ hcsContractCheckLogMapper.insert(checkLog);
+ } catch (Exception exception) {
+ log.error("HCS风险校验留痕写入失败:contractNo={}", contractManage.getContractNo(), exception);
+ }
+ }
+
+ /**
+ * 单方风险查询结果
+ */
+ private record RiskResult(String blackStatus, String riskLevel, boolean blacklist) {
+ }
+}
From eaab17b2200c407d5ed7cc02bab345d6f1f025a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 22:58:12 +0800
Subject: [PATCH 007/124] =?UTF-8?q?feat:=20HCS=E6=8E=A8=E9=80=81=E4=BB=BB?=
=?UTF-8?q?=E5=8A=A1=E4=B8=8E=E6=89=8B=E5=8A=A8=E7=AB=AF=E7=82=B9=EF=BC=88?=
=?UTF-8?q?=E5=AE=9A=E6=97=B6=E6=89=AB=E6=8F=8F=E9=A6=96=E6=8E=A8/?=
=?UTF-8?q?=E8=A1=A5=E5=81=BF+=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81?=
=?UTF-8?q?=E3=80=81=E5=8D=95=E6=9D=A1=E9=87=8D=E6=8E=A8/=E6=89=B9?=
=?UTF-8?q?=E9=87=8F=E6=8E=A8=E9=80=81/=E4=B8=BB=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=88=B7=E6=96=B0=E7=AB=AF=E7=82=B9=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/HcsSyncController.java | 106 +++++++++++
.../support/hcs/HcsContractSyncSupport.java | 27 +++
.../support/hcs/HcsProjectSyncSupport.java | 27 +++
.../transport/task/HcsPushTask.java | 178 ++++++++++++++++++
4 files changed, 338 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
new file mode 100644
index 00000000..8392512f
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -0,0 +1,106 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.controller;
+
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.api.R;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsMasterDataSupport;
+import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
+import org.springblade.transport.task.HcsPushTask;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * HCS 合规推送端点(设计文档 §5.0 手动入口)
+ *
+ * 前端列表页"推送合规/重推"按钮(照"同步金蝶"按钮模式):
+ * 单条重推重置 hcs_sync_status=unsynced 并清零重试计数后立即推送;批量端点复用定时扫描逻辑。
+ * 排查推送问题以 hcs_sync_status/hcs_sync_msg 为准,不以审批日志为准(ADR-0001)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@RestController
+@AllArgsConstructor
+@RequestMapping("hcs")
+@Tag(name = "HCS合规推送", description = "HCS合规推送端点")
+public class HcsSyncController {
+
+ private final HcsPushTask hcsPushTask;
+ private final HcsProjectSyncSupport hcsProjectSyncSupport;
+ private final HcsContractSyncSupport hcsContractSyncSupport;
+ private final HcsMasterDataSupport hcsMasterDataSupport;
+
+ /**
+ * 单条重推(bizType:project=项目立项 / contract=合同)
+ */
+ @PostMapping("/sync/{bizType}/{id}")
+ @ApiOperationSupport(order = 1)
+ @Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project=项目立项、contract=合同")
+ public R repush(@Parameter(description = "业务类型", required = true) @PathVariable String bizType,
+ @Parameter(description = "单据ID", required = true) @PathVariable Long id) {
+ String result = switch (bizType) {
+ case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsProjectSyncSupport.repushProject(id);
+ case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsContractSyncSupport.repushContract(id);
+ default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
+ };
+ return R.data(result);
+ }
+
+ /**
+ * 批量推送(bizType:project=项目立项 / contract=合同;复用定时扫描逻辑,单轮批量上限 push.batch-size)
+ */
+ @PostMapping("/sync/{bizType}")
+ @ApiOperationSupport(order = 2)
+ @Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project=项目立项、contract=合同")
+ public R pushBatch(@Parameter(description = "业务类型", required = true) @PathVariable String bizType) {
+ String result = switch (bizType) {
+ case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects();
+ case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts();
+ default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
+ };
+ return R.data(result);
+ }
+
+ /**
+ * 刷新主数据缓存(C103-6 模板 / C103-7 分类 / C103-12 币种)
+ */
+ @PostMapping("/master-data/refresh")
+ @ApiOperationSupport(order = 3)
+ @Operation(summary = "刷新HCS主数据缓存", description = "全量重拉模板/分类/币种列表并缓存,同时校验模板映射配置")
+ public R refreshMasterData() {
+ return R.data(hcsMasterDataSupport.refreshAll());
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 734f0e3d..4b06fcf8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -28,6 +28,7 @@ package org.springblade.transport.support.hcs;
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.config.HcsProperties;
import org.springblade.hcs.dto.HcsContractDraftDTO;
@@ -342,6 +343,32 @@ public class HcsContractSyncSupport {
return result.getData().getFdContractId();
}
+ /**
+ * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
+ *
+ * @param id 合同单ID
+ * @return 推送结果描述
+ */
+ public String repushContract(Long id) {
+ ContractManage contractManage = contractManageMapper.selectById(id);
+ if (contractManage == null) {
+ throw new ServiceException("合同单不存在:" + id);
+ }
+ if (!List.of("approved", "change_approved").contains(contractManage.getApprovalStatus())) {
+ throw new ServiceException("仅审批通过的合同可推送合规(当前状态:" + contractManage.getApprovalStatus() + ")");
+ }
+ contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ contractManage.setHcsRetryCount(0);
+ contractManageMapper.updateById(contractManage);
+ try {
+ return pushContract(contractManage);
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ markFailed(contractManage, message);
+ throw new ServiceException("HCS合同推送失败:" + message);
+ }
+ }
+
/**
* 回填合规 fdId 并置 synced
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
index 6a162c95..52aefe9e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
@@ -28,6 +28,7 @@ package org.springblade.transport.support.hcs;
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.config.HcsProperties;
import org.springblade.hcs.dto.HcsBpmOperationDTO;
@@ -234,6 +235,32 @@ public class HcsProjectSyncSupport {
return page.getContent().get(0).getFdId();
}
+ /**
+ * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
+ *
+ * @param id 项目立项单ID
+ * @return 推送结果描述
+ */
+ public String repushProject(Long id) {
+ ProjectApply projectApply = projectApplyMapper.selectById(id);
+ if (projectApply == null) {
+ throw new ServiceException("项目立项单不存在:" + id);
+ }
+ if (!List.of("approved", "change_approved").contains(projectApply.getApprovalStatus())) {
+ throw new ServiceException("仅审批通过的项目立项可推送合规(当前状态:" + projectApply.getApprovalStatus() + ")");
+ }
+ projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ projectApply.setHcsRetryCount(0);
+ projectApplyMapper.updateById(projectApply);
+ try {
+ return pushProject(projectApply);
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ markFailed(projectApply, message);
+ throw new ServiceException("HCS项目立项推送失败:" + message);
+ }
+ }
+
/**
* 回填合规 fdId 并置 synced
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
new file mode 100644
index 00000000..896a3d21
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -0,0 +1,178 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.task;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.ProjectApplyMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.ProjectApply;
+import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * HCS 合规推送定时扫描任务(设计文档 §5.0,ADR-0001)
+ *
+ * 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
+ * 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
+ * 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 P2 接线)。
+ * 多实例部署时以 Redis 分布式锁防重扫(照既有 BladeRedis 用法,单实例部署行为与既有金蝶任务一致)。
+ * 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts,供手动端点(HcsSyncController)复用。
+ *
+ * 调度配置(Nacos thirdParty.hcs.push.*):sync-enabled 总开关(默认关,灰度打开)、cron、batch-size、max-retry。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsPushTask {
+
+ /**
+ * 分布式锁 TTL(秒):须小于 cron 间隔(默认 5 分钟)且大于单轮预期执行时长,实例崩溃后锁自动过期
+ */
+ private static final long PUSH_TASK_LOCK_TTL_SECONDS = 240L;
+
+ /**
+ * 审批通过状态集合(含变更通过)
+ */
+ private static final List APPROVED_STATUSES = List.of("approved", "change_approved");
+
+ private final HcsProperties hcsProperties;
+ private final ProjectApplyMapper projectApplyMapper;
+ private final ContractManageMapper contractManageMapper;
+ private final HcsProjectSyncSupport hcsProjectSyncSupport;
+ private final HcsContractSyncSupport hcsContractSyncSupport;
+ private final BladeRedis bladeRedis;
+
+ /**
+ * HCS 推送任务开关,默认关闭(灰度打开)
+ */
+ @Value("${thirdParty.hcs.push.sync-enabled:false}")
+ private boolean syncEnabled;
+
+ /**
+ * HCS 推送调度,默认每 5 分钟(准实时首推)
+ */
+ @Scheduled(cron = "${thirdParty.hcs.push.cron:0 */5 * * * ?}")
+ public void pushHcs() {
+ if (!syncEnabled) {
+ return;
+ }
+ // 多实例部署防重扫:setIfAbsent 抢锁,持有值比对后释放
+ String lockHolder = UUID.randomUUID().toString();
+ Boolean locked = bladeRedis.getValueOps()
+ .setIfAbsent(HcsSyncConstant.PUSH_TASK_LOCK_KEY, lockHolder, Duration.ofSeconds(PUSH_TASK_LOCK_TTL_SECONDS));
+ if (!Boolean.TRUE.equals(locked)) {
+ log.info("HCS推送任务其他实例执行中,本轮跳过");
+ return;
+ }
+ try {
+ // 任务顺序(设计文档 §5.0):项目 → 合同;两类互不影响
+ log.info("定时推送HCS项目立项:{}", pushUnsyncedProjects());
+ log.info("定时推送HCS合同:{}", pushUnsyncedContracts());
+ } finally {
+ if (lockHolder.equals(bladeRedis.get(HcsSyncConstant.PUSH_TASK_LOCK_KEY))) {
+ bladeRedis.del(HcsSyncConstant.PUSH_TASK_LOCK_KEY);
+ }
+ }
+ }
+
+ /**
+ * 扫描并推送待同步/待补偿的项目立项(C102),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedProjects() {
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ List projectList = projectApplyMapper.selectList(Wrappers.lambdaQuery()
+ .in(ProjectApply::getApprovalStatus, APPROVED_STATUSES)
+ .in(ProjectApply::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .and(wrapper -> wrapper.isNull(ProjectApply::getHcsRetryCount).or().lt(ProjectApply::getHcsRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ int successCount = 0;
+ int failCount = 0;
+ for (ProjectApply projectApply : projectList) {
+ try {
+ hcsProjectSyncSupport.pushProject(projectApply);
+ successCount++;
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ hcsProjectSyncSupport.markFailed(projectApply, message);
+ failCount++;
+ }
+ }
+ String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", projectList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS项目立项推送存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 扫描并推送待同步/待补偿的合同(C103),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedContracts() {
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ List contractList = contractManageMapper.selectList(Wrappers.lambdaQuery()
+ .in(ContractManage::getApprovalStatus, APPROVED_STATUSES)
+ .in(ContractManage::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .and(wrapper -> wrapper.isNull(ContractManage::getHcsRetryCount).or().lt(ContractManage::getHcsRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ int successCount = 0;
+ int failCount = 0;
+ for (ContractManage contractManage : contractList) {
+ try {
+ hcsContractSyncSupport.pushContract(contractManage);
+ successCount++;
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ hcsContractSyncSupport.markFailed(contractManage, message);
+ failCount++;
+ }
+ }
+ String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", contractList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS合同推送存在失败:{}", summary);
+ }
+ return summary;
+ }
+}
From 892035ee6ff68d0cffdbbc44140c4accf85f57d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:27:29 +0800
Subject: [PATCH 008/124] =?UTF-8?q?feat:=20HCS=E5=B1=A5=E7=BA=A6=E8=A7=84?=
=?UTF-8?q?=E5=88=99=E6=95=B0=E6=8D=AE=E5=B1=82=E4=B8=8E=E8=A1=A8=E5=8D=95?=
=?UTF-8?q?=E7=BB=B4=E6=8A=A4=EF=BC=88blade=5Fcontract=5Fexe=5Fstage?=
=?UTF-8?q?=E8=A1=A8/=E8=A1=8C=E7=BA=A7=E5=B7=AE=E5=BC=82=E6=A0=87?=
=?UTF-8?q?=E8=AE=B0=E7=BB=B4=E6=8A=A4=E6=8E=A5=E5=8F=A3/=E5=90=88?=
=?UTF-8?q?=E5=90=8C=E5=AE=A1=E6=89=B9=E7=BD=AE=E8=A1=8C=E5=BE=85=E5=90=8C?=
=?UTF-8?q?=E6=AD=A5=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/HcsContractExeStage.java | 101 ++++++++++++
.../ContractExeStageController.java | 91 +++++++++++
.../mapper/HcsContractExeStageMapper.java | 53 ++++++
.../service/IContractExeStageService.java | 64 ++++++++
.../impl/ContractExeStageServiceImpl.java | 154 ++++++++++++++++++
.../impl/ContractManageServiceImpl.java | 13 ++
.../blade_hcs_exe_stage_20260927.sql | 36 ++++
7 files changed, 512 insertions(+)
create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractExeStage.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractExeStageController.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractExeStageMapper.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractExeStageServiceImpl.java
create mode 100644 doc/sql/transport/blade_hcs_exe_stage_20260927.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractExeStage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractExeStage.java
new file mode 100644
index 00000000..95bdb385
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsContractExeStage.java
@@ -0,0 +1,101 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 合同履约规则实体类(HCS 履约阶段,设计文档 §4.1,ADR-0002)
+ *
+ * 合同维度的资金计划阶段,C103-9 行级同步的数据源:合同审批通过置 row_op=add,
+ * 后续编辑按行差异置 add/update/delete,由扫描任务随合同链路同步;
+ * 合规返回的阶段ID 必须持久化(hcsStageFdId),行更新/删除全靠它。
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_contract_exe_stage")
+@Schema(description = "合同履约规则(HCS 履约阶段)")
+public class HcsContractExeStage extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "合同ID")
+ private Long contractId;
+
+ @Schema(description = "履约类型 payment=付款/collection=收款")
+ private String performType;
+
+ @Schema(description = "款项性质 prepayment/progress/acceptance/retention")
+ private String fundNature;
+
+ @Schema(description = "计划金额(元)")
+ private BigDecimal fundAmount;
+
+ @Schema(description = "计划资金日期")
+ private LocalDateTime fundDate;
+
+ @Schema(description = "开票规则(如 payFirst)")
+ private String invoiceRule;
+
+ @Schema(description = "账期")
+ private Integer invoiceCycle;
+
+ @Schema(description = "金额类型")
+ private String fundType;
+
+ @Schema(description = "是否完成")
+ private Integer isFinish;
+
+ @Schema(description = "行级同步操作 none/add/update/delete")
+ private String rowOp;
+
+ @Schema(description = "合规阶段ID(C103-9 返回,update/delete 必用)")
+ private String hcsStageFdId;
+
+ @Schema(description = "同步状态 unsynced/synced/failed")
+ private String hcsSyncStatus;
+
+ @Schema(description = "同步结果信息/失败原因")
+ @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
+ private String hcsSyncMsg;
+
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractExeStageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractExeStageController.java
new file mode 100644
index 00000000..17eae220
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractExeStageController.java
@@ -0,0 +1,91 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.controller;
+
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.springblade.core.tool.api.R;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+import org.springblade.transport.service.IContractExeStageService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 合同履约规则维护端点(合同表单"履约规则"页签,设计文档 §4.1,ADR-0002)
+ *
+ * 行级差异(add/update/delete)由本端点落库标记,行级同步由扫描任务随合同链路调 C103-9 完成;
+ * 行删除为软删+标记,不可物理删除。
+ *
+ * @author Chill
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("contract-exe-stage")
+@Tag(name = "合同履约规则", description = "合同履约规则维护(HCS 履约阶段同步数据源)")
+public class ContractExeStageController {
+
+ private final IContractExeStageService contractExeStageService;
+
+ /**
+ * 按合同查询履约规则行
+ */
+ @GetMapping("/list")
+ @ApiOperationSupport(order = 1)
+ @Operation(summary = "按合同查询履约规则", description = "传入合同ID,返回规则行列表")
+ public R> list(@Parameter(description = "合同ID", required = true) @RequestParam Long contractId) {
+ return R.data(contractExeStageService.listByContractId(contractId));
+ }
+
+ /**
+ * 保存履约规则行(整批提交,按行差异置同步标记)
+ */
+ @PostMapping("/submit")
+ @ApiOperationSupport(order = 2)
+ @Operation(summary = "保存履约规则", description = "传入合同ID与规则行列表,新行/未同步行标记 add、已同步行标记 update")
+ public R submit(@Parameter(description = "合同ID", required = true) @RequestParam Long contractId,
+ @RequestBody List rows) {
+ return R.status(contractExeStageService.submitRows(contractId, rows));
+ }
+
+ /**
+ * 删除履约规则行(软删+标记,已同步行待 C103-9 上报删除)
+ */
+ @PostMapping("/remove")
+ @ApiOperationSupport(order = 3)
+ @Operation(summary = "删除履约规则行", description = "传入规则行ID,软删并标记 delete 待同步")
+ public R remove(@Parameter(description = "规则行ID", required = true) @RequestParam Long id) {
+ return R.status(contractExeStageService.removeRow(id));
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractExeStageMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractExeStageMapper.java
new file mode 100644
index 00000000..c2021347
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsContractExeStageMapper.java
@@ -0,0 +1,53 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+
+import java.util.List;
+
+/**
+ * 合同履约规则 Mapper
+ *
+ * @author Chill
+ */
+@Mapper
+public interface HcsContractExeStageMapper extends BaseMapper {
+
+ /**
+ * 查询合同的待同步行(含软删行:C103-9 的 delete 行已逻辑删除但须随行级同步上报,
+ * 原生 SQL 绕过 @TableLogic 过滤)
+ *
+ * @param contractId 合同ID
+ * @return 行级同步操作非 none 的规则行
+ */
+ @Select("select * from blade_contract_exe_stage where contract_id = #{contractId} and row_op <> 'none' order by id")
+ List selectSyncRows(@Param("contractId") Long contractId);
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java
new file mode 100644
index 00000000..15133197
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java
@@ -0,0 +1,64 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.service;
+
+import org.springblade.core.mp.base.BaseService;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+
+import java.util.List;
+
+/**
+ * 合同履约规则 服务接口
+ *
+ * @author Chill
+ */
+public interface IContractExeStageService extends BaseService {
+
+ /**
+ * 按合同查询履约规则行(表单维护用,不含软删行)
+ *
+ * @param contractId 合同ID
+ * @return 规则行列表
+ */
+ List listByContractId(Long contractId);
+
+ /**
+ * 保存合同履约规则行(按行差异置同步标记:新行/未同步行 add,已同步行 update)
+ *
+ * @param contractId 合同ID
+ * @param rows 规则行
+ * @return 保存结果
+ */
+ boolean submitRows(Long contractId, List rows);
+
+ /**
+ * 删除履约规则行(软删+标记:已同步行置 row_op=delete 待 C103-9 上报;未同步行直接软删)
+ *
+ * @param id 规则行ID
+ * @return 删除结果
+ */
+ boolean removeRow(Long id);
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractExeStageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractExeStageServiceImpl.java
new file mode 100644
index 00000000..08f0cc06
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractExeStageServiceImpl.java
@@ -0,0 +1,154 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.service.impl;
+
+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.mp.base.BaseServiceImpl;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+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.springblade.transport.service.IContractExeStageService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 合同履约规则 服务实现类(设计文档 §4.1,ADR-0002)
+ *
+ * 行级差异标记:新行/未同步行 add;已同步行编辑 update;已同步行删除 delete(软删+标记,不可物理删除)。
+ * 行级同步由扫描任务随合同链路调 C103-9 完成(HcsExeStageSyncSupport)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ContractExeStageServiceImpl extends BaseServiceImpl implements IContractExeStageService {
+
+ /**
+ * 行级同步操作:无
+ */
+ private static final String ROW_OP_NONE = "none";
+
+ private final ContractManageMapper contractManageMapper;
+ private final HcsProperties hcsProperties;
+
+ @Override
+ public List listByContractId(Long contractId) {
+ return list(Wrappers.lambdaQuery()
+ .eq(HcsContractExeStage::getContractId, contractId)
+ .orderByAsc(HcsContractExeStage::getId));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean submitRows(Long contractId, List rows) {
+ ContractManage contractManage = contractManageMapper.selectById(contractId);
+ if (contractManage == null) {
+ throw new ServiceException("合同不存在:" + contractId);
+ }
+ if (Func.isEmpty(rows)) {
+ throw new ServiceException("履约规则行不能为空");
+ }
+ for (HcsContractExeStage row : rows) {
+ validateRow(row);
+ row.setContractId(contractId);
+ if (row.getId() == null) {
+ // 新行:待随合同链路首推(row_op=add)
+ row.setRowOp("add");
+ row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ row.setHcsSyncMsg("待随合同链路同步(C103-9)");
+ row.setFundType(StringUtil.isBlank(row.getFundType()) ? hcsProperties.getExeStage().getFundType() : row.getFundType());
+ row.setIsFinish(row.getIsFinish() == null ? 0 : row.getIsFinish());
+ save(row);
+ } else {
+ HcsContractExeStage existing = getById(row.getId());
+ if (existing == null || !contractId.equals(existing.getContractId())) {
+ throw new ServiceException("履约规则行不属于该合同:" + row.getId());
+ }
+ // 已同步行编辑 → update;未同步行保持 add
+ String rowOp = StringUtil.isBlank(existing.getHcsStageFdId()) ? "add" : "update";
+ existing.setPerformType(row.getPerformType());
+ existing.setFundNature(row.getFundNature());
+ existing.setFundAmount(row.getFundAmount());
+ existing.setFundDate(row.getFundDate());
+ existing.setInvoiceRule(row.getInvoiceRule());
+ existing.setInvoiceCycle(row.getInvoiceCycle());
+ existing.setFundType(StringUtil.isBlank(row.getFundType()) ? hcsProperties.getExeStage().getFundType() : row.getFundType());
+ existing.setIsFinish(row.getIsFinish() == null ? 0 : row.getIsFinish());
+ existing.setRowOp(rowOp);
+ existing.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ existing.setHcsSyncMsg("待随合同链路同步(C103-9 " + rowOp + ")");
+ updateById(existing);
+ }
+ }
+ log.info("合同履约规则保存:contractId={},共 {} 行", contractId, rows.size());
+ return true;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean removeRow(Long id) {
+ HcsContractExeStage row = getById(id);
+ if (row == null) {
+ throw new ServiceException("履约规则行不存在:" + id);
+ }
+ if (StringUtil.isNotBlank(row.getHcsStageFdId())) {
+ // 已同步行:软删+标记 delete,由 C103-9 凭合规阶段ID 上报删除(ADR-0002,不可物理删除)
+ row.setRowOp("delete");
+ row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ row.setHcsSyncMsg("待随合同链路同步(C103-9 delete)");
+ updateById(row);
+ }
+ removeById(id);
+ log.info("合同履约规则行删除:id={},contractId={},rowOp={}", id, row.getContractId(), row.getRowOp());
+ return true;
+ }
+
+ /**
+ * 行字段校验
+ */
+ private void validateRow(HcsContractExeStage row) {
+ if (StringUtil.isBlank(row.getPerformType())) {
+ throw new ServiceException("履约类型不能为空(payment/collection)");
+ }
+ if (!List.of("payment", "collection").contains(row.getPerformType())) {
+ throw new ServiceException("履约类型非法:" + row.getPerformType());
+ }
+ if (row.getFundAmount() != null && row.getFundAmount().signum() < 0) {
+ throw new ServiceException("计划金额不能为负数");
+ }
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index b6cf8840..41cc4285 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -25,6 +25,8 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
@@ -36,7 +38,9 @@ import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.ContractManageExcel;
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.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.TransportBusinessSupport;
@@ -62,9 +66,13 @@ import java.util.Set;
*
* @author Chill
*/
+@Slf4j
@Service
+@RequiredArgsConstructor
public class ContractManageServiceImpl extends BaseServiceImpl implements IContractManageService {
+ private final HcsContractExeStageMapper hcsContractExeStageMapper;
+
private static final String STAGE_DRAFT = "draft";
private static final String STAGE_TEMPORARY = "temporary";
private static final String STAGE_FORMAL = "formal";
@@ -217,6 +225,11 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaUpdate()
+ .eq(HcsContractExeStage::getContractId, contractManage.getId())
+ .isNull(HcsContractExeStage::getHcsStageFdId)
+ .set(HcsContractExeStage::getRowOp, "add"));
return updateById(contractManage);
}
diff --git a/doc/sql/transport/blade_hcs_exe_stage_20260927.sql b/doc/sql/transport/blade_hcs_exe_stage_20260927.sql
new file mode 100644
index 00000000..97b3b811
--- /dev/null
+++ b/doc/sql/transport/blade_hcs_exe_stage_20260927.sql
@@ -0,0 +1,36 @@
+-- 桂物合同合规系统(HCS)接入:履约规则表(设计文档 §4.1,ADR-0002,P2)。
+-- 合同维度资金计划阶段,C103-9 行级同步的数据源;行删除为软删+标记,不可物理删除。
+-- 通过 information_schema 判断表是否存在,可重复执行。
+SET @db_name = DATABASE();
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = @db_name AND table_name = 'blade_contract_exe_stage'),
+ 'SELECT 1',
+ 'CREATE TABLE blade_contract_exe_stage (
+ id bigint NOT NULL COMMENT ''主键'',
+ contract_id bigint NOT NULL COMMENT ''合同ID(blade_contract_manage.id)'',
+ perform_type varchar(16) NOT NULL COMMENT ''履约类型 payment=付款/collection=收款'',
+ fund_nature varchar(16) DEFAULT NULL COMMENT ''款项性质 prepayment/progress/acceptance/retention'',
+ fund_amount decimal(18,2) DEFAULT NULL COMMENT ''计划金额(元)'',
+ fund_date datetime DEFAULT NULL COMMENT ''计划资金日期'',
+ invoice_rule varchar(16) DEFAULT NULL COMMENT ''开票规则(如 payFirst)'',
+ invoice_cycle int DEFAULT NULL COMMENT ''账期'',
+ fund_type varchar(16) DEFAULT ''byAmount'' COMMENT ''金额类型'',
+ is_finish tinyint DEFAULT 0 COMMENT ''是否完成'',
+ row_op varchar(8) DEFAULT ''none'' COMMENT ''行级同步操作 none/add/update/delete'',
+ hcs_stage_fd_id varchar(64) DEFAULT NULL COMMENT ''合规阶段ID(C103-9 返回,update/delete 必用)'',
+ hcs_sync_status varchar(16) DEFAULT ''unsynced'' COMMENT ''同步状态 unsynced/synced/failed'',
+ hcs_sync_msg varchar(512) DEFAULT NULL COMMENT ''同步结果信息/失败原因'',
+ tenant_id varchar(12) DEFAULT ''000000'' COMMENT ''租户ID'',
+ create_user bigint DEFAULT NULL COMMENT ''创建人'',
+ create_dept bigint DEFAULT NULL COMMENT ''创建部门'',
+ create_time datetime DEFAULT NULL COMMENT ''创建时间'',
+ update_user bigint DEFAULT NULL COMMENT ''更新人'',
+ update_time datetime DEFAULT NULL COMMENT ''更新时间'',
+ status int DEFAULT 1 COMMENT ''业务状态'',
+ is_deleted int DEFAULT 0 COMMENT ''是否已删除'',
+ PRIMARY KEY (id),
+ KEY idx_hcs_exe_stage_contract (contract_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''合同履约规则(HCS 履约阶段)'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
From d147ef82091425c3fb26477eac0131d1f49f595f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:35:20 +0800
Subject: [PATCH 009/124] =?UTF-8?q?feat:=20HCS=E5=B1=A5=E7=BA=A6=E8=A7=84?=
=?UTF-8?q?=E5=88=99C103-9=E8=A1=8C=E7=BA=A7=E5=90=8C=E6=AD=A5=EF=BC=88?=
=?UTF-8?q?=E9=9A=8F=E5=90=88=E5=90=8C=E9=93=BE=E8=B7=AF=E6=89=A7=E8=A1=8C?=
=?UTF-8?q?/=E8=A1=8C=E7=BA=A7=E5=A2=9E=E5=88=A0=E6=94=B9=E8=81=9A?=
=?UTF-8?q?=E5=90=88/=E5=B1=A5=E7=BA=A6=E8=AE=A1=E5=88=92ID=E4=B8=8E?=
=?UTF-8?q?=E9=98=B6=E6=AE=B5fdId=E5=9B=9E=E5=A1=AB=E6=8C=81=E4=B9=85?=
=?UTF-8?q?=E5=8C=96=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsContractSyncSupport.java | 25 +-
.../support/hcs/HcsExeStageSyncSupport.java | 271 ++++++++++++++++++
.../transport/task/HcsPushTask.java | 2 +-
3 files changed, 288 insertions(+), 10 deletions(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 4b06fcf8..471f5004 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -70,7 +70,7 @@ import java.util.Objects;
*
* 扫描任务按序执行:① 客商风险校验(可配开关,黑名单阻断)→ ② 附件转传(挂载点待 §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;
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
new file mode 100644
index 00000000..301c385c
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
@@ -0,0 +1,271 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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)
+ *
+ * 随合同推送链路执行(合同须已同步、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 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 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 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 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 并清除行级操作标记
+ *
+ * TODO 待联调确认:fdChangeStages 与请求行的对应关系 v4 未明确,优先按请求行序对应(首次全 add 场景),
+ * 行数不一致时按业务属性组合键匹配,仍匹配不到的 add 行保持待同步(下轮重试)。
+ */
+ private void backfill(ContractManage contractManage, List pendingRows,
+ List requestStages, HcsExeplanVO exeplan) {
+ // 履约计划ID 回填(后续行更新/删除依赖,必须持久化)
+ if (StringUtil.isNotBlank(exeplan.getFdExeplanId())
+ && !exeplan.getFdExeplanId().equals(contractManage.getHcsExeplanId())) {
+ contractManage.setHcsExeplanId(exeplan.getFdExeplanId());
+ }
+ backfillExeplanId(contractManage);
+ List changeStages = exeplan.getFdChangeStages() == null ? List.of() : exeplan.getFdChangeStages();
+ // 行数一致时按请求行序一一对应;不一致时按业务属性组合键匹配
+ boolean positional = changeStages.size() == requestStages.size();
+ Map 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 mapByAttribute(List changeStages) {
+ Map 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();
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
index 896a3d21..6d35899c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -51,7 +51,7 @@ import java.util.UUID;
*
* 审批代码零埋点:扫描只认 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)复用。
*
From 594773d7c7b345f9f1faad6a5d8826821e792d91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:38:55 +0800
Subject: [PATCH 010/124] =?UTF-8?q?feat:=20HCS=E4=BB=98=E6=AC=BE=E7=94=B3?=
=?UTF-8?q?=E8=AF=B7=E6=95=B0=E6=8D=AE=E5=B1=82=E4=B8=8E=E9=9B=B6=E5=9F=8B?=
=?UTF-8?q?=E7=82=B9=E6=8E=A5=E7=BA=BF=EF=BC=88=E7=89=B9=E6=89=B9=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5=E7=BB=84/=E5=B1=A5=E7=BA=A6=E8=AE=B0=E5=BD=95ID?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5/=E5=AE=A1=E6=89=B9=E4=B8=8E=E4=BB=98?=
=?UTF-8?q?=E6=AC=BE=E6=8E=A8=E8=BF=9B=E7=BD=AE=E5=BE=85=E5=90=8C=E6=AD=A5?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/PaymentApplication.java | 23 +++++++++
.../transport/constant/HcsSyncConstant.java | 49 +++++++++++++++++++
.../transport/mk/MkProcessBizHandlers.java | 2 +
.../impl/PaymentApplicationServiceImpl.java | 7 +++
.../blade_hcs_payment_fields_20260927.sql | 48 ++++++++++++++++++
5 files changed, 129 insertions(+)
create mode 100644 doc/sql/transport/blade_hcs_payment_fields_20260927.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
index 45ab1f4c..e4021397 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
@@ -33,6 +33,7 @@ import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.LocalDateTime;
/** 付款申请实体。 @author Chill */
@Data
@@ -120,4 +121,26 @@ public class PaymentApplication extends TenantEntity {
private String attachmentsJson;
/** 备注 */
private String remark;
+ /** 合规系统单据ID(fdId) */
+ private String hcsFdId;
+ /** 合规同步状态:unsynced 待同步 / synced 已同步 / failed 同步失败 */
+ private String hcsSyncStatus;
+ /** 合规推送失败重试次数 */
+ private Integer hcsRetryCount;
+ /** 合规最近同步时间 */
+ private LocalDateTime hcsSyncTime;
+ /** 合规最近同步结果信息/失败原因 */
+ private String hcsSyncMsg;
+ /** 额度校验结果:pass 通过 / over 超额(C106-1) */
+ private String hcsCreditStatus;
+ /** 额度校验说明(超额原因 fdMessage) */
+ private String hcsCreditMsg;
+ /** 额度校验时间 */
+ private LocalDateTime hcsCreditTime;
+ /** 结算付款特批流程ID(C106-3 返回,C106-4 必传) */
+ private String hcsSpecFdId;
+ /** 特批状态:none/need/pushing/processing/approved/rejected */
+ private String hcsSpecStatus;
+ /** 履约记录ID(C108-1 返回,C108-2 更新必传) */
+ private String hcsPerfFdId;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index ad7238fb..6e128731 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -59,6 +59,11 @@ public final class HcsSyncConstant {
*/
public static final String BIZ_TYPE_CONTRACT = "contract";
+ /**
+ * 推送业务类型:付款申请(C106 特批 / C108 履约记录)
+ */
+ public static final String BIZ_TYPE_PAYMENT = "payment";
+
/**
* 校验类型:客商风险(C101-1)
*/
@@ -84,6 +89,50 @@ public final class HcsSyncConstant {
*/
public static final String CHECK_RESULT_SKIP = "skip";
+ // ==================== 额度校验结果(C106-1,D15) ====================
+
+ /**
+ * 额度校验通过
+ */
+ public static final String CREDIT_STATUS_PASS = "pass";
+
+ /**
+ * 额度校验超额(不拦截提交,转入特批链路)
+ */
+ public static final String CREDIT_STATUS_OVER = "over";
+
+ // ==================== 特批状态(hcs_spec_status,D15) ====================
+
+ /**
+ * 特批状态:无需特批
+ */
+ public static final String SPEC_STATUS_NONE = "none";
+
+ /**
+ * 特批状态:待特批(额度超额标记,扫描任务据此发起 C106-3)
+ */
+ public static final String SPEC_STATUS_NEED = "need";
+
+ /**
+ * 特批状态:发起中
+ */
+ public static final String SPEC_STATUS_PUSHING = "pushing";
+
+ /**
+ * 特批状态:特批中(C106-3 已发起,结果人工确认,§9-U1)
+ */
+ public static final String SPEC_STATUS_PROCESSING = "processing";
+
+ /**
+ * 特批状态:特批通过(人工确认)
+ */
+ public static final String SPEC_STATUS_APPROVED = "approved";
+
+ /**
+ * 特批状态:特批驳回(人工确认)
+ */
+ public static final String SPEC_STATUS_REJECTED = "rejected";
+
// ==================== Redis 缓存 key(照 BFI blade:third: 前缀惯例) ====================
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
index 6c9bce09..d82c820f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
@@ -426,6 +426,8 @@ public final class MkProcessBizHandlers {
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
+ // HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
+ entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 38ea696e..0ca877da 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -47,6 +47,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.BillLedgerMapper;
@@ -350,6 +351,8 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核");
if (isBillPayment(entity.getPaymentMethod())) useBillBalance(entity);
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
+ // HCS 推送零埋点:审批通过仅置待同步状态,扫描任务据此推特批(如需)与履约记录(ADR-0001)
+ entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
updateById(entity);
refreshFormalSettlement(entity);
}
@@ -611,6 +614,10 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
} else if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
}
+ // HCS 履约记录推进:实际付款变化后置待同步,扫描任务凭 hcs_perf_fd_id 走 C108-2 更新(D11)
+ if (entity.getHcsPerfFdId() != null) {
+ entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ }
updateById(entity);
if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
// 付款完成:联动履约付款数据(预结算预付记录、正式结算付款记录)
diff --git a/doc/sql/transport/blade_hcs_payment_fields_20260927.sql b/doc/sql/transport/blade_hcs_payment_fields_20260927.sql
new file mode 100644
index 00000000..5de893b4
--- /dev/null
+++ b/doc/sql/transport/blade_hcs_payment_fields_20260927.sql
@@ -0,0 +1,48 @@
+-- 桂物合同合规系统(HCS)接入:付款申请特批字段组与履约记录字段(设计文档 §4,P3)。
+-- blade_payment_application 追加额度校验结果、特批流程ID/状态、履约记录ID 字段。
+-- 通过 information_schema 判断字段是否存在,可重复执行。
+SET @db_name = DATABASE();
+
+-- ==================== blade_payment_application 扩展字段(C106-1 结果、C106-3/4 特批、C108 履约记录) ====================
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_payment_application' AND column_name = 'hcs_credit_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_credit_status varchar(16) DEFAULT NULL COMMENT ''额度校验结果 pass=通过/over=超额(C106-1)'''
+);
+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_payment_application' AND column_name = 'hcs_credit_msg'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_credit_msg varchar(512) DEFAULT NULL COMMENT ''额度校验说明(超额原因 fdMessage)'''
+);
+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_payment_application' AND column_name = 'hcs_credit_time'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_credit_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_payment_application' AND column_name = 'hcs_spec_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_spec_fd_id varchar(64) DEFAULT NULL COMMENT ''结算付款特批流程ID(C106-3 返回,C106-4 必传)'''
+);
+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_payment_application' AND column_name = 'hcs_spec_status'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_spec_status varchar(16) DEFAULT ''none'' COMMENT ''特批状态 none/need/pushing/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_payment_application' AND column_name = 'hcs_perf_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_perf_fd_id varchar(64) DEFAULT NULL COMMENT ''履约记录ID(C108-1 返回,C108-2 更新必传)'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
From cb54b569cd2a34ad8ef56fc442334e001d8d1472 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:41:56 +0800
Subject: [PATCH 011/124] =?UTF-8?q?feat:=20HCS=E4=BB=98=E6=AC=BE=E6=8F=90?=
=?UTF-8?q?=E4=BA=A4=E5=90=8C=E6=AD=A5=E6=A0=A1=E9=AA=8C=EF=BC=88C101-1?=
=?UTF-8?q?=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9=E9=BB=91=E5=90=8D=E5=8D=95?=
=?UTF-8?q?=E9=98=BB=E6=96=AD/C106-1=E9=A2=9D=E5=BA=A6=E6=A0=A1=E9=AA=8C?=
=?UTF-8?q?=E8=B6=85=E9=A2=9D=E8=BD=AC=E7=89=B9=E6=89=B9/=E6=A0=A1?=
=?UTF-8?q?=E9=AA=8C=E7=95=99=E7=97=95check=5Flog=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/PaymentApplicationServiceImpl.java | 4 +
.../support/hcs/HcsCreditCheckSupport.java | 203 ++++++++++++++++++
2 files changed, 207 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 0ca877da..a52fcc28 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -95,6 +95,7 @@ import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
+import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -145,6 +146,7 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
private final IBfiService bfiService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
private final BfiProperties bfiProperties;
+ private final HcsCreditCheckSupport hcsCreditCheckSupport;
@Override
public IPage selectPage(IPage page, PaymentApplicationVO query) {
@@ -323,6 +325,8 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
validateProjectAdvanceContract(entity);
validateSelectedBill(entity, false);
validateQuota(entity);
+ // HCS 提交同步校验(事务内查询类同步,D15/R2-Q3):C101-1 风险(黑名单阻断)+ C106-1 额度(超额转特批)
+ hcsCreditCheckSupport.checkOnSubmit(entity);
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
new file mode 100644
index 00000000..42605ab7
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -0,0 +1,203 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+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.config.HcsProperties;
+import org.springblade.hcs.dto.HcsCreditCheckDTO;
+import org.springblade.hcs.dto.HcsCreditCheckVO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsRiskQueryDTO;
+import org.springblade.hcs.dto.HcsRiskVO;
+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.CustomerArchiveMapper;
+import org.springblade.transport.mapper.HcsContractCheckLogMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.CustomerArchive;
+import org.springblade.transport.pojo.entity.HcsContractCheckLog;
+import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3)
+ *
+ * 付款申请提交(submit)事务内同步调用(查询类同步):
+ * ① C101-1 按收款方客商编号查风险,黑名单且 block-blacklist=true 时阻断提交;
+ * ② C106-1 校验申请金额是否超出合同额度,超额不拦截提交,置 hcs_credit_status=over 并标记
+ * hcs_spec_status=need 转特批链路(D15/R2-Q2)。
+ * 校验依赖合同已推送(hcs_fd_id 存在),未推送时按 credit.require-contract-synced 配置阻断或跳过。
+ * 结果留痕 blade_hcs_check_log(biz_type=payment)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsCreditCheckSupport {
+
+ /**
+ * C101-1 黑名单状态取值(示例 no 表示非黑名单,§9-U3 字典值联调确认)
+ */
+ private static final String BLACK_STATUS_YES = "yes";
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final ContractManageMapper contractManageMapper;
+ private final CustomerArchiveMapper customerArchiveMapper;
+ private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
+
+ /**
+ * 付款提交同步校验入口(submit 事务内调用,校验结果字段随提交一并落库)
+ *
+ * @param payment 付款申请单(须含合同与收款方信息)
+ */
+ public void checkOnSubmit(PaymentApplication payment) {
+ // ① 客商风险校验(可配开关),黑名单阻断提交
+ if (hcsProperties.getRisk().isCheckOnPayment()) {
+ checkRelativeRisk(payment);
+ }
+ // ② 额度校验(超额不拦截,转特批)
+ checkCredit(payment);
+ }
+
+ /**
+ * C101-1 收款方客商风险校验:黑名单且 block-blacklist=true 时阻断提交
+ */
+ private void checkRelativeRisk(PaymentApplication payment) {
+ String customerCode = queryCustomerCode(payment.getPayeeName());
+ if (StringUtil.isBlank(customerCode)) {
+ writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP,
+ "收款方[" + payment.getPayeeName() + "]反查不到客商编号,跳过风险校验(§9-U6)", null);
+ log.warn("HCS付款提交风险校验跳过:paymentNo={},收款方反查不到客商编号", payment.getPaymentNo());
+ return;
+ }
+ HcsRiskQueryDTO param = new HcsRiskQueryDTO();
+ // 【必填】客商编号:按收款方名称反查 CustomerArchive 客商编码(用哪个编码待联调,§9-U6)
+ param.setFdNumber(customerCode);
+ HcsResult result = hcsClient.getRelativeRisk(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS客商风险查询失败响应:{},paymentNo={},fdNumber={}",
+ result == null ? "无响应" : result.getMsg(), payment.getPaymentNo(), 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);
+ writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
+ "收款方[" + payment.getPayeeName() + "]黑名单=" + blackStatus + ",风险等级=" + (risk == null ? null : risk.getFdRiskLevel()),
+ result.getTraceId());
+ if (blacklist && hcsProperties.getRisk().isBlockBlacklist()) {
+ log.error("HCS付款提交被风险校验阻断:paymentNo={},收款方[{}]黑名单", payment.getPaymentNo(), payment.getPayeeName());
+ throw new ServiceException("收款方客商黑名单,禁止提交付款申请");
+ }
+ }
+
+ /**
+ * C106-1 额度校验:通过置 pass;超额置 over 并标记待特批(不拦截提交,D15/R2-Q2)
+ */
+ private void checkCredit(PaymentApplication payment) {
+ ContractManage contractManage = payment.getContractId() == null ? null : contractManageMapper.selectById(payment.getContractId());
+ if (contractManage == null || StringUtil.isBlank(contractManage.getHcsFdId())) {
+ if (hcsProperties.getCredit().isRequireContractSynced()) {
+ throw new ServiceException("关联合同尚未推送合规,无法校验额度(paymentNo=" + payment.getPaymentNo() + ")");
+ }
+ log.warn("HCS付款提交额度校验跳过:paymentNo={},合同未推送合规(require-contract-synced=false)", payment.getPaymentNo());
+ writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_SKIP, "合同未推送合规,跳过额度校验", null);
+ return;
+ }
+ HcsCreditCheckDTO param = new HcsCreditCheckDTO();
+ // 【必填】合同ID:HCS 合同ID(blade_contract_manage.hcs_fd_id)
+ param.setFdContractId(contractManage.getHcsFdId());
+ // 【必填】申请金额:PaymentApplication.appliedAmount(元)
+ param.setFdApplyAmount(payment.getAppliedAmount() == null ? BigDecimal.ZERO : payment.getAppliedAmount());
+ HcsResult result = hcsClient.contractCreditCheck(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS额度校验失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ throw new HcsException("C106-1 额度校验失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ HcsCreditCheckVO credit = result.getData();
+ payment.setHcsCreditTime(LocalDateTime.now());
+ if (credit != null && Boolean.TRUE.equals(credit.getFdIsCredit())) {
+ // 额度通过:正常提交
+ payment.setHcsCreditStatus(HcsSyncConstant.CREDIT_STATUS_PASS);
+ payment.setHcsCreditMsg(null);
+ writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_PASS, "额度校验通过", result.getTraceId());
+ return;
+ }
+ // 超额:success 仍为 true,必须以 fdIsCredit 为准;不拦截提交,转入特批链路
+ String message = credit == null ? "响应数据为空" : credit.getFdMessage();
+ payment.setHcsCreditStatus(HcsSyncConstant.CREDIT_STATUS_OVER);
+ payment.setHcsCreditMsg(message);
+ payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
+ writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_FAIL, "额度超额:" + message, result.getTraceId());
+ log.warn("HCS付款提交额度超额(转特批):paymentNo={},{}", payment.getPaymentNo(), message);
+ }
+
+ /**
+ * 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
+ */
+ private String queryCustomerCode(String customerName) {
+ if (StringUtil.isBlank(customerName)) {
+ return null;
+ }
+ CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.lambdaQuery()
+ .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, customerName).or().eq(CustomerArchive::getShortName, customerName))
+ .eq(CustomerArchive::getIsDeleted, 0)
+ .last("limit 1"));
+ return customerArchive == null ? null : customerArchive.getCustomerCode();
+ }
+
+ /**
+ * 校验留痕(blade_hcs_check_log,异常不影响主流程)
+ */
+ private void writeCheckLog(PaymentApplication payment, String checkType, String result, String msg, String traceId) {
+ try {
+ HcsContractCheckLog checkLog = new HcsContractCheckLog();
+ checkLog.setTenantId(payment.getTenantId());
+ checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_PAYMENT);
+ checkLog.setBizId(payment.getId());
+ checkLog.setCheckType(checkType);
+ checkLog.setResult(result);
+ checkLog.setMsg(msg);
+ checkLog.setTraceId(traceId);
+ checkLog.setCreateTime(LocalDateTime.now());
+ hcsContractCheckLogMapper.insert(checkLog);
+ } catch (Exception exception) {
+ log.error("HCS付款校验留痕写入失败:paymentNo={}", payment.getPaymentNo(), exception);
+ }
+ }
+}
From d6561dda17ccd2f4318501d90dbe2be15620a752 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:47:36 +0800
Subject: [PATCH 012/124] =?UTF-8?q?feat:=20HCS=E4=BB=98=E6=AC=BE=E9=93=BE?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=EF=BC=88C106-3/4=E7=BB=93=E7=AE=97=E4=BB=98?=
=?UTF-8?q?=E6=AC=BE=E7=89=B9=E6=89=B9/C108-1/2=E5=B1=A5=E7=BA=A6=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95/=E6=89=AB=E6=8F=8F=E7=BC=96=E6=8E=92=E4=B8=8E?=
=?UTF-8?q?=E6=89=8B=E5=8A=A8=E7=AB=AF=E7=82=B9/=E7=89=B9=E6=89=B9?=
=?UTF-8?q?=E7=BB=93=E6=9E=9C=E4=BA=BA=E5=B7=A5=E7=A1=AE=E8=AE=A4=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/HcsSyncController.java | 26 +-
.../support/hcs/HcsPaymentSyncSupport.java | 156 ++++++++++++
.../support/hcs/HcsPerfRecordSyncSupport.java | 230 ++++++++++++++++++
.../support/hcs/HcsSpecPaySyncSupport.java | 229 +++++++++++++++++
.../transport/task/HcsPushTask.java | 43 +++-
.../springblade/hcs/config/HcsProperties.java | 12 +
doc/nacos/third-party-api.yaml | 4 +
7 files changed, 692 insertions(+), 8 deletions(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 8392512f..f10871d1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -34,11 +34,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.api.R;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
import org.springblade.transport.support.hcs.HcsMasterDataSupport;
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
import org.springblade.transport.task.HcsPushTask;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -61,44 +63,58 @@ public class HcsSyncController {
private final HcsPushTask hcsPushTask;
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
+ private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
private final HcsMasterDataSupport hcsMasterDataSupport;
/**
- * 单条重推(bizType:project=项目立项 / contract=合同)
+ * 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请)
*/
@PostMapping("/sync/{bizType}/{id}")
@ApiOperationSupport(order = 1)
- @Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project=项目立项、contract=合同")
+ @Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project=项目立项、contract=合同、payment=付款申请")
public R repush(@Parameter(description = "业务类型", required = true) @PathVariable String bizType,
@Parameter(description = "单据ID", required = true) @PathVariable Long id) {
String result = switch (bizType) {
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsProjectSyncSupport.repushProject(id);
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsContractSyncSupport.repushContract(id);
+ case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPaymentSyncSupport.repushPayment(id);
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
};
return R.data(result);
}
/**
- * 批量推送(bizType:project=项目立项 / contract=合同;复用定时扫描逻辑,单轮批量上限 push.batch-size)
+ * 批量推送(bizType:project=项目立项 / contract=合同 / payment=付款申请;复用定时扫描逻辑,单轮批量上限 push.batch-size)
*/
@PostMapping("/sync/{bizType}")
@ApiOperationSupport(order = 2)
- @Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project=项目立项、contract=合同")
+ @Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project=项目立项、contract=合同、payment=付款申请")
public R pushBatch(@Parameter(description = "业务类型", required = true) @PathVariable String bizType) {
String result = switch (bizType) {
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects();
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts();
+ case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPushTask.pushUnsyncedPayments();
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
};
return R.data(result);
}
+ /**
+ * 特批结果人工确认(§9-U1 过渡方案:v4 无结果查询/回调接口)
+ */
+ @PostMapping("/spec-pay/confirm")
+ @ApiOperationSupport(order = 3)
+ @Operation(summary = "特批结果人工确认", description = "传入付款申请ID与结果(approved/rejected),仅特批中(processing)允许确认")
+ public R confirmSpecResult(@Parameter(description = "付款申请ID", required = true) @RequestParam Long id,
+ @Parameter(description = "确认结果 approved/rejected", required = true) @RequestParam String result) {
+ return R.data(hcsPaymentSyncSupport.confirmSpecResult(id, result));
+ }
+
/**
* 刷新主数据缓存(C103-6 模板 / C103-7 分类 / C103-12 币种)
*/
@PostMapping("/master-data/refresh")
- @ApiOperationSupport(order = 3)
+ @ApiOperationSupport(order = 4)
@Operation(summary = "刷新HCS主数据缓存", description = "全量重拉模板/分类/币种列表并缓存,同时校验模板映射配置")
public R refreshMasterData() {
return R.data(hcsMasterDataSupport.refreshAll());
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
new file mode 100644
index 00000000..8fbb1062
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
@@ -0,0 +1,156 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+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.transport.constant.HcsSyncConstant;
+import org.springblade.transport.mapper.PaymentApplicationMapper;
+import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约记录 C108-1/2,设计文档 §5.0/§5.5/§5.6)
+ *
+ * 编排顺序:特批(额度超额标记 need 时发起)→ 履约记录(审批通过新建 / 实际付款推进更新)。
+ * 特批流程ID 与履约记录ID 在各自 Support 内即时持久化;整体同步状态(hcs_sync_status)由本组件统一回写。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsPaymentSyncSupport {
+
+ /**
+ * 审批通过状态集合
+ */
+ private static final List APPROVED_STATUSES = List.of("approved");
+
+ private final PaymentApplicationMapper paymentApplicationMapper;
+ private final HcsSpecPaySyncSupport hcsSpecPaySyncSupport;
+ private final HcsPerfRecordSyncSupport hcsPerfRecordSyncSupport;
+
+ /**
+ * 推送付款申请到 HCS:特批(如需)→ 履约记录(新建/更新)→ 回填整体同步状态
+ *
+ * @param payment 付款申请单(approvalStatus=approved)
+ * @return 推送结果描述
+ */
+ public String pushPayment(PaymentApplication payment) {
+ StringBuilder result = new StringBuilder();
+ // ① 结算付款特批:提交时额度超额(hcs_spec_status=need)且未发起时发起 C106-3
+ if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) {
+ result.append(hcsSpecPaySyncSupport.pushSpecPay(payment));
+ }
+ // ② 履约记录:审批通过新建 C108-1 / 实际付款推进更新 C108-2
+ if (result.length() > 0) {
+ result.append(";");
+ }
+ result.append(hcsPerfRecordSyncSupport.pushPerfRecord(payment));
+ markSynced(payment, result.toString());
+ return result.toString();
+ }
+
+ /**
+ * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
+ *
+ * @param id 付款申请单ID
+ * @return 推送结果描述
+ */
+ public String repushPayment(Long id) {
+ PaymentApplication payment = paymentApplicationMapper.selectById(id);
+ if (payment == null) {
+ throw new ServiceException("付款申请单不存在:" + id);
+ }
+ if (!APPROVED_STATUSES.contains(payment.getApprovalStatus())) {
+ throw new ServiceException("仅审批通过的付款申请可推送合规(当前状态:" + payment.getApprovalStatus() + ")");
+ }
+ payment.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ payment.setHcsRetryCount(0);
+ paymentApplicationMapper.updateById(payment);
+ try {
+ return pushPayment(payment);
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ markFailed(payment, message);
+ throw new ServiceException("HCS付款申请推送失败:" + message);
+ }
+ }
+
+ /**
+ * 特批结果人工确认(§9-U1 过渡方案:v4 无结果查询/回调接口)
+ *
+ * @param id 付款申请单ID
+ * @param result confirmed approved / rejected
+ * @return 确认结果描述
+ */
+ public String confirmSpecResult(Long id, String result) {
+ PaymentApplication payment = paymentApplicationMapper.selectById(id);
+ if (payment == 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(payment.getHcsSpecStatus())) {
+ throw new ServiceException("仅特批中的付款申请允许确认结果(当前状态:" + payment.getHcsSpecStatus() + ")");
+ }
+ payment.setHcsSpecStatus(result);
+ paymentApplicationMapper.updateById(payment);
+ log.info("HCS特批结果人工确认:paymentNo={},specFdId={},结果={}", payment.getPaymentNo(), payment.getHcsSpecFdId(), result);
+ return "特批结果已确认:" + result;
+ }
+
+ /**
+ * 回填整体同步状态置 synced
+ */
+ public void markSynced(PaymentApplication payment, String message) {
+ payment.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
+ payment.setHcsRetryCount(0);
+ payment.setHcsSyncTime(LocalDateTime.now());
+ payment.setHcsSyncMsg(message);
+ paymentApplicationMapper.updateById(payment);
+ log.info("HCS付款申请推送成功:paymentNo={},{}", payment.getPaymentNo(), message);
+ }
+
+ /**
+ * 推送失败回写:failed + 重试次数+1 + 失败原因
+ */
+ public void markFailed(PaymentApplication payment, String message) {
+ payment.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
+ payment.setHcsRetryCount(payment.getHcsRetryCount() == null ? 1 : payment.getHcsRetryCount() + 1);
+ payment.setHcsSyncTime(LocalDateTime.now());
+ payment.setHcsSyncMsg(message);
+ paymentApplicationMapper.updateById(payment);
+ log.error("HCS付款申请推送失败:paymentNo={},retryCount={},{}", payment.getPaymentNo(), payment.getHcsRetryCount(), message);
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
new file mode 100644
index 00000000..1019a314
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
@@ -0,0 +1,230 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.HcsAttachmentRefDTO;
+import org.springblade.hcs.dto.HcsMechanismsDTO;
+import org.springblade.hcs.dto.HcsPerfRecordFormDataDTO;
+import org.springblade.hcs.dto.HcsPerfRecordPropsDTO;
+import org.springblade.hcs.dto.HcsPerfRecordSaveDTO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsUserRefVO;
+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.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.HcsContractExeStageMapper;
+import org.springblade.transport.mapper.PaymentApplicationMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.List;
+
+/**
+ * HCS 履约记录同步支撑组件(C108-1 新建 / C108-2 更新,设计文档 §5.6,D11)
+ *
+ * 付款申请为主线:审批通过 → C108-1 新增(返回 fdId 回填 hcs_perf_fd_id);
+ * 实际付款推进(paidAmount 变化置待同步)→ C108-2 更新(formData.fdId=hcs_perf_fd_id 必填,响应无 data)。
+ * 关联履约阶段:按合同+付款类型匹配已同步的履约规则行(付款/收款执行与计划阶段对应),
+ * 无关联则不传 fd_id(§5.6)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsPerfRecordSyncSupport {
+
+ /**
+ * 履约记录附件字段标识
+ */
+ private static final String PERF_ATTACH_ENTITY_KEY = "fd_plan_att";
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final ContractManageMapper contractManageMapper;
+ private final HcsContractExeStageMapper hcsContractExeStageMapper;
+ private final PaymentApplicationMapper paymentApplicationMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
+
+ /**
+ * 推送履约记录:无记录ID 走 C108-1 新增,有记录ID 走 C108-2 更新;记录ID 立即持久化
+ *
+ * @param payment 付款申请单(approvalStatus=approved)
+ * @return 推送结果描述
+ */
+ public String pushPerfRecord(PaymentApplication payment) {
+ String operatorLoginName = resolveLoginName(payment);
+ if (StringUtil.isBlank(payment.getHcsPerfFdId())) {
+ HcsPerfRecordSaveDTO param = buildAddParam(payment, operatorLoginName);
+ log.info("HCS履约记录新建开始:paymentNo={}", payment.getPaymentNo());
+ HcsResult result = hcsClient.perfRecordAdd(param);
+ if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
+ log.error("HCS履约记录新建失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ throw new HcsException("C108-1 履约记录新建失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ // 履约记录ID 立即持久化(调用方必须保存)
+ payment.setHcsPerfFdId(result.getData().getFdId());
+ paymentApplicationMapper.updateById(payment);
+ log.info("HCS履约记录新建成功:paymentNo={},perfFdId={}", payment.getPaymentNo(), payment.getHcsPerfFdId());
+ return "C108-1 履约记录新建成功";
+ }
+ // C108-2 更新:formData.fdId 必填,响应无 data,success=true 即成功
+ HcsPerfRecordSaveDTO param = buildUpdateParam(payment, operatorLoginName);
+ log.info("HCS履约记录更新开始:paymentNo={},perfFdId={}", payment.getPaymentNo(), payment.getHcsPerfFdId());
+ HcsResult result = hcsClient.perfRecordUpdate(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS履约记录更新失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ throw new HcsException("C108-2 履约记录更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return "C108-2 履约记录更新成功";
+ }
+
+ /**
+ * C108-1 新建入参(字段标注照 §3.4 规范,映射见设计文档 §5.6)
+ */
+ private HcsPerfRecordSaveDTO buildAddParam(PaymentApplication payment, String operatorLoginName) {
+ HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO();
+ // 【必填】当前操作人 {fdLoginName}(付款申请创建人,§9-U2)
+ param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
+ param.setFormData(buildFormData(payment));
+ // 附件挂载:attachmentsJson 转传,fdEntityKey=fd_plan_att
+ param.setMechanisms(buildMechanisms(payment, operatorLoginName));
+ return param;
+ }
+
+ /**
+ * C108-2 更新入参:formData.fdId 必填,传实际金额与合同ID
+ */
+ private HcsPerfRecordSaveDTO buildUpdateParam(PaymentApplication payment, String operatorLoginName) {
+ HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO();
+ param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
+ HcsPerfRecordFormDataDTO formData = buildFormData(payment);
+ // 【必填】履约记录ID(C108-1 新建返回)
+ formData.setFdId(payment.getHcsPerfFdId());
+ param.setFormData(formData);
+ return param;
+ }
+
+ /**
+ * 履约记录 formData(新建/更新共用)
+ */
+ private HcsPerfRecordFormDataDTO buildFormData(PaymentApplication payment) {
+ ContractManage contractManage = payment.getContractId() == null ? null : contractManageMapper.selectById(payment.getContractId());
+ HcsContractExeStage stageRow = findPaymentStageRow(payment);
+ HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO();
+ HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO();
+ // 申请金额:appliedAmount(接口按字符串传值)
+ props.setFdApplyAmount(payment.getAppliedAmount() == null ? null : payment.getAppliedAmount().toPlainString());
+ // 实际金额:paidAmount(实际付款推进后更新)
+ props.setFdFactAmount(payment.getPaidAmount() == null ? null : payment.getPaidAmount().toPlainString());
+ // 申请编号:付款申请单号(接口文档 §8.3 字段)
+ props.setFdApplyNo(payment.getPaymentNo());
+ // 计划日期:applyDate(毫秒时间戳)
+ props.setFdPlanDate(toMillis(payment.getApplyDate()));
+ // 计划类型:paymentType(原文拼写 fd_paln_type 必须沿用)
+ props.setFdPalnType(payment.getPaymentType());
+ // 性质:款项性质取关联履约规则行的款项性质(无关联不传)
+ props.setFdProperty(stageRow == null ? null : stageRow.getFundNature());
+ // 来源履约计划项ID:关联履约阶段 hcs_stage_fd_id(无关联则不传,§5.6)
+ props.setFdId(stageRow == null ? null : stageRow.getHcsStageFdId());
+ // 合同ID:HCS 合同ID(合同未推送时不传)
+ props.setFdContractId(contractManage == null ? null : contractManage.getHcsFdId());
+ // 表单模板ID:配置 perf-record.xform-id(环境值,§9-U9,未配置不传)
+ props.setFdXformId(StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId());
+ formData.setDynamicProps(props);
+ return formData;
+ }
+
+ /**
+ * 履约记录附件挂载(无附件或开关关闭返回 null)
+ */
+ private HcsMechanismsDTO buildMechanisms(PaymentApplication payment, String operatorLoginName) {
+ if (!hcsProperties.getAttachment().isTransferEnabled() || StringUtil.isBlank(payment.getAttachmentsJson())) {
+ return null;
+ }
+ List attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName);
+ if (attachmentFdIds.isEmpty()) {
+ return null;
+ }
+ HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
+ mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> {
+ HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO();
+ // 附件ID(C103-2 转传返回)
+ attachment.setFdId(fdId);
+ // 履约记录附件字段标识
+ attachment.setFdEntityKey(PERF_ATTACH_ENTITY_KEY);
+ return attachment;
+ }).toList());
+ return mechanisms;
+ }
+
+ /**
+ * 匹配合同的付款履约规则行(perform_type=payment 且已同步,取第一条)
+ */
+ private HcsContractExeStage findPaymentStageRow(PaymentApplication payment) {
+ if (payment.getContractId() == null) {
+ return null;
+ }
+ return hcsContractExeStageMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(HcsContractExeStage::getContractId, payment.getContractId())
+ .eq(HcsContractExeStage::getPerformType, "payment")
+ .isNotNull(HcsContractExeStage::getHcsStageFdId)
+ .last("limit 1"));
+ }
+
+ /**
+ * 操作人登录名(付款申请创建人,§9-U2)
+ */
+ private String resolveLoginName(PaymentApplication payment) {
+ if (payment.getCreateUser() == null) {
+ throw new HcsException("C108 履约记录推送失败,付款申请创建人未维护(paymentNo=" + payment.getPaymentNo() + ")");
+ }
+ User user = UserCache.getUser(payment.getCreateUser());
+ if (user == null || StringUtil.isBlank(user.getAccount())) {
+ throw new HcsException("C108 履约记录推送失败,创建人登录名解析不到(createUser=" + payment.getCreateUser() + ")");
+ }
+ return user.getAccount();
+ }
+
+ /**
+ * LocalDate → 毫秒时间戳
+ */
+ private Long toMillis(LocalDate localDate) {
+ return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
new file mode 100644
index 00000000..990490e6
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
@@ -0,0 +1,229 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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.HcsAttachmentRefDTO;
+import org.springblade.hcs.dto.HcsMechanismsDTO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsSpecPayFormDataDTO;
+import org.springblade.hcs.dto.HcsSpecPayPropsDTO;
+import org.springblade.hcs.dto.HcsSpecPaySaveDTO;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.system.cache.UserCache;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.system.pojo.entity.User;
+import org.springblade.transport.mapper.PaymentApplicationMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springblade.transport.mapper.ContractManageMapper;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * HCS 结算付款特批同步支撑组件(C106-3 发起 / C106-4 更新,设计文档 §5.5)
+ *
+ * 触发:付款申请审批通过且 hcs_spec_status=need(提交时额度超额标记)→ 扫描任务发起 C106-3,
+ * 返回 data.data.fdId(双层嵌套)→ hcs_spec_fd_id,状态置 processing;
+ * 单据变更且特批流程存在时走 C106-4(顶层 fdId=hcs_spec_fd_id,附件不带 fdBindType)。
+ * 特批结果:v4 无结果查询/回调接口,approved/rejected 一期由页面人工确认(§9-U1)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsSpecPaySyncSupport {
+
+ /**
+ * 特批附件字段标识
+ */
+ private static final String SPEC_ATTACH_ENTITY_KEY = "fd_spc_approval_attach";
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final ContractManageMapper contractManageMapper;
+ private final PaymentApplicationMapper paymentApplicationMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
+
+ /**
+ * 推送结算付款特批:未发起走 C106-3,已发起走 C106-4;流程ID 与状态立即持久化
+ *
+ * @param payment 付款申请单(hcs_spec_status=need)
+ * @return 推送结果描述
+ */
+ public String pushSpecPay(PaymentApplication payment) {
+ String operatorLoginName = resolveLoginName(payment);
+ HcsSpecPaySaveDTO param = buildSpecPayParam(payment, operatorLoginName);
+ if (StringUtil.isBlank(payment.getHcsSpecFdId())) {
+ // C106-3 发起:返回双层嵌套 data.data.fdId
+ log.info("HCS结算付款特批发起开始:paymentNo={}", payment.getPaymentNo());
+ HcsResult> result = hcsClient.addSpecApplyPay(param);
+ String specFdId = extractNestedFdId(payment, result);
+ payment.setHcsSpecFdId(specFdId);
+ payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_PROCESSING);
+ paymentApplicationMapper.updateById(payment);
+ log.info("HCS结算付款特批发起成功:paymentNo={},specFdId={}", payment.getPaymentNo(), specFdId);
+ return "C106-3 特批发起成功";
+ }
+ // C106-4 更新:顶层 fdId 必填,内层无业务数据,以内层 success 判断
+ log.info("HCS结算付款特批更新开始:paymentNo={},specFdId={}", payment.getPaymentNo(), payment.getHcsSpecFdId());
+ HcsResult> result = hcsClient.updateSpecApplyPay(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS结算付款特批更新失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ throw new HcsException("C106-4 特批更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return "C106-4 特批更新成功";
+ }
+
+ /**
+ * 组装 C106-3/C106-4 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.5)
+ */
+ private HcsSpecPaySaveDTO buildSpecPayParam(PaymentApplication payment, String operatorLoginName) {
+ ContractManage contractManage = payment.getContractId() == null ? null : contractManageMapper.selectById(payment.getContractId());
+ HcsSpecPaySaveDTO param = new HcsSpecPaySaveDTO();
+ // 结算付款流程ID:发起不传,更新必填(C106-4 顶层 fdId)
+ param.setFdId(payment.getHcsSpecFdId());
+ // 【必填】操作人登录名:付款申请创建人登录名(§9-U2)
+ param.setFdLoginName(operatorLoginName);
+ HcsSpecPayFormDataDTO formData = new HcsSpecPayFormDataDTO();
+ HcsSpecPayPropsDTO props = new HcsSpecPayPropsDTO();
+ // 【必填】合同:传合规合同ID(blade_contract_manage.hcs_fd_id,非名称)
+ props.setFdContractName(requireContractFdId(payment, contractManage));
+ // 合同编号
+ props.setFdContractNo(payment.getContractNo());
+ // 单据编号:付款申请单号(来源付款单标识)
+ props.setFdDocNo(payment.getPaymentNo());
+ // 单据主题:"结算付款特批-" + 付款单号
+ props.setFdDocSubject("结算付款特批-" + payment.getPaymentNo());
+ // 来源付款单ID:付款申请单号(§5.5 映射表)
+ props.setFdId(payment.getPaymentNo());
+ // 付款金额:appliedAmount
+ props.setFdPayAmount(payment.getAppliedAmount());
+ // 合计金额:settlementAmount
+ props.setFdTotalAmount(payment.getSettlementAmount() == null ? BigDecimal.ZERO : payment.getSettlementAmount());
+ // 付款人 / 收款人
+ props.setFdPayer(payment.getPayerName());
+ props.setFdPayee(payment.getPayeeName());
+ // 收款账户名 / 收款账号 / 开户行
+ props.setFdReceiveAccountName(payment.getReceiptAccountName());
+ props.setFdReceiveAccount(payment.getBankAccount());
+ props.setFdBankName(payment.getBankName());
+ // 结算币种:TMS 无币种字段,按 currency-name-mapping 默认值(人民币)
+ props.setFdSettleCurrency(hcsProperties.getContract().getCurrencyNameMapping().getOrDefault("RMB", "人民币"));
+ // 付款用途 / 付款性质 / 付款类型:字典值配置(§9-U3 待联调后配置)
+ props.setFdPaymentPurpose(hcsProperties.getSpecPay().getPaymentPurpose());
+ props.setFdPaymentNature(hcsProperties.getSpecPay().getPaymentNature());
+ props.setFdPaymentType(hcsProperties.getSpecPay().getPaymentType());
+ // 事由 / 补充说明:remark
+ props.setFdReason(payment.getRemark());
+ props.setFdSupplementNote(payment.getRemark());
+ // 单据状态:配置 spec-pay.doc-status(示例 10)
+ props.setFdDocStatus(hcsProperties.getSpecPay().getDocStatus());
+ // 特批附件字段标识:固定 fd_spc_approval_attach
+ props.setFdSpcApprovalAttach(SPEC_ATTACH_ENTITY_KEY);
+ formData.setDynamicProps(props);
+ // 表单层单据主题
+ formData.setFdDocSubject("结算付款特批-" + payment.getPaymentNo());
+ // 表单层单据状态:配置 spec-pay.doc-status
+ formData.setFdDocStatus(hcsProperties.getSpecPay().getDocStatus());
+ param.setFormData(formData);
+ // 附件挂载:attachmentsJson 经 C103-2 转传,特批附件 fdEntityKey=fd_spc_approval_attach,
+ // 发起带 fdBindType=ADD,更新不带
+ param.setMechanisms(buildMechanisms(payment, operatorLoginName));
+ return param;
+ }
+
+ /**
+ * 特批附件挂载(附件转传开关关闭或无附件时返回 null)
+ */
+ private HcsMechanismsDTO buildMechanisms(PaymentApplication payment, String operatorLoginName) {
+ if (!hcsProperties.getAttachment().isTransferEnabled() || StringUtil.isBlank(payment.getAttachmentsJson())) {
+ return null;
+ }
+ List attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName);
+ if (attachmentFdIds.isEmpty()) {
+ return null;
+ }
+ HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
+ boolean add = StringUtil.isBlank(payment.getHcsSpecFdId());
+ mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> {
+ HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO();
+ // 附件ID(C103-2 转传返回)
+ attachment.setFdId(fdId);
+ // 特批附件字段标识
+ attachment.setFdEntityKey(SPEC_ATTACH_ENTITY_KEY);
+ // 绑定类型:仅发起传 ADD
+ attachment.setFdBindType(add ? "ADD" : null);
+ return attachment;
+ }).toList());
+ return mechanisms;
+ }
+
+ /**
+ * 解析 C106-3 双层嵌套返回 data.data.fdId
+ */
+ private String extractNestedFdId(PaymentApplication payment, HcsResult> 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结算付款特批发起失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ throw new HcsException("C106-3 特批发起失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result.getData().getData().getFdId();
+ }
+
+ /**
+ * 合规合同ID(fd_contract_name 必填,合同未推送时失败提示)
+ */
+ private String requireContractFdId(PaymentApplication payment, ContractManage contractManage) {
+ if (contractManage == null || StringUtil.isBlank(contractManage.getHcsFdId())) {
+ throw new HcsException("C106 特批推送失败,关联合同尚未推送合规(paymentNo=" + payment.getPaymentNo() + ")");
+ }
+ return contractManage.getHcsFdId();
+ }
+
+ /**
+ * 操作人登录名(付款申请创建人,§9-U2)
+ */
+ private String resolveLoginName(PaymentApplication payment) {
+ if (payment.getCreateUser() == null) {
+ throw new HcsException("C106 特批推送失败,付款申请创建人未维护(paymentNo=" + payment.getPaymentNo() + ")");
+ }
+ User user = UserCache.getUser(payment.getCreateUser());
+ if (user == null || StringUtil.isBlank(user.getAccount())) {
+ throw new HcsException("C106 特批推送失败,创建人登录名解析不到(createUser=" + payment.getCreateUser() + ")");
+ }
+ return user.getAccount();
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
index 6d35899c..dc6fcbf5 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -33,10 +33,13 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
@@ -51,9 +54,9 @@ import java.util.UUID;
*
* 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
* 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
- * 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步)。
+ * 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步)→ 付款特批/履约记录。
* 多实例部署时以 Redis 分布式锁防重扫(照既有 BladeRedis 用法,单实例部署行为与既有金蝶任务一致)。
- * 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts,供手动端点(HcsSyncController)复用。
+ * 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts/pushUnsyncedPayments,供手动端点(HcsSyncController)复用。
*
* 调度配置(Nacos thirdParty.hcs.push.*):sync-enabled 总开关(默认关,灰度打开)、cron、batch-size、max-retry。
*
@@ -77,8 +80,10 @@ public class HcsPushTask {
private final HcsProperties hcsProperties;
private final ProjectApplyMapper projectApplyMapper;
private final ContractManageMapper contractManageMapper;
+ private final PaymentApplicationMapper paymentApplicationMapper;
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
+ private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
private final BladeRedis bladeRedis;
/**
@@ -104,9 +109,10 @@ public class HcsPushTask {
return;
}
try {
- // 任务顺序(设计文档 §5.0):项目 → 合同;两类互不影响
+ // 任务顺序(设计文档 §5.0):项目 → 合同 → 付款;各类互不影响
log.info("定时推送HCS项目立项:{}", pushUnsyncedProjects());
log.info("定时推送HCS合同:{}", pushUnsyncedContracts());
+ log.info("定时推送HCS付款链:{}", pushUnsyncedPayments());
} finally {
if (lockHolder.equals(bladeRedis.get(HcsSyncConstant.PUSH_TASK_LOCK_KEY))) {
bladeRedis.del(HcsSyncConstant.PUSH_TASK_LOCK_KEY);
@@ -175,4 +181,35 @@ public class HcsPushTask {
}
return summary;
}
+
+ /**
+ * 扫描并推送待同步/待补偿的付款申请(C106 特批 + C108 履约记录),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedPayments() {
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ List paymentList = paymentApplicationMapper.selectList(Wrappers.lambdaQuery()
+ .eq(PaymentApplication::getApprovalStatus, "approved")
+ .in(PaymentApplication::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .and(wrapper -> wrapper.isNull(PaymentApplication::getHcsRetryCount).or().lt(PaymentApplication::getHcsRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ int successCount = 0;
+ int failCount = 0;
+ for (PaymentApplication payment : paymentList) {
+ try {
+ hcsPaymentSyncSupport.pushPayment(payment);
+ successCount++;
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ hcsPaymentSyncSupport.markFailed(payment, message);
+ failCount++;
+ }
+ }
+ String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", paymentList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS付款链推送存在失败:{}", summary);
+ }
+ return summary;
+ }
}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
index 93f2fefb..3963b0d8 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
@@ -100,6 +100,10 @@ public class HcsProperties {
* 结算付款特批(C106-3/4)配置
*/
private SpecPay specPay = new SpecPay();
+ /**
+ * 履约记录(C108-1/2)配置
+ */
+ private PerfRecord perfRecord = new PerfRecord();
/**
* 开票登记(C111)配置
*/
@@ -245,6 +249,14 @@ public class HcsProperties {
private Map invoiceTypeMapping = new HashMap<>();
}
+ @Data
+ public static class PerfRecord {
+ /**
+ * 履约记录表单模板ID(fd_xform_id,环境相关值,§9-U9 待合规方提供)
+ */
+ private String xformId = "";
+ }
+
@Data
public static class Attachment {
/**
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index db3628d9..d25cd9ad 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -138,6 +138,10 @@ thirdParty:
receipt-bill:
# TMS 发票类型 -> HCS 收票发票类型字典值(§9-U3 待联调后配置)
invoice-type-mapping: {}
+ # 履约记录(C108-1/2)
+ perf-record:
+ # 表单模板ID(fd_xform_id,环境相关值,§9-U9 待合规方提供)
+ xform-id: ${HCS_PERF_RECORD_XFORM_ID:}
# 附件转传(C103-2)
attachment:
# 附件转传总开关
From 165c22c2d7dace7aba2937323d97c29bd6fbed76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:51:07 +0800
Subject: [PATCH 013/124] =?UTF-8?q?feat:=20HCS=E5=BC=80=E7=A5=A8=E6=94=B6?=
=?UTF-8?q?=E7=A5=A8=E6=95=B0=E6=8D=AE=E5=B1=82=E4=B8=8E=E9=9B=B6=E5=9F=8B?=
=?UTF-8?q?=E7=82=B9=E6=8E=A5=E7=BA=BF=EF=BC=88=E5=AE=9E=E4=BD=93=E5=90=88?=
=?UTF-8?q?=E8=A7=84=E5=AD=97=E6=AE=B5=E7=BB=84/=E6=94=B6=E7=A5=A8?=
=?UTF-8?q?=E5=90=88=E5=90=8C=E8=A1=A5=E5=BD=95=E5=AD=97=E6=AE=B5/?=
=?UTF-8?q?=E5=AE=A1=E6=89=B9=E4=B8=8E=E9=87=91=E8=9D=B6=E5=BC=80=E7=A5=A8?=
=?UTF-8?q?=E5=9B=9E=E5=86=99=E7=BD=AE=E5=BE=85=E5=90=8C=E6=AD=A5=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/InvoiceApplication.java | 10 ++++++++++
.../transport/pojo/entity/InvoiceReceipt.java | 14 ++++++++++++++
.../impl/InvoiceApplicationServiceImpl.java | 7 +++++++
.../impl/InvoiceReceiptServiceImpl.java | 5 +++++
.../blade_hcs_receipt_fields_20260927.sql | 18 ++++++++++++++++++
5 files changed, 54 insertions(+)
create mode 100644 doc/sql/transport/blade_hcs_receipt_fields_20260927.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
index 960cb23d..48ae4d6e 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
@@ -179,4 +179,14 @@ public class InvoiceApplication extends TenantEntity {
* 作废原因,驳回时记录驳回原因
*/
private String voidReason;
+ /** 合规系统单据ID(fdId) */
+ private String hcsFdId;
+ /** 合规同步状态:unsynced 待同步 / synced 已同步 / failed 同步失败 */
+ private String hcsSyncStatus;
+ /** 合规推送失败重试次数 */
+ private Integer hcsRetryCount;
+ /** 合规最近同步时间 */
+ private LocalDateTime hcsSyncTime;
+ /** 合规最近同步结果信息/失败原因 */
+ private String hcsSyncMsg;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
index 0843525b..143c27d1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
@@ -176,4 +176,18 @@ public class InvoiceReceipt extends TenantEntity {
* 作废原因
*/
private String voidReason;
+ /** 合规系统单据ID(fdId) */
+ private String hcsFdId;
+ /** 合规同步状态:unsynced 待同步 / synced 已同步 / failed 同步失败 */
+ private String hcsSyncStatus;
+ /** 合规推送失败重试次数 */
+ private Integer hcsRetryCount;
+ /** 合规最近同步时间 */
+ private LocalDateTime hcsSyncTime;
+ /** 合规最近同步结果信息/失败原因 */
+ private String hcsSyncMsg;
+ /** 合规推送合同ID(应付结算回溯不到时人工补录,D13) */
+ private Long hcsContractId;
+ /** 合规推送合同编号(应付结算回溯不到时人工补录,D13) */
+ private String hcsContractNo;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index e27d0c3c..baf49500 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -52,6 +52,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.config.BfiProperties;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.ContractManageMapper;
@@ -886,6 +887,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl
Date: Sun, 27 Sep 2026 23:57:38 +0800
Subject: [PATCH 014/124] =?UTF-8?q?feat:=20HCS=E5=BC=80=E7=A5=A8=E7=99=BB?=
=?UTF-8?q?=E8=AE=B0=E5=90=8C=E6=AD=A5=E6=94=AF=E6=92=91=EF=BC=88C111-1?=
=?UTF-8?q?=E5=88=A4=E9=87=8D/C111-2=E6=96=B0=E5=A2=9E/C111-3=E7=A5=A8?=
=?UTF-8?q?=E6=8D=AE=E6=97=A5=E6=9C=9F=E5=9B=9E=E5=A1=AB/=E5=BA=94?=
=?UTF-8?q?=E6=94=B6=E7=BB=93=E7=AE=97=E9=93=BE=E8=B7=AF=E5=90=88=E5=90=8C?=
=?UTF-8?q?=E5=9B=9E=E6=BA=AF=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsInvoiceRegSyncSupport.java | 334 ++++++++++++++++++
.../hcs/dto/HcsInvoiceRegUpdateDTO.java | 4 +
2 files changed, 338 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
new file mode 100644
index 00000000..f4ca4d03
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
@@ -0,0 +1,334 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+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.Func;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.dto.HcsIdVO;
+import org.springblade.hcs.dto.HcsInvoiceRegAddDTO;
+import org.springblade.hcs.dto.HcsInvoiceRegUpdateDTO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsPageVO;
+import org.springblade.hcs.dto.HcsInvoiceRegVO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.transport.constant.KingdeeBfiConstant;
+import org.springblade.system.cache.SysCache;
+import org.springblade.system.cache.UserCache;
+import org.springblade.system.pojo.entity.Dept;
+import org.springblade.system.pojo.entity.User;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper;
+import org.springblade.transport.mapper.InvoiceApplicationMapper;
+import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
+import org.springblade.transport.mapper.FormalSettlementMapper;
+import org.springblade.transport.pojo.entity.FormalSettlement;
+import org.springblade.transport.pojo.entity.InvoiceApplication;
+import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
+import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * HCS 开票登记同步支撑组件(C111-1 查询判重 / C111-2 新增 / C111-3 更新,设计文档 §5.7,D12)
+ *
+ * 审批通过即推 C111-2(票据日期空);金蝶开票完成(kingdeeStatus=invoiced,由金蝶结果回写置待同步)
+ * → C111-3 回填 fdBillDate/fdStatus。判重 C111-1 conditions={"fdNumber": applicationNo}(§9-U4 联调确认)。
+ * fdContractNumber 缺口:InvoiceApplication 无合同字段,经应收结算关联表回溯合同编号(§9-U10,回溯不到失败提示)。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsInvoiceRegSyncSupport {
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final InvoiceApplicationMapper invoiceApplicationMapper;
+ private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper;
+ private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper;
+ private final FormalSettlementMapper formalSettlementMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
+
+ /**
+ * 推送开票登记:无合规ID 走判重+C111-2 新增(金蝶已开票完成时紧接着 C111-3),有合规ID 走 C111-3
+ *
+ * @param application 开票申请单(approvalStatus=approved)
+ * @return 推送结果描述
+ */
+ public String pushInvoiceRegistration(InvoiceApplication application) {
+ if (StringUtil.isBlank(application.getHcsFdId())) {
+ String existFdId = findExistFdId(application);
+ if (StringUtil.isNotBlank(existFdId)) {
+ markSynced(application, existFdId, "判重命中,回填已有合规开票登记ID");
+ return "判重命中,回填已有合规开票登记ID";
+ }
+ String fdId = addInvoiceRegistration(application);
+ // 开票登记ID 立即持久化(调用方必须保存)
+ markSynced(application, fdId, "C111-2 开票登记新增成功");
+ String result = "C111-2 开票登记新增成功";
+ // 金蝶已开票完成(新增前回写竞态):紧接着回填票据日期
+ if (KingdeeBfiConstant.KINGDEE_STATUS_INVOICED.equals(application.getKingdeeStatus())) {
+ result += ";" + updateInvoiceRegistration(application);
+ }
+ return result;
+ }
+ String result = updateInvoiceRegistration(application);
+ markSynced(application, application.getHcsFdId(), "C111-3 开票登记更新成功");
+ return result;
+ }
+
+ /**
+ * 单条手动重推:重置为待同步并清零重试计数后立即推送
+ *
+ * @param id 开票申请单ID
+ * @return 推送结果描述
+ */
+ public String repushInvoiceRegistration(Long id) {
+ InvoiceApplication application = invoiceApplicationMapper.selectById(id);
+ if (application == null) {
+ throw new ServiceException("开票申请单不存在:" + id);
+ }
+ if (!"approved".equals(application.getApprovalStatus())) {
+ throw new ServiceException("仅审批通过的开票申请可推送合规(当前状态:" + application.getApprovalStatus() + ")");
+ }
+ application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ application.setHcsRetryCount(0);
+ invoiceApplicationMapper.updateById(application);
+ try {
+ return pushInvoiceRegistration(application);
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ markFailed(application, message);
+ throw new ServiceException("HCS开票登记推送失败:" + message);
+ }
+ }
+
+ /**
+ * C111-2 新增开票登记,返回合规 fdId(字段标注照 §3.4 规范,映射见设计文档 §5.7)
+ */
+ private String addInvoiceRegistration(InvoiceApplication application) {
+ HcsInvoiceRegAddDTO param = new HcsInvoiceRegAddDTO();
+ // 单据编号:开票申请单号(同租户唯一,判重候选键)
+ param.setFdNumber(application.getApplicationNo());
+ // 登记日期:applicationDate(毫秒时间戳)
+ param.setFdRegisterDate(toMillis(application.getApplicationDate()));
+ // 【必填】合同编号:经应收结算链路回溯(§9-U10 关联字段确认后细化)
+ param.setFdContractNumber(requireContractNumber(application));
+ // 发票类型:invoice-type-mapping 字典映射(§9-U3,未配置不传)
+ param.setFdInvoiceType(hcsProperties.getInvoice().getInvoiceTypeMapping().get(application.getInvoiceType()));
+ if (param.getFdInvoiceType() == null) {
+ log.warn("HCS开票登记发票类型映射缺失:invoiceType=[{}] 不在 invoice.invoice-type-mapping,fdInvoiceType 不传(§9-U3)",
+ application.getInvoiceType());
+ }
+ // 【必填】结算币种:currency-id-mapping(TMS 币种码 → HCS 币种fdId,经 C103-12 缓存取值)
+ param.setFdCurrencyId(requireCurrencyId(application));
+ // 开票方(销方):issuerName
+ param.setFdInvoiceParty(application.getIssuerName());
+ // 款项性质:配置 invoice.fund-nature-default(默认 progress)
+ param.setFdFundNature(hcsProperties.getInvoice().getFundNatureDefault());
+ // 开票金额(含税):invoiceAmount
+ param.setFdInvoiceAmount(application.getInvoiceAmount());
+ // 备注:remark
+ param.setFdRemark(application.getRemark());
+ // 状态:新增固定 0(已开票)
+ param.setFdStatus(0);
+ // 第三方创建人编码:申请人工号(User.code)
+ param.setFdThirdCreatorCode(resolveCreatorCode(application));
+ // 第三方创建部门编码:申请人部门编码
+ param.setFdThirdCreatorDeptCode(resolveDeptCode(application));
+ // 附件ID列表:attachmentsJson 经 C103-2 转传
+ if (hcsProperties.getAttachment().isTransferEnabled() && StringUtil.isNotBlank(application.getAttachmentsJson())) {
+ param.setFdAttIds(hcsAttachmentSupport.transferAttachments(application.getAttachmentsJson(), resolveCreatorCode(application)));
+ }
+ log.info("HCS开票登记新增开始:applicationNo={}", application.getApplicationNo());
+ HcsResult result = hcsClient.invoiceRegistrationAdd(param);
+ if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
+ log.error("HCS开票登记新增失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ throw new HcsException("C111-2 开票登记新增失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result.getData().getFdId();
+ }
+
+ /**
+ * C111-3 更新开票登记:回填票据日期与状态(金蝶开票完成后)
+ */
+ private String updateInvoiceRegistration(InvoiceApplication application) {
+ HcsInvoiceRegUpdateDTO param = new HcsInvoiceRegUpdateDTO();
+ // 【必填】开票登记ID(C111-2 新增返回)
+ param.setFdId(application.getHcsFdId());
+ // 票据日期:金蝶回写发票的最新开票日期(无发票记录取回写时间,待联调确认口径)
+ param.setFdBillDate(latestInvoiceDate(application));
+ // 状态:0 已开票
+ param.setFdStatus(0);
+ log.info("HCS开票登记更新开始:applicationNo={},fdId={}", application.getApplicationNo(), application.getHcsFdId());
+ HcsResult result = hcsClient.invoiceRegistrationUpdate(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS开票登记更新失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ throw new HcsException("C111-3 开票登记更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return "C111-3 开票登记更新成功";
+ }
+
+ /**
+ * C111-1 开票登记列表判重(conditions={"fdNumber": applicationNo},§9-U4 查询能力联调确认)
+ */
+ private String findExistFdId(InvoiceApplication application) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ query.setColumns(List.of("fdId", "fdNumber"));
+ // 判重条件:单据编号精确匹配
+ query.setConditions(Map.of("fdNumber", Objects.toString(application.getApplicationNo(), "")));
+ query.setOffset(0);
+ query.setPageSize(10);
+ HcsResult> result = hcsClient.invoiceRegistrationList(query);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS开票登记判重查询失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ throw new HcsException("C111-1 开票登记判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ HcsPageVO page = result.getData();
+ if (page == null || page.getContent() == null || page.getContent().isEmpty()) {
+ return null;
+ }
+ return page.getContent().get(0).getFdId();
+ }
+
+ /**
+ * 应收结算链路回溯合同编号:开票申请 → 应收结算关联 → 正式结算单合同编号(§9-U10)
+ */
+ private String requireContractNumber(InvoiceApplication application) {
+ InvoiceApplicationSettlement relation = invoiceApplicationSettlementMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, application.getId())
+ .last("limit 1"));
+ if (relation != null && relation.getFormalSettlementId() != null) {
+ FormalSettlement settlement = formalSettlementMapper.selectById(relation.getFormalSettlementId());
+ if (settlement != null && StringUtil.isNotBlank(settlement.getContractNo())) {
+ return settlement.getContractNo();
+ }
+ }
+ throw new HcsException("C111 开票登记推送失败,应收结算链路回溯不到合同编号(applicationNo=" + application.getApplicationNo()
+ + ",§9-U10 待确认关联字段)");
+ }
+
+ /**
+ * 结算币种ID(currency-id-mapping,必填;TMS 无币种字段按 RMB 取映射)
+ */
+ private String requireCurrencyId(InvoiceApplication application) {
+ String currencyFdId = hcsProperties.getInvoice().getCurrencyIdMapping().get("RMB");
+ if (StringUtil.isBlank(currencyFdId)) {
+ throw new HcsException("C111 开票登记推送失败,币种ID 映射未配置(invoice.currency-id-mapping RMB,§9-U3)");
+ }
+ return currencyFdId;
+ }
+
+ /**
+ * 金蝶回写发票的最新开票日期(毫秒),无发票记录取金蝶回写时间
+ */
+ private Long latestInvoiceDate(InvoiceApplication application) {
+ InvoiceApplicationInvoice invoice = invoiceApplicationInvoiceMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(InvoiceApplicationInvoice::getInvoiceApplicationId, application.getId())
+ .orderByDesc(InvoiceApplicationInvoice::getInvoiceDate)
+ .last("limit 1"));
+ LocalDateTime invoiceDate = invoice != null && invoice.getInvoiceDate() != null
+ ? invoice.getInvoiceDate() : application.getSyncedTime();
+ return toMillis(invoiceDate);
+ }
+
+ /**
+ * 回填整体同步状态置 synced
+ */
+ public void markSynced(InvoiceApplication application, String fdId, String message) {
+ application.setHcsFdId(fdId);
+ application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
+ application.setHcsRetryCount(0);
+ application.setHcsSyncTime(LocalDateTime.now());
+ application.setHcsSyncMsg(message);
+ invoiceApplicationMapper.updateById(application);
+ log.info("HCS开票登记推送成功:applicationNo={},fdId={},{}", application.getApplicationNo(), fdId, message);
+ }
+
+ /**
+ * 推送失败回写:failed + 重试次数+1 + 失败原因
+ */
+ public void markFailed(InvoiceApplication application, String message) {
+ application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
+ application.setHcsRetryCount(application.getHcsRetryCount() == null ? 1 : application.getHcsRetryCount() + 1);
+ application.setHcsSyncTime(LocalDateTime.now());
+ application.setHcsSyncMsg(message);
+ invoiceApplicationMapper.updateById(application);
+ log.error("HCS开票登记推送失败:applicationNo={},retryCount={},{}", application.getApplicationNo(), application.getHcsRetryCount(), message);
+ }
+
+ /**
+ * 申请人工号(User.code,fdThirdCreatorCode)
+ */
+ private String resolveCreatorCode(InvoiceApplication application) {
+ User user = application.getCreateUser() == null ? null : UserCache.getUser(application.getCreateUser());
+ return user == null ? null : user.getCode();
+ }
+
+ /**
+ * 申请人部门编码(fdThirdCreatorDeptCode)
+ */
+ private String resolveDeptCode(InvoiceApplication application) {
+ User user = application.getCreateUser() == null ? null : UserCache.getUser(application.getCreateUser());
+ if (user == null) {
+ return null;
+ }
+ // 用户冗余部门编码优先(可能逗号分隔,取第一个)
+ if (StringUtil.isNotBlank(user.getDeptCodes())) {
+ return user.getDeptCodes().split(",")[0].trim();
+ }
+ Long deptId = Func.toLong(user.getDeptId() == null ? null : user.getDeptId().split(",")[0].trim());
+ Dept dept = deptId == null ? null : SysCache.getDept(deptId);
+ return dept == null ? null : dept.getDeptCode();
+ }
+
+ /**
+ * LocalDateTime/LocalDate → 毫秒时间戳
+ */
+ private Long toMillis(LocalDateTime localDateTime) {
+ return localDateTime == null ? null : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+
+ /**
+ * LocalDate → 毫秒时间戳
+ */
+ private Long toMillis(java.time.LocalDate localDate) {
+ return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java
index 44465dfb..0795fe05 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java
@@ -50,4 +50,8 @@ public class HcsInvoiceRegUpdateDTO extends HcsInvoiceRegAddDTO {
* 开票登记ID(必填,C111-2 新增返回,持久化于 hcs_fd_id)
*/
private String fdId;
+ /**
+ * 票据日期(毫秒时间戳,金蝶开票完成后回填)
+ */
+ private Long fdBillDate;
}
From 12e2825e55ad434c55026d88f178498c39895532 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:57:43 +0800
Subject: [PATCH 015/124] =?UTF-8?q?feat:=20HCS=E6=94=B6=E7=A5=A8=E5=8D=95?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=8E=E4=BB=BB=E5=8A=A1=E7=AB=AF=E7=82=B9?=
=?UTF-8?q?=E6=8E=A5=E7=BA=BF=EF=BC=88C109-1/2=E5=90=AB=E5=90=88=E5=90=8C?=
=?UTF-8?q?=E5=9B=9E=E6=BA=AF=E4=B8=8E=E4=BA=BA=E5=B7=A5=E8=A1=A5=E5=BD=95?=
=?UTF-8?q?/=E6=89=AB=E6=8F=8F=E7=BC=96=E6=8E=92=E6=89=A9=E5=B1=95?=
=?UTF-8?q?=E5=BC=80=E7=A5=A8=E6=94=B6=E7=A5=A8/=E9=87=8D=E6=8E=A8?=
=?UTF-8?q?=E7=AB=AF=E7=82=B9=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/constant/HcsSyncConstant.java | 10 +
.../controller/HcsSyncController.java | 2 +
.../hcs/HcsReceiptBillSyncSupport.java | 311 ++++++++++++++++++
.../transport/task/HcsPushTask.java | 77 ++++-
4 files changed, 399 insertions(+), 1 deletion(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 6e128731..8515dabd 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -64,6 +64,16 @@ public final class HcsSyncConstant {
*/
public static final String BIZ_TYPE_PAYMENT = "payment";
+ /**
+ * 推送业务类型:开票申请(C111 开票登记)
+ */
+ public static final String BIZ_TYPE_INVOICE_APPLICATION = "invoice-application";
+
+ /**
+ * 推送业务类型:收票单(C109)
+ */
+ public static final String BIZ_TYPE_INVOICE_RECEIPT = "invoice-receipt";
+
/**
* 校验类型:客商风险(C101-1)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index f10871d1..d04ff7a7 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -34,7 +34,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.api.R;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
+import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport;
import org.springblade.transport.support.hcs.HcsMasterDataSupport;
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
import org.springblade.transport.task.HcsPushTask;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
new file mode 100644
index 00000000..e070a97e
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
@@ -0,0 +1,311 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support.hcs;
+
+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.config.HcsProperties;
+import org.springblade.hcs.dto.HcsAttachmentRefDTO;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsMechanismsDTO;
+import org.springblade.hcs.dto.HcsReceiptBillFormDataDTO;
+import org.springblade.hcs.dto.HcsReceiptBillPropsDTO;
+import org.springblade.hcs.dto.HcsReceiptBillSaveDTO;
+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.FormalSettlementMapper;
+import org.springblade.transport.mapper.InvoiceReceiptMapper;
+import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.FormalSettlement;
+import org.springblade.transport.pojo.entity.InvoiceReceipt;
+import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * HCS 收票单同步支撑组件(C109-1 新建 / C109-2 更新,设计文档 §5.8,D13)
+ *
+ * 审批通过 → C109-1;票据变更 → C109-2(顶层 fdId=hcs_fd_id 必填,只传变更字段、不带文档状态;
+ * v4 无作废字段,作废推送待联调确认)。合同关联:应付结算链路回溯(收票 → 应收/应付结算关联 → 结算单合同),
+ * 回溯不到时取页面人工补录的 hcs_contract_id/hcs_contract_no(D13)。
+ * fd_ap_bill_src_subject(认领应付单号键)待联调(§9-U11),一期不传。
+ * 返回双层嵌套 data.data.fdId → hcs_fd_id。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsReceiptBillSyncSupport {
+
+ /**
+ * 收票单附件字段标识
+ */
+ private static final String RECEIPT_ATTACH_ENTITY_KEY = "fd_attach";
+
+ /**
+ * 收票日期格式(fd_bill_date 为 YYYY-MM-DD 字符串,非毫秒时间戳)
+ */
+ private static final DateTimeFormatter BILL_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+ private final InvoiceReceiptMapper invoiceReceiptMapper;
+ private final InvoiceReceiptSettlementMapper invoiceReceiptSettlementMapper;
+ private final FormalSettlementMapper formalSettlementMapper;
+ private final ContractManageMapper contractManageMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
+
+ /**
+ * 推送收票单:无合规ID 走 C109-1 新建,有合规ID 走 C109-2 更新;整体状态由调用方回写
+ *
+ * @param invoiceReceipt 收票单(approvalStatus=approved)
+ * @return 推送结果描述
+ */
+ public String pushReceiptBill(InvoiceReceipt invoiceReceipt) {
+ String operatorLoginName = resolveLoginName(invoiceReceipt);
+ if (StringUtil.isBlank(invoiceReceipt.getHcsFdId())) {
+ HcsReceiptBillSaveDTO param = buildSaveParam(invoiceReceipt, operatorLoginName, true);
+ log.info("HCS收票单新建开始:invoiceNo={}", invoiceReceipt.getInvoiceNo());
+ HcsResult> result = hcsClient.addReceiveBill(param);
+ String fdId = extractNestedFdId(invoiceReceipt, result);
+ // 收票单ID 立即持久化(调用方必须保存)
+ invoiceReceipt.setHcsFdId(fdId);
+ invoiceReceiptMapper.updateById(invoiceReceipt);
+ log.info("HCS收票单新建成功:invoiceNo={},fdId={}", invoiceReceipt.getInvoiceNo(), fdId);
+ return "C109-1 收票单新建成功";
+ }
+ // C109-2 更新:顶层 fdId 必填,只传变更字段、不带文档状态
+ HcsReceiptBillSaveDTO param = buildSaveParam(invoiceReceipt, operatorLoginName, false);
+ log.info("HCS收票单更新开始:invoiceNo={},fdId={}", invoiceReceipt.getInvoiceNo(), invoiceReceipt.getHcsFdId());
+ HcsResult> result = hcsClient.updateReceiveBill(param);
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS收票单更新失败响应:{},invoiceNo={}", result == null ? "无响应" : result.getMsg(), invoiceReceipt.getInvoiceNo());
+ throw new HcsException("C109-2 收票单更新失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return "C109-2 收票单更新成功";
+ }
+
+ /**
+ * 单条手动重推:重置为待同步并清零重试计数后立即推送
+ *
+ * @param id 收票单ID
+ * @return 推送结果描述
+ */
+ public String repushReceiptBill(Long id) {
+ InvoiceReceipt invoiceReceipt = invoiceReceiptMapper.selectById(id);
+ if (invoiceReceipt == null) {
+ throw new ServiceException("收票单不存在:" + id);
+ }
+ if (!"approved".equals(invoiceReceipt.getApprovalStatus())) {
+ throw new ServiceException("仅审批通过的收票单可推送合规(当前状态:" + invoiceReceipt.getApprovalStatus() + ")");
+ }
+ invoiceReceipt.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ invoiceReceipt.setHcsRetryCount(0);
+ invoiceReceiptMapper.updateById(invoiceReceipt);
+ try {
+ return pushReceiptBill(invoiceReceipt);
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ markFailed(invoiceReceipt, message);
+ throw new ServiceException("HCS收票单推送失败:" + message);
+ }
+ }
+
+ /**
+ * 组装 C109-1/C109-2 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.8)
+ */
+ private HcsReceiptBillSaveDTO buildSaveParam(InvoiceReceipt invoiceReceipt, String operatorLoginName, boolean add) {
+ HcsReceiptBillSaveDTO param = new HcsReceiptBillSaveDTO();
+ // 收票单主键:新建不传,更新必填(顶层 fdId)
+ param.setFdId(invoiceReceipt.getHcsFdId());
+ // 【必填】操作人登录名:收票单创建人登录名(§9-U2)
+ param.setFdLoginName(operatorLoginName);
+ HcsReceiptBillFormDataDTO formData = new HcsReceiptBillFormDataDTO();
+ HcsReceiptBillPropsDTO props = new HcsReceiptBillPropsDTO();
+ // 发票号码:invoiceNo
+ props.setFdInvoiceCode(invoiceReceipt.getInvoiceNo());
+ // 开票/收票日期:invoiceDate(YYYY-MM-DD 字符串)
+ props.setFdBillDate(formatBillDate(invoiceReceipt.getInvoiceDate()));
+ // 开票方(销方):issuerName
+ props.setFdInvoicer(invoiceReceipt.getIssuerName());
+ // 收票方(购方):receiverName
+ props.setFdBuyer(invoiceReceipt.getReceiverName());
+ // 价税合计金额:invoiceAmount
+ props.setFdInvoiceAmtTaxIncl(invoiceReceipt.getInvoiceAmount());
+ // 税率(小数):TMS taxRate(如 13)÷ 100 → 0.13
+ props.setFdTaxRate(toTaxRate(invoiceReceipt.getTaxRate()));
+ // 发票类型:receipt-bill.invoice-type-mapping 字典映射(§9-U3,未配置不传)
+ props.setFdInvoiceType(hcsProperties.getReceiptBill().getInvoiceTypeMapping().get(invoiceReceipt.getInvoiceType()));
+ // 合同主题/编号:应付结算链路回溯,回溯不到取人工补录(D13)
+ ContractReference contractReference = resolveContract(invoiceReceipt);
+ props.setFdContractSubject(contractReference.subject());
+ props.setFdContractNo(contractReference.contractNo());
+ // TODO 待联调(§9-U11):fd_ap_bill_src_subject 关联结算付款单的编号体系(应付单号键)v4 未明确,一期不传
+ // 备注:remark
+ props.setFdRemark(invoiceReceipt.getRemark());
+ formData.setDynamicProps(props);
+ param.setFormData(formData);
+ // 附件挂载(发票扫描件):fdEntityKey=fd_attach,发起带 fdBindType=ADD,更新不带
+ if (hcsProperties.getAttachment().isTransferEnabled() && StringUtil.isNotBlank(invoiceReceipt.getAttachmentsJson())) {
+ List attachmentFdIds = hcsAttachmentSupport.transferAttachments(invoiceReceipt.getAttachmentsJson(), operatorLoginName);
+ if (!attachmentFdIds.isEmpty()) {
+ HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
+ mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> {
+ HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO();
+ // 附件ID(C103-2 转传返回)
+ attachment.setFdId(fdId);
+ // 收票单附件字段标识
+ attachment.setFdEntityKey(RECEIPT_ATTACH_ENTITY_KEY);
+ // 绑定类型:仅发起传 ADD
+ attachment.setFdBindType(add ? "ADD" : null);
+ return attachment;
+ }).toList());
+ param.setMechanisms(mechanisms);
+ }
+ }
+ return param;
+ }
+
+ /**
+ * 解析 C109-1 双层嵌套返回 data.data.fdId
+ */
+ private String extractNestedFdId(InvoiceReceipt invoiceReceipt, HcsResult> 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收票单新建失败响应:{},invoiceNo={}", result == null ? "无响应" : result.getMsg(), invoiceReceipt.getInvoiceNo());
+ throw new HcsException("C109-1 收票单新建失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result.getData().getData().getFdId();
+ }
+
+ /**
+ * 合同引用解析:收票 → 应收结算关联 → 结算单合同 → 合同已推送传合规ID,否则传人工补录值(D13)
+ */
+ private ContractReference resolveContract(InvoiceReceipt invoiceReceipt) {
+ // ① 人工补录兜底优先可直接用;先走链路回溯
+ InvoiceReceiptSettlement relation = invoiceReceiptSettlementMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, invoiceReceipt.getId())
+ .last("limit 1"));
+ if (relation != null && relation.getFormalSettlementId() != null) {
+ FormalSettlement settlement = formalSettlementMapper.selectById(relation.getFormalSettlementId());
+ if (settlement != null && settlement.getContractId() != null) {
+ ContractManage contractManage = contractManageMapper.selectById(settlement.getContractId());
+ if (contractManage != null) {
+ // 合同已推送传合规合同ID(fd_contract_subject 支持 ID 引用),编号传 TMS 合同编号
+ String subject = StringUtil.isNotBlank(contractManage.getHcsFdId()) ? contractManage.getHcsFdId() : contractManage.getContractName();
+ return new ContractReference(subject, contractManage.getContractNo());
+ }
+ }
+ }
+ // ② 回溯不到:页面人工补录(hcs_contract_id/hcs_contract_no,D13)
+ if (invoiceReceipt.getHcsContractId() != null) {
+ ContractManage contractManage = contractManageMapper.selectById(invoiceReceipt.getHcsContractId());
+ if (contractManage != null) {
+ String subject = StringUtil.isNotBlank(contractManage.getHcsFdId()) ? contractManage.getHcsFdId() : contractManage.getContractName();
+ return new ContractReference(subject, StringUtil.isNotBlank(invoiceReceipt.getHcsContractNo()) ? invoiceReceipt.getHcsContractNo() : contractManage.getContractNo());
+ }
+ }
+ if (StringUtil.isNotBlank(invoiceReceipt.getHcsContractNo())) {
+ return new ContractReference(null, invoiceReceipt.getHcsContractNo());
+ }
+ log.warn("HCS收票单合同关联缺失:invoiceNo={},应付结算回溯与人工补录均为空,fd_contract_* 不传", invoiceReceipt.getInvoiceNo());
+ return new ContractReference(null, null);
+ }
+
+ /**
+ * 回填整体同步状态置 synced
+ */
+ public void markSynced(InvoiceReceipt invoiceReceipt, String message) {
+ invoiceReceipt.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
+ invoiceReceipt.setHcsRetryCount(0);
+ invoiceReceipt.setHcsSyncTime(LocalDateTime.now());
+ invoiceReceipt.setHcsSyncMsg(message);
+ invoiceReceiptMapper.updateById(invoiceReceipt);
+ log.info("HCS收票单推送成功:invoiceNo={},{}", invoiceReceipt.getInvoiceNo(), message);
+ }
+
+ /**
+ * 推送失败回写:failed + 重试次数+1 + 失败原因
+ */
+ public void markFailed(InvoiceReceipt invoiceReceipt, String message) {
+ invoiceReceipt.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
+ invoiceReceipt.setHcsRetryCount(invoiceReceipt.getHcsRetryCount() == null ? 1 : invoiceReceipt.getHcsRetryCount() + 1);
+ invoiceReceipt.setHcsSyncTime(LocalDateTime.now());
+ invoiceReceipt.setHcsSyncMsg(message);
+ invoiceReceiptMapper.updateById(invoiceReceipt);
+ log.error("HCS收票单推送失败:invoiceNo={},retryCount={},{}", invoiceReceipt.getInvoiceNo(), invoiceReceipt.getHcsRetryCount(), message);
+ }
+
+ /**
+ * 操作人登录名(收票单创建人,§9-U2)
+ */
+ private String resolveLoginName(InvoiceReceipt invoiceReceipt) {
+ if (invoiceReceipt.getCreateUser() == null) {
+ throw new HcsException("C109 收票单推送失败,收票单创建人未维护(invoiceNo=" + invoiceReceipt.getInvoiceNo() + ")");
+ }
+ User user = UserCache.getUser(invoiceReceipt.getCreateUser());
+ if (user == null || StringUtil.isBlank(user.getAccount())) {
+ throw new HcsException("C109 收票单推送失败,创建人登录名解析不到(createUser=" + invoiceReceipt.getCreateUser() + ")");
+ }
+ return user.getAccount();
+ }
+
+ /**
+ * 收票日期格式化(fd_bill_date 为 YYYY-MM-DD 字符串)
+ */
+ private String formatBillDate(LocalDate invoiceDate) {
+ return invoiceDate == null ? null : invoiceDate.format(BILL_DATE_FORMATTER);
+ }
+
+ /**
+ * 税率换算:TMS 整数值(如 13)÷ 100 → 小数(0.13)
+ */
+ private BigDecimal toTaxRate(BigDecimal taxRate) {
+ return taxRate == null ? null : taxRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 合同引用(fd_contract_subject / fd_contract_no)
+ */
+ private record ContractReference(String subject, String contractNo) {
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
index dc6fcbf5..74f1b422 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -33,14 +33,20 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.InvoiceApplicationMapper;
+import org.springblade.transport.mapper.InvoiceReceiptMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.InvoiceApplication;
+import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
+import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -81,9 +87,13 @@ public class HcsPushTask {
private final ProjectApplyMapper projectApplyMapper;
private final ContractManageMapper contractManageMapper;
private final PaymentApplicationMapper paymentApplicationMapper;
+ private final InvoiceApplicationMapper invoiceApplicationMapper;
+ private final InvoiceReceiptMapper invoiceReceiptMapper;
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
+ private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
+ private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
private final BladeRedis bladeRedis;
/**
@@ -109,10 +119,12 @@ public class HcsPushTask {
return;
}
try {
- // 任务顺序(设计文档 §5.0):项目 → 合同 → 付款;各类互不影响
+ // 任务顺序(设计文档 §5.0):项目 → 合同 → 付款 → 开票 → 收票;各类互不影响
log.info("定时推送HCS项目立项:{}", pushUnsyncedProjects());
log.info("定时推送HCS合同:{}", pushUnsyncedContracts());
log.info("定时推送HCS付款链:{}", pushUnsyncedPayments());
+ log.info("定时推送HCS开票登记:{}", pushUnsyncedInvoiceApplications());
+ log.info("定时推送HCS收票单:{}", pushUnsyncedInvoiceReceipts());
} finally {
if (lockHolder.equals(bladeRedis.get(HcsSyncConstant.PUSH_TASK_LOCK_KEY))) {
bladeRedis.del(HcsSyncConstant.PUSH_TASK_LOCK_KEY);
@@ -212,4 +224,67 @@ public class HcsPushTask {
}
return summary;
}
+
+ /**
+ * 扫描并推送待同步/待补偿的开票申请(C111 开票登记,含金蝶开票完成后回填),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedInvoiceApplications() {
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ List applicationList = invoiceApplicationMapper.selectList(Wrappers.lambdaQuery()
+ .eq(InvoiceApplication::getApprovalStatus, "approved")
+ .in(InvoiceApplication::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .and(wrapper -> wrapper.isNull(InvoiceApplication::getHcsRetryCount).or().lt(InvoiceApplication::getHcsRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ int successCount = 0;
+ int failCount = 0;
+ for (InvoiceApplication application : applicationList) {
+ try {
+ hcsInvoiceRegSyncSupport.pushInvoiceRegistration(application);
+ successCount++;
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ hcsInvoiceRegSyncSupport.markFailed(application, message);
+ failCount++;
+ }
+ }
+ String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", applicationList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS开票登记推送存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 扫描并推送待同步/待补偿的收票单(C109),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedInvoiceReceipts() {
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ List receiptList = invoiceReceiptMapper.selectList(Wrappers.lambdaQuery()
+ .eq(InvoiceReceipt::getApprovalStatus, "approved")
+ .in(InvoiceReceipt::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .and(wrapper -> wrapper.isNull(InvoiceReceipt::getHcsRetryCount).or().lt(InvoiceReceipt::getHcsRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ int successCount = 0;
+ int failCount = 0;
+ for (InvoiceReceipt invoiceReceipt : receiptList) {
+ try {
+ hcsReceiptBillSyncSupport.pushReceiptBill(invoiceReceipt);
+ hcsReceiptBillSyncSupport.markSynced(invoiceReceipt, "C109 收票单推送成功");
+ successCount++;
+ } catch (Exception exception) {
+ String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
+ hcsReceiptBillSyncSupport.markFailed(invoiceReceipt, message);
+ failCount++;
+ }
+ }
+ String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", receiptList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS收票单推送存在失败:{}", summary);
+ }
+ return summary;
+ }
}
From 815f3f68e53d9ec95edbd4450396eb2d8eebb1e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Sun, 27 Sep 2026 23:59:25 +0800
Subject: [PATCH 016/124] =?UTF-8?q?feat:=20HCS=E6=8E=A8=E9=80=81=E5=A4=B1?=
=?UTF-8?q?=E8=B4=A5=E6=97=A5=E5=BF=97=E8=A1=A5=E5=85=A8=E5=90=88=E8=A7=84?=
=?UTF-8?q?traceId=EF=BC=88=E7=9B=91=E6=8E=A7=E5=9F=8B=E7=82=B9=EF=BC=8C?=
=?UTF-8?q?=E4=BE=BF=E4=BA=8E=E5=90=91=E5=90=88=E8=A7=84=E6=96=B9=E6=8E=92?=
=?UTF-8?q?=E6=9F=A5=E9=93=BE=E8=B7=AF=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/support/hcs/HcsContractSyncSupport.java | 8 ++++----
.../transport/support/hcs/HcsCreditCheckSupport.java | 4 ++--
.../transport/support/hcs/HcsExeStageSyncSupport.java | 2 +-
.../transport/support/hcs/HcsInvoiceRegSyncSupport.java | 6 +++---
.../transport/support/hcs/HcsPerfRecordSyncSupport.java | 4 ++--
.../transport/support/hcs/HcsProjectSyncSupport.java | 6 +++---
.../transport/support/hcs/HcsReceiptBillSyncSupport.java | 4 ++--
.../transport/support/hcs/HcsSpecPaySyncSupport.java | 4 ++--
8 files changed, 19 insertions(+), 19 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 471f5004..4cbaf90e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -175,7 +175,7 @@ public class HcsContractSyncSupport {
HcsResult result = hcsClient.getRelativeRisk(param);
if (result == null || !result.isSuccess()) {
log.error("HCS客商风险查询失败响应:{},contractNo={},standing={},fdNumber={}",
- result == null ? "无响应" : result.getMsg(), contractManage.getContractNo(), standing, customerCode);
+ result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo(), standing, customerCode);
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsRiskVO risk = result.getData();
@@ -199,7 +199,7 @@ public class HcsContractSyncSupport {
query.setPageSize(10);
HcsResult> result = hcsClient.contractList(query);
if (result == null || !result.isSuccess()) {
- log.error("HCS合同判重查询失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ log.error("HCS合同判重查询失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
throw new HcsException("C103-3 合同判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsPageVO page = result.getData();
@@ -227,7 +227,7 @@ public class HcsContractSyncSupport {
log.info("HCS合同推送开始:{}(C103-11 更新,§9-U5 定位方式待联调)", contractManage.getContractNo());
HcsResult result = hcsClient.contractUpdate(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS合同更新失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ log.error("HCS合同更新失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
throw new HcsException("C103-11 合同更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
}
@@ -344,7 +344,7 @@ public class HcsContractSyncSupport {
*/
private String validateDraftResult(ContractManage contractManage, HcsResult result) {
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdContractId())) {
- log.error("HCS合同发起失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ log.error("HCS合同发起失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
throw new HcsException("C103-1 合同发起失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getFdContractId();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index 42605ab7..34c9be20 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -111,7 +111,7 @@ public class HcsCreditCheckSupport {
HcsResult result = hcsClient.getRelativeRisk(param);
if (result == null || !result.isSuccess()) {
log.error("HCS客商风险查询失败响应:{},paymentNo={},fdNumber={}",
- result == null ? "无响应" : result.getMsg(), payment.getPaymentNo(), customerCode);
+ result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo(), customerCode);
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsRiskVO risk = result.getData();
@@ -146,7 +146,7 @@ public class HcsCreditCheckSupport {
param.setFdApplyAmount(payment.getAppliedAmount() == null ? BigDecimal.ZERO : payment.getAppliedAmount());
HcsResult result = hcsClient.contractCreditCheck(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS额度校验失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ log.error("HCS额度校验失败响应:{},paymentNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo());
throw new HcsException("C106-1 额度校验失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsCreditCheckVO credit = result.getData();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
index 301c385c..9c1cbf6e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
@@ -99,7 +99,7 @@ public class HcsExeStageSyncSupport {
param.getFdEopExeplanOperateType(), pendingRows.size());
HcsResult result = hcsClient.exeplanAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null) {
- log.error("HCS履约规则同步失败响应:{},contractNo={}", result == null ? "无响应" : result.getMsg(), contractManage.getContractNo());
+ log.error("HCS履约规则同步失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
throw new HcsException("C103-9 履约规则同步失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsExeplanVO exeplan = result.getData();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
index f4ca4d03..b130a851 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java
@@ -179,7 +179,7 @@ public class HcsInvoiceRegSyncSupport {
log.info("HCS开票登记新增开始:applicationNo={}", application.getApplicationNo());
HcsResult result = hcsClient.invoiceRegistrationAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
- log.error("HCS开票登记新增失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ log.error("HCS开票登记新增失败响应:{},applicationNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), application.getApplicationNo());
throw new HcsException("C111-2 开票登记新增失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getFdId();
@@ -199,7 +199,7 @@ public class HcsInvoiceRegSyncSupport {
log.info("HCS开票登记更新开始:applicationNo={},fdId={}", application.getApplicationNo(), application.getHcsFdId());
HcsResult result = hcsClient.invoiceRegistrationUpdate(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS开票登记更新失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ log.error("HCS开票登记更新失败响应:{},applicationNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), application.getApplicationNo());
throw new HcsException("C111-3 开票登记更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return "C111-3 开票登记更新成功";
@@ -217,7 +217,7 @@ public class HcsInvoiceRegSyncSupport {
query.setPageSize(10);
HcsResult> result = hcsClient.invoiceRegistrationList(query);
if (result == null || !result.isSuccess()) {
- log.error("HCS开票登记判重查询失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo());
+ log.error("HCS开票登记判重查询失败响应:{},applicationNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), application.getApplicationNo());
throw new HcsException("C111-1 开票登记判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsPageVO page = result.getData();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
index 1019a314..0ed6a3b1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
@@ -94,7 +94,7 @@ public class HcsPerfRecordSyncSupport {
log.info("HCS履约记录新建开始:paymentNo={}", payment.getPaymentNo());
HcsResult result = hcsClient.perfRecordAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
- log.error("HCS履约记录新建失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ log.error("HCS履约记录新建失败响应:{},paymentNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo());
throw new HcsException("C108-1 履约记录新建失败:" + (result == null ? "响应为空" : result.getMsg()));
}
// 履约记录ID 立即持久化(调用方必须保存)
@@ -108,7 +108,7 @@ public class HcsPerfRecordSyncSupport {
log.info("HCS履约记录更新开始:paymentNo={},perfFdId={}", payment.getPaymentNo(), payment.getHcsPerfFdId());
HcsResult result = hcsClient.perfRecordUpdate(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS履约记录更新失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ log.error("HCS履约记录更新失败响应:{},paymentNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo());
throw new HcsException("C108-2 履约记录更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return "C108-2 履约记录更新成功";
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
index 52aefe9e..df3e7b80 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java
@@ -131,7 +131,7 @@ public class HcsProjectSyncSupport {
log.info("HCS项目立项推送开始:{}(C102-1 新建)", projectApply.getProjectCode());
HcsResult result = hcsClient.projectAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
- log.error("HCS项目立项新建失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ log.error("HCS项目立项新建失败响应:{},projectCode={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), projectApply.getProjectCode());
throw new HcsException("C102-1 项目立项新建失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getFdId();
@@ -153,7 +153,7 @@ public class HcsProjectSyncSupport {
log.info("HCS项目立项推送开始:{}(C102-2 更新)", projectApply.getProjectCode());
HcsResult result = hcsClient.projectUpdate(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS项目立项更新失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ log.error("HCS项目立项更新失败响应:{},projectCode={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), projectApply.getProjectCode());
throw new HcsException("C102-2 项目立项更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
}
@@ -225,7 +225,7 @@ public class HcsProjectSyncSupport {
query.setCurUser(HcsUserRefVO.ofLoginName(requireLoginName(projectApply, "C102-3")));
HcsResult> result = hcsClient.projectList(query);
if (result == null || !result.isSuccess()) {
- log.error("HCS项目立项判重查询失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
+ log.error("HCS项目立项判重查询失败响应:{},projectCode={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), projectApply.getProjectCode());
throw new HcsException("C102-3 项目立项判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsPageVO page = result.getData();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
index e070a97e..143d94e8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java
@@ -118,7 +118,7 @@ public class HcsReceiptBillSyncSupport {
log.info("HCS收票单更新开始:invoiceNo={},fdId={}", invoiceReceipt.getInvoiceNo(), invoiceReceipt.getHcsFdId());
HcsResult> result = hcsClient.updateReceiveBill(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS收票单更新失败响应:{},invoiceNo={}", result == null ? "无响应" : result.getMsg(), invoiceReceipt.getInvoiceNo());
+ log.error("HCS收票单更新失败响应:{},invoiceNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), invoiceReceipt.getInvoiceNo());
throw new HcsException("C109-2 收票单更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return "C109-2 收票单更新成功";
@@ -211,7 +211,7 @@ public class HcsReceiptBillSyncSupport {
private String extractNestedFdId(InvoiceReceipt invoiceReceipt, HcsResult> 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收票单新建失败响应:{},invoiceNo={}", result == null ? "无响应" : result.getMsg(), invoiceReceipt.getInvoiceNo());
+ log.error("HCS收票单新建失败响应:{},invoiceNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), invoiceReceipt.getInvoiceNo());
throw new HcsException("C109-1 收票单新建失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getData().getFdId();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
index 990490e6..98370a1d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
@@ -101,7 +101,7 @@ public class HcsSpecPaySyncSupport {
log.info("HCS结算付款特批更新开始:paymentNo={},specFdId={}", payment.getPaymentNo(), payment.getHcsSpecFdId());
HcsResult> result = hcsClient.updateSpecApplyPay(param);
if (result == null || !result.isSuccess()) {
- log.error("HCS结算付款特批更新失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ log.error("HCS结算付款特批更新失败响应:{},paymentNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo());
throw new HcsException("C106-4 特批更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return "C106-4 特批更新成功";
@@ -197,7 +197,7 @@ public class HcsSpecPaySyncSupport {
private String extractNestedFdId(PaymentApplication payment, HcsResult> 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结算付款特批发起失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
+ log.error("HCS结算付款特批发起失败响应:{},paymentNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo());
throw new HcsException("C106-3 特批发起失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getData().getFdId();
From b46906559b73ee4826b6d316c242802025fb0295 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 28 Sep 2026 00:02:44 +0800
Subject: [PATCH 017/124] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90=E6=94=B6?=
=?UTF-8?q?=E7=A5=A8/=E5=BC=80=E7=A5=A8=E5=8D=95=E6=9D=A1=E9=87=8D?=
=?UTF-8?q?=E6=8E=A8=E4=B8=8E=E6=89=B9=E9=87=8F=E6=8E=A8=E9=80=81=E7=AB=AF?=
=?UTF-8?q?=E7=82=B9=E6=8E=A5=E7=BA=BF=EF=BC=88=E6=AD=A4=E5=89=8D=E6=8F=90?=
=?UTF-8?q?=E4=BA=A4=E9=81=97=E6=BC=8Fswitch=E5=88=86=E6=94=AF=E4=B8=8E?=
=?UTF-8?q?=E6=B3=A8=E5=85=A5=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/controller/HcsSyncController.java | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index d04ff7a7..0363060f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -66,36 +66,42 @@ public class HcsSyncController {
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
+ private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
+ private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
private final HcsMasterDataSupport hcsMasterDataSupport;
/**
- * 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请)
+ * 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请 / invoice-application=开票申请 / invoice-receipt=收票单)
*/
@PostMapping("/sync/{bizType}/{id}")
@ApiOperationSupport(order = 1)
- @Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project=项目立项、contract=合同、payment=付款申请")
+ @Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project、contract、payment、invoice-application、invoice-receipt")
public R repush(@Parameter(description = "业务类型", required = true) @PathVariable String bizType,
@Parameter(description = "单据ID", required = true) @PathVariable Long id) {
String result = switch (bizType) {
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsProjectSyncSupport.repushProject(id);
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsContractSyncSupport.repushContract(id);
case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPaymentSyncSupport.repushPayment(id);
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsInvoiceRegSyncSupport.repushInvoiceRegistration(id);
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsReceiptBillSyncSupport.repushReceiptBill(id);
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
};
return R.data(result);
}
/**
- * 批量推送(bizType:project=项目立项 / contract=合同 / payment=付款申请;复用定时扫描逻辑,单轮批量上限 push.batch-size)
+ * 批量推送(复用定时扫描逻辑,单轮批量上限 push.batch-size)
*/
@PostMapping("/sync/{bizType}")
@ApiOperationSupport(order = 2)
- @Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project=项目立项、contract=合同、payment=付款申请")
+ @Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project、contract、payment、invoice-application、invoice-receipt")
public R pushBatch(@Parameter(description = "业务类型", required = true) @PathVariable String bizType) {
String result = switch (bizType) {
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects();
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts();
case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPushTask.pushUnsyncedPayments();
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsPushTask.pushUnsyncedInvoiceApplications();
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsPushTask.pushUnsyncedInvoiceReceipts();
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
};
return R.data(result);
From 62917c6864fd33bf4349c9549d853ba2937606ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 28 Sep 2026 00:03:22 +0800
Subject: [PATCH 018/124] =?UTF-8?q?feat:=20HCS=E5=AF=B9=E8=B4=A6=E6=9F=A5?=
=?UTF-8?q?=E8=AF=A2=E7=AB=AF=E7=82=B9=EF=BC=88=E9=A1=B9=E7=9B=AE=E7=AB=8B?=
=?UTF-8?q?=E9=A1=B9/=E5=90=88=E5=90=8C/=E5=90=88=E5=90=8C=E8=AF=A6?=
=?UTF-8?q?=E6=83=85/=E5=BC=80=E7=A5=A8=E7=99=BB=E8=AE=B0=E6=8C=89?=
=?UTF-8?q?=E7=BC=96=E5=8F=B7=E5=9B=9E=E6=9F=A5=E5=90=88=E8=A7=84=E4=BE=A7?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/HcsSyncController.java | 44 ++++++
.../support/hcs/HcsReconSupport.java | 142 ++++++++++++++++++
2 files changed, 186 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReconSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 0363060f..10c8f586 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -37,9 +37,11 @@ import org.springblade.transport.support.hcs.HcsContractSyncSupport;
import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport;
+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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -69,6 +71,7 @@ public class HcsSyncController {
private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
private final HcsMasterDataSupport hcsMasterDataSupport;
+ private final HcsReconSupport hcsReconSupport;
/**
* 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请 / invoice-application=开票申请 / invoice-receipt=收票单)
@@ -127,4 +130,45 @@ public class HcsSyncController {
public R refreshMasterData() {
return R.data(hcsMasterDataSupport.refreshAll());
}
+
+ /**
+ * 对账查询:按项目编号回查 HCS 项目立项(C102-3)
+ */
+ @GetMapping("/recon/project")
+ @ApiOperationSupport(order = 5)
+ @Operation(summary = "项目立项对账查询", description = "按项目编号回查合规侧项目立项数据")
+ public R> reconProject(@Parameter(description = "项目编号", required = true) @RequestParam String projectCode) {
+ return R.data(hcsReconSupport.reconProject(projectCode));
+ }
+
+ /**
+ * 对账查询:按合同编号回查 HCS 合同(C103-3)
+ */
+ @GetMapping("/recon/contract")
+ @ApiOperationSupport(order = 6)
+ @Operation(summary = "合同对账查询", description = "按合同编号回查合规侧合同数据")
+ public R> reconContract(@Parameter(description = "合同编号", required = true) @RequestParam String contractNo) {
+ return R.data(hcsReconSupport.reconContract(contractNo));
+ }
+
+ /**
+ * 对账查询:合同详情(C103-4,fdId 与 fdNumber 二选一)
+ */
+ @GetMapping("/recon/contract/detail")
+ @ApiOperationSupport(order = 7)
+ @Operation(summary = "合同详情对账查询", description = "按合规合同ID或合同编号回查合规侧合同详情")
+ public R> reconContractDetail(@Parameter(description = "合规合同ID") @RequestParam(required = false) String fdId,
+ @Parameter(description = "合同编号") @RequestParam(required = false) String fdNumber) {
+ return R.data(hcsReconSupport.reconContractDetail(fdId, fdNumber));
+ }
+
+ /**
+ * 对账查询:按申请单号回查 HCS 开票登记(C111-1)
+ */
+ @GetMapping("/recon/invoice-registration")
+ @ApiOperationSupport(order = 8)
+ @Operation(summary = "开票登记对账查询", description = "按开票申请单号回查合规侧开票登记数据")
+ public R> reconInvoiceRegistration(@Parameter(description = "开票申请单号", required = true) @RequestParam String applicationNo) {
+ return R.data(hcsReconSupport.reconInvoiceRegistration(applicationNo));
+ }
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReconSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReconSupport.java
new file mode 100644
index 00000000..8b45eb5c
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReconSupport.java
@@ -0,0 +1,142 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * 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.dto.HcsContractDetailQueryDTO;
+import org.springblade.hcs.dto.HcsPageQueryDTO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * HCS 对账查询支撑组件(设计文档 §5.9/§8-P5,运维收尾)
+ *
+ * 按来源单据编号回查 HCS 侧数据(项目立项/合同/合同详情/开票登记),
+ * 供对账查询页与人工排查使用;返回 HCS 原始数据结构。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsReconSupport {
+
+ private final IHcsClient hcsClient;
+
+ /**
+ * 按项目编号回查 HCS 项目立项(C102-3)
+ *
+ * @param projectCode 项目编号
+ * @return HCS 项目立项列表
+ */
+ public HcsResult> reconProject(String projectCode) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ // C102-3 入参 columns 用下划线字段名
+ query.setColumns(List.of("fd_id", "fd_num", "fd_pro_name"));
+ // 项目编号精确匹配
+ query.setConditions(Map.of("fd_num", Objects.toString(projectCode, "")));
+ query.setOffset(0);
+ query.setPageSize(50);
+ HcsResult> result = hcsClient.projectList(query);
+ return requireSuccess("项目立项对账", result);
+ }
+
+ /**
+ * 按合同编号回查 HCS 合同(C103-3)
+ *
+ * @param contractNo 合同编号
+ * @return HCS 合同列表
+ */
+ public HcsResult> reconContract(String contractNo) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ query.setColumns(List.of("fdId", "fdSubject", "fdNumber", "fdCateHierarchyId"));
+ // 合同编号精确匹配(§9-U4 查询能力联调确认)
+ query.setConditions(Map.of("fdNumber", Objects.toString(contractNo, "")));
+ query.setOffset(0);
+ query.setPageSize(50);
+ HcsResult> result = hcsClient.contractList(query);
+ return requireSuccess("合同对账", result);
+ }
+
+ /**
+ * 合同详情回查(C103-4,fdId 与 fdNumber 二选一)
+ *
+ * TODO 待联调确认:出参 data 结构 v4 未定义,原样返回实际响应(设计文档 §5.9)
+ *
+ * @param fdId 合同ID(可空)
+ * @param fdNumber 合同编号(可空)
+ * @return HCS 合同详情
+ */
+ public HcsResult> reconContractDetail(String fdId, String fdNumber) {
+ if (StringUtil.isBlank(fdId) && StringUtil.isBlank(fdNumber)) {
+ throw new HcsException("合同详情对账失败,fdId 与 fdNumber 至少传一项");
+ }
+ HcsContractDetailQueryDTO query = new HcsContractDetailQueryDTO();
+ // 合同ID(与编号二选一)
+ query.setFdId(fdId);
+ // 合同编号(与ID二选一)
+ query.setFdNumber(fdNumber);
+ HcsResult> result = hcsClient.contractDetail(query);
+ return requireSuccess("合同详情对账", result);
+ }
+
+ /**
+ * 按申请单号回查 HCS 开票登记(C111-1)
+ *
+ * @param applicationNo 开票申请单号
+ * @return HCS 开票登记列表
+ */
+ public HcsResult> reconInvoiceRegistration(String applicationNo) {
+ HcsPageQueryDTO query = new HcsPageQueryDTO();
+ query.setColumns(List.of("fdId", "fdNumber", "fdBillDate", "fdContractNumber", "fdInvoiceAmount", "fdStatus"));
+ // 单据编号精确匹配(§9-U4 查询能力联调确认)
+ query.setConditions(Map.of("fdNumber", Objects.toString(applicationNo, "")));
+ query.setOffset(0);
+ query.setPageSize(50);
+ HcsResult> result = hcsClient.invoiceRegistrationList(query);
+ return requireSuccess("开票登记对账", result);
+ }
+
+ /**
+ * 校验查询结果并返回
+ */
+ private HcsResult> requireSuccess(String scene, HcsResult> result) {
+ if (result == null || !result.isSuccess()) {
+ log.error("HCS{}查询失败响应:{}", scene, result == null ? "无响应"
+ : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()));
+ throw new HcsException("HCS" + scene + "失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ return result;
+ }
+}
From 6dd1d678440b7eb2614f7bc66d61072c2f6a7c18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 28 Sep 2026 00:10:20 +0800
Subject: [PATCH 019/124] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9EHCS=E6=8E=A5?=
=?UTF-8?q?=E5=85=A5=E5=AE=9E=E6=96=BD=E7=8A=B6=E6=80=81=E4=B8=8E=E7=A1=AE?=
=?UTF-8?q?=E8=AE=A4=E6=B8=85=E5=8D=95=EF=BC=88=E5=B7=B2=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?/=E6=9C=AA=E5=AE=9E=E7=8E=B0/=E5=BE=85=E7=A1=AE=E8=AE=A4?=
=?UTF-8?q?=E4=BA=8B=E9=A1=B9=E4=B8=8E=E4=B8=8A=E7=BA=BF=E6=A3=80=E6=9F=A5?=
=?UTF-8?q?=E6=B8=85=E5=8D=95=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../2026-09-28-HCS接入实施状态与确认清单.md | 192 ++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 doc/docs/hcs合同/2026-09-28-HCS接入实施状态与确认清单.md
diff --git a/doc/docs/hcs合同/2026-09-28-HCS接入实施状态与确认清单.md b/doc/docs/hcs合同/2026-09-28-HCS接入实施状态与确认清单.md
new file mode 100644
index 00000000..570f222a
--- /dev/null
+++ b/doc/docs/hcs合同/2026-09-28-HCS接入实施状态与确认清单.md
@@ -0,0 +1,192 @@
+# 桂物合同合规系统(HCS)接入实施状态与确认清单
+
+> **日期**:2026-09-28
+> **依据**:[2026-09-27-桂物合同合规系统HCS接入技术设计.md](./2026-09-27-桂物合同合规系统HCS接入技术设计.md)(§1 决策表 D1~D15、§8 实施拆分 P0~P5、§9 未决事项 U1~U12)
+> **分支**:`feature/hcs-api-p0`(基于 `feature/payment-apply-bfi-sync`),共 18 个提交(P0 一个 + P1~P5 分步 17 个),全量编译通过
+> **提交范围**:`a739211`(P0 骨架)→ `61b2c62`(P5 对账端点)
+
+本文档回答三个问题:**哪些已经实现、哪些没有实现、哪些需要确认**。供代码评审、联调准备与上线检查使用。
+
+---
+
+## 1. 已实现清单
+
+### 1.1 客户端模块 blade-third-party-api/blade-hcs-api(P0,包 `org.springblade.hcs`)
+
+| 组件 | 内容 | 状态 |
+|---|---|---|
+| `feign/IHcsClient` | `@FeignClient(name="HCS", url="${thirdParty.hcs.baseUrl}")`,**24 个业务接口端点全部建成**(C101-1、C102-1/2/3、C103-1/2/3/4/5/6/7/9/11/12、C106-1/3/4、C108-1/2、C109-1/2、C111-1/2/3)+ OAuth2 getToken;URI 均可被 `thirdParty.hcs.*Url` 覆盖 | ✅ |
+| `interceptor/HcsAuthInterceptor` | oauth2(URL 追加 access_token)/ basic(Authorization 头)双模式并存,切换只改配置;取令牌请求有路径+空令牌双重守卫 | ✅ |
+| `token/HcsTokenService` | getToken → Redis(`blade:third:hcs:token`,120s 过期缓冲),`refreshAccessToken()` 供 401 强刷重放 | ✅ |
+| `config/HcsProperties` | `third-party.hcs.*` 全量配置(§7 清单 + master-data.ttl-minutes + perf-record.xform-id 两个落实性补充),含默认值 | ✅ |
+| `config/HcsFeignClientConfig` | connect 10s / read 60s,空拦截器覆盖全局,**无 Retryer**(D7) | ✅ |
+| `dto/`(49 个文件) | 24 接口全量请求/响应 DTO + `HcsResult`/`HcsPageVO`/引用对象等公共结构;字段按《桂物合同集成接口逻辑梳理》核对,双层嵌套(C106-3/4、C109-1/2)以 `HcsResult>` 承接 | ✅ |
+| `util/HcsPageUtil` | offset 翻页(从 0 起),默认上限 50 页 | ✅ |
+| `exception/HcsException` | 客户端异常 | ✅ |
+
+> **口径说明**:D4 明确列出 23 个接口,§2.2 又将 C103-5 附件下载标注"备用接入";为对齐"24 接口"口径并覆盖文档全部接口,C103-5 一并建了端点(当前无业务调用方)。
+> **技术说明**:`@ConfigurationProperties` 注解前缀为规范要求的 `third-party.hcs`(大写驼峰前缀 Spring Boot 拒绝启动),YAML 仍按设计文档用 `thirdParty.hcs.*`,宽松绑定等价生效——与 BFI 现状一致。
+
+### 1.2 业务侧支撑组件(`org.springblade.transport.support.hcs`,12 个)
+
+| 组件 | 内容 | 状态 |
+|---|---|---|
+| `HcsProjectSyncSupport` | C102-3 判重(命中回填跳过)→ C102-1 新建 / C102-2 更新;万元×10000 换算;合规 fdId 回填;手动重推 | ✅ |
+| `HcsContractSyncSupport` | 链路编排:①C101-1 甲乙双方风险校验(留痕 check_log + `hcs_risk_*`,黑名单且 block-blacklist 阻断)→ ②附件转传 → ③C103-3 判重 → ④C103-1 起草/C103-11 更新(先持久化 fdId 再走后续)→ ⑤履约规则同步 | ✅ |
+| `HcsExeStageSyncSupport` | C103-9 行级同步:聚合 row_op 非 none 行(add 不带 fdId、update/delete 必带),整体操作首推 add/后续 update;fdExeplanId 与阶段 fdId 按行回填持久化;软删行经原生 SQL 照常上报 delete | ✅ |
+| `HcsCreditCheckSupport` | 付款提交事务内同步校验:C101-1 黑名单阻断提交;C106-1 超额不拦截、置 over + spec=need 转特批(超额时 success 仍 true,以 fdIsCredit 为准) | ✅ |
+| `HcsSpecPaySyncSupport` | C106-3 发起(双层嵌套 fdId → hcs_spec_fd_id,状态 processing)/ C106-4 更新;特批附件 fdEntityKey=fd_spc_approval_attach、发起带 fdBindType=ADD | ✅ |
+| `HcsPerfRecordSyncSupport` | C108-1 新建(fdId 回填 hcs_perf_fd_id)/ C108-2 更新(实际金额);关联履约阶段 fd_id;附件 fd_plan_att | ✅ |
+| `HcsPaymentSyncSupport` | 付款链编排(特批→履约记录)+ 整体状态回写 + 特批结果人工确认(§9-U1 过渡) | ✅ |
+| `HcsInvoiceRegSyncSupport` | C111-1 判重 / C111-2 新增 / C111-3 票据日期回填(金蝶回写发票最新 invoiceDate);合同编号经应收结算链路回溯 | ✅ |
+| `HcsReceiptBillSyncSupport` | C109-1 新建(双层嵌套 fdId)/ C109-2 更新;合同回溯 + 人工补录兜底;税率 ÷100;fd_bill_date 为 YYYY-MM-DD 字符串 | ✅ |
+| `HcsAttachmentSupport` | 附件解析 → 文件服务下载 → C103-2 转传 → 合规附件ID 映射缓存(7 天,未命中重传) | ✅ |
+| `HcsMasterDataSupport` | C103-6/7/12 模板/分类/币种全量拉取 + Redis 缓存(TTL 可配)+ template-mapping 存在性校验 | ✅ |
+| `HcsReconSupport` | 对账回查:C102-3 / C103-3 / C103-4 / C111-1 | ✅ |
+
+**字段组装规范**:所有 Support 按 §3.4 规范逐字段标注外部字段中文名与 TMS 取值来源(照 `pushArBill` 惯例),待联调字段带 `TODO 待确认` 并标注 §9 编号。
+
+### 1.3 触发与任务(D9 / ADR-0001)
+
+| 项 | 内容 | 状态 |
+|---|---|---|
+| 审批零埋点 | 项目立项/合同:`/approve` + MK 回调(4 处);付款申请:`/approve` + MK PaymentHandler;开票申请/收票单:`changeStatus` 到 approved 统一置 `hcs_sync_status=unsynced` | ✅ |
+| 付款推进触发 | 金蝶 A103-2 付款结果回写(`applyPayApplyResult`)paidAmount 变化时,已有履约记录ID 则置待同步 → 扫描走 C108-2 | ✅ |
+| 金蝶开票联动 | `syncKingdeeInvoiceResult` 推进 invoiced 时置待同步 → 扫描走 C111-3 回填票据日期(D12) | ✅ |
+| 合同审批置行标记 | 合同审批通过将未同步履约规则行置 row_op=add(ADR-0002) | ✅ |
+| `HcsPushTask` | `@Scheduled`(cron/开关可配,默认关),顺序:项目→合同→付款→开票→收票;Redis 分布式锁(setIfAbsent + 持有值比对释放,TTL 240s);批量 push.batch-size(默认 50);失败 failed+retry_count+1,重试上限 max-retry(默认 5)超限人工 | ✅ |
+
+### 1.4 手动端点(`HcsSyncController` + `ContractExeStageController`)
+
+| 端点 | 用途 |
+|---|---|
+| `POST /hcs/sync/{bizType}/{id}` | 单条重推(bizType:project / contract / payment / invoice-application / invoice-receipt),重置 unsynced+清零计数后立即推送 |
+| `POST /hcs/sync/{bizType}` | 批量推送一轮(复用扫描逻辑) |
+| `POST /hcs/spec-pay/confirm?id=&result=approved|rejected` | 特批结果人工确认(仅 processing 可确认) |
+| `POST /hcs/master-data/refresh` | 刷新主数据缓存并校验模板映射 |
+| `GET /hcs/recon/project?projectCode=` | 项目立项对账(C102-3) |
+| `GET /hcs/recon/contract?contractNo=` | 合同对账(C103-3) |
+| `GET /hcs/recon/contract/detail?fdId=|fdNumber=` | 合同详情对账(C103-4) |
+| `GET /hcs/recon/invoice-registration?applicationNo=` | 开票登记对账(C111-1) |
+| `GET /contract-exe-stage/list?contractId=` | 履约规则行查询 |
+| `POST /contract-exe-stage/submit?contractId=` | 履约规则行保存(行级差异标记) |
+| `POST /contract-exe-stage/remove?id=` | 履约规则行删除(软删+标记) |
+
+### 1.5 数据模型与配置
+
+| 交付物 | 内容 | 状态 |
+|---|---|---|
+| `doc/sql/transport/blade_hcs_sync_fields_20260927.sql` | 5 张业务表统一字段组(hcs_fd_id/sync_status/retry_count/sync_time/sync_msg),可重复执行 | ✅ |
+| `blade_hcs_contract_fields_20260927.sql` | 合同扩展字段(exeplan_id/risk_level/black_status/risk_time)+ `blade_hcs_check_log` 建表 | ✅ |
+| `blade_hcs_exe_stage_20260927.sql` | `blade_contract_exe_stage` 建表 | ✅ |
+| `blade_hcs_payment_fields_20260927.sql` | 付款申请特批字段组 + hcs_perf_fd_id | ✅ |
+| `blade_hcs_receipt_fields_20260927.sql` | 收票单合同补录字段(hcs_contract_id/no) | ✅ |
+| 实体字段 | ProjectApply / ContractManage / PaymentApplication / InvoiceApplication / InvoiceReceipt 已加 hcs 字段;新实体 HcsContractExeStage / HcsContractCheckLog | ✅ |
+| Nacos 配置段 | `doc/nacos/third-party-api.yaml` 新增 `thirdParty.hcs` 完整节点(凭据走 `${HCS_*:}` 环境变量注入) | ✅ |
+
+---
+
+## 2. 未实现清单
+
+### 2.1 按约束明确不做
+
+| 项 | 说明 |
+|---|---|
+| C104 收款流水(C104-1/2/3)、C107 认领记录(C107-1/2/3) | ADR-0003 边界排除:推送方与链路待定,TMS 不承担;Feign 端点也未建 |
+| 付款申请/开票/收票的 MK 处理器**补齐** | 约束"他人负责"。注:现状 PaymentHandler 实际已由他人注册(探索发现),本次仅按零埋点约定补了 1 行状态置位,未动处理器归属;开票/收票仍无 MK 处理器,直批入口已接线,未来新处理器只要置 approved 即被扫描自动覆盖 |
+| 前端页面改动 | 列表页"推送合规/重推"按钮、履约规则页签、特批结果确认入口、对账查询页——本次只交付后端端点,前端(tms-erp-web)未动 |
+
+### 2.2 有意暂缓(待联调/待确认后补)
+
+| 项 | 现状 | 补充条件 |
+|---|---|---|
+| C103-1 报文附件挂载 | 附件已转传并缓存 fdId,但 C103-1 请求不带附件字段 | §9-U8 合规方明确挂载字段后接线 |
+| 收票单作废推送 C109-2 | `voidBill` 不触发推送 | v4 收票更新入参无作废状态字段,口径确认后补 |
+| C106-4 特批更新的自动触发 | 仅手动重推且特批流程已存在时走 C106-4 | "单据变更"的判定口径确认后补自动触发 |
+| C103-4 详情数据结构解析 | `HcsResult` 原样承接 | 联调拿到实际结构后补充 DTO |
+| `blade_hcs_check_log` 查询界面/端点 | 仅写入留痕 | 需要排查页面时补查询端点 |
+| 附件下载 C103-5 | 仅建 Feign 端点 | 对账/排查需要时接入业务 |
+
+### 2.3 简化实现(功能可用但口径粗,联调后细化)
+
+| 项 | 现状 |
+|---|---|
+| 付款申请与履约阶段关联 | 取合同下第一条 perform_type=payment 且已同步的规则行(fd_id/fd_property),无按款项性质等精确匹配 |
+| C103-9 返回阶段ID 匹配 | 返回行数与请求一致时按行序对应;不一致时按业务属性组合键兜底,仍匹配不到的 add 行保持待同步重试 |
+| 收票"票据变更"触发 | 无自动变更检测(收票单无变更流程),变更场景走手动重推 |
+| C111-2 新增后金蝶已 invoiced 的竞态 | 同一轮紧跟一次 C111-3 兜底回填 |
+
+---
+
+## 3. 需要确认清单
+
+### 3.1 需要合规方/联调确认(对应设计文档 §9,代码均已占位不阻塞)
+
+| # | 事项 | 当前代码/配置占位 |
+|---|---|---|
+| U1 | 特批流程结果回传方式 | 人工确认端点 `POST /hcs/spec-pay/confirm`(过渡方案);若有回调/查询接口需改造 |
+| U2 | 组织/人员 fdId 体系与登录名同源性 | 引用一律先传名称或登录名(经办人/创建人经 UserCache 解析 account) |
+| U3 | 字典值映射:fd_settlement_type、fd_invoice_type、付款用途/性质/类型、CONTRACT_STATUS 是否必传、fdBlackStatus 取值(示例 no) | Nacos 各 mapping 空占位;未配置时该字段不传并 warn(黑名单判定按 "yes" 等值,联调校准);CONTRACT_STATUS 一期不传 |
+| U4 | 判重 conditions 是否支持 fd_num/fdNumber 精确查询;HCS 是否按 fd_kingdee_id 幂等 | 按文档 conditions 精确匹配实现,联调验证;判重查询 C102-3 额外携带 curUser(文档示例如此) |
+| U5 | C103-11 updateDraft 如何定位已有合同 | 入参同发起;补偿重试路径会重复调 C103-11,需确认幂等性 |
+| U6 | 相对方 fdNumber 编码体系、fdType(inner/外部)判定、C101-1 用客商哪个编码 | 按甲乙方名称反查 CustomerArchive.customerCode,反查不到留空/跳过校验;fdType 不传 |
+| U7 | 合规模板ID清单 | `contract.template-mapping` 空;未配置时合同推送失败并提示配置项 |
+| U8 | 合同附件向 HCS 的传递方式 | 已转传+缓存 fdId,挂载点确认后接线 |
+| U9 | 立项 templateCode 与 C108 fd_xform_id 环境值 | `project.template-code`、`perf-record.xform-id` 空占位;未配置时立项/履约记录推送失败(xform_id 未配置则不传) |
+| U10 | 开票申请→合同关联字段 | 现实现:开票申请 → 应收结算关联表 → FormalSettlement.contractNo;回溯不到推送失败提示补录 |
+| U11 | fd_ap_bill_src_subject 关联键 | 一期不传 |
+| U12 | C104/C107 推送方及链路 | 范围外备忘(ADR-0003) |
+
+### 3.2 需要用户(我方)确认/决策
+
+| # | 事项 | 说明与建议 |
+|---|---|---|
+| 1 | **前端配套改动归属** | 推送/重推按钮、履约规则维护页签、特批结果确认入口、对账查询页需要前端开发。是前端团队按本文 1.4 端点对接,还是需要后端继续补前端? |
+| 2 | **菜单与接口权限** | `HcsSyncController`(/hcs/**)与 `ContractExeStageController`(/contract-exe-stage/**)当前未加 `@PreAuth(menu)`(新功能无菜单,登录即可访问)。是否需要建菜单并补权限注解? |
+| 3 | **特批结果人工确认的业务归属** | 谁负责在什么时机确认(财务?项目部?);确认后是否需要同步更新付款申请的其他业务状态(如是否放行后续付款) |
+| 4 | **C108-2 履约记录更新的触发范围** | 当前仅金蝶 A103-2 付款结果回写触发;付款处理单记录(applyPayBillRecord)等其他 paidAmount 推进路径是否也要触发? |
+| 5 | **C106-4 特批更新的触发场景** | "单据变更且特批流程存在时调用"——付款申请在特批中(processing)允许哪些变更?当前未做自动触发 |
+| 6 | **收票作废是否推 HCS** | v4 无作废字段,当前不推;若合规方要求作废同步需其提供口径 |
+| 7 | **付款与履约规则行的关联口径** | 当前取合同第一条付款类型规则行;若一笔付款需精确对应某一期/某性质阶段,需明确匹配规则(如按款项性质、按期次) |
+| 8 | **履约规则行维护的状态限制** | 当前维护接口未限制合同状态(草稿/变更中均可编辑);是否要求仅草稿与 change_reviewing 可编辑? |
+| 9 | **实体命名偏差** | 留痕表按设计叫 `blade_hcs_check_log`,实体类名为 `HcsContractCheckLog`(P1 命名,含付款校验留痕后名字略窄)。是否改名 `HcsCheckLog`?影响面小(1 实体+1 Mapper) |
+| 10 | **C106-3 的 fd_id 取值** | 设计 §5.5 表格映射为 paymentNo(单据号),接口文档 §8.2 语义为"来源付款单ID(主键)"。当前按设计传 paymentNo,联调时需确认以哪个为准 |
+
+---
+
+## 4. 上线前检查清单(Nacos `thirdParty.hcs.*`)
+
+| 配置 | 必填 | 说明 |
+|---|---|---|
+| `base-url` / `auth-type` / `app-key` / `app-secret`(或 basic 凭据) | ✅ | 合规方测试环境地址与凭据(P0 依赖项) |
+| `push.sync-enabled` | — | 总开关,**默认 false**;联调稳定后灰度打开 |
+| `push.cron` / `batch-size` / `max-retry` | — | 默认每 5 分钟 / 50 / 5 |
+| `project.template-code` | ✅(推立项前) | U9 环境值 |
+| `contract.template-mapping` | ✅(推合同前) | U7 模板清单,按合同类别配置 |
+| `contract.settlement-type-mapping` | 联调 | U3;未配置 fdSettlementType 不传 |
+| `invoice.currency-id-mapping`(RMB) | ✅(推开票前) | HCS 币种 fdId,可经 `/hcs/master-data/refresh` 后从币种缓存取值 |
+| `invoice.invoice-type-mapping` / `receipt-bill.invoice-type-mapping` | 联调 | U3 字典值 |
+| `spec-pay.payment-purpose/nature/type` | 联调 | U3 字典值 |
+| `perf-record.xform-id` | 推履约记录前 | U9 环境值(不配置则不传该字段) |
+| `attachment.file-download-base` | ✅(附件转传启用时) | TMS 文件服务基址 |
+| `risk.block-blacklist` / `credit.require-contract-synced` 等 | — | 有默认值,按业务松紧调整 |
+| SQL 脚本执行 | ✅ | 5 个 HCS SQL 脚本按文件名顺序执行(均幂等可重跑) |
+
+---
+
+## 5. 遗留 TODO 代码索引
+
+| 位置 | 内容 |
+|---|---|
+| `HcsContractSyncSupport#buildXformFields` | CONTRACT_STATUS 是否必传(§9-U3) |
+| `HcsContractSyncSupport#transferAttachmentsIfEnabled` | 附件挂载点(§9-U8) |
+| `HcsContractSyncSupport#updateContract` | C103-11 定位方式(§9-U5) |
+| `HcsContractSyncSupport#buildSigners` | 相对方编号/ fdType 判定(§9-U6) |
+| `HcsExeStageSyncSupport#backfill` | fdChangeStages 对应关系(行序/属性匹配待联调校准) |
+| `HcsExeStageSyncSupport#markRowSynced` | add 行未匹配到阶段ID 时保持待同步重试的重复建行风险 |
+| `HcsPerfRecordSyncSupport#buildFormData` | fd_property/关联阶段口径 |
+| `HcsInvoiceRegSyncSupport#requireContractNumber` | 应收结算回溯口径(§9-U10) |
+| `HcsInvoiceRegSyncSupport#latestInvoiceDate` | 票据日期取值口径(发票日期 vs 回写时间) |
+| `HcsReceiptBillSyncSupport#buildSaveParam` | fd_ap_bill_src_subject(§9-U11);作废推送缺接口依据 |
+| `HcsTokenVO` / `HcsTokenService#cacheToken` | getToken 响应字段与 expires_in 单位(v4 未给出示例) |
+| `HcsReconSupport#reconContractDetail` | C103-4 出参结构 |
From fc5bca2b108a5a6f714893a016737435eec17fef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 28 Sep 2026 00:21:49 +0800
Subject: [PATCH 020/124] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9EHCS=E5=90=88?=
=?UTF-8?q?=E5=90=8C=E6=8E=A8=E9=80=81=E4=B8=9A=E5=8A=A1=E6=B5=81=E7=A8=8B?=
=?UTF-8?q?=E6=A2=B3=E7=90=86=EF=BC=88=E5=AE=A1=E6=89=B9=E9=9B=B6=E5=9F=8B?=
=?UTF-8?q?=E7=82=B9/=E5=AE=9A=E6=97=B6=E6=89=AB=E6=8F=8F/=E4=BA=94?=
=?UTF-8?q?=E6=AD=A5=E6=8E=A8=E9=80=81=E9=93=BE/=E5=B1=A5=E7=BA=A6?=
=?UTF-8?q?=E8=A7=84=E5=88=99=E8=A1=8C=E7=BA=A7=E5=90=8C=E6=AD=A5/?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E4=B8=8E=E8=A1=A5=E5=81=BF=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../2026-09-28-HCS合同推送业务流程梳理.md | 235 ++++++++++++++++++
1 file changed, 235 insertions(+)
create mode 100644 doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md
diff --git a/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md b/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md
new file mode 100644
index 00000000..269176cb
--- /dev/null
+++ b/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md
@@ -0,0 +1,235 @@
+# HCS 合同推送业务流程梳理(现状实现)
+
+> **日期**:2026-09-28
+> **范围**:仅合同部分(C103-1 发起 / C103-11 更新 / C103-3 判重 / C103-9 履约规则 / C101-1 风险校验 / C103-2 附件转传),项目立项、付款、开票、收票链路不在本文
+> **依据**:[2026-09-27-桂物合同合规系统HCS接入技术设计.md](./2026-09-27-桂物合同合规系统HCS接入技术设计.md) §5.2/§5.3/§6、[ADR-0001](../../adr/0001-hcs推送采用定时扫描模式.md)、[ADR-0002](../../adr/0002-履约规则独立数据表.md)
+> **状态清单**:[2026-09-28-HCS接入实施状态与确认清单.md](./2026-09-28-HCS接入实施状态与确认清单.md)
+
+---
+
+## 1. 链路总览
+
+```mermaid
+flowchart TD
+ A[合同起草/变更 履约规则页签维护行] --> B{审批入口}
+ B -->|页面 /approve| C[ContractManageServiceImpl.approve]
+ B -->|MK 审批中台回调| D[ContractHandler.approveFromProcess]
+ C --> E[approvalStatus = approved / change_approved hcs_sync_status = unsynced 履约规则未同步行置 row_op=add 合同主文件置 approved=true]
+ D --> E
+ E --> F{HcsPushTask @Scheduled sync-enabled 开关 + 分布式锁}
+ F -->|扫 unsynced / failed且未超限| G[HcsContractSyncSupport.pushContract]
+ G --> G1[① C101-1 客商风险校验]
+ G --> G2[② 附件转传 C103-2]
+ G --> G3{③ hcs_fd_id 已存在?}
+ G3 -->|否| G4[C103-3 判重 → C103-1 起草]
+ G3 -->|是| G5[C103-11 更新]
+ G4 --> H[回填 hcs_fd_id 置 synced]
+ G5 --> H
+ H --> I[④ 履约规则 C103-9 行级同步]
+ I --> J[回填 fdExeplanId 与阶段 fdId]
+ I -->|任一步失败| K[failed + retry_count+1 + msg 下轮扫描补偿,超限人工重推]
+ G1 -->|黑名单且阻断开启| K
+ G2 -->|转传失败| K
+```
+
+一句话概括:**审批只落状态(零埋点),定时任务扫描 `unsynced` 后按"风险校验 → 附件转传 → 判重/起草或更新 → 履约规则"四步推送,失败自动补偿,超限人工重推。**
+
+---
+
+## 2. 阶段一:数据准备与审批(不调 HCS)
+
+### 2.1 履约规则行维护(合同表单"履约规则"页签)
+
+- 端点:`GET/POST /contract-exe-stage/list|submit|remove`(`ContractExeStageController` → `ContractExeStageServiceImpl`)
+- 数据表:`blade_contract_exe_stage`(ADR-0002),合同维度维护
+- 行级差异标记(保存时自动判定):
+
+| 操作 | 生成的标记 | 说明 |
+|---|---|---|
+| 新增行 | `row_op=add`,`hcs_sync_status=unsynced` | 待随合同链路首推 |
+| 编辑未同步行(无 hcs_stage_fd_id) | 保持 `add` | 尚未推过 HCS,仍按新增上报 |
+| 编辑已同步行(有 hcs_stage_fd_id) | 置 `update` | C103-9 行级更新必须带合规阶段ID |
+| 删除已同步行 | 软删(is_deleted=1)+ 置 `delete` | C103-9 凭 hcs_stage_fd_id 上报删除;**不可物理删除** |
+| 删除未同步行 | 仅软删,`row_op` 不变 | HCS 侧无此行,无需上报 |
+
+### 2.2 审批通过(两条入口,零埋点)
+
+| 入口 | 代码位置 | 共同动作 |
+|---|---|---|
+| 页面审批 | `ContractManageController /approve` → `ContractManageServiceImpl.approve` | ① `approvalStatus = approved`(变更单为 `change_approved`);② `hcs_sync_status = unsynced`(**唯一的 HCS 埋点,不调任何接口**,ADR-0001);③ 履约规则未同步行批量置 `row_op=add`(ADR-0002);④ 变更单合并变更附件、清空变更字段;⑤ 合同主文件 JSON 置 `approved=true`(`markContractFilesApproved`,供附件转传筛选) |
+| MK 审批中台回调 | `MkProcessBizHandlers.ContractHandler.approveFromProcess` | 同上 ①②(幂等:已 approved/change_approved 直接返回) |
+
+> 重审/变更场景不重置 `unsynced` 就会漏推:首次入库靠数据库默认值 `unsynced`,但已 `synced` 的单据变更后必须重新置位才能再次进入推送队列。
+
+---
+
+## 3. 阶段二:定时扫描(`HcsPushTask`)
+
+| 项 | 实现 |
+|---|---|
+| 调度 | `@Scheduled(cron = "${thirdParty.hcs.push.cron:0 */5 * * * ?}")`,总开关 `push.sync-enabled` **默认 false**(Nacos 灰度打开) |
+| 多实例防重 | Redis 分布式锁 `blade:third:hcs:push:task:lock`,`setIfAbsent` 抢锁 + 持有值比对释放,TTL 240s |
+| 任务顺序 | 项目 → **合同** → 付款 → 开票 → 收票,各类独立 try/catch 互不影响 |
+| 合同扫描条件 | `approvalStatus IN (approved, change_approved)` 且 `hcs_sync_status IN (unsynced, failed)` 且(`hcs_retry_count` 为空 或 `< push.max-retry`),`LIMIT push.batch-size`(默认 50),按主键序 |
+| 单条处理 | 调 `HcsContractSyncSupport.pushContract`;异常统一 `markFailed`(`failed` + 重试计数+1 + msg),不中断本轮其余单据 |
+
+---
+
+## 4. 阶段三:单条推送链(`pushContract` 五步)
+
+### ① 客商风险校验(开关 `contract.require-risk-check`,默认开)
+
+1. 甲方、乙方各查一次:按名称反查 `CustomerArchive`(fullName/shortName 精确匹配)取客商编号,作为 C101-1 的 `fdNumber`(**用哪个编码待联调,§9-U6**;反查不到 → 留痕 `skip`,不阻断)。
+2. 调 C101-1 `getRelativeRisk` → `fdBlackStatus` / `fdRiskLevel`。
+3. 留痕:`blade_hcs_check_log`(biz_type=contract, check_type=risk, result=pass/fail/skip, trace_id)+ 合同 `hcs_risk_level`(取先查到的非空值)/ `hcs_black_status`(任一方黑名单记 yes)/ `hcs_risk_time`。
+4. **阻断条件**:黑名单(fdBlackStatus=yes,取值口径待联调 §9-U3)且 `risk.block-blacklist=true` → 抛异常,合同落 `failed`,msg="客商黑名单,禁止推送"。
+
+### ② 附件转传(开关 `attachment.transfer-enabled`,默认开)
+
+- 范围:合同主文件 `contractFileJson` 中 `approved=true` 的文件 + 其它附件 `attachmentsJson`(去重键照 `attachmentKey` 惯例:url|link|fileUrl|downloadUrl|uid|originalName|name)。
+- 流程:按元数据 URL 下载(相对路径拼 `attachment.file-download-base`)→ C103-2 multipart 上传(fdAnonymous=false, fdLoginName=承办人)→ 取合规附件 fdId。
+- 映射缓存:Redis `blade:third:hcs:att:{attachmentKey}` → fdId,TTL 7 天,命中不重传;未命中重传(重复上传仅浪费存储无业务风险)。
+- **任一附件失败 → 整单失败进补偿重试**(§6.5)。
+- ⚠️ **挂载点未定(§9-U8)**:v4 的 C103-1 入参未定义附件字段,当前 fdId 仅转传+缓存,C103-1 报文不带附件;合规方确认后接线。
+
+### ③ 合同起草 / 更新(先判重)
+
+**首推(`hcs_fd_id` 为空)**:
+
+1. 判重:C103-3 `conditions={"fdNumber": contractNo}`(查询能力待联调 §9-U4)→ 命中直接回填已有合规合同ID 置 `synced`,跳过新建。
+2. 未命中 → C103-1 `draft`,顶层与字段:
+ - `fdTemplateId` ← `contract.template-mapping[contractCategory]`(**未配置则推送失败并提示,§9-U7**)
+ - `fdOuterSystem` ← `src-system`(约定值 TMS,D14);`fdLoginName` ← 承办人(handlerUserId → UserCache 登录名,解析不到失败);`saveType=submit`
+ - `xformFields`(值一律字符串传,时间字段带 `fdType=timestamp`):
+
+| 字段 | TMS 来源 | 备注 |
+|---|---|---|
+| fdNumber / CONTRACT_NO | contractNo | 判重候选键 |
+| fdSubject / CONTRACT_NAME | contractName | |
+| fdAmount / AMOUNT | contractAmount | 字符串传值 |
+| fdCurrencyId / CURRENCY | settlementCurrency → `currency-name-mapping` | 默认 RMB→人民币 |
+| fdSettlementType | settlementTypeMapping[settlementMode] | 未配置不传并 warn(§9-U3) |
+| fdSepcailContract | 固定 "false" | D15,原文拼写笔误必须沿用 |
+| CONTRACT_TYPE | contractCategory | |
+| PARTY_A / PARTY_B | partyA / partyB | 名称字符串 |
+| SIGN_DATE / fdEstimateStartTime / fdEstimateEndTime | signDate / startDate / endDate | timestamp |
+| CREATE_TIME / fdCreateTime / fd_syn_time | createTime / 推送时刻 | timestamp |
+| CREATE_USER_NAME | handlerUserName | 承办人名称 |
+| CONTRACT_STATUS | 不传 | 是否必传待联调(§9-U3) |
+
+ - `signer[]`:甲/乙各一条 `{fdStanding, fdName=partyA/partyB, fdNumber=反查客商编号(查不到留空)}`;`fdType`(inner/外部)不传(§9-U6)
+3. 成功返回 `data.fdContractId` → **立即 markSynced 持久化**(`hcs_fd_id`、`synced`、retry=0、sync_time、msg)——履约规则同步若失败,下轮凭已持久化的 fdId 走更新补偿,不会在 HCS 重复建合同。
+
+**更新(`hcs_fd_id` 非空,即变更单 change_approved 或补偿重试)**:C103-11 `updateDraft`,入参同发起;响应无 data,success 判断(**HCS 侧如何定位已有合同待联调,§9-U5**)。
+
+### ④ 履约规则 C103-9 行级同步(`HcsExeStageSyncSupport.syncExeStages`)
+
+前置:合同已推送(`hcs_fd_id` 非空)。
+
+1. 取行:`selectSyncRows` 原生 SQL 查 `row_op <> 'none'` 的行(**含软删行**,delete 行需照常上报;无待同步行直接返回提示)。
+2. 组装:顶层 `fdContractId` = **HCS 合同ID(hcs_fd_id)**、`fdOuterSystem`、`fdLoginName`(承办人)、`fdEopExeplanOperateType` = 首次 `add`(无 hcs_exeplan_id)/ 后续 `update`、`fdExeplanId` = 已回填的计划ID(有则传)。
+3. 行字段:`fdPerformType`(payment/collection)、`fdFundNature`、`fdFundAmount`(元)、`fdFundDate`(毫秒)、`fdInvoiceRule`、`fdInvoiceCycle`、`fdFundType`(行值为空取 `exe-stage.fund-type` 默认 byAmount)、`fdIsFinish`、`fdExtendContent="{}"`;**update/delete 行必须带 `fdId` = `hcs_stage_fd_id`**,add 行不带。
+4. 回填(合规返回的 fdId **必须持久化**,ADR-0002):
+ - `data.fdExeplanId` → `blade_contract_manage.hcs_exeplan_id`
+ - `data.fdChangeStages[].fdId` → 行 `hcs_stage_fd_id`:返回行数与请求一致时**按行序对应**;不一致时按业务属性组合键(performType|fundNature|amount|date)兜底匹配
+ - 成功行:`row_op=none`、`hcs_sync_status=synced`
+ - **add 行未匹配到阶段ID**:保持待同步(failed + msg),下轮重试——存在重复建行风险,联调时校准对应关系
+5. 同步失败 → 抛异常 → 合同 `markFailed`(fdId 已保留,下轮走 C103-11 → C103-9 继续补偿)。
+
+### ⑤ 结果
+
+全链成功:合同 `synced`,返回汇总(如 "C103-1 合同发起成功;C103-9 履约规则同步成功(add,3 行)")。
+
+---
+
+## 5. 状态机与失败补偿
+
+```
+ 审批通过/MK回调 推送成功
+ (未推送)unsynced ──────────→ 扫描推送 ──────────→ synced
+ ↑ │
+ │ 人工重推(重置unsynced+清零) │ 失败
+ │ ↓
+ └────────────── failed (retry_count+1)
+ retry < max-retry → 下轮自动重试
+ retry ≥ max-retry → 仅人工重推
+```
+
+- 权威状态在 TMS 侧字段(`hcs_sync_status`/`hcs_sync_msg`),HCS 不回写业务状态;**排查推送问题以此为准,不以审批日志为准**。
+- 失败日志均携带合规 traceId(`msg=xx, traceId=xx`),排查时提供给合规方。
+
+---
+
+## 6. 手动操作与对账入口
+
+| 端点 | 用途 |
+|---|---|
+| `POST /hcs/sync/contract/{id}` | 单条重推:重置 unsynced + retry=0 后立即推送(仅 approved/change_approved 允许) |
+| `POST /hcs/sync/contract` | 批量推一轮(复用扫描逻辑) |
+| `POST /hcs/master-data/refresh` | 刷新模板/分类/币种缓存,并校验 template-mapping 中模板ID 是否存在(仅告警) |
+| `GET /hcs/recon/contract?contractNo=` | 按合同编号回查 HCS 侧合同(C103-3) |
+| `GET /hcs/recon/contract/detail?fdId=|fdNumber=` | 合同详情回查(C103-4,出参结构联调后明确) |
+| `GET /contract-exe-stage/list?contractId=` | 查合同履约规则行 |
+
+---
+
+## 7. 数据落点汇总
+
+| 表 | 字段 | 写入时机 |
+|---|---|---|
+| `blade_contract_manage` | hcs_fd_id | C103-1/C103-3 判重命中后立即持久化 |
+| | hcs_sync_status / hcs_sync_time / hcs_sync_msg / hcs_retry_count | 每次推送成功/失败回写 |
+| | hcs_exeplan_id | C103-9 返回 fdExeplanId 后回填 |
+| | hcs_risk_level / hcs_black_status / hcs_risk_time | 每次推送前风险校验后留痕 |
+| `blade_contract_exe_stage` | row_op | 维护接口/审批通过置 add;同步成功置 none |
+| | hcs_stage_fd_id / hcs_sync_status / hcs_sync_msg | C103-9 回填与结果回写 |
+| `blade_hcs_check_log` | 整行 | 每次风险校验留痕(pass/fail/skip + trace_id) |
+
+---
+
+## 8. 合同链路配置依赖(Nacos `thirdParty.hcs.*`)
+
+| 配置 | 默认 | 说明 |
+|---|---|---|
+| push.sync-enabled | false | 总开关,联调稳定后打开 |
+| push.cron / batch-size / max-retry | 5分钟 / 50 / 5 | 扫描节奏与补偿上限 |
+| contract.template-mapping | 空 | **合同推送必配**(U7,合规方提供模板清单) |
+| contract.settlement-type-mapping | 空 | fdSettlementType 字典映射,未配置不传(U3) |
+| contract.currency-name-mapping | {"RMB": "人民币"} | 币种名称映射 |
+| contract.require-risk-check | true | 合同推送前风险校验开关 |
+| risk.block-blacklist | true | 黑名单是否阻断(置 failed) |
+| exe-stage.fund-type | byAmount | 履约规则行金额类型默认值 |
+| attachment.transfer-enabled / file-download-base | true / 空 | 附件转传开关与文件服务基址 |
+| src-system | TMS | fdOuterSystem / fd_sources(D14) |
+| auth-type / app-key / app-secret / base-url | — | 认证与地址(P0 依赖项) |
+
+---
+
+## 9. 合同链路待联调确认点(对应 §9)
+
+| # | 事项 | 当前处理 |
+|---|---|---|
+| U3 | fdSettlementType 字典值、CONTRACT_STATUS 是否必传、fdBlackStatus 取值 | 未配置不传 / 不传 / 按 "yes" 等值判定 |
+| U4 | C103-3 conditions 按 fdNumber 精确查询是否支持;是否按 fd_kingdee_id 幂等 | 按文档 conditions 实现,联调验证 |
+| U5 | C103-11 如何定位已有合同 | 入参同发起,补偿重试会重复调 C103-11 |
+| U6 | signer 编号体系与 fdType 判定;C101-1 用哪个客商编码 | 按名称反查 CustomerArchive.customerCode,fdType 不传 |
+| U7 | 模板ID清单 | template-mapping 空占位,未配置推送失败 |
+| U8 | 合同附件传递方式 | 已转传+缓存 fdId,报文暂不挂载 |
+| (新增) | C103-9 返回 fdChangeStages 与请求行的对应关系 | 行序优先、属性组合键兜底 |
+
+---
+
+## 10. 代码索引
+
+| 类 | 位置 | 职责 |
+|---|---|---|
+| `ContractExeStageController` / `ContractExeStageServiceImpl` | blade-transport controller / service.impl | 履约规则行维护与行级差异标记 |
+| `HcsPushTask#pushUnsyncedContracts` | blade-transport task | 合同扫描推送(含锁) |
+| `HcsContractSyncSupport#pushContract` | blade-transport support.hcs | 五步链编排 |
+| `HcsExeStageSyncSupport#syncExeStages` | blade-transport support.hcs | C103-9 行级同步与回填 |
+| `HcsAttachmentSupport#transferContractAttachments` | blade-transport support.hcs | 附件转传与映射缓存 |
+| `HcsMasterDataSupport` | blade-transport support.hcs | 模板/分类/币种缓存与映射校验 |
+| `HcsReconSupport#reconContract / reconContractDetail` | blade-transport support.hcs | 对账回查 |
+| `ContractManageServiceImpl#approve` / `MkProcessBizHandlers$ContractHandler` | blade-transport service.impl / mk | 审批零埋点置状态 |
+| `HcsSyncController` | blade-transport controller | 手动重推/批量/主数据刷新/对账端点 |
+| `IHcsClient`(contractDraft/contractUpdate/contractList/contractDetail/exeplanAdd/uploadAttachment/getRelativeRisk) | blade-third-party-api/blade-hcs-api | HCS 接口客户端 |
From 1906e8c80b87d2c47f5cd9d41d389a733bdd1fa1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 1 Oct 2026 02:06:04 +0800
Subject: [PATCH 021/124] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E6=A1=82?=
=?UTF-8?q?=E7=89=A9=E5=90=88=E5=90=8C=E9=9B=86=E6=88=90=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=AF=B4=E6=98=8E-v4.docx=EF=BC=88=E5=90=88?=
=?UTF-8?q?=E8=A7=84=E6=96=B9=E6=8E=A5=E5=8F=A3=E8=AF=B4=E6=98=8E=E5=8E=9F?=
=?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=9C=AC=E6=AC=A1=E8=AE=A4=E8=AF=81=E6=96=B9?=
=?UTF-8?q?=E6=A1=88=E5=8F=98=E6=9B=B4=E4=BE=9D=E6=8D=AE=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
doc/docs/桂物合同集成相关接口说明-v4.docx | Bin 0 -> 1122304 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 doc/docs/桂物合同集成相关接口说明-v4.docx
diff --git a/doc/docs/桂物合同集成相关接口说明-v4.docx b/doc/docs/桂物合同集成相关接口说明-v4.docx
new file mode 100644
index 0000000000000000000000000000000000000000..89280f3743e8c9a590f0f8b25401435a5b1544ac
GIT binary patch
literal 1122304
zcmbrlV~}ml(ly%LZQIyw+qP}nwr$(CxqG*5+qP|g{harn8|S?7{kd^xRK(24$~CJh
zN6uU;Yt@jG1O|Zw_~#0h+2i}?{_hLoM=-KAlyk7PbEK8~F+=`Qfd9*EK786)3J?H5
z4hR4M{y)w1?d)jWtgW)+du{sY;DxS}-{9o0b_RlH2#3pn9+e=71RRBG8VOX8)*E1g
zyRU&Lik7!rOfQaaX)gfTU{6-{04$fU1V*b$6;sU}SMh#OyyfIkw4K1OEYMQ&
z!9s5TPReKi=kzX%*d5$XiDaH?<);(4;yqk(4CKFPb
zBuNWr*)XJdEZHlBQ{+Y7mNBlZU>vTM*s{A9ZS9MiCvWlo+w-v*cH0_1o;UdMJi>o^
z-q6;;_#fAMC&)_n)1il62W;mbY>{X;w+8tZK?P=PFr#9gka5Sw$7tvfQGdiJmhK`Ukh1GtqF^#soUxa63&sMv
zK~NN{NGCi?g+*BGzKKp^Xxj>;bfPSiZ&&173wZuigIrV2G15
zc8KgEPG*{fl;u>!NUS{w(cyTXd-0++h*PzsZ_=XE+x^o^$5!PUW%bu$C6QF7_^aPD
zeb~Hki}$-4jrRwq8CD6*hwosQbjV*(LRUUdTxU-(l;?mt-1~dgAl?54LWFOQ&sAsN`JbF-qA14kgCcqhmp&JqQQV6J`^5E~2lf-XQR*Q!JMTb~I6o)KPnSh9c>j
z5QM^UJaz{{n&J0AO5NFXy}(p_&4+prnOO_FH>;rkvLj??sV!Tb>dlyPU%rtRhhl^P
z{zY&R0OMd2LKnWergNx;{WjazwvKRm-*XT1KA0q>6GBQm(D6$cO=jaw
z2?f={U8hMuvD}EBHQ1Zs_4eE
zunPANi2wI=2l@k|tF434|4MZ|8jqYeKRM1W0RRB*-zJVu?pDT*|6qKnEt$C9g3wh;
z@$Fx%=ujtGJIiG;tZC>@;&QoS&$YkFD9r!6=2cE??CvG!AkL76-Ns?e{JLD?OWK9<
z70sLpAb}820RW;_5k+^XXynW9YA93$7^LgvP7s~#T0G&+-iaQa8y6NE(fxKV_KNnG
z?R5=aHf$D?V_`AY075G|SDe$$VPfw0MdYS?1~&+iGap}4PUzO4Ubb=OePQ8(X5x%A
z-Y^zoZ)_zS#3nRN;(OtY^p-ViYzNy$OM3SWFs~;YCR}JEAR3Y`^j~qODPb;1Z`$yA
zx8zyXsr|*VXYlXeTVk-i#oz`Ln&o6=EiKZN&4CMI?OiXCj{URfshZqhSbTRS#wp52Mie~KJ7ivnyZd;T?URb9t~R8(x4Y3d
zUJ2OlDH|NO2J)fCHnVGK3zfd0?B_2S9`hG?y}pNiKksE9%xSBk*gftB5%waxiyf@)
z6zWSUf`1)@sCF7#Ll+INleMEqna=R#2N*qOT3V^10o(6e|9MJ1$Yc)RDlvkvPUm%#
z6!>oK)pRJQosPqOM4Lc^eaHM)HteAW
zxQRT?J+!68(Vn$=W@K}*6U|o2m3n`DiRV8;tD9!q@!nxuEl8v`!-%U7s#=)g5x*3v
z#+zxN<~NT6L04kxst`TZ>u4!**0Rms7U`AAtP7z@F!XzMrIwA9lx4c=|@m(~YfR7Qia0eM}L@MJ5UPGTN{>P}@V
zIFWXyn&8b{?rH9gg7sw6)t&TIEwn^50e+4)!+%~TZwu@U983eFsZ>;;%0A@b1L{c&
z0`;^-P(@X?!tCM8sI2$oq)veba8rVVc$$O-TB8@MBnJVoI0peNDI3iz*8f6f;Hx74
zptwzaqKcZUPDd-&Y^0WGHc?BmS*}1`a!mFb>T9S;aDu9APji~!9#3t5o_Q{Cviypf
ztA6E_Xg<0o+sx=L{7*%qDXe-m{#Nr!Y${#Vs}DrK22|cXHPJ@TvchPflhZP=pcd%C
zma-J@6aJSoV(m$)oqf@ZTNxNMN4lYN(C~q#BeB@vYNT74T_thz&Zn%lm~TIr)!6A7
zuA`JNo^56DX76&--GNl)bz>|TO8N%Yw@)^E^EoV%CgTT9ojlJa%mfo7*8DXxR`7h@
zyxdC>|BGb9Lvy}dwE235G+Zd1m44&C3;0D04DbMkS7|j9;4