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 1d3ab279..655ae234 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 @@ -220,4 +220,53 @@ 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; + + @Schema(description = "合规特批流程ID(C103-8 返回)") + private String hcsSpecFdId; + + @Schema(description = "合规特批状态 none/need/processing/approved/rejected") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private String hcsSpecStatus; + + @Schema(description = "合规最近特批动作时间") + private LocalDateTime hcsSpecTime; + + @Schema(description = "合规特批结果/发起失败原因") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private String hcsSpecMsg; + + @Schema(description = "特批申请材料+内部审批附件JSON(含C103-2 合规附件ID)") + private String hcsSpecFilesJson; + + @Schema(description = "金蝶合同ID(contractLegal/result 合同特批金蝶同步结果回调回填)") + private String kingdeeId; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsAttachmentSync.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsAttachmentSync.java new file mode 100644 index 00000000..4fa7fb40 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/HcsAttachmentSync.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.transport.pojo.entity; + +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.time.LocalDateTime; + +/** + * HCS 附件转传映射实体类(C103-2 转传取得合规附件ID,单附件行级,照金蝶附件同步表设计) + *

+ * 仅落成功记录:HCS 转传任一附件失败即抛异常整单重试(失败原因落单据 hcs_sync_msg),故无失败态字段。 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_hcs_attachment_sync") +@Schema(description = "HCS附件转传映射") +public class HcsAttachmentSync extends TenantEntity { + @Serial + private static final long serialVersionUID = 1L; + /** + * 业务单据类型(contract/payment/invoice-application/invoice-receipt) + */ + private String bizType; + /** + * 业务单据id + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long bizId; + /** + * 附件幂等键(url|link|fileUrl|downloadUrl|uid|originalName|name 依次取首个非空) + */ + private String attachmentKey; + /** + * 附件名称(展示/排查用) + */ + private String attachmentName; + /** + * 合规附件ID(C103-2 返回 fdId) + */ + private String fdId; + /** + * 最近转传时间 + */ + private LocalDateTime syncTime; +} 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/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..eb42723f --- /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-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 2b89b282..60ede7e1 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 @@ -183,4 +183,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 9023e07e..91a33972 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 @@ -180,4 +180,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-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-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 eb85e2c4..46ede04c 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,27 @@ 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; + + @Schema(description = "金蝶项目ID(D902 项目金蝶同步结果回调回填)") + private String kingdeeId; + + @Schema(description = "金蝶项目编号(D902 项目金蝶同步结果回调回填)") + private String kingdeeNumber; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java index fc429ffe..5f76a439 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java @@ -55,6 +55,14 @@ public class ContractManageVO extends ContractManage { @Schema(description = "到期快捷筛选") private String expireScope; + @TableField(exist = false) + @Schema(description = "是否已同步金蝶(金蝶合同ID是否已回填)") + private Boolean kingdeeSynced; + + @TableField(exist = false) + @Schema(description = "是否已同步合规(合规同步状态是否为 synced)") + private Boolean hcsSynced; + @TableField(exist = false) @Schema(description = "生效类型筛选") private String effectiveType; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java new file mode 100644 index 00000000..da93615f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java @@ -0,0 +1,72 @@ +/** + * 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.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 客商风险展示 VO(C101-1 查询结果 + TMS 侧组装提示,前端接入设计 §3.3) + *

+ * 内部组织类型客商默认无风险(不调 C101-1);外部客商高风险/黑名单时 risky=true 并返回 riskTip + * + * @author Chill + */ +@Data +@Schema(description = "客商风险展示") +public class CustomerRiskVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "客商编号") + private String customerCode; + + @Schema(description = "客商类型:internal内部组织(默认无风险)/external外部客商") + private String customerKind; + + @Schema(description = "客商名称") + private String customerName; + + @Schema(description = "黑名单状态:yes/no") + private String fdBlackStatus; + + @Schema(description = "风险等级:0=低风险,1=中风险,2=高风险") + private String fdRiskLevel; + + @Schema(description = "风险等级名称(服务端转义:黑名单/高风险/中风险/低风险,黑名单优先;无等级为空)") + private String riskLevelName; + + @Schema(description = "是否高风险/黑名单客商(外部客商专用,内部组织恒为 false)") + private Boolean risky; + + @Schema(description = "风险客商提示文案(基本信息备注后展示,安全客商为空)") + private String riskTip; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java index aed01d69..c4cd0fa6 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java @@ -86,6 +86,14 @@ public class ProjectApplyVO extends ProjectApply { @Schema(description = "是否仅查询可用于合同选择的项目(含临时项目与正式审批通过项目)") private Boolean contractSelectable; + @TableField(exist = false) + @Schema(description = "是否已同步金蝶(金蝶项目ID是否已回填)") + private Boolean kingdeeSynced; + + @TableField(exist = false) + @Schema(description = "是否已同步合规(合规同步状态是否为 synced)") + private Boolean hcsSynced; + @TableField(exist = false) @Schema(description = "资金使用风险等级:high、medium、none") private String fundUseRisk; diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml index 02e9bab8..a8b4c713 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/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java index 760d80ec..5fe089fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java @@ -28,6 +28,7 @@ package org.springblade.transport.config; import lombok.Data; import org.springblade.thirdparty.bfi.constant.BfiConstant; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; import java.util.LinkedHashMap; @@ -45,6 +46,7 @@ import java.util.Map; */ @Data @Component +@RefreshScope @ConfigurationProperties(prefix = "thirdparty.bfi") public class BfiProperties { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java new file mode 100644 index 00000000..798c77d1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java @@ -0,0 +1,223 @@ +/** + * 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(合同合规系统)C103-1/C103-11 合同发起 xformFields 字段名常量 + *

+ * 字段名取自 v4 接口文档「合同字段说明」,fdSepcailContract 为文档原文拼写(Special 笔误,必须沿用); + * 命名照 {@link HcsSyncConstant} 惯例。 + * + * @author Chill + */ +public final class HcsContractFieldConstant { + + // ==================== 合同发起 xformFields 字段名(fdXformField.name) ==================== + + /** + * 合同ID(更新时传 C103-1 返回的 fdContractId) + */ + public static final String FIELD_FD_ID = "fdId"; + + /** + * 合同编号(fd 模板字段) + */ + public static final String FIELD_FD_NUMBER = "fdNumber"; + + /** + * 合同编号(CONTRACT 模板字段) + */ + public static final String FIELD_CONTRACT_NO = "CONTRACT_NO"; + + /** + * 合同标题(fd 模板字段) + */ + public static final String FIELD_FD_SUBJECT = "fdSubject"; + + /** + * 合同名称(CONTRACT 模板字段) + */ + public static final String FIELD_CONTRACT_NAME = "CONTRACT_NAME"; + + /** + * 合同金额(fd 模板字段) + */ + public static final String FIELD_FD_AMOUNT = "fdAmount"; + + /** + * 合同金额(AMOUNT 模板字段) + */ + public static final String FIELD_AMOUNT = "AMOUNT"; + + /** + * 币种(fd 模板字段) + */ + public static final String FIELD_FD_CURRENCY_ID = "fdCurrencyId"; + + /** + * 币种(CURRENCY 模板字段) + */ + public static final String FIELD_CURRENCY = "CURRENCY"; + + /** + * 合同结算类型:payment=付款,payment_collection=收支双向,collection=收款,none=不结算 + */ + public static final String FIELD_FD_SETTLEMENT_TYPE = "fdSettlementType"; + + /** + * 特批流程(文档原文拼写笔误,必须沿用) + */ + public static final String FIELD_FD_SEPCAIL_CONTRACT = "fdSepcailContract"; + + /** + * 合同类型 + */ + public static final String FIELD_CONTRACT_TYPE = "CONTRACT_TYPE"; + + /** + * 甲方名称 + */ + public static final String FIELD_PARTY_A = "PARTY_A"; + + /** + * 乙方名称 + */ + public static final String FIELD_PARTY_B = "PARTY_B"; + + /** + * 签订日期(yyyy-MM-dd,fdType=timestamp) + */ + public static final String FIELD_SIGN_DATE = "SIGN_DATE"; + + /** + * 预计开始时间(合同开始期限为指定日期时必填) + */ + public static final String FIELD_FD_ESTIMATE_START_TIME = "fdEstimateStartTime"; + + /** + * 预计结束时间(合同结束期限为指定日期时必填) + */ + public static final String FIELD_FD_ESTIMATE_END_TIME = "fdEstimateEndTime"; + + /** + * 合同开始期限:signDate=签订之日起算,specifiedDate=指定日期 + */ + public static final String FIELD_FD_START_TERM = "fdStartTerm"; + + /** + * 合同结束期限:signTerm=按签订后期限,specifiedDate=指定日期,noTerm=无固定期限 + */ + public static final String FIELD_FD_END_TERM = "fdEndTerm"; + + /** + * 创建时间(yyyy-MM-dd,fdType=timestamp) + */ + public static final String FIELD_CREATE_TIME = "CREATE_TIME"; + + /** + * 创建时间(fd 模板字段,yyyy-MM-dd,fdType=timestamp) + */ + public static final String FIELD_FD_CREATE_TIME = "fdCreateTime"; + + /** + * 同步时间(yyyy-MM-dd,fdType=timestamp) + */ + public static final String FIELD_FD_SYN_TIME = "fd_syn_time"; + + /** + * 承办人名称 + */ + public static final String FIELD_CREATE_USER_NAME = "CREATE_USER_NAME"; + + /** + * 合同状态:draft/approving/approved 等(v4 必填) + */ + public static final String FIELD_CONTRACT_STATUS = "CONTRACT_STATUS"; + + /** + * 合同份数(v4 必填) + */ + public static final String FIELD_FD_SERVINGS = "fdServings"; + + /** + * 合同说明(选填) + */ + public static final String FIELD_FD_DESC = "fdDesc"; + + /** + * 关联项目(选填,对象传值如 {"fdId": "项目id"},传合规项目ID) + */ + public static final String FIELD_FD_PROJECT = "fdProject"; + + /** + * 合同结算模式:open=开口合同,close=闭口合同 + */ + public static final String FIELD_FD_CONTRACT_GW_NATURE = "fdContractGwNature"; + + // ==================== 结算类型枚举值 ==================== + + /** + * 合同结算类型取值:付款(fdSettlementType,承运商合同固定取此值;合规系统字典中文名,2026-10-17 确认) + */ + public static final String SETTLEMENT_TYPE_PAYMENT = "付款"; + + /** + * 合同结算类型取值:收支双向(fdSettlementType,合规系统字典中文名) + */ + public static final String SETTLEMENT_TYPE_PAYMENT_COLLECTION = "收支双向"; + + /** + * 合同结算类型取值:收款(fdSettlementType,客户合同固定取此值;合规系统字典中文名) + */ + public static final String SETTLEMENT_TYPE_COLLECTION = "收款"; + + /** + * 合同结算类型取值:不结算(fdSettlementType,合规系统字典中文名) + */ + public static final String SETTLEMENT_TYPE_NONE = "不结算"; + + // ==================== 结算模式枚举值 ==================== + + /** + * 合同结算模式取值:开口合同(fdContractGwNature,TMS 固定取此值) + */ + public static final String CONTRACT_GW_NATURE_OPEN = "open"; + + /** + * 合同结算模式取值:闭口合同(fdContractGwNature) + */ + public static final String CONTRACT_GW_NATURE_CLOSE = "close"; + + // ==================== 期限枚举值 ==================== + + /** + * 期限取值:指定日期(fdStartTerm/fdEndTerm,TMS 固定按指定日期起算) + */ + public static final String TERM_SPECIFIED_DATE = "specifiedDate"; + + private HcsContractFieldConstant() { + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsFieldConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsFieldConstant.java new file mode 100644 index 00000000..749aed1c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsFieldConstant.java @@ -0,0 +1,116 @@ +/** + * 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 HcsContractFieldConstant}(C103-1/C103-11 表单字段)区分; + * 项目立项查询用下划线字段名,合同/开票查询用驼峰字段名,以实际接口为准。 + * + * @author Chill + */ +public final class HcsFieldConstant { + + // ==================== C102-3 项目立项列表查询(下划线字段名) ==================== + + /** + * 项目ID + */ + public static final String PROJ_FD_ID = "fd_id"; + + /** + * 项目编号(判重条件键) + */ + public static final String PROJ_FD_NUM = "fd_num"; + + /** + * 项目名称 + */ + public static final String PROJ_FD_PRO_NAME = "fd_pro_name"; + + /** + * 创建时间(排序字段) + */ + public static final String PROJ_FD_CREATE_TIME = "fd_create_time"; + + // ==================== C103-3 合同列表查询(驼峰字段名) ==================== + + /** + * 合同ID + */ + public static final String CONTRACT_FD_ID = "fdId"; + + /** + * 合同标题 + */ + public static final String CONTRACT_FD_SUBJECT = "fdSubject"; + + /** + * 合同编号(查询条件键) + */ + public static final String CONTRACT_FD_NUMBER = "fdNumber"; + + /** + * 合同分类层级ID + */ + public static final String CONTRACT_FD_CATE_HIERARCHY_ID = "fdCateHierarchyId"; + + // ==================== C111-1 开票登记列表查询(驼峰字段名) ==================== + + /** + * 单据ID + */ + public static final String INVOICE_FD_ID = "fdId"; + + /** + * 单据编号(查询条件键) + */ + public static final String INVOICE_FD_NUMBER = "fdNumber"; + + /** + * 开票日期 + */ + public static final String INVOICE_FD_BILL_DATE = "fdBillDate"; + + /** + * 合同编号 + */ + public static final String INVOICE_FD_CONTRACT_NUMBER = "fdContractNumber"; + + /** + * 开票金额 + */ + public static final String INVOICE_FD_INVOICE_AMOUNT = "fdInvoiceAmount"; + + /** + * 单据状态 + */ + public static final String INVOICE_FD_STATUS = "fdStatus"; + + private HcsFieldConstant() { + } +} 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..42eaaf09 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java @@ -0,0 +1,342 @@ +/** + * 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"; + + /** + * 推送业务类型:付款申请(C106 特批 / C108 履约记录) + */ + 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) + */ + public static final String CHECK_TYPE_RISK = "risk"; + + /** + * 校验类型:额度(C106-1,P3 付款链使用) + */ + public static final String CHECK_TYPE_CREDIT = "credit"; + + /** + * 校验类型:合同特批(合规特批结果推送/查询联动留痕;原 C103-8 发起留痕随接口删除) + */ + public static final String CHECK_TYPE_SPEC = "spec"; + + /** + * 校验类型:项目金蝶同步结果回调(D902,projectLegal/result 推送留痕) + */ + public static final String CHECK_TYPE_KINGDEE = "kingdee"; + + /** + * 校验结果:通过 + */ + public static final String CHECK_RESULT_PASS = "pass"; + + /** + * 校验结果:不通过(黑名单) + */ + public static final String CHECK_RESULT_FAIL = "fail"; + + /** + * 校验结果:跳过(客商编号反查不到等) + */ + 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 已发起,TMS 挂起「特批审核」,等待合规 paymentLegal/result 回调) + */ + public static final String SPEC_STATUS_PROCESSING = "processing"; + + /** + * 特批状态:特批通过(合规回调/人工确认,联动恢复"审批通过"放行) + */ + public static final String SPEC_STATUS_APPROVED = "approved"; + + /** + * 特批状态:特批驳回(合规回调/人工确认,联动付款审批驳回走重新提交闭环) + */ + public static final String SPEC_STATUS_REJECTED = "rejected"; + + /** + * 合规侧特批结果回传状态:通过(contractLegal/paymentLegal result 接口 status,D15 回传机制) + */ + public static final String LEGAL_RESULT_APPROVED = "APPROVED"; + + /** + * 合规侧特批结果回传状态:驳回 + */ + public static final String LEGAL_RESULT_REJECTED = "REJECTED"; + + // ==================== 客商风险(C101-1,外部客商高风险/黑名单管控) ==================== + + /** + * C101-1 风险等级:高风险(0=低风险,1=中风险,2=高风险) + */ + public static final String RISK_LEVEL_HIGH = "2"; + + /** + * 客商类型:内部组织(默认无风险,不查 C101-1) + */ + public static final String CUSTOMER_KIND_INTERNAL = "internal"; + + /** + * 合同附件类型字段名(与前端附件元数据 fileType 字段对齐,值取业务字典 contract_attachment_type) + */ + public static final String ATTACHMENT_TYPE_FIELD = "fileType"; + + /** + * 附件类型:特批附件(高风险/黑名单客商合同必须上传;值取业务字典 contract_attachment_type,中文匹配沿用 FILE_TYPE_SEAL_ARCHIVE="双章归档文件" 惯例) + */ + public static final String ATTACHMENT_TYPE_SPEC_EXPLANATION = "特批附件"; + + /** + * 付款申请附件类型值:特批附件(前端付款表单附件类型枚举 attachmentType,付款附件结构与合同 fileType 结构不同) + */ + public static final String PAYMENT_ATTACH_TYPE_SPECIAL = "special_approval"; + + /** + * 附件类型:用印附件(双章归档文件,取值沿用 ContractManageServiceImpl#FILE_TYPE_SEAL_ARCHIVE) + */ + public static final String ATTACHMENT_TYPE_SEAL = "双章归档文件"; + + // ==================== 合同附件挂载 fdEntityKey(eopCdraft 附件面板字段 key,C103-1) ==================== + + /** + * 合同附件字段 key:特批附件 + */ + public static final String CONTRACT_ATTACH_KEY_SPECIAL = "eopCdraftSpecial"; + + /** + * 合同附件字段 key:用印附件 + */ + public static final String CONTRACT_ATTACH_KEY_SEAL = "eopCdraftSign"; + + /** + * 合同附件字段 key:其他附件(含合同主文件) + */ + public static final String CONTRACT_ATTACH_KEY_OTHER = "eopCdraftOther"; + + /** + * 特批审核挂起节点展示名(currentNode,特批发起后挂起展示,结果回传后恢复) + */ + public static final String SPEC_REVIEW_NODE = "特批审核"; + + // ==================== 履约计划行(blade_contract_exe_stage,C103-9) ==================== + + /** + * 行级同步操作:无(同步成功后清除标记) + */ + public static final String ROW_OP_NONE = "none"; + + /** + * 行级同步操作:新增 + */ + public static final String ROW_OP_ADD = "add"; + + /** + * 行级同步操作:更新 + */ + public static final String ROW_OP_UPDATE = "update"; + + /** + * 行级同步操作:删除 + */ + public static final String ROW_OP_DELETE = "delete"; + + /** + * 履约类型:付款 + */ + public static final String PERFORM_TYPE_PAYMENT = "payment"; + + /** + * 履约类型:收款 + */ + public static final String PERFORM_TYPE_COLLECTION = "collection"; + + /** + * 款项性质:进度款 + */ + public static final String FUND_NATURE_PROGRESS = "progress"; + + /** + * 款项性质:预付款 + */ + public static final String FUND_NATURE_PREPAYMENT = "prepayment"; + + /** + * 付款类型→款项性质映射:正式结算付款对应进度款计划行,预付款(进度预付/项目预付)对应预付款计划行 + * + * @param paymentType 付款申请付款类型 + * @return 款项性质 + */ + public static String fundNatureOfPayment(String paymentType) { + return TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType) ? FUND_NATURE_PROGRESS : FUND_NATURE_PREPAYMENT; + } + + // ==================== 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 前缀(多实例部署防重扫,完整 key = 前缀 + 业务类型,任务间互不阻塞) + */ + public static final String PUSH_TASK_LOCK_KEY_PREFIX = "blade:third:hcs:push:task:lock:"; + + /** + * 即时推送单条锁 key 前缀(审批通过即时推送防与扫描任务并发双推,完整 key = 前缀 + 业务类型 + ":" + 单据ID) + */ + public static final String PUSH_SINGLE_LOCK_KEY_PREFIX = "blade:third:hcs:push:lock:"; + + /** + * 即时推送单条锁 TTL(秒):大于单条推送预期时长,异常残留锁自动过期后由扫描兜底 + */ + public static final long PUSH_SINGLE_LOCK_TTL_SECONDS = 120L; + + /** + * hcs_sync_msg 字段安全截断长度(列宽 varchar(512),留余量防远程错误报文超长导致落库失败) + */ + public static final int SYNC_MSG_MAX_LENGTH = 500; + + /** + * 截断超长的推送结果/失败原因(如 Feign 异常携带的远程 404 响应体),防止 Data too long 落库失败 + */ + public static String truncateMsg(String message) { + if (message == null) { + return null; + } + return message.length() > SYNC_MSG_MAX_LENGTH ? message.substring(0, SYNC_MSG_MAX_LENGTH) : message; + } + + /** + * 审批状态中文名(变更记录展示用,金蝶同步结果回调等系统链路共用;未匹配回退原值) + */ + public static String approvalStatusName(String approvalStatus) { + if (approvalStatus == null || approvalStatus.isEmpty()) { + return null; + } + return switch (approvalStatus) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "withdrawn" -> "已撤回"; + case "approved" -> "审批通过"; + case "rejected" -> "已驳回"; + case "change_reviewing" -> "变更审批中"; + case "change_rejected" -> "变更驳回"; + case "change_approved" -> "变更审批通过"; + case "voided" -> "已作废"; + default -> approvalStatus; + }; + } + + private HcsSyncConstant() { + } +} 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/controller/ContractManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java index 3ff3eaf1..364dbcc9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java @@ -40,6 +40,7 @@ import org.springblade.transport.excel.ContractManageExcel; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.vo.ContractManageVO; import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.support.hcs.HcsImmediatePushSupport; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -63,6 +64,7 @@ import java.util.Map; public class ContractManageController extends BladeController { private final IContractManageService contractManageService; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @GetMapping("/detail") @ApiOperationSupport(order = 1) @@ -110,7 +112,14 @@ public class ContractManageController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "审批通过", description = "传入id") public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) { - return R.status(contractManageService.approve(id)); + contractManageService.approve(id); + // 审批事务已提交,再同步推送合规;失败原因以警告文案返回(data=false 供前端区分提示样式),照正式结算单审批接口 + hcsImmediatePushSupport.pushContractAndWait(id); + String syncFailure = HcsImmediatePushSupport.pollSyncFailure(); + if (syncFailure != null) { + return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure); + } + return R.success("审批通过"); } @PostMapping("/reject") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 3ec0e5fe..d128ed17 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -204,12 +204,16 @@ public class FormalSettlementController extends BladeController { return approveResponse(); } - /** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */ + /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ private R approveResponse() { String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); + String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); if (syncFailure != null) { return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); } + if (syncResult != null) { + return R.success("审批通过,已同步金蝶:" + syncResult); + } return R.success("审批通过"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java deleted file mode 100644 index 247f0e87..00000000 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * 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 cn.hutool.core.util.StrUtil; -import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; -import org.springblade.core.log.exception.ServiceException; -import org.springblade.core.tenant.annotation.TenantIgnore; -import org.springblade.core.tool.api.R; -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.RestController; - -import java.util.Map; -import java.util.Objects; - -/** - * HCS 合规回调 Mock 端点 - *

- * 合同法务审批结果回调(D103)、付款法务审批结果回调(D104)、项目金蝶同步结果回调(D902)、 - * 合同金蝶同步结果回传接收(D903)四个入站回调的纯 mock 实现:仅校验来源系统标识后返回固定结果, - * 不读写业务数据、不联动状态。真实处理逻辑保留在对应 Support 中,联调时把端点切回 - * {@link } 并调用 Support 方法即可。 - * - * @author Chill - */ -@Slf4j -@RestController -@TenantIgnore -@RequestMapping("hcs") -@Tag(name = "HCS合规回调Mock", description = "HCS合规回调Mock端点(仅自测,返回固定 mock 数据)") -public class HcsMockController { - - /** - * 来源系统标识(合规方约定值,与 third-party.hcs.src-system 默认值一致) - */ - private static final String SOURCE_SYSTEM = "WLTMS"; - - /** - * 合同法务审批结果回调(D103) - */ - @PostMapping("/contractLegal/result") - @ApiOperationSupport(order = 15) - @Operation(summary = "合同法务审批结果回调Mock", description = "D103 合规推送合同特批(金蝶同步)结果,仅校验来源系统并返回 mock 数据") - public R contractLegalResult(@RequestBody(required = false) Map param) { - this.checkSourceSystem("合同法务审批结果", param); - return R.data("mock特批结果已处理:approved"); - } - - /** - * 付款法务审批结果回调(D104) - */ - @PostMapping("/paymentLegal/result") - @ApiOperationSupport(order = 16) - @Operation(summary = "付款法务审批结果回调Mock", description = "D104 合规推送付款特批结果,仅校验来源系统并返回 mock 数据") - public R paymentLegalResult(@RequestBody(required = false) Map param) { - this.checkSourceSystem("付款法务审批结果", param); - return R.data("mock付款特批结果接收已处理:approved"); - } - - /** - * 项目金蝶同步结果回调(D902) - */ - @PostMapping("/projectLegal/result") - @ApiOperationSupport(order = 18) - @Operation(summary = "项目金蝶同步结果回调Mock", description = "D902 合规推送项目金蝶同步结果,仅校验来源系统并返回 mock 数据") - public R projectLegalResult(@RequestBody(required = false) Map param) { - this.checkSourceSystem("项目金蝶同步结果", param); - return R.data("mock项目金蝶同步结果接收已处理:approved"); - } - - /** - * 合同金蝶同步结果回传接收(D903) - */ - @PostMapping("/contractKingdee/result") - @ApiOperationSupport(order = 19) - @Operation(summary = "合同金蝶同步结果回传接收Mock", description = "D903 合规推送合同金蝶最终同步结果,仅校验来源系统并返回 mock 数据") - public R contractKingdeeResult(@RequestBody(required = false) Map param) { - this.checkSourceSystem("合同金蝶同步结果", param); - return R.data("mock合同金蝶同步结果接收处理:approved"); - } - - /** - * 来源系统校验:仅接受合规方约定的来源标识,防伪造回传 - * - * @param bizName 业务名称(日志用) - * @param param 回调报文 - */ - private void checkSourceSystem(String bizName, Map param) { - String sourceSystem = param == null ? null : Objects.toString(param.get("sourceSystem"), null); - if (!StrUtil.equalsIgnoreCase(sourceSystem, SOURCE_SYSTEM)) { - log.warn("{}推送来源系统标识错误:sourceSystem={}", bizName, sourceSystem); - throw new ServiceException("来源系统标识错误"); - } - } -} 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..e192d772 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java @@ -0,0 +1,349 @@ +/** + * 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 feign.Response; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tenant.annotation.TenantIgnore; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.dto.HcsContractLegalResultDTO; +import org.springblade.hcs.dto.HcsCurrencyVO; +import org.springblade.hcs.dto.HcsLegalResultDTO; +import org.springblade.hcs.dto.HcsProjectLegalResultDTO; +import org.springblade.hcs.dto.HcsTemplateVO; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.pojo.vo.CustomerRiskVO; +import org.springblade.transport.support.hcs.HcsAttachmentSupport; +import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport; +import org.springblade.transport.support.hcs.HcsContractSyncSupport; +import org.springblade.transport.support.hcs.HcsCreditCheckSupport; +import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport; +import org.springblade.transport.support.hcs.HcsMasterDataSupport; +import org.springblade.transport.support.hcs.HcsPaymentSyncSupport; +import org.springblade.transport.support.hcs.HcsProjectSyncSupport; +import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport; +import org.springblade.transport.task.HcsPushTask; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * HCS 合规推送端点(设计文档 §5.0 手动入口) + *

+ * 前端列表页"推送合规/重推"按钮(照"同步金蝶"按钮模式): + * 单条重推重置 hcs_sync_status=unsynced 并清零重试计数后立即推送;批量端点复用定时扫描逻辑。 + * 排查推送问题以 hcs_sync_status/hcs_sync_msg 为准,不以审批日志为准(ADR-0001)。 + * + * @author Chill + */ +@Slf4j +@RestController +@AllArgsConstructor +@TenantIgnore +@RequestMapping("hcs") +@Tag(name = "HCS合规推送", description = "HCS合规推送端点") +public class HcsSyncController { + + private final HcsPushTask hcsPushTask; + private final HcsProjectSyncSupport hcsProjectSyncSupport; + private final HcsContractSyncSupport hcsContractSyncSupport; + private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport; + private final HcsCreditCheckSupport hcsCreditCheckSupport; + private final HcsAttachmentSupport hcsAttachmentSupport; + private final HcsPaymentSyncSupport hcsPaymentSyncSupport; + private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport; + private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport; + private final HcsMasterDataSupport hcsMasterDataSupport; + + /** + * 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请 / invoice-application=开票申请 / invoice-receipt=收票单) + */ + @PostMapping("/sync/{bizType}/{id}") + @ApiOperationSupport(order = 1) + @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); + } + + /** + * 批量推送(复用定时扫描逻辑,单轮批量上限 push.batch-size) + */ + @PostMapping("/sync/{bizType}") + @ApiOperationSupport(order = 2) + @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); + } + + /** + * 页面勾选批量重推(项目立项/合同列表勾选行后批量推送,逐条推送互不影响,单条失败计入失败明细) + */ + @PostMapping("/sync/{bizType}/batch") + @ApiOperationSupport(order = 17) + @Operation(summary = "按勾选ID批量推送/重推合规", description = "仅支持 project(项目立项)、contract(合同),逐条重置待同步后立即推送,单条失败不影响其余,返回成功数与失败明细") + public R repushBatch(@Parameter(description = "业务类型 project/contract", required = true) @PathVariable String bizType, + @Parameter(description = "单据ID列表", required = true) @RequestBody List ids) { + if (!List.of(HcsSyncConstant.BIZ_TYPE_PROJECT, HcsSyncConstant.BIZ_TYPE_CONTRACT).contains(bizType)) { + throw new IllegalArgumentException("不支持的HCS批量推送业务类型:" + bizType); + } + if (ids == null || ids.isEmpty()) { + return R.fail("请先勾选要推送的单据"); + } + int successCount = 0; + List failureList = new ArrayList<>(); + for (Long id : ids) { + try { + if (HcsSyncConstant.BIZ_TYPE_PROJECT.equals(bizType)) { + hcsProjectSyncSupport.repushProject(id); + } else { + hcsContractSyncSupport.repushContract(id); + } + successCount++; + } catch (Exception exception) { + failureList.add(id + ":" + exception.getMessage()); + } + } + String summary = "批量推送完成:成功 " + successCount + " 条,失败 " + failureList.size() + " 条"; + if (!failureList.isEmpty()) { + summary += ";失败明细:" + String.join(";", failureList); + } + return R.data(summary); + } + + /** + * 特批结果人工确认(§9-U1 过渡兜底:与合规 paymentLegal/result 回调等效,均联动付款审批状态) + */ + @PostMapping("/spec-pay/confirm") + @ApiOperationSupport(order = 3) + @Operation(summary = "特批结果人工确认", description = "传入付款申请ID与结果(approved/rejected),仅特批中(processing)允许确认;approved 恢复审批通过放行,rejected 联动付款审批驳回") + 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 = 4) + @Operation(summary = "刷新HCS主数据缓存", description = "全量重拉模板/分类/币种列表并缓存,同时校验模板映射配置") + public R refreshMasterData() { + return R.data(hcsMasterDataSupport.refreshAll()); + } + + /** + * 合规模板列表(C103-6,Redis 缓存优先,前端接入设计 §3.1) + */ + @GetMapping("/master-data/template") + @ApiOperationSupport(order = 9) + @Operation(summary = "合规模板列表查询", description = "C103-6 模板列表,走服务端 Redis 缓存,未命中自动全量拉取") + public R> templates() { + return R.data(hcsMasterDataSupport.getTemplates()); + } + + /** + * 合规币种列表(C103-12,Redis 缓存优先,前端接入设计 §3.1) + */ + @GetMapping("/master-data/currency") + @ApiOperationSupport(order = 16) + @Operation(summary = "合规币种列表查询", description = "C103-12 币种列表,走服务端 Redis 缓存,未命中自动全量拉取") + public R> currencies() { + return R.data(hcsMasterDataSupport.getCurrencies()); + } + + /** + * 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路): + * 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案 + */ + @GetMapping("/risk") + @ApiOperationSupport(order = 10) + @Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕") + public R risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode) { + return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode)); + } + + /** + * 客商风险批量查询(C101-1,客商列表页展示用,前端接入设计 §3.3): + * 按客商编号集合逐个查询(口径同 /risk),展示用不作为安全边界,单个失败跳过不阻断整页 + */ + @GetMapping("/risk/batch") + @ApiOperationSupport(order = 13) + @Operation(summary = "客商风险批量查询", description = "C101-1 按客商编号集合批量查询风险等级与黑名单状态(展示用),口径同单个查询,单个失败跳过不阻断") + public R> riskBatch(@Parameter(description = "客商编号集合,逗号分隔", required = true) @RequestParam String customerCodes) { + return R.data(hcsCreditCheckSupport.queryCustomerRiskBatch(Func.toStrList(customerCodes))); + } + + /** + * 客商风险按名称查询(C101-1,客商档案表单展示用,前端接入设计 §3.3): + * 按名称反查客商编号后口径同 /risk;反查不到客商编号不阻断(risky=false,§9-U6) + */ + @GetMapping("/risk/by-name") + @ApiOperationSupport(order = 14) + @Operation(summary = "客商风险按名称查询", description = "C101-1 按客商名称查询风险(展示用),名称反查编号后口径同编号查询,反查不到默认无风险") + public R riskByName(@Parameter(description = "客商名称", required = true) @RequestParam String customerName) { + return R.data(hcsCreditCheckSupport.queryCustomerRiskByName(customerName)); + } + + /** + * 附件上传到合同系统(C103-2 代理,前端接入设计 §3.2),返回合规附件ID + */ + @PostMapping(value = "/attachment/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ApiOperationSupport(order = 11) + @Operation(summary = "附件上传到合同系统", description = "C103-2 代理:单文件上传至合规,返回合规附件ID(fdId)") + public R uploadAttachment(@Parameter(description = "附件文件", required = true) @RequestParam("fdFile") MultipartFile fdFile) { + return R.data(hcsAttachmentSupport.uploadOne(fdFile, AuthUtil.getUserAccount())); + } + + /** + * 合同附件下载(C103-5 代理,前端接入设计 §3.2):文件流透传浏览器 + */ + @GetMapping("/attachment/download") + @ApiOperationSupport(order = 12) + @Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名") + public void downloadAttachment(@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId, + @Parameter(description = "下载文件名(可选)") @RequestParam(required = false) String name, + HttpServletResponse response) throws Exception { + String fileName = StringUtil.isBlank(name) ? "hcs-attachment-" + fdId : name; + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20")); + try (Response hcsResponse = hcsAttachmentSupport.downloadRaw(fdId); + InputStream inputStream = hcsResponse.body().asInputStream(); + OutputStream outputStream = response.getOutputStream()) { + inputStream.transferTo(outputStream); + outputStream.flush(); + } + } + + /** + * 合同特批结果人工确认(§6-U1 过渡方案:与合规推送等效) + */ + @PostMapping("/contract/spec/confirm") + @ApiOperationSupport(order = 14) + @Operation(summary = "合同特批结果人工确认", description = "传入合同ID 与结果(approved/rejected),仅特批中(processing)允许确认") + public R confirmContractSpec(@Parameter(description = "合同ID", required = true) @RequestParam Long id, + @Parameter(description = "确认结果 approved/rejected", required = true) @RequestParam String result) { + return R.data(hcsContractSpecSyncSupport.confirmSpecResult(id, result)); + } + + /** + * 合同特批结果回传接收(合规系统 contractLegal/result 推送,替代未建设的 D103,原文留痕) + *

+ * 免登免授权:blade.secure skip-url + auth permitAll 放行(doc/nacos/blade.yaml); + * iPaaS 网关注册映射:/gwzh/HTHG/contractLegal/result → /hcs/contractLegal/result; + * 报文 {sourceSystem, requestId=合规合同ID(hcs_fd_id), id, status=APPROVED/REJECTED, reason}, + * 按 requestId=hcs_fd_id 定位合同,approved 恢复流程展示,rejected 联动合同审批驳回。 + */ + @PostMapping("/contractLegal/result") + @ApiOperationSupport(order = 15) + @Operation(summary = "合同特批结果回传接收", description = "合规系统推送合同特批结果,按合规合同ID(hcs_fd_id=requestId)定位并联动特批状态,原文留痕") + public R contractLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) { + return R.data(hcsContractSpecSyncSupport.handleLegalResult(param)); + } + + /** + * 付款特批结果回传接收(合规系统 paymentLegal/result 推送) + *

+ * 免登免授权同合同接口(blade.secure 放行); + * iPaaS 网关注册映射:/gwzh/HTHG/paymentLegal/result → /hcs/paymentLegal/result; + * 报文 {sourceSystem, requestId=TMS付款申请ID, status=APPROVED/REJECTED, reason}, + * approved 恢复"审批通过"放行,rejected 联动付款审批驳回走重新提交闭环。 + */ + @PostMapping("/paymentLegal/result") + @ApiOperationSupport(order = 16) + @Operation(summary = "付款特批结果回传接收", description = "合规系统推送付款特批结果,按付款申请ID定位并联动特批状态与付款审批状态,原文留痕") + public R paymentLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) { + return R.data(hcsPaymentSyncSupport.handleLegalResult(param)); + } + + /** + * 项目金蝶同步结果回传接收(合规系统 projectLegal/result 推送,D902,原文留痕) + *

+ * 免登免授权:blade.secure skip-url + auth permitAll 放行(doc/nacos/blade.yaml); + * iPaaS 网关注册映射:/gwzh/HTHG/projectLegal/result → /hcs/projectLegal/result; + * 报文 {sourceSystem, projectCode, requestId, success, kingdeeId, projectNumber, reason}, + * 按 projectCode=TMS 项目编号定位,success=true 回填金蝶项目ID/编号,success=false 记录失败原因。 + */ + @PostMapping("/projectLegal/result") + @ApiOperationSupport(order = 18) + @Operation(summary = "项目金蝶同步结果回传接收", description = "合规系统推送项目金蝶同步结果(D902),按项目编号定位并回填金蝶ID,原文留痕") + public R projectLegalResult(@RequestBody(required = false) HcsProjectLegalResultDTO param) { + return R.data(hcsProjectSyncSupport.handleLegalResult(param)); + } + + /** + * 合同金蝶同步结果回传接收(合规系统 contractKingdee/result 推送,D903,投送人/钢厂/结算中心合同共用,原文留痕) + *

+ * 免登免授权:blade.secure skip-url + auth permitAll 放行(doc/nacos/blade.yaml); + * iPaaS 网关注册映射:/gwzh/HTHG/contractKingdee/result → /hcs/contractKingdee/result; + * 报文 {contractType?, contractCode, requestId, success, reason?, kingdeeId?, sourceSystem?}, + * 本接口不处理法务审批结论,仅按 contractCode=TMS 合同编号定位回填金蝶合同ID/记录失败原因。 + */ + @PostMapping("/contractKingdee/result") + @ApiOperationSupport(order = 19) + @Operation(summary = "合同金蝶同步结果回传接收", description = "合规系统推送合同金蝶最终同步结果(D903,投送人/钢厂/结算中心共用),按合同编号定位回填金蝶ID,不处理法务审批结论,原文留痕") + public R contractKingdeeResult(@RequestBody(required = false) HcsContractLegalResultDTO param) { + return R.data(hcsContractSyncSupport.handleKingdeeResult(param)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java index bbe0bcf3..34c74ec3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -134,12 +134,16 @@ public class InvoiceApplicationController extends BladeController { return approveResponse(); } - /** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */ + /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ private R approveResponse() { String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); + String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); if (syncFailure != null) { return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); } + if (syncResult != null) { + return R.success("审批通过,已同步金蝶:" + syncResult); + } return R.success("审批通过"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java index 59eb30b6..c9023ceb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java @@ -150,12 +150,16 @@ public class InvoiceReceiptController extends BladeController { return approveResponse(); } - /** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */ + /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ private R approveResponse() { String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); + String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); if (syncFailure != null) { return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); } + if (syncResult != null) { + return R.success("审批通过,已同步金蝶:" + syncResult); + } return R.success("审批通过"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index ae6f6796..149f908e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -29,13 +29,16 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.listener.KingdeeApprovalSyncListener; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO; import org.springblade.transport.service.IPaymentApplicationService; +import org.springblade.transport.support.hcs.HcsImmediatePushSupport; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -43,6 +46,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.Arrays; import java.util.List; /** 付款申请控制器。 @author Chill */ @@ -53,6 +57,7 @@ import java.util.List; @Tag(name = "付款管理", description = "付款申请管理") public class PaymentApplicationController extends BladeController { private final IPaymentApplicationService paymentApplicationService; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @GetMapping("/list") @ApiOperationSupport(order = 1) @Operation(summary = "付款申请分页") @@ -96,15 +101,33 @@ public class PaymentApplicationController extends BladeController { @Operation(summary = "审批通过付款申请") public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); - return approveResponse(); + // 审批生效后立即发起付款特批(C106-3,不受即时推送开关控制);失败仅告警不阻断审批结果,保留重推 + hcsImmediatePushSupport.pushPaymentAndWait(request.getId()); + return approveResponse(request.getId()); } - /** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */ - private R approveResponse() { + /** 审批接口统一响应:审批已生效,付款特批发起/自动同步结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ + private R approveResponse(Long id) { + String hcsFailure = HcsImmediatePushSupport.pollSyncFailure(); String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); + String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); + if (hcsFailure != null) { + return R.data(Boolean.FALSE, "审批通过,但发起付款特批/推送合规失败:" + hcsFailure + ",可稍后重推合规"); + } + PaymentApplication payment = paymentApplicationService.getById(id); + String specStatus = payment == null ? null : payment.getHcsSpecStatus(); + if (HcsSyncConstant.SPEC_STATUS_NEED.equals(specStatus)) { + return R.success("审批通过,付款特批待发起,可在详情页重推合规"); + } + if (Arrays.asList(HcsSyncConstant.SPEC_STATUS_PUSHING, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(specStatus)) { + return R.success("审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶"); + } if (syncFailure != null) { return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); } + if (syncResult != null) { + return R.success("审批通过,已同步金蝶:" + syncResult); + } return R.success("审批通过"); } @PostMapping("/return") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java index 29b3962c..83b96682 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java @@ -40,6 +40,7 @@ import org.springblade.transport.excel.ProjectApplyExcel; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.vo.ProjectApplyVO; import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.support.hcs.HcsImmediatePushSupport; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -63,6 +64,7 @@ import java.util.Map; public class ProjectApplyController extends BladeController { private final IProjectApplyService projectApplyService; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @GetMapping("/detail") @ApiOperationSupport(order = 1) @@ -119,7 +121,14 @@ public class ProjectApplyController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "审批通过", description = "传入id") public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) { - return R.status(projectApplyService.approve(id)); + projectApplyService.approve(id); + // 审批事务已提交,再同步推送合规;失败原因以警告文案返回(data=false 供前端区分提示样式),照正式结算单审批接口 + hcsImmediatePushSupport.pushProjectAndWait(id); + String syncFailure = HcsImmediatePushSupport.pollSyncFailure(); + if (syncFailure != null) { + return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure); + } + return R.success("审批通过"); } @PostMapping("/reject") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java index d03d8d1d..86772411 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java @@ -39,12 +39,16 @@ public class SettlementAdjustmentController extends BladeController { String hint = service.approve(request); // 未推送场景自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); + String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); if (Func.isNotBlank(syncFailure)) { return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); } if (Func.isNotBlank(hint)) { return R.data(Boolean.TRUE, "审批通过;" + hint); } + if (Func.isNotBlank(syncResult)) { + return R.success("审批通过,已同步金蝶:" + syncResult); + } return R.success("审批通过"); } @PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java index 185a0c56..fbecca86 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java @@ -22,7 +22,9 @@ package org.springblade.transport.listener; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.transport.config.BfiProperties; +import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.event.KingdeeBillApprovedEvent; +import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IInvoiceApplicationService; import org.springblade.transport.service.IInvoiceReceiptService; @@ -66,6 +68,9 @@ public class KingdeeApprovalSyncListener { /** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */ private static final ThreadLocal SYNC_FAILURE = new ThreadLocal<>(); + /** 当前审批请求线程的自动推送成功结果(金蝶单据号/推送摘要),供 Controller 拼进提示文案 */ + private static final ThreadLocal SYNC_RESULT = new ThreadLocal<>(); + /** * 取走并清除当前审批请求的自动推送失败原因;null 表示推送成功或未触发推送。 * 须在 service.approve 返回后立即调用(监听器在审批事务提交时同步执行完毕)。 @@ -76,6 +81,23 @@ public class KingdeeApprovalSyncListener { return failure; } + /** + * 取走并清除当前审批请求的自动推送成功结果;null 表示推送失败或未触发推送。 + * 与 {@link #pollSyncFailure()} 成对调用,避免线程复用导致结果串单。 + */ + public static String pollSyncResult() { + String result = SYNC_RESULT.get(); + SYNC_RESULT.remove(); + return result; + } + + /** 记录本次自动推送的成功结果,空值不记录(未触发推送/接口无返回) */ + private static void recordResult(String result) { + if (result != null && !result.isBlank()) { + SYNC_RESULT.set(result); + } + } + /** * 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。 * @@ -84,6 +106,7 @@ public class KingdeeApprovalSyncListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void onBillApproved(KingdeeBillApprovedEvent event) { SYNC_FAILURE.remove(); + SYNC_RESULT.remove(); try { switch (event.bizType()) { case KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT -> pushSettlement(event.id()); @@ -95,6 +118,7 @@ public class KingdeeApprovalSyncListener { } catch (Exception exception) { // 推送失败只记日志(失败状态已在推送方法内独立事务落库),不影响审批结果,交给重推任务与手动按钮兜底; // 原因暂存线程变量,由审批 Controller 取走提示前端 + SYNC_RESULT.remove(); log.error("审批通过自动同步金蝶失败 bizType={} id={}", event.bizType(), event.id(), exception); String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage(); // 手动按钮同文案已带"推送金蝶失败:"前缀,拼提示时去掉避免重复 @@ -108,6 +132,7 @@ public class KingdeeApprovalSyncListener { } log.info("正式结算单[{}]审批通过自动同步金蝶开始", id); String billNo = formalSettlementService.syncKingdee(id); + recordResult(billNo); log.info("正式结算单[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo); } @@ -115,8 +140,18 @@ public class KingdeeApprovalSyncListener { if (!bfiProperties.getAutoSync().isPayment()) { return; } + // 付款特批未完成(高风险/黑名单/额度超额)时金蝶必然拒推:跳过而非置失败, + // 特批通过后由 HcsPaymentSyncSupport 重新发布本事件自动补推 + PaymentApplication payment = paymentApplicationService.getById(id); + if (payment != null && payment.getHcsSpecStatus() != null + && List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PUSHING, + HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(payment.getHcsSpecStatus())) { + log.info("付款申请[{}]审批通过自动同步金蝶跳过(付款特批未完成):hcsSpecStatus={}", id, payment.getHcsSpecStatus()); + return; + } log.info("付款申请[{}]审批通过自动同步金蝶开始", id); String kingdeeBillId = paymentApplicationService.syncKingdee(id); + recordResult(kingdeeBillId); log.info("付款申请[{}]审批通过自动同步金蝶完成:kingdeeBillId={}", id, kingdeeBillId); } @@ -126,6 +161,7 @@ public class KingdeeApprovalSyncListener { } log.info("开票申请[{}]审批通过自动同步金蝶开始", id); String billNo = invoiceApplicationService.syncKingdee(id); + recordResult(billNo); log.info("开票申请[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo); } @@ -135,6 +171,7 @@ public class KingdeeApprovalSyncListener { } log.info("收票登记[{}]审批通过自动推送金蝶收票单开始", id); String summary = invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id), true); + recordResult(summary); log.info("收票登记[{}]审批通过自动推送金蝶收票单完成:{}", id, summary); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsAttachmentSyncMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsAttachmentSyncMapper.java new file mode 100644 index 00000000..637862ed --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/HcsAttachmentSyncMapper.java @@ -0,0 +1,39 @@ +/** + * 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.HcsAttachmentSync; + +/** + * HCS 附件转传映射 Mapper + * + * @author Chill + */ +@Mapper +public interface HcsAttachmentSyncMapper extends BaseMapper { +} 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/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/mapper/PaymentApplicationSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java index 78e02855..29d34fa0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java @@ -1,10 +1,21 @@ package org.springblade.transport.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; /** 付款申请关联正式结算单 Mapper。 */ @Mapper public interface PaymentApplicationSettlementMapper extends BaseMapper { + + /** + * 按付款申请主键物理删除关联记录(表存在不含逻辑删除字段的关系唯一索引,重存前必须物理清理) + * + * @param paymentApplicationId 付款申请主键 + * @return 删除行数 + */ + @Delete("DELETE FROM blade_payment_application_settlement WHERE payment_application_id = #{paymentApplicationId}") + int deleteByPaymentApplicationId(@Param("paymentApplicationId") Long paymentApplicationId); } 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 b96c8469..668d232a 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 @@ -25,7 +25,10 @@ import org.springblade.transport.service.IInvoiceApplicationService; import org.springblade.transport.service.IInvoiceReceiptService; 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.springblade.transport.support.hcs.HcsImmediatePushSupport; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -75,6 +78,7 @@ public final class MkProcessBizHandlers { @RequiredArgsConstructor public static class ProjectHandler implements IMkProcessBizHandler { private final IProjectApplyService service; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @Override public String bizType() { @@ -122,7 +126,13 @@ public final class MkProcessBizHandlers { entity.setCurrentNode("审核通过"); entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); entity.setApprovedTime(LocalDateTime.now()); - return service.updateById(entity); + // HCS 推送:审批通过置待同步状态,更新成功后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6) + entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + boolean updated = service.updateById(entity); + if (updated) { + hcsImmediatePushSupport.pushProjectAfterCommit(id); + } + return updated; } @Override @@ -145,6 +155,7 @@ public final class MkProcessBizHandlers { @RequiredArgsConstructor public static class ContractHandler implements IMkProcessBizHandler { private final IContractManageService service; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @Override public String bizType() { @@ -190,7 +201,13 @@ public final class MkProcessBizHandlers { entity.setApprovalStatus(change ? "change_approved" : "approved"); entity.setCurrentNode("审核通过"); entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); - return service.updateById(entity); + // HCS 推送:审批通过置待同步状态,更新成功后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6) + entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + boolean updated = service.updateById(entity); + if (updated) { + hcsImmediatePushSupport.pushContractAfterCommit(id); + } + return updated; } @Override @@ -361,6 +378,7 @@ public final class MkProcessBizHandlers { @RequiredArgsConstructor public static class PaymentHandler implements IMkProcessBizHandler { private final IPaymentApplicationService service; + private final HcsImmediatePushSupport hcsImmediatePushSupport; @Override public String bizType() { @@ -408,8 +426,12 @@ public final class MkProcessBizHandlers { if (updated != null) { updated.setCurrentNode("审核通过"); updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); + // HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001) + updated.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); service.updateById(updated); } + // 审批通过立即发起付款特批(C106-3,不受即时推送开关控制;事务提交后异步执行,失败留待重推兜底) + hcsImmediatePushSupport.pushPaymentAfterCommit(id); return true; } 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..6c8937b7 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java @@ -0,0 +1,112 @@ +/** + * 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.ContractManage; +import org.springblade.transport.pojo.entity.HcsContractExeStage; +import org.springblade.transport.pojo.entity.PaymentApplication; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 合同履约规则 服务接口 + * + * @author Chill + */ +public interface IContractExeStageService extends BaseService { + + /** + * 按合同查询履约规则行(表单维护用,不含软删行) + * + * @param contractId 合同ID + * @return 规则行列表 + */ + List listByContractId(Long contractId); + + /** + * 匹配合同的付款履约计划行(已同步,优先按款项性质精确匹配,无则取首条付款行兜底) + * + * @param contractId 合同ID + * @param fundNature 款项性质(progress/prepayment 等,空则直接取首条付款行) + * @return 付款履约计划行,无已同步付款行时返回 null + */ + HcsContractExeStage findPaymentStageRow(Long contractId, String fundNature); + + /** + * 匹配合同的收款履约计划行(已同步,优先按款项性质精确匹配,无则取首条收款行兜底) + * + * @param contractId 合同ID + * @param fundNature 款项性质(progress/prepayment 等,空则直接取首条收款行) + * @return 收款履约计划行,无已同步收款行时返回 null + */ + HcsContractExeStage findCollectionStageRow(Long contractId, String fundNature); + + /** + * 金蝶认领推送完成后累加收款履约计划行款项金额(认领金额累加,多次认领累加更新): + * 原子累加 fund_amount;已同步行置 row_op=update 凭已存合规阶段ID 随 C103-9 重推, + * 未同步行保持 add 待合同链路首推(首推即携带累加后金额) + * + * @param contractId 合同ID + * @param claimedAmount 本次累加的认领金额(须大于0) + */ + void accumulateClaimedAmountOnPush(Long contractId, BigDecimal claimedAmount); + + /** + * 付款审批通过累加履约计划行款项金额(款项金额=申请付款金额,多次付款累加更新): + * 按款项性质匹配付款计划行,原子累加 fund_amount;已同步行置 row_op=update 凭已存合规阶段ID 随 C103-9 重推, + * 未同步行保持 add 待合同链路首推(首推即携带累加后金额) + * + * @param payment 付款申请单(审批通过) + */ + void accumulatePaidAmountOnApprove(PaymentApplication payment); + + /** + * 审批通过自动创建默认履约计划行(合同下无规则行时):承运商合同=付款进度款(收票规则取合同结算模式), + * 客户合同=收款进度款;金额/资金日期留空,随合同链路推 C103-9 + * + * @param contractManage 合同单(审批通过) + */ + void createDefaultRowOnApprove(ContractManage contractManage); + + /** + * 保存合同履约规则行(按行差异置同步标记:新行/未同步行 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..b6ce2270 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractExeStageServiceImpl.java @@ -0,0 +1,302 @@ +/** + * 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.pojo.entity.PaymentApplication; +import org.springblade.transport.service.IContractExeStageService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +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 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 + public HcsContractExeStage findPaymentStageRow(Long contractId, String fundNature) { + if (contractId == null) { + return null; + } + if (StringUtil.isNotBlank(fundNature)) { + HcsContractExeStage natureRow = getOne(Wrappers.lambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_PAYMENT) + .eq(HcsContractExeStage::getFundNature, fundNature) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + if (natureRow != null) { + return natureRow; + } + } + // 兜底:无该款项性质已同步行时取首条已同步付款行(与 C108 履约记录历史口径一致) + return getOne(Wrappers.lambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_PAYMENT) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + } + + @Override + public void accumulatePaidAmountOnApprove(PaymentApplication payment) { + if (payment == null || payment.getContractId() == null || payment.getAppliedAmount() == null) { + return; + } + String fundNature = HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType()); + HcsContractExeStage stageRow = this.findPaymentStageRow(payment.getContractId(), fundNature); + // 仅精确匹配款项性质的计划行累加,兜底行款项性质不一致时不累加(避免预付款金额计入进度款计划) + if (stageRow == null || !fundNature.equals(stageRow.getFundNature())) { + log.info("付款审批未匹配到款项性质[{}]的履约计划行,跳过款项金额累加:paymentNo={},contractId={}", + fundNature, payment.getPaymentNo(), payment.getContractId()); + return; + } + // 原子累加防并发付款审批丢失更新(coalesce 兼容多数据库) + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .setSql("fund_amount = coalesce(fund_amount, 0) + " + payment.getAppliedAmount().toPlainString())); + boolean resyncNeeded = StringUtil.isNotBlank(stageRow.getHcsStageFdId()); + if (resyncNeeded) { + // 已同步行置 update 凭已存合规阶段ID 随 C103-9 重推;未同步行保持 add 待合同链路首推 + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .set(HcsContractExeStage::getRowOp, HcsSyncConstant.ROW_OP_UPDATE) + .set(HcsContractExeStage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_UNSYNCED) + .set(HcsContractExeStage::getHcsSyncMsg, "付款审批款项金额累加,待重推C103-9")); + } + log.info("付款审批履约计划款项金额累加:paymentNo={},stageRowId={},累加金额={},rowOp={}", + payment.getPaymentNo(), stageRow.getId(), payment.getAppliedAmount(), resyncNeeded ? "update" : "add(待合同链路首推)"); + } + + @Override + public HcsContractExeStage findCollectionStageRow(Long contractId, String fundNature) { + if (contractId == null) { + return null; + } + if (StringUtil.isNotBlank(fundNature)) { + HcsContractExeStage natureRow = getOne(Wrappers.lambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_COLLECTION) + .eq(HcsContractExeStage::getFundNature, fundNature) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + if (natureRow != null) { + return natureRow; + } + } + // 兜底:无该款项性质已同步行时取首条已同步收款行 + return getOne(Wrappers.lambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_COLLECTION) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + } + + @Override + public void accumulateClaimedAmountOnPush(Long contractId, BigDecimal claimedAmount) { + if (contractId == null || claimedAmount == null || claimedAmount.signum() <= 0) { + return; + } + HcsContractExeStage stageRow = this.findCollectionStageRow(contractId, HcsSyncConstant.FUND_NATURE_PROGRESS); + if (stageRow == null) { + log.info("收款认领未匹配到已同步收款履约计划行,跳过款项金额累加:contractId={}", contractId); + return; + } + // 原子累加防并发认领推送丢失更新(coalesce 兼容多数据库) + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .setSql("fund_amount = coalesce(fund_amount, 0) + " + claimedAmount.toPlainString())); + boolean resyncNeeded = StringUtil.isNotBlank(stageRow.getHcsStageFdId()); + if (resyncNeeded) { + // 已同步行置 update 凭已存合规阶段ID 随 C103-9 重推;未同步行保持 add 待合同链路首推 + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .set(HcsContractExeStage::getRowOp, HcsSyncConstant.ROW_OP_UPDATE) + .set(HcsContractExeStage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_UNSYNCED) + .set(HcsContractExeStage::getHcsSyncMsg, "收款认领金额累加,待重推C103-9")); + } + log.info("收款认领履约计划款项金额累加:contractId={},stageRowId={},累加金额={},rowOp={}", + contractId, stageRow.getId(), claimedAmount, resyncNeeded ? "update" : "add(待合同链路首推)"); + } + + @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(HcsSyncConstant.ROW_OP_ADD); + row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + row.setHcsSyncMsg("待随合同链路同步"); + 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()) ? HcsSyncConstant.ROW_OP_ADD : HcsSyncConstant.ROW_OP_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("待随合同链路同步(" + 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(HcsSyncConstant.ROW_OP_DELETE); + row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + row.setHcsSyncMsg("待随合同链路同步(delete)"); + updateById(row); + } + removeById(id); + log.info("合同履约规则行删除:id={},contractId={},rowOp={}", id, row.getContractId(), row.getRowOp()); + return true; + } + + @Override + public void createDefaultRowOnApprove(ContractManage contractManage) { + Long contractId = contractManage.getId(); + if (count(Wrappers.lambdaQuery().eq(HcsContractExeStage::getContractId, contractId)) > 0) { + return; + } + HcsContractExeStage row = new HcsContractExeStage(); + row.setContractId(contractId); + if ("承运商合同".equals(contractManage.getContractCategory())) { + row.setPerformType(HcsSyncConstant.PERFORM_TYPE_PAYMENT); + row.setInvoiceRule(mapInvoiceRule(contractManage.getSettlementMode())); + } else if ("客户合同".equals(contractManage.getContractCategory())) { + row.setPerformType(HcsSyncConstant.PERFORM_TYPE_COLLECTION); + } else { + log.info("合同类别非承运商/客户,不自动创建履约计划行:contractNo={},contractCategory={}", contractManage.getContractNo(), contractManage.getContractCategory()); + return; + } + // 默认进度款计划行:金额/资金日期留空,随合同链路推 C103-9 + row.setFundNature(HcsSyncConstant.FUND_NATURE_PROGRESS); + row.setFundType(hcsProperties.getExeStage().getFundType()); + row.setIsFinish(0); + row.setRowOp(HcsSyncConstant.ROW_OP_ADD); + row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + row.setHcsSyncMsg("审批通过自动创建,待随合同链路同步"); + save(row); + log.info("审批通过自动创建履约计划行:contractNo={},contractCategory={},performType={},invoiceRule={}", + contractManage.getContractNo(), contractManage.getContractCategory(), row.getPerformType(), row.getInvoiceRule()); + } + + /** + * 结算模式文本映射 C103-9 开票规则:先款后票=payFirst、先票后款=invoiceFirst,其余(月结/现结等)不传 + */ + private String mapInvoiceRule(String settlementMode) { + if ("先款后票".equals(settlementMode)) { + return "payFirst"; + } + if ("先票后款".equals(settlementMode)) { + return "invoiceFirst"; + } + return null; + } + + /** + * 行字段校验 + */ + private void validateRow(HcsContractExeStage row) { + if (StringUtil.isBlank(row.getPerformType())) { + throw new ServiceException("履约类型不能为空(payment/collection)"); + } + if (!List.of(HcsSyncConstant.PERFORM_TYPE_PAYMENT, HcsSyncConstant.PERFORM_TYPE_COLLECTION).contains(row.getPerformType())) { + throw new ServiceException("履约类型非法:" + row.getPerformType()); + } + if (row.getFundAmount() != null && row.getFundAmount().signum() < 0) { + 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 f1a4a920..bb9d57d0 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; @@ -35,12 +37,17 @@ import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; +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.IContractExeStageService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.hcs.HcsCreditCheckSupport; import org.springblade.transport.wrapper.ContractManageWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,9 +72,15 @@ import java.util.regex.Pattern; * * @author Chill */ +@Slf4j @Service +@RequiredArgsConstructor public class ContractManageServiceImpl extends BaseServiceImpl implements IContractManageService { + private final HcsContractExeStageMapper hcsContractExeStageMapper; + private final HcsCreditCheckSupport hcsCreditCheckSupport; + private final IContractExeStageService contractExeStageService; + private static final String STAGE_DRAFT = "draft"; private static final String STAGE_TEMPORARY = "temporary"; private static final String STAGE_FORMAL = "formal"; @@ -150,6 +163,8 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaUpdate() + .eq(HcsContractExeStage::getContractId, contractManage.getId()) + .isNull(HcsContractExeStage::getHcsStageFdId) + .set(HcsContractExeStage::getRowOp, "add")); return updateById(contractManage); } @@ -371,6 +399,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl queryWrapper, Boolean kingdeeSynced, Boolean hcsSynced) { + if (Boolean.TRUE.equals(kingdeeSynced)) { + queryWrapper.isNotNull(ContractManage::getKingdeeId).ne(ContractManage::getKingdeeId, ""); + } else if (Boolean.FALSE.equals(kingdeeSynced)) { + queryWrapper.and(wrapper -> wrapper.isNull(ContractManage::getKingdeeId) + .or().eq(ContractManage::getKingdeeId, "")); + } + if (Boolean.TRUE.equals(hcsSynced)) { + queryWrapper.eq(ContractManage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED); + } else if (Boolean.FALSE.equals(hcsSynced)) { + queryWrapper.and(wrapper -> wrapper.isNull(ContractManage::getHcsSyncStatus) + .or().ne(ContractManage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED)); + } + } + private void applyEffectiveType(LambdaQueryWrapper queryWrapper, String effectiveType) { if (Func.isEmpty(effectiveType)) { return; @@ -1205,6 +1258,49 @@ public class ContractManageServiceImpl extends BaseServiceImpl HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION + .equals(Objects.toString(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD), null))); + } + + /** + * 单方客商是否高风险/黑名单(C101-1 实时查询,名称反查不到客商编号不阻断) + */ + private boolean isRiskyCustomer(String partyName) { + if (Func.isEmpty(partyName)) { + return false; + } + return Boolean.TRUE.equals(hcsCreditCheckSupport.queryCustomerRiskByName(partyName).getRisky()); + } + private String mergeAttachmentJson(String existingJson, String pendingJson) { List merged = parseJsonArray(existingJson); List pending = parseJsonArray(pendingJson); 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 ce836642..7b74830e 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.BfiPageVO; import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.transport.config.BfiProperties; import org.springblade.transport.constant.KingdeeAttachmentConstant; +import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.constant.KingdeeBfiConstant; import org.springblade.transport.constant.KingdeeLockConstant; import org.springblade.transport.pojo.entity.KingdeeBillLock; @@ -1271,6 +1272,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl implements IInvoiceReceiptService { @@ -2073,6 +2077,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl selectPage(IPage page, PaymentApplicationVO query) { @@ -460,6 +466,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl settlements, List lines) { if (settlements.isEmpty()) return; - settlementRelationMapper.delete(Wrappers.lambdaQuery() - .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); - boolean customAmounts = this.useSpecifiedLineAmounts(lines); + // 关系唯一索引不含 is_deleted,逻辑删除会残留旧行导致重存冲突,此处必须物理删除 + settlementRelationMapper.deleteByPaymentApplicationId(entity.getId()); + boolean customAmounts = settlements.size() > 1 && lines != null && !lines.isEmpty(); BigDecimal totalBase = settlements.stream() .map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index d72428c5..cf107fbd 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -37,6 +37,7 @@ import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; +import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.excel.ProjectApplyExcel; import org.springblade.transport.mapper.ProjectApplyMapper; import org.springblade.transport.mapper.FormalSettlementMapper; @@ -213,6 +214,8 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl queryWrapper, Boolean kingdeeSynced, Boolean hcsSynced) { + if (Boolean.TRUE.equals(kingdeeSynced)) { + queryWrapper.isNotNull(ProjectApply::getKingdeeId).ne(ProjectApply::getKingdeeId, ""); + } else if (Boolean.FALSE.equals(kingdeeSynced)) { + queryWrapper.and(wrapper -> wrapper.isNull(ProjectApply::getKingdeeId) + .or().eq(ProjectApply::getKingdeeId, "")); + } + if (Boolean.TRUE.equals(hcsSynced)) { + queryWrapper.eq(ProjectApply::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED); + } else if (Boolean.FALSE.equals(hcsSynced)) { + queryWrapper.and(wrapper -> wrapper.isNull(ProjectApply::getHcsSyncStatus) + .or().ne(ProjectApply::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED)); + } + } + private void fillFundUseRisk(List records) { if (records == null || records.isEmpty()) { return; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index e6c6aecd..899315a4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -65,6 +65,7 @@ import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IReceiptFlowService; import org.springblade.transport.support.KingdeeSyncLockSupport; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.hcs.HcsClaimSyncSupport; import org.springblade.transport.wrapper.ReceiptFlowWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -130,6 +131,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl selectPage(IPage page, ReceiptFlowVO query) { @@ -904,10 +906,15 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl doPushClaimToKingdee(flowId, announce)); + ReceiptClaimPushResult result = transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce)); + if (result != null && result.isSuccess()) { + hcsClaimSyncSupport.pushClaimToHcs(flowId); + } + return result; } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java index db94c665..84ee99ce 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -282,6 +282,8 @@ public class KingdeeApBillSyncSupport { param.setCurrencyNumber(currency); // 【必填】付款组织.编码 param.setPayorgNumber(orgNumber); + // 部门.编码:项目业务部门自身的金蝶编码(结算/付款组织仍传上溯平台公司编码,口径与付款申请 A103-3 一致) + param.setDepartmentNumber(this.kingdeeDeptNumber(contract.getProjectId(), context)); // 采购组织.编码(非必传) // param.setPurorgNumber(orgNumber); // 【必填】款项性质.编码 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java index 64a3a99a..f9e214c1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java @@ -218,6 +218,8 @@ public class KingdeeArBillSyncSupport { param.setOrgNumber(orgNumber); // 收款组织.编码:同结算组织 param.setRecorgNumber(orgNumber); + // 部门.编码:项目业务部门自身的金蝶编码(结算/收款组织仍传上溯平台公司编码,口径与付款申请/应付单一致) + param.setDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context)); // 【必填】单据日期 param.setBizdate(billDateText); // 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径) 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..07bcfeb0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java @@ -0,0 +1,367 @@ +/** + * 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 com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.dto.HcsAttachmentDownloadDTO; +import org.springblade.hcs.dto.HcsAttachmentRefDTO; +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.springblade.transport.mapper.HcsAttachmentSyncMapper; +import org.springblade.transport.pojo.entity.HcsAttachmentSync; +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.LocalDateTime; +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 → 落映射同步表(blade_hcs_attachment_sync,照金蝶附件同步表设计: + * biz_type+biz_id+attachment_key 唯一,转传前查命中即复用,仅落成功记录)。 + * 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试(失败原因落单据 hcs_sync_msg)。 + *

+ * 合同链路挂载字段 fdEntityKey(v4 枚举):主文件 approved=true → eopCdraftSign(用印)、 + * 特批附件(fileType=特批附件)→ eopCdraftSpecial(随 C103-1 fdSepcailContract=true 特批流程)、其它附件 → eopCdraftOther。 + * + * @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 HcsAttachmentSyncMapper hcsAttachmentSyncMapper; + + /** + * 解析单据附件 JSON 并转传,返回合规附件引用列表(保持行序) + * + * @param bizType 业务单据类型(HcsSyncConstant.BIZ_TYPE_*) + * @param bizId 业务单据id + * @param attachmentsJson 附件元数据 JSON 数组 + * @param operatorLoginName 操作人登录名(C103-2 fdLoginName) + * @return 合规附件引用列表(fdId + fdEntityKey) + */ + public List transferAttachments(String bizType, Long bizId, String attachmentsJson, String operatorLoginName) { + if (StringUtil.isBlank(attachmentsJson)) { + return List.of(); + } + List> attachments = parseJsonArray(attachmentsJson); + List attachmentRefs = new ArrayList<>(); + for (Map attachment : attachments) { + attachmentRefs.add(this.transferOne(bizType, bizId, attachment, operatorLoginName)); + } + return attachmentRefs; + } + + /** + * 解析合同主文件 JSON(仅 approved=true 的文件,挂用印字段)与其它附件 JSON(挂其他附件字段)并转传 + * + * @param bizType 业务单据类型(HcsSyncConstant.BIZ_TYPE_*) + * @param bizId 业务单据id + * @param contractFileJson 合同主文件 JSON + * @param attachmentsJson 其它附件 JSON + * @param operatorLoginName 操作人登录名 + * @return 合规附件引用列表(fdId + fdEntityKey) + */ + public List transferContractAttachments(String bizType, Long bizId, String contractFileJson, String attachmentsJson, String operatorLoginName) { + List attachmentRefs = new ArrayList<>(); + for (Map file : parseJsonArray(contractFileJson)) { + if (isApproved(file)) { + attachmentRefs.add(this.transferOne(bizType, bizId, file, operatorLoginName, + HcsSyncConstant.CONTRACT_ATTACH_KEY_SEAL)); + } + } + for (Map attachment : parseJsonArray(attachmentsJson)) { + // 特批附件(fileType=特批附件)挂特批面板字段,其余挂其他附件字段(2026-10-13:特批改随 C103-1 fdSepcailContract 发起) + String entityKey = HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION.equals(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD)) + ? HcsSyncConstant.CONTRACT_ATTACH_KEY_SPECIAL : HcsSyncConstant.CONTRACT_ATTACH_KEY_OTHER; + attachmentRefs.add(this.transferOne(bizType, bizId, attachment, operatorLoginName, entityKey)); + } + return attachmentRefs; + } + + /** + * 前端单文件直传合规(C103-2 代理,前端接入设计 §3.2):返回合规附件ID; + * 不写映射缓存(无 TMS 附件元数据键),调用方自行持久化 fdId(如特批材料 hcs_spec_files_json) + * + * @param file 上传文件 + * @param loginName 操作人登录名(fdLoginName) + * @return 合规附件ID + */ + public String uploadOne(MultipartFile file, String loginName) { + if (file == null || file.isEmpty()) { + throw new HcsException("HCS附件上传失败:文件为空"); + } + HcsResult result = hcsClient.uploadAttachment(file, "false", loginName); + if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) { + log.error("HCS附件上传失败响应:{}", result == null ? "无响应" : String.format("code=%s, msg=%s, traceId=%s", + result.getCode(), result.getMsg(), result.getTraceId())); + throw new HcsException("HCS附件上传失败:" + (result == null ? "响应为空" : result.getMsg())); + } + return result.getData().getFdId(); + } + + /** + * 合规附件下载(C103-5 代理,前端接入设计 §3.2):返回原始文件流响应,由控制器透传浏览器 + * + * @param fdId 合规附件ID + * @return Feign 文件流响应(调用方负责关闭) + */ + public feign.Response downloadRaw(String fdId) { + HcsAttachmentDownloadDTO param = new HcsAttachmentDownloadDTO(); + // 【必填】附件ID:上传时返回的 fdId + param.setFdId(fdId); + feign.Response response = hcsClient.downloadAttachment(param); + if (response == null || response.body() == null) { + throw new HcsException("HCS附件下载失败:响应为空(fdId=" + fdId + ")"); + } + return response; + } + + /** + * 转传单个附件:映射同步表命中直接复用合规附件ID,未命中下载上传后落成功记录; + * 返回引用默认不带 fdEntityKey,由调用方(或合同主文件/其他附件重载)指定挂载字段 + */ + private HcsAttachmentRefDTO transferOne(String bizType, Long bizId, Map attachment, String operatorLoginName) { + return this.transferOne(bizType, bizId, attachment, operatorLoginName, null); + } + + /** + * 转传单个附件并指定挂载字段 key(fdEntityKey,如合同 eopCdraftSpecial/eopCdraftSign/eopCdraftOther) + */ + private HcsAttachmentRefDTO transferOne(String bizType, Long bizId, Map attachment, String operatorLoginName, String fdEntityKey) { + String attachmentKey = attachmentKey(attachment); + HcsAttachmentSync existing = hcsAttachmentSyncMapper.selectOne(Wrappers.lambdaQuery() + .eq(HcsAttachmentSync::getBizType, bizType) + .eq(HcsAttachmentSync::getBizId, bizId) + .eq(HcsAttachmentSync::getAttachmentKey, attachmentKey) + .last("limit 1")); + if (existing != null && StringUtil.isNotBlank(existing.getFdId())) { + String url = resolveUrl(attachment); + return buildRef(existing.getFdId(), resolveFileName(attachment, url == null ? "" : url), fdEntityKey); + } + 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附件转传失败[" + fileName + "]:" + (result == null ? "响应为空" : result.getMsg())); + } + String attachmentFdId = result.getData().getFdId(); + HcsAttachmentSync record = new HcsAttachmentSync(); + record.setBizType(bizType); + record.setBizId(bizId); + record.setAttachmentKey(attachmentKey); + record.setAttachmentName(fileName); + record.setFdId(attachmentFdId); + record.setSyncTime(LocalDateTime.now()); + hcsAttachmentSyncMapper.insert(record); + log.info("HCS附件转传完成:{} → 合规附件ID {}(业务单据[{}:{}],fdEntityKey={})", fileName, attachmentFdId, bizType, bizId, fdEntityKey); + return buildRef(attachmentFdId, fileName, fdEntityKey); + } + + /** + * 合规附件引用(fdId + 挂载字段 key) + */ + private HcsAttachmentRefDTO buildRef(String fdId, String fileName, String fdEntityKey) { + HcsAttachmentRefDTO ref = new HcsAttachmentRefDTO(); + ref.setFdId(fdId); + ref.setFdFileName(fileName); + ref.setFdEntityKey(fdEntityKey); + return ref; + } + + /** + * 按元数据 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/HcsClaimSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java new file mode 100644 index 00000000..a74391fd --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java @@ -0,0 +1,379 @@ +/** + * 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 + * is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for their own commercial purposes. + *

+ * Using this software signifies agreement to this license agreement and terms of use, + * and you expressly agree to be bound by the terms of use of this software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support.hcs; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +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.HcsClaimRecordAddDTO; +import org.springblade.hcs.dto.HcsCollectionFlowVO; +import org.springblade.hcs.dto.HcsFormSaveVO; +import org.springblade.hcs.dto.HcsIdVO; +import org.springblade.hcs.dto.HcsMechanismsDTO; +import org.springblade.hcs.dto.HcsPageQueryDTO; +import org.springblade.hcs.dto.HcsPageVO; +import org.springblade.hcs.dto.HcsPerfRecordFormDataDTO; +import org.springblade.hcs.dto.HcsPerfRecordPropsDTO; +import org.springblade.hcs.dto.HcsPerfRecordSaveDTO; +import org.springblade.hcs.dto.HcsRefVO; +import org.springblade.hcs.dto.HcsResult; +import org.springblade.hcs.dto.HcsUserRefVO; +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.FormalSettlementMapper; +import org.springblade.transport.mapper.KingdeeReceiptFlowMapper; +import org.springblade.transport.mapper.ReceiptClaimMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.HcsContractExeStage; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; +import org.springblade.transport.service.IContractExeStageService; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * HCS 收款认领链推送编排支撑组件(C103-9 更新履约计划 + C107-1 认领记录 + C108-1 履约流水) + *

+ * 触发点:金蝶认领推送(A301-3)成功之后(手动按钮与定时任务共用钩子)。 + * 开关复用既有配置(不单独新增):exe-stage.sync-enabled 门禁 C103-9/C107-1(与合同链履约计划同步同开关), + * perf-record.sync-enabled 门禁 C108-1(与付款链履约记录同步同开关)。 + * 编排顺序:先 C103-9(认领金额累加到收款履约计划行并重推,C107-1/C108-1 引用其返回的阶段ID), + * 再按「认领主单 × 合同」推 C107-1(合规流水ID 经 C104-2 按明细流水号定位),最后按合同推 C108-1 履约流水。 + * 合同未推送合规(hcs_fd_id 空)时仅累加计划行金额,随合同链路首推。 + * 推送失败仅记日志不回滚金蝶推送结果(单合同失败不影响其余合同)。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsClaimSyncSupport { + + /** + * 认领完成状态(与 ReceiptFlowServiceImpl.CLAIMED 同值) + */ + private static final String CLAIMED = "claimed"; + + private final IHcsClient hcsClient; + private final HcsProperties hcsProperties; + private final KingdeeReceiptFlowMapper receiptFlowMapper; + private final ReceiptClaimMapper claimMapper; + private final ReceiptClaimSettlementMapper claimSettlementMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ContractManageMapper contractManageMapper; + private final IContractExeStageService contractExeStageService; + private final HcsExeStageSyncSupport hcsExeStageSyncSupport; + + /** + * 金蝶认领推送成功后推送合同合规(C103-9 → C107-1 → C108-1) + *

+ * 复用既有开关:exe-stage.sync-enabled(履约计划同步)门禁 C103-9/C107-1, + * perf-record.sync-enabled(履约记录同步)门禁 C108-1;开关关闭或推送异常均静默返回, + * 不影响金蝶认领推送主流程与事务。 + * + * @param flowId 收款流水ID + */ + public void pushClaimToHcs(Long flowId) { + if (!hcsProperties.getExeStage().isSyncEnabled()) { + return; + } + try { + doPushClaimToHcs(flowId); + } catch (Exception exception) { + log.error("HCS收款认领链推送异常 flowId:{}", flowId, exception); + } + } + + /** + * 推送主体:归集有效认领分摊(按合同分组)→ 逐合同推送(单合同失败不影响其余合同) + */ + private void doPushClaimToHcs(Long flowId) { + KingdeeReceiptFlow flow = receiptFlowMapper.selectById(flowId); + if (flow == null) { + return; + } + // 未作废认领主单(作废认领不同步合规) + List claims = claimMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceiptClaim::getReceiptFlowId, flowId) + .eq(ReceiptClaim::getClaimStatus, CLAIMED)); + if (claims.isEmpty()) { + return; + } + // 有效分摊行(作废时已置 status=0) + List allocations = claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptFlowId, flowId) + .eq(ReceiptClaimSettlement::getStatus, 1) + .eq(ReceiptClaimSettlement::getIsDeleted, 0)); + if (allocations.isEmpty()) { + return; + } + // 结算单批量定位合同(一次 IN 查询,避免逐行 N+1) + Map settlements = formalSettlementMapper.selectBatchIds( + allocations.stream().map(ReceiptClaimSettlement::getFormalSettlementId) + .filter(Objects::nonNull).distinct().toList()).stream() + .collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first)); + // 合同id → 认领主单id → 分摊金额 + Map> amountByContractByClaim = new LinkedHashMap<>(); + for (ReceiptClaimSettlement allocation : allocations) { + FormalSettlement settlement = settlements.get(allocation.getFormalSettlementId()); + if (settlement == null || settlement.getContractId() == null + || allocation.getReceiptClaimId() == null || allocation.getAllocatedReceiptAmount() == null) { + continue; + } + amountByContractByClaim + .computeIfAbsent(settlement.getContractId(), key -> new LinkedHashMap<>()) + .merge(allocation.getReceiptClaimId(), allocation.getAllocatedReceiptAmount(), BigDecimal::add); + } + if (amountByContractByClaim.isEmpty()) { + log.info("HCS收款认领链推送跳过:分摊行未关联合同 flowId={}", flowId); + return; + } + // 合规侧收款流水ID(C107-1 必填引用,一次定位全部合同共用;定位失败仅跳过 C107-1,不阻塞履约计划更新) + String hcsCollectionFlowId = null; + try { + hcsCollectionFlowId = locateHcsCollectionFlowId(flow); + } catch (Exception exception) { + log.error("HCS收款流水定位失败 flowId:{},跳过C107-1认领记录推送", flowId, exception); + } + for (Map.Entry> contractEntry : amountByContractByClaim.entrySet()) { + try { + pushOneContract(flow, claims, contractEntry.getKey(), contractEntry.getValue(), hcsCollectionFlowId); + } catch (Exception exception) { + log.error("HCS收款认领链推送失败 flowId:{},contractId:{}", flowId, contractEntry.getKey(), exception); + } + } + } + + /** + * 推送单个合同:① C103-9 更新履约计划(认领金额累加重推)→ ② C107-1 认领记录(按认领主单分组)→ ③ C108-1 履约流水 + */ + private void pushOneContract(KingdeeReceiptFlow flow, List claims, Long contractId, + Map amountByClaim, String hcsCollectionFlowId) { + ContractManage contractManage = contractManageMapper.selectById(contractId); + if (contractManage == null) { + log.warn("HCS收款认领链推送跳过:合同不存在 contractId={}", contractId); + return; + } + BigDecimal claimedTotal = amountByClaim.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); + // ① C103-9 更新合同履约计划:认领金额累加到收款计划行;已同步行置 update 凭阶段ID 重推 + contractExeStageService.accumulateClaimedAmountOnPush(contractId, claimedTotal); + HcsContractExeStage stageRow = contractExeStageService.findCollectionStageRow(contractId, HcsSyncConstant.FUND_NATURE_PROGRESS); + if (StringUtil.isNotBlank(contractManage.getHcsFdId())) { + // 行级重推(无待同步行返回提示,不视为失败);合同未推送合规时随合同链路首推 + hcsExeStageSyncSupport.syncExeStages(contractManage); + } + // ② C107-1 认领记录:按「认领主单 × 合同」推送,金额为该认领主单分摊到本合同的金额 + if (StringUtil.isBlank(hcsCollectionFlowId)) { + log.warn("HCS认领记录推送跳过:未定位到合规收款流水 flowId={},detailSerialNo={}", flow.getId(), flow.getDetailSerialNo()); + } else { + Map claimsById = claims.stream() + .collect(Collectors.toMap(ReceiptClaim::getId, item -> item, (first, second) -> first)); + for (Map.Entry claimEntry : amountByClaim.entrySet()) { + pushClaimRecord(flow, claimsById.get(claimEntry.getKey()), contractManage, stageRow, claimEntry.getValue(), hcsCollectionFlowId); + } + } + // ③ C108-1 履约流水:按合同一条(开关 perf-record.sync-enabled,与付款链履约记录同步同开关), + // 须已推送合规且存在已同步收款计划行(fd_plan_id 必填) + if (!hcsProperties.getPerfRecord().isSyncEnabled()) { + log.info("HCS履约流水推送跳过:履约记录同步开关未开启 contractNo={}", contractManage.getContractNo()); + return; + } + if (StringUtil.isBlank(contractManage.getHcsFdId())) { + log.info("HCS履约流水推送跳过:合同尚未推送合规 contractNo={}", contractManage.getContractNo()); + return; + } + if (stageRow == null || StringUtil.isBlank(stageRow.getHcsStageFdId())) { + log.info("HCS履约流水推送跳过:无已同步收款履约计划行 contractNo={}", contractManage.getContractNo()); + return; + } + pushPerfRecord(flow, contractManage, stageRow, claimedTotal, latestClaimDate(claims)); + } + + /** + * C107-1 认领记录推送(单认领主单 × 单合同) + */ + private void pushClaimRecord(KingdeeReceiptFlow flow, ReceiptClaim claim, ContractManage contractManage, + HcsContractExeStage stageRow, BigDecimal claimAmount, String hcsCollectionFlowId) { + if (claim == null || claimAmount == null || claimAmount.signum() <= 0) { + return; + } + HcsClaimRecordAddDTO param = new HcsClaimRecordAddDTO(); + // 【必填】收款流水引用(合规侧流水ID,C104-2 按明细流水号定位) + HcsRefVO collectionFlow = new HcsRefVO(); + collectionFlow.setFdId(hcsCollectionFlowId); + param.setFdCollectionFlow(collectionFlow); + // 认领合同引用(fdId 为合规合同ID) + HcsRefVO contractRef = new HcsRefVO(); + contractRef.setFdId(contractManage.getHcsFdId()); + contractRef.setFdName(contractManage.getContractName()); + param.setFdContract(contractRef); + // 【必填】认领合同编号(须在合规系统本地存在) + param.setFdContractNumber(contractManage.getContractNo()); + // 对应履约项ID 与 认领款项类型(取合同收款计划行,无已同步行不传) + if (stageRow != null) { + param.setFdExePlanStageId(stageRow.getHcsStageFdId()); + param.setFdFundNature(stageRow.getFundNature()); + } + // 认领人/部门:TMS 与合规用户不同源,仅传名称(§9-U2) + param.setFdClaimer(HcsRefVO.ofName(claim.getClaimerName())); + param.setFdClaimerName(claim.getClaimerName()); + param.setFdClaimerDept(HcsRefVO.ofName(claim.getClaimerDeptName())); + param.setFdClaimerDeptName(claim.getClaimerDeptName()); + // 【必填】认领金额 + param.setFdClaimAmount(claimAmount); + param.setFdClaimDate(toMillis(claim.getClaimDate())); + param.setFdRemark(claim.getRemark()); + // 状态 0=已认领(仅推送未作废认领) + param.setFdStatus(0); + log.info("HCS认领记录推送开始 flowId:{},claimId:{},contractNo:{},认领金额 {}", + flow.getId(), claim.getId(), contractManage.getContractNo(), claimAmount); + HcsResult result = hcsClient.claimRecordAdd(param); + if (result == null || !result.isSuccess()) { + log.error("HCS认领记录推送失败响应:{},claimId={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), claim.getId()); + throw new HcsException("C107-1 认领记录推送失败:" + (result == null ? "响应为空" : result.getMsg())); + } + // shortcut: C107-1 返回 fdId 未持久化(认领作废同步 C107-3 未实现,重推会重复);需作废联动时先落库 + log.info("HCS认领记录推送成功 flowId:{},claimId:{},contractNo:{},claimRecordFdId:{}", + flow.getId(), claim.getId(), contractManage.getContractNo(), + result.getData() == null ? "响应未返回" : result.getData().getFdId()); + } + + /** + * C108-1 履约流水推送(单合同一条:申请金额=实际金额=该合同累计认领金额) + */ + private void pushPerfRecord(KingdeeReceiptFlow flow, ContractManage contractManage, + HcsContractExeStage stageRow, BigDecimal claimedTotal, LocalDate planDate) { + // 表单模板ID(配置 perf-record.xform-id,环境值 §9-U9,未配置不传) + String xformId = StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId(); + HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO(); + // 【必填】申请编号:收款通知单号,兜底明细流水号 + props.setFdApplyNo(firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo())); + // 申请金额 / 实际金额:该合同累计认领金额 + props.setFdApplyAmount(claimedTotal.toPlainString()); + props.setFdFactAmount(claimedTotal.toPlainString()); + // 【必填】履约日期:最近认领日期(毫秒时间戳) + props.setFdPlanDate(toMillis(planDate)); + // 履约类型:收款 + props.setFdPalnType(HcsSyncConstant.PERFORM_TYPE_COLLECTION); + // 履约性质:收款计划行款项性质 + props.setFdProperty(stageRow.getFundNature()); + // 履约计划id / 来源履约计划项ID:收款计划行合规阶段ID(HCS 只认自己的阶段ID) + props.setFdId(stageRow.getHcsStageFdId()); + props.setFdPlanId(stageRow.getHcsStageFdId()); + // 金蝶单据号:A301-3 推送返回的金蝶收款单号 + props.setFdJdDocNo(flow.getKingdeeBillNo()); + // 【必填】合同ID:HCS 合同ID + props.setFdContractId(contractManage.getHcsFdId()); + // 业务ID:TMS 收款流水ID(溯源) + props.setFdBusinessId(String.valueOf(flow.getId())); + props.setFdCreateTime(System.currentTimeMillis()); + props.setFdXformId(xformId); + HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO(); + formData.setFdXformId(xformId); + formData.setDynamicProps(props); + // 机制数据(C108-1 必填):认领附件暂不转传,传空列表 + HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); + mechanisms.setAttachment(List.of()); + HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO(); + param.setCurUser(HcsUserRefVO.ofLoginName(hcsProperties.getOperatorLoginName())); + param.setFormData(formData); + param.setMechanisms(mechanisms); + log.info("HCS履约流水新建开始 flowId:{},contractNo:{},认领金额 {}", flow.getId(), contractManage.getContractNo(), claimedTotal); + HcsResult result = hcsClient.perfRecordAdd(param); + if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) { + log.error("HCS履约流水新建失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo()); + throw new HcsException("C108-1 履约流水新建失败:" + (result == null ? "响应为空" : result.getMsg())); + } + // shortcut: 履约流水 fdId 未持久化(认领链无 C108-2 更新场景);需推进实际金额更新时先落库 + log.info("HCS履约流水新建成功 flowId:{},contractNo:{},perfFdId:{}", + flow.getId(), contractManage.getContractNo(), result.getData().getFdId()); + } + + /** + * C104-2 定位合规侧收款流水ID:按明细流水号精确查询(合规流水由合规自 ERP 同步,TMS 镜像同源) + */ + private String locateHcsCollectionFlowId(KingdeeReceiptFlow flow) { + if (StringUtil.isBlank(flow.getDetailSerialNo())) { + log.warn("HCS收款流水定位跳过:流水缺少明细流水号 flowId={}", flow.getId()); + return null; + } + HcsPageQueryDTO param = new HcsPageQueryDTO(); + param.setColumns(List.of("fdId", "fdClaimNotice", "fdDetailSerialNumber", "fdCollectionAmount", "fdClaimStatus", "fdClaimedAmount")); + param.setConditions(Map.of("fdDetailSerialNumber", flow.getDetailSerialNo())); + param.setOffset(0); + param.setPageSize(10); + HcsResult> result = hcsClient.collectionFlowList(param); + if (result == null || !result.isSuccess() || result.getData() == null) { + throw new HcsException("C104-2 收款流水查询失败:" + (result == null ? "响应为空" : result.getMsg())); + } + return result.getData().getContent().stream() + .filter(item -> flow.getDetailSerialNo().equals(item.getFdDetailSerialNumber())) + .map(HcsCollectionFlowVO::getFdId) + .filter(StringUtil::isNotBlank) + .findFirst().orElse(null); + } + + /** + * 最近认领日期(C108-1 履约日期) + */ + private LocalDate latestClaimDate(List claims) { + return claims.stream() + .map(ReceiptClaim::getClaimDate) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElse(LocalDate.now()); + } + + private String firstNonBlank(String... values) { + for (String value : values) { + if (Func.isNotEmpty(value)) { + return value.trim(); + } + } + return null; + } + + 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/HcsContractSpecSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSpecSyncSupport.java new file mode 100644 index 00000000..67d98ca2 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSpecSyncSupport.java @@ -0,0 +1,284 @@ +/** + * 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.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.dto.HcsContractDetailQueryDTO; +import org.springblade.hcs.dto.HcsContractDetailVO; +import org.springblade.hcs.dto.HcsLegalResultDTO; +import org.springblade.hcs.dto.HcsResult; +import org.springblade.hcs.feign.IHcsClient; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.HcsContractCheckLogMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.HcsContractCheckLog; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +/** + * HCS 合同特批同步支撑组件(合规特批结果接收:contractLegal/result 推送 + C103-4 定时查询兜底 + 人工确认)。 + *

+ * 业务闭环(2026-10-13 起):高风险/黑名单客商合同随 C103-1/C103-11 报文 fdSepcailContract=true 在合规侧走特批流程 + * (C103-8 独立发起接口合规未提供,已删除),TMS 挂起"特批审核"(processing)→ + * 集团审批结果经合规 contractLegal/result 接口推送 TMS(替代未建设的 D103),或定时 C103-4 查询兜底联动 → + * approved 恢复流程,rejected 联动合同审批驳回。 + *

+ * 结果推送报文 {sourceSystem, requestId=合规合同ID, id, status=APPROVED/REJECTED, reason},原文留痕 blade_hcs_check_log。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsContractSpecSyncSupport { + + /** + * C103-4 合同审批状态:已审批(fdApprovalStatus,小写枚举) + */ + private static final String LEGAL_STATUS_APPROVED = "approved"; + + /** + * C103-4 合同审批状态:审批作废(特批驳回映射待联调确认,暂按作废联动驳回) + */ + private static final String LEGAL_STATUS_INVALID = "approvalinvalid"; + + /** + * 结果推送原文留痕最大长度(blade_hcs_check_log.msg 为 varchar(512)) + */ + private static final int CALLBACK_MSG_MAX_LENGTH = 500; + + private final IHcsClient hcsClient; + private final ContractManageMapper contractManageMapper; + private final HcsContractCheckLogMapper hcsContractCheckLogMapper; + private final HcsProperties hcsProperties; + + /** + * 特批结果定时查询兜底(合规推送 contractLegal/result 未达/未配置时的补偿,2026-10-13): + * 扫描特批中(processing)的合同,按 C103-4 合同详情 fdApprovalStatus 联动—— + * approved 恢复"审批通过"、approvalinvalid 联动审批驳回(特批驳回的合规侧状态待联调确认,暂按作废映射)、其余等待中跳过; + * 与结果推送互不冲突(推送先到则状态已非 processing,查询自然跳过) + * + * @return 结果汇总(供任务日志) + */ + public String syncProcessingSpecResults() { + List contracts = contractManageMapper.selectList(Wrappers.lambdaQuery() + .eq(ContractManage::getHcsSpecStatus, HcsSyncConstant.SPEC_STATUS_PROCESSING) + .isNotNull(ContractManage::getHcsFdId) + .last("limit 50")); + if (contracts.isEmpty()) { + return "特批结果查询:无特批中合同"; + } + int approvedCount = 0; + int rejectedCount = 0; + for (ContractManage contractManage : contracts) { + try { + HcsContractDetailQueryDTO query = new HcsContractDetailQueryDTO(); + // 【定位】合规合同ID:C103-4 fdId 与 fdNumber 二选一 + query.setFdId(contractManage.getHcsFdId()); + HcsResult result = hcsClient.contractDetail(query); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("HCS特批结果查询失败响应:contractNo={},{}", contractManage.getContractNo(), + result == null ? "无响应" : result.getMsg()); + continue; + } + String approvalStatus = result.getData().getFdApprovalStatus(); + if (LEGAL_STATUS_APPROVED.equalsIgnoreCase(approvalStatus)) { + syncContractOnSpecResult(contractManage, true); + markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED, "特批结果查询联动:fdApprovalStatus=approved"); + writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_PASS, "特批结果查询联动:fdApprovalStatus=approved", null); + approvedCount++; + } else if (LEGAL_STATUS_INVALID.equalsIgnoreCase(approvalStatus)) { + syncContractOnSpecResult(contractManage, false); + markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_REJECTED, "特批结果查询联动:fdApprovalStatus=approvalinvalid"); + writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_FAIL, "特批结果查询联动:fdApprovalStatus=approvalinvalid", null); + rejectedCount++; + } + } catch (Exception exception) { + // 单条失败不影响其余,下轮扫描自动重试 + log.warn("HCS特批结果查询单条失败(下轮重试):contractNo={},{}", contractManage.getContractNo(), exception.getMessage()); + } + } + return "特批结果查询:扫描 " + contracts.size() + " 单,联动通过 " + approvedCount + ",联动驳回 " + rejectedCount; + } + + /** + * 特批结果人工确认(§6-U1 过渡方案:页面确认与合规推送等效) + * + * @param id 合同ID + * @param result approved/rejected + * @return 结果描述 + */ + public String confirmSpecResult(Long id, String result) { + ContractManage contractManage = contractManageMapper.selectById(id); + if (contractManage == null) { + throw new ServiceException("合同单不存在:" + id); + } + if (!Arrays.asList(HcsSyncConstant.SPEC_STATUS_APPROVED, HcsSyncConstant.SPEC_STATUS_REJECTED).contains(result)) { + throw new ServiceException("特批结果仅支持 approved/rejected"); + } + if (!HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())) { + throw new ServiceException("仅特批中的合同允许确认结果(当前状态:" + contractManage.getHcsSpecStatus() + ")"); + } + // 特批结果联动合同审批状态(通过恢复展示,驳回联动审批驳回),随 markSpec 一并落库 + syncContractOnSpecResult(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)); + markSpec(contractManage, result, "特批结果人工确认:" + result); + writeCheckLog(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result) + ? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL, "特批结果人工确认:" + result, null); + log.info("HCS合同特批结果人工确认:contractNo={},specFdId={},结果={}", contractManage.getContractNo(), contractManage.getHcsSpecFdId(), result); + return "特批结果已确认:" + result; + } + + /** + * 合同特批结果接收处理(合规 contractLegal/result 推送,2026-10-23 按联调实际报文修正): + * 报文 {sourceSystem, requestId=合规合同ID, id, status=APPROVED/REJECTED, reason}; + * sourceSystem 须与 third-party.hcs.src-system 一致(不符抛"来源系统标识错误"异常); + * requestId 即合规合同ID(与 id 同值),按 blade_contract_manage.hcs_fd_id 定位合同; + * approved 联动恢复"审批通过",rejected 联动合同审批驳回走重新提交闭环(reason 随原文留痕) + * + * @param param 合规推送报文 + * @return 处理结果描述 + */ + public String handleLegalResult(HcsLegalResultDTO param) { + // 来源系统校验:仅接受合规方约定的来源标识,防伪造回传 + if (!StrUtil.equalsIgnoreCase(param == null ? null : param.getSourceSystem(), hcsProperties.getSrcSystem())) { + log.warn("合同特批结果推送来源系统标识错误:sourceSystem={}", param == null ? null : param.getSourceSystem()); + throw new ServiceException("来源系统标识错误"); + } + String raw = JSONUtil.toJsonStr(param); + String requestId = param.getRequestId(); + ContractManage contractManage = locateContractByFdId(requestId); + if (contractManage == null) { + log.warn("合同特批结果推送未定位到合同:requestId={},报文已留痕", requestId); + writeCallbackLog(null, HcsSyncConstant.CHECK_RESULT_SKIP, "合同特批结果推送未定位到合同:" + raw); + return "已留痕:未定位到合同(requestId=" + requestId + ")"; + } + String normalizedStatus = param.getStatus() == null ? "" : param.getStatus().trim(); + if (!HcsSyncConstant.LEGAL_RESULT_APPROVED.equalsIgnoreCase(normalizedStatus) + && !HcsSyncConstant.LEGAL_RESULT_REJECTED.equalsIgnoreCase(normalizedStatus)) { + log.warn("合同特批结果推送状态未识别:contractNo={},status={},报文已留痕", contractManage.getContractNo(), param.getStatus()); + writeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP, "合同特批结果推送状态未识别:" + raw); + return "已留痕:状态字段未识别(contractNo=" + contractManage.getContractNo() + ")"; + } + boolean approved = HcsSyncConstant.LEGAL_RESULT_APPROVED.equalsIgnoreCase(normalizedStatus); + // 特批结果联动合同审批状态(通过恢复展示,驳回联动审批驳回),随 markSpec 一并落库 + syncContractOnSpecResult(contractManage, approved); + markSpec(contractManage, approved ? HcsSyncConstant.SPEC_STATUS_APPROVED : HcsSyncConstant.SPEC_STATUS_REJECTED, + "合同特批结果推送:" + normalizedStatus + (StringUtil.isBlank(param.getReason()) ? "" : ",reason=" + param.getReason())); + writeCallbackLog(contractManage, approved ? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL, + "合同特批结果推送:" + raw); + log.info("合同特批结果推送处理完成:contractNo={},结果={},reason={}", contractManage.getContractNo(), normalizedStatus, param.getReason()); + return "特批结果已处理:" + (approved ? "approved" : "rejected"); + } + + /** + * 按合规合同ID定位合同(合规推送 requestId/id,即 blade_contract_manage.hcs_fd_id) + */ + public ContractManage locateContractByFdId(String requestId) { + if (StringUtil.isBlank(requestId)) { + return null; + } + return contractManageMapper.selectOne(Wrappers.lambdaQuery() + .eq(ContractManage::getHcsFdId, requestId.trim()) + .last("limit 1")); + } + + /** + * 特批结果联动合同审批状态(handleLegalResult 与 confirmSpecResult 共用,仅改字段,随 markSpec 落库): + * approved 恢复"审批通过"展示(C103-1 已起草完成,hcs_sync_status 保持 synced,无需再推送); + * rejected 联动合同审批驳回(approvalStatus=rejected),走重新编辑→提交→审批→特批闭环 + */ + private void syncContractOnSpecResult(ContractManage contractManage, boolean approved) { + if (approved) { + contractManage.setCurrentNode("审批通过"); + } else { + contractManage.setApprovalStatus("rejected"); + contractManage.setCurrentNode("已驳回"); + contractManage.setCurrentProcessor("待处理"); + } + } + + /** + * 回写特批状态与动作时间 + */ + private void markSpec(ContractManage contractManage, String status, String message) { + contractManage.setHcsSpecStatus(status); + contractManage.setHcsSpecTime(LocalDateTime.now()); + contractManage.setHcsSpecMsg(StringUtil.left(message, 500)); + contractManageMapper.updateById(contractManage); + } + + /** + * 特批留痕(blade_hcs_check_log,biz_type=contract,check_type=spec,异常不影响主流程) + */ + private void writeCheckLog(ContractManage contractManage, String result, String msg, String traceId) { + try { + HcsContractCheckLog checkLog = new HcsContractCheckLog(); + checkLog.setTenantId(contractManage.getTenantId()); + checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT); + checkLog.setBizId(contractManage.getId()); + checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_SPEC); + checkLog.setResult(result); + checkLog.setMsg(StringUtil.left(msg, CALLBACK_MSG_MAX_LENGTH)); + checkLog.setTraceId(traceId); + checkLog.setCreateTime(LocalDateTime.now()); + hcsContractCheckLogMapper.insert(checkLog); + } catch (Exception exception) { + log.error("HCS合同特批留痕写入失败:contractNo={}", contractManage.getContractNo(), exception); + } + } + + /** + * 结果推送留痕(合同未定位到时 bizId 为空,原文留痕便于联调解析) + */ + private void writeCallbackLog(ContractManage contractManage, String result, String msg) { + if (contractManage == null) { + try { + HcsContractCheckLog checkLog = new HcsContractCheckLog(); + checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT); + checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_SPEC); + checkLog.setResult(result); + checkLog.setMsg(StringUtil.left(msg, CALLBACK_MSG_MAX_LENGTH)); + checkLog.setCreateTime(LocalDateTime.now()); + hcsContractCheckLogMapper.insert(checkLog); + } catch (Exception exception) { + log.error("合同特批结果推送留痕写入失败", exception); + } + return; + } + writeCheckLog(contractManage, result, msg, null); + } +} 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..30fe801d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java @@ -0,0 +1,787 @@ +/** + * 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.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.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.HcsAttachmentRefDTO; +import org.springblade.hcs.dto.HcsContractDraftDTO; +import org.springblade.hcs.dto.HcsContractDraftVO; +import org.springblade.hcs.dto.HcsContractFieldJsonDTO; +import org.springblade.hcs.dto.HcsContractLegalResultDTO; +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.HcsXformFieldDTO; +import org.springblade.hcs.exception.HcsException; +import org.springblade.hcs.feign.IHcsClient; +import org.springblade.transport.constant.HcsContractFieldConstant; +import org.springblade.transport.constant.HcsFieldConstant; +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.mapper.ProjectApplyMapper; +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.ProjectApply; +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.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * HCS 合同同步支撑组件(C103-1 发起 / C103-11 更新,设计文档 §5.2) + *

+ * 扫描任务按序执行:⓪ 特批挂起门禁 → ① 客商风险校验(可配开关,仅留痕,高风险不再阻断)→ + * ② 附件转传(挂载点为与 fdFieldJson 同级的顶层 attachment,随 C103-1 挂载)→ ③ C103-3 判重(§9-U4 查询能力联调确认)→ + * ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→ ⑤ 履约计划 C103-9 行级同步({@link HcsExeStageSyncSupport}, + * 仅依赖 hcs_fd_id,先于特批挂起执行,失败补偿不被特批门禁阻塞)→ ⑥ 高风险/黑名单挂起"特批审核" + * (外部客商管控:报文随 fdSepcailContract=true 在合规侧走特批,{@link HcsContractSpecSyncSupport} 接收结果)。 + * 结果留痕:风险校验写 blade_hcs_check_log 与合同 hcs_risk_* 字段;推送结果回填 hcs_fd_id/hcs_sync_status。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsContractSyncSupport { + + /** + * 合同金额等 xformFields 值一律字符串传值;时间字段 v4 要求 yyyy-MM-dd(fdType=timestamp) + */ + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * 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 ProjectApplyMapper projectApplyMapper; + private final HcsContractCheckLogMapper hcsContractCheckLogMapper; + private final HcsAttachmentSupport hcsAttachmentSupport; + private final HcsExeStageSyncSupport hcsExeStageSyncSupport; + private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport; + + /** + * 合同金蝶同步结果接收处理(合规 contractKingdee/result 推送,D903,投送人/钢厂/结算中心合同共用): + * 合规侧收到金蝶最终同步结果后通知 TMS 更新同步状态,本接口不处理法务审批结论(特批联动走 contractLegal/result + C103-4 兜底); + * contractType(deliverer/gc/sett_center)非必填仅随原文留痕;sourceSystem 非必填,传值时须为 ; + * contractCode=TMS 合同编号定位(requestId 仅 X-ERP-Request-Id 回显不可定位); + * success=true 回填金蝶合同ID(kingdeeId),success=false 记录失败原因;均追加变更记录并原文留痕 blade_hcs_check_log + * + * @param param 合规推送报文 + * @return 处理结果描述 + */ + public String handleKingdeeResult(HcsContractLegalResultDTO param) { + // sourceSystem 非必填(D903),传值时仅接受合规方约定的 ,防伪造回传 + String sourceSystem = param == null ? null : param.getSourceSystem(); + if (!StrUtil.equalsIgnoreCase(param == null ? null : param.getSourceSystem(), hcsProperties.getSrcSystem())) { + log.warn("合同金蝶同步结果推送来源系统标识错误:sourceSystem={}", sourceSystem); + throw new ServiceException("来源系统标识错误"); + } + String raw = JSONUtil.toJsonStr(param); + String requestId = param.getRequestId(); + ContractManage contractManage = hcsContractSpecSyncSupport.locateContractByFdId(requestId); + if (contractManage == null) { + log.warn("合同金蝶同步结果推送未定位到合同:contractCode={},报文已留痕", param.getContractCode()); + writeKingdeeCallbackLog(null, HcsSyncConstant.CHECK_RESULT_SKIP, "合同金蝶同步结果推送未定位到合同:" + raw); + return "已留痕:未定位到合同(contractCode=" + param.getContractCode() + ")"; + } + if (param.getSuccess() == null) { + log.warn("合同金蝶同步结果推送 success 字段缺失:contractNo={},报文已留痕", contractManage.getContractNo()); + writeKingdeeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP, "合同金蝶同步结果推送 success 字段缺失:" + raw); + return "已留痕:success 字段缺失(contractNo=" + contractManage.getContractNo() + ")"; + } + if (param.getSuccess()) { + // 成功:回填金蝶合同ID,同步结果信息随 hcs_sync_msg 留痕,不联动审批/特批状态 + contractManage.setKingdeeId(param.getKingdeeId()); + contractManage.setHcsSyncMsg(HcsSyncConstant.truncateMsg("合同金蝶同步结果回调:金蝶同步成功,kingdeeId=" + param.getKingdeeId())); + appendKingdeeResultRecord(contractManage, "金蝶同步成功:金蝶合同ID=" + param.getKingdeeId()); + contractManageMapper.updateById(contractManage); + writeKingdeeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_PASS, "合同金蝶同步结果推送:" + raw); + log.info("合同金蝶同步结果推送处理完成:contractNo={},结果=success,kingdeeId={}", contractManage.getContractNo(), param.getKingdeeId()); + return "金蝶同步结果已处理:success"; + } + // 失败:记录失败原因(金蝶同步在合规侧完成,TMS 不重推,失败重试由合规侧负责) + contractManage.setHcsSyncMsg(HcsSyncConstant.truncateMsg("合同金蝶同步结果回调:金蝶同步失败:" + param.getReason())); + appendKingdeeResultRecord(contractManage, "金蝶同步失败:" + param.getReason()); + contractManageMapper.updateById(contractManage); + writeKingdeeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_FAIL, "合同金蝶同步结果推送:" + raw); + log.info("合同金蝶同步结果推送处理完成:contractNo={},结果=fail,reason={}", contractManage.getContractNo(), param.getReason()); + return "金蝶同步结果已处理:fail"; + } + + /** + * 按 ERP 合同编号定位合同(合规推送 contractCode,合同编号唯一) + */ + private ContractManage locateContractByNo(String contractCode) { + if (StringUtil.isBlank(contractCode)) { + return null; + } + return contractManageMapper.selectOne(Wrappers.lambdaQuery() + .eq(ContractManage::getContractNo, contractCode.trim()) + .last("limit 1")); + } + + /** + * 合同金蝶同步结果推送留痕(blade_hcs_check_log,biz_type=contract,合同未定位到时 bizId 为空,异常不影响主流程) + */ + private void writeKingdeeCallbackLog(ContractManage contractManage, String result, String msg) { + try { + HcsContractCheckLog checkLog = new HcsContractCheckLog(); + if (contractManage != null) { + checkLog.setTenantId(contractManage.getTenantId()); + checkLog.setBizId(contractManage.getId()); + } + checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT); + checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_KINGDEE); + checkLog.setResult(result); + checkLog.setMsg(StringUtil.left(msg, HcsSyncConstant.SYNC_MSG_MAX_LENGTH)); + checkLog.setCreateTime(LocalDateTime.now()); + hcsContractCheckLogMapper.insert(checkLog); + } catch (Exception exception) { + log.error("合同金蝶同步结果推送留痕写入失败:contractNo={}", contractManage == null ? null : contractManage.getContractNo(), exception); + } + } + + /** + * 金蝶同步结果回填变更记录(changeRecordJson 追加一条;系统回调无登录用户,操作人记来源标识) + */ + private void appendKingdeeResultRecord(ContractManage contractManage, String changeContent) { + List records = StringUtil.isBlank(contractManage.getChangeRecordJson()) + ? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(contractManage.getChangeRecordJson(), Object.class)); + Map record = new LinkedHashMap<>(); + record.put("changeDate", LocalDate.now().toString()); + record.put("handlerUserName", "合规系统回调"); + record.put("changeType", "金蝶同步结果"); + record.put("changeContent", changeContent); + record.put("status", contractManage.getApprovalStatus()); + record.put("statusName", HcsSyncConstant.approvalStatusName(contractManage.getApprovalStatus())); + records.add(record); + contractManage.setChangeRecordJson(JSONUtil.toJsonStr(records)); + } + + /** + * 推送合同到 HCS:特批挂起门禁 → 风险校验(仅留痕,高风险不再阻断)→ 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced + * → 履约计划行级同步(C103-9,先于特批挂起)→ 高风险/黑名单客商合同挂起"特批审核"等待合规特批结果(报文随 fdSepcailContract=true,外部客商管控) + * + * @param contractManage 合同单(approvalStatus 须为 approved/change_approved) + * @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库) + */ + public String pushContract(ContractManage contractManage) { + log.info("HCS合同推送开始:contractNo={},approvalStatus={},首推={},hcsSpecStatus={}", + contractManage.getContractNo(), contractManage.getApprovalStatus(), + StringUtil.isBlank(contractManage.getHcsFdId()), contractManage.getHcsSpecStatus()); + // ⓪ 特批挂起门禁:特批中(processing)的合同跳过推送,等待合规推送/定时查询特批结果后恢复; + // need(存量待特批/历史卡单)不拦截——重推时随起草报文带 fdSepcailContract=true 自愈转特批 + if (HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())) { + log.info("HCS合同推送跳过(特批挂起中):contractNo={},hcsSpecStatus={}", contractManage.getContractNo(), contractManage.getHcsSpecStatus()); + return "合同特批挂起中(processing),跳过推送"; + } + // ① 客商风险校验(可配开关):留痕 hcs_risk_* 字段并返回高风险/黑名单标志,不再阻断推送(高风险合同起草后自动转特批) + boolean risky = false; + if (hcsProperties.getContract().isRequireRiskCheck()) { + risky = checkRelativeRisk(contractManage); + } + // ② 附件转传(v4 已明确挂载点:C103-1 顶层 attachment,与 fdFieldJson 同级,转传取得合规附件引用后随发起报文挂载) + List attachmentRefs = 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"); + resultMessage = "判重命中,回填已有合规合同ID"; + } else { + String fdContractId = draftContract(contractManage, attachmentRefs, risky); + // 先持久化合规合同ID(调用方必须保存),履约规则同步失败时下轮凭其走更新补偿 + resultMessage = "合同发起成功:" + contractManage.getContractNo(); + markSynced(contractManage, fdContractId, resultMessage); + } + } else { + updateContract(contractManage, risky); + resultMessage = "合同更新成功:" + contractManage.getContractNo(); + markSynced(contractManage, contractManage.getHcsFdId(), resultMessage); + } + // ⑤ 履约规则 C103-9 行级同步(随合同链路,§5.3;仅依赖 hcs_fd_id 与特批无关,先于特批挂起执行—— + // 失败抛出落 failed 时特批未挂起,修复后可直接重推补偿,不被特批门禁阻塞) + if (hcsProperties.getExeStage().isSyncEnabled()) { + resultMessage = resultMessage + ";" + hcsExeStageSyncSupport.syncExeStages(contractManage); + } + // ⑥ 高风险/黑名单客商(外部客商管控):起草/更新报文已随 fdSepcailContract=true 在合规侧发起特批流程, + // 此处挂起"特批审核"等待结果(合规推送 contractLegal/result 或定时 C103-4 查询联动恢复/驳回) + if (risky) { + hangSpecReview(contractManage); + } + return resultMessage; + } + + /** + * C101-1 客商风险校验:甲乙双方各查一次,留痕 check_log 与合同 hcs_risk_* 字段; + * 返回是否高风险/黑名单(风险等级=2 或任一方黑名单),不再阻断推送—— + * 推送成功后由自动特批链路挂起处理(外部客商管控,见 {@link #}) + * + * @return true=高风险或黑名单客商(触发自动特批) + */ + private boolean 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); + boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || blacklist; + log.info("HCS合同风险校验完成:contractNo={},甲方[{}={}/{}],乙方[{}={}/{}],高风险/黑名单={}", + contractManage.getContractNo(), contractManage.getPartyA(), riskPartyA.blackStatus(), riskPartyA.riskLevel(), + contractManage.getPartyB(), riskPartyB.blackStatus(), riskPartyB.riskLevel(), risky); + return risky; + } + + /** + * 高风险/黑名单客商合同挂起"特批审核":合规侧特批流程已随 C103-1/C103-11 报文 fdSepcailContract=true 发起 + * (C103-8 独立发起接口合规未提供,2026-10-13 联调确认),TMS 挂起展示"特批审核"(currentNode), + * 结果经合规推送(contractLegal/result)或定时 C103-4 查询联动后恢复或驳回;特批已通过(单次有效)不再重复挂起 + */ + private void hangSpecReview(ContractManage contractManage) { + // 终态守卫:合规回调(秒批/秒驳)先于挂起执行时,不得覆盖已落地的结果 + // Arrays.asList 允许 null 元素:List.of 的不可变列表 contains(null) 会抛 NPE,而首次推送时 hcsSpecStatus 为 null + if (Arrays.asList(HcsSyncConstant.SPEC_STATUS_APPROVED, HcsSyncConstant.SPEC_STATUS_REJECTED).contains(contractManage.getHcsSpecStatus())) { + log.info("HCS合同高风险/黑名单但特批已有终态(单次有效),跳过挂起:contractNo={},hcsSpecStatus={}", + contractManage.getContractNo(), contractManage.getHcsSpecStatus()); + return; + } + contractManage.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_PROCESSING); + contractManage.setHcsSpecTime(LocalDateTime.now()); + contractManage.setHcsSpecMsg("高风险/黑名单客商,合同报文随 fdSepcailContract=true 走合规特批流程"); + // TMS 流程挂起为"特批审核",特批通过恢复展示、驳回联动合同审批驳回 + contractManage.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE); + contractManageMapper.updateById(contractManage); + log.info("HCS合同高风险/黑名单挂起特批审核:contractNo={},riskLevel={},blackStatus={},挂起节点={}", + contractManage.getContractNo(), contractManage.getHcsRiskLevel(), contractManage.getHcsBlackStatus(), + contractManage.getCurrentNode()); + } + + /** + * 解析 JSON 数组(照 ContractManage#parseJsonArray 惯例,异常返回空列表) + */ + private List> parseJsonArray(String json) { + if (StringUtil.isBlank(json)) { + return List.of(); + } + try { + List> result = new ArrayList<>(); + for (Object item : JsonUtil.parse(json, List.class)) { + if (item instanceof Map map) { + result.add(map); + } + } + return result; + } catch (Exception exception) { + log.warn("合同附件元数据解析失败,按无附件处理:{}", json); + return List.of(); + } + } + + /** + * 查询单方客商风险:按名称反查客商档案(§9-U6 编码体系联调确认),反查不到记 skip 留痕不阻断; + * 内部组织客商默认无风险跳过(与展示/提交/付款链路口径一致,不调 C101-1) + */ + private RiskResult queryRisk(ContractManage contractManage, String standing, String partyName) { + CustomerArchive customerArchive = queryCustomerArchive(partyName); + if (customerArchive == null) { + writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP, + standing + "方[" + partyName + "]反查不到客商编号,跳过风险校验(§9-U6)", null); + return new RiskResult(null, null, false); + } + // 内部组织类型客商默认无风险,不调 C101-1(与展示/提交/付款链路口径一致) + if (HcsCreditCheckSupport.isInternalCustomer(customerArchive)) { + writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP, + standing + "方[" + partyName + "]为内部组织客商,默认无风险跳过校验", null); + return new RiskResult(null, null, false); + } + HcsRiskQueryDTO param = new HcsRiskQueryDTO(); + // 【必填】客商编号:按甲乙方名称反查 CustomerArchive 客商编码(§9-U6) + param.setFdNumber(customerArchive.getCustomerCode()); + HcsResult result = hcsClient.getRelativeRisk(param); + if (result == null || !result.isSuccess()) { + log.error("HCS客商风险查询失败响应:{},contractNo={},standing={},fdNumber={}", + result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo(), standing, customerArchive.getCustomerCode()); + 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(HcsFieldConstant.CONTRACT_FD_ID, HcsFieldConstant.CONTRACT_FD_NUMBER)); + // 判重条件:合同编号精确匹配 + query.setConditions(Map.of(HcsFieldConstant.CONTRACT_FD_NUMBER, 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 ? "无响应" : 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(); + if (page == null || page.getContent() == null || page.getContent().isEmpty()) { + return null; + } + return page.getContent().get(0).getFdId(); + } + + /** + * C103-1 合同发起,返回合规合同 fdContractId;risky=true 时报文带 fdSepcailContract=true 走合规特批流程 + */ + private String draftContract(ContractManage contractManage, List attachmentRefs, boolean risky) { + HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentRefs, risky); + log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个,特批流程={})", contractManage.getContractNo(), attachmentRefs.size(), risky); + HcsResult result = hcsClient.contractDraft(param); + return validateDraftResult(contractManage, result); + } + + /** + * C103-11 合同更新(入参同发起,v4 新增顶层 fdId 定位已有合同);risky=true 时报文带 fdSepcailContract=true + */ + private void updateContract(ContractManage contractManage, boolean risky) { + HcsContractDraftDTO param = buildDraftParam(contractManage, null, risky); + log.info("HCS合同推送开始:{}(C103-11 更新,附件不重复挂载待联调确认,特批流程={})", contractManage.getContractNo(), risky); + HcsResult result = hcsClient.contractUpdate(param); + if (result == null || !result.isSuccess()) { + 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())); + } + } + + /** + * 组装 C103-1/C103-11 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.2); + * attachmentRefs 新建传合规附件引用列表(顶层 attachment,fdId+fdEntityKey,必填,无附件传空列表),更新传 null 不挂载 + */ + private HcsContractDraftDTO buildDraftParam(ContractManage contractManage, List attachmentRefs, boolean risky) { + HcsContractDraftDTO param = new HcsContractDraftDTO(); + // 【必填】合同ID:C103-11 更新顶层必填(传 C103-1 返回的 fdContractId),C103-1 新建不传(v4 新增) + param.setFdId(contractManage.getHcsFdId()); + // 【必填】合同模板ID:template-mapping[contractCategory](§9-U7 待合规方提供模板ID清单) + param.setFdTemplateId(requireTemplateId(contractManage)); + // 【必填】外部系统标识:fd_sources,配置 thirdParty.hcs.src-system(D14) + param.setFdOuterSystem(hcsProperties.getSrcSystem()); + // 【必填】操作人登录名:配置 thirdParty.hcs.operator-login-name(默认 srvadmin,§9-U2) + param.setFdLoginName(hcsProperties.getOperatorLoginName()); + // 【必填】保存类型:submit(提交) + param.setSaveType("submit"); + HcsContractFieldJsonDTO fieldJson = new HcsContractFieldJsonDTO(); + fieldJson.setXformFields(buildXformFields(contractManage, risky)); + // 相对方(签署方):甲方/乙方,编号按名称反查 CustomerArchive(§9-U6) + fieldJson.setSigner(buildSigners(contractManage)); + // 【必填】附件数据:与 fdFieldJson 同级的顶层 attachment,C103-1 必填(无附件传空列表),fdEntityKey=特批 eopCdraftSpecial/用印 eopCdraftSign/其他 eopCdraftOther, + // fdBindType=ADD;更新不挂载(传 null) + if (attachmentRefs != null) { + param.setAttachment(new ArrayList<>(attachmentRefs)); + for (HcsAttachmentRefDTO attachment : param.getAttachment()) { + // 【必填】操作类型:ADD 新增 + attachment.setFdBindType("ADD"); + } + } + param.setFdFieldJson(fieldJson); + return param; + } + + /** + * xformFields 模板字段(字段名取 {@link HcsContractFieldConstant},中文名取自《桂物合同集成接口逻辑梳理》§5.1 合同字段说明) + */ + private List buildXformFields(ContractManage contractManage, boolean risky) { + List fields = new ArrayList<>(); + // 【必填】合同ID:更新时传合同发起返回的 fdContractId 的值(v4 合同字段说明) + fields.add(field(HcsContractFieldConstant.FIELD_FD_ID, "合同ID", contractManage.getHcsFdId())); + // 【必填】合同编号:ContractManage.contractNo + fields.add(field(HcsContractFieldConstant.FIELD_FD_NUMBER, "合同编号", contractManage.getContractNo())); + fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NO, "合同编号", contractManage.getContractNo())); + // 【必填】合同标题:ContractManage.contractName + fields.add(field(HcsContractFieldConstant.FIELD_FD_SUBJECT, "合同标题", contractManage.getContractName())); + fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NAME, "合同名称", contractManage.getContractName())); + // 【必填】合同金额:contractAmount(v4 字段表 fdAmount=Double,数值传值;AMOUNT 模板字段无类型说明照示例字符串传值) + fields.add(field(HcsContractFieldConstant.FIELD_FD_AMOUNT, "合同金额", contractManage.getContractAmount())); + fields.add(field(HcsContractFieldConstant.FIELD_AMOUNT, "合同金额", toPlainString(contractManage.getContractAmount()))); + // 币种:TMS 结算币种经 currency-name-mapping 映射(默认 RMB→人民币,照金蝶惯例) + // fields.add(field(HcsContractFieldConstant.FIELD_FD_CURRENCY_ID, "币种", mapCurrencyName(contractManage.getSettlementCurrency()))); + fields.add(field(HcsContractFieldConstant.FIELD_CURRENCY, "币种", mapCurrencyName(contractManage.getSettlementCurrency()))); + // 合同结算类型:按合同类别固定规则——承运商合同=付款,客户合同=收款 + // 传中文字典名(2026-10-17 确认;此前传枚举键 collection+fdIsSystem 合规侧未命中字典落为"不结算") + String settlementType = null; + if ("承运商合同".equals(contractManage.getContractCategory())) { + settlementType = HcsContractFieldConstant.SETTLEMENT_TYPE_PAYMENT; + } else if ("客户合同".equals(contractManage.getContractCategory())) { + settlementType = HcsContractFieldConstant.SETTLEMENT_TYPE_COLLECTION; + } + if (StringUtil.isNotBlank(settlementType)) { + fields.add(field(HcsContractFieldConstant.FIELD_FD_SETTLEMENT_TYPE, "合同结算类型", settlementType)); + } else { + log.warn("HCS合同结算类型无法确定:contractCategory=[{}] 非承运商合同/客户合同,fdSettlementType 不传", contractManage.getContractCategory()); + } + // 结算模式:TMS 合同固定开口合同(open,v4 字段说明) + fields.add(field(HcsContractFieldConstant.FIELD_FD_CONTRACT_GW_NATURE, "结算模式", HcsContractFieldConstant.CONTRACT_GW_NATURE_OPEN)); + // 特批流程:高风险/黑名单客商报文带 true(合规侧随起草自动走特批流程,C103-8 独立接口未提供); + // 配置项 contract.special-contract-enabled 可强制全量走特批(原文拼写笔误必须沿用,D15) + fields.add(field(HcsContractFieldConstant.FIELD_FD_SEPCAIL_CONTRACT, "特批流程", risky || hcsProperties.getContract().isSpecialContractEnabled() ? "true" : "false")); + // 合同类型:ContractManage.contractCategory + fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_TYPE, "合同类型", contractManage.getContractCategory())); + // 甲方名称/乙方名称:TMS 存名称字符串 + fields.add(field(HcsContractFieldConstant.FIELD_PARTY_A, "甲方名称", contractManage.getPartyA())); + fields.add(field(HcsContractFieldConstant.FIELD_PARTY_B, "乙方名称", contractManage.getPartyB())); + // 【必填】签订日期:signDate(yyyy-MM-dd,fdType=timestamp) + fields.add(timestampField(HcsContractFieldConstant.FIELD_SIGN_DATE, "签订日期", contractManage.getSignDate())); + // 预计开始/结束时间:startDate/endDate(fdType=timestamp,期限为指定日期时必填) + fields.add(timestampField(HcsContractFieldConstant.FIELD_FD_ESTIMATE_START_TIME, "预计开始时间", contractManage.getStartDate())); + fields.add(timestampField(HcsContractFieldConstant.FIELD_FD_ESTIMATE_END_TIME, "预计结束时间", contractManage.getEndDate())); + // 【必填】合同开始期限:TMS 固定指定日期起算,日期取 fdEstimateStartTime(v4 合同字段说明) + fields.add(field(HcsContractFieldConstant.FIELD_FD_START_TERM, "合同开始期限", contractManage.getStartDate() == null ? null : HcsContractFieldConstant.TERM_SPECIFIED_DATE)); + // 【必填】合同结束期限:TMS 固定指定日期,日期取 fdEstimateEndTime(v4 合同字段说明) + fields.add(field(HcsContractFieldConstant.FIELD_FD_END_TERM, "合同结束期限", contractManage.getEndDate() == null ? null : HcsContractFieldConstant.TERM_SPECIFIED_DATE)); + // 【必填】创建时间:createTime(yyyy-MM-dd,fdType=timestamp) + fields.add(timestampField(HcsContractFieldConstant.FIELD_CREATE_TIME, "创建时间", contractManage.getCreateTime())); + fields.add(timestampField(HcsContractFieldConstant.FIELD_FD_CREATE_TIME, "创建时间", contractManage.getCreateTime())); + // 【必填】同步时间:推送时刻(yyyy-MM-dd,fdType=timestamp) + fields.add(timestampField(HcsContractFieldConstant.FIELD_FD_SYN_TIME, "同步时间", LocalDateTime.now())); + // 【必填】承办人名称:handlerUserName + fields.add(field(HcsContractFieldConstant.FIELD_CREATE_USER_NAME, "承办人名称", contractManage.getHandlerUserName())); + // 【必填】合同状态:status-mapping[approvalStatus](v4 必填,draft/approving/approved 等,§9-U3 待联调后配置,未配置不传) + String contractStatus = hcsProperties.getContract().getStatusMapping().get(contractManage.getApprovalStatus()); + if (StringUtil.isNotBlank(contractStatus)) { + fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_STATUS, "合同状态", contractStatus)); + } else { + log.warn("HCS合同状态映射缺失:approvalStatus=[{}] 不在 contract.status-mapping,CONTRACT_STATUS 不传(§9-U3)", contractManage.getApprovalStatus()); + } + // 【必填】合同份数:copyCount(v4 字段表 fdServings=Integer,数值传值;空值默认 1) + fields.add(field(HcsContractFieldConstant.FIELD_FD_SERVINGS, "合同份数", contractManage.getCopyCount() == null ? 1 : contractManage.getCopyCount())); + // 合同说明:remark(选填) + fields.add(field(HcsContractFieldConstant.FIELD_FD_DESC, "合同说明", contractManage.getRemark())); + // 关联项目(选填,对象传值):合同所选项目对应合同系统项目ID(blade_project_apply.hcs_fd_id,C102-1 立项同步回填), + // 未选择项目或项目未立项同步时不传 + Map fdProject = queryProjectFdId(contractManage.getProjectId()); + if (fdProject != null) { + fields.add(field(HcsContractFieldConstant.FIELD_FD_PROJECT, "关联项目", fdProject)); + } + // TODO 待联调(v4 选填字段,暂不传):fdCoTakers 协办人(TMS 无对应)、fdUnderTaker/fdUnderTakerDept 承办人及部门(组织架构对象传值方式待确认)、 + // fdRelations 关联合同(数组)、 + // fdContractAttribute 合同属性(枚举值文档未给出,TMS 无对应)、fdUrgencyLevel 紧急程度(TMS 无对应) + return fields; + } + + /** + * signer 相对方(签署方):甲/乙双方。fdType 档案优先:客商档案为内部组织(customerKind=internal/内部组织) + * 的一方为我方签约主体传 inner,另一方传 customer(外部相对方,2026-10-14 联调口径); + * 双方均非内部组织或查不到档案时按合同类别推定(承运商合同甲=inner,客户合同乙=inner,其余 customer); + * 丙方(三方合同)TMS 无签约主体字段来源,暂不支持 + */ + private List buildSigners(ContractManage contractManage) { + List signers = new ArrayList<>(); + // 甲乙方各反查一次客商档案,编号与内部组织判定共用查询结果 + CustomerArchive partyAArchive = queryCustomerArchive(contractManage.getPartyA()); + CustomerArchive partyBArchive = queryCustomerArchive(contractManage.getPartyB()); + boolean partyAInternal = HcsCreditCheckSupport.isInternalCustomer(partyAArchive); + boolean partyBInternal = HcsCreditCheckSupport.isInternalCustomer(partyBArchive); + String partyAType; + String partyBType; + if (partyAInternal || partyBInternal) { + // 档案命中:内部组织一方=我方签约主体(inner),外部一方统一 customer + partyAType = partyAInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyBType = partyBInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + } else if ("承运商合同".equals(contractManage.getContractCategory())) { + // 兜底:档案未命中内部组织,按类别推定——我方为付款/发包方,签约主体在甲方 + partyAType = HcsConstant.SIGNER_FD_TYPE_INNER; + partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + } else if ("客户合同".equals(contractManage.getContractCategory())) { + // 兜底:我方为收款/承运方,签约主体在乙方 + partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyBType = HcsConstant.SIGNER_FD_TYPE_INNER; + } else { + // 兜底:其他类别双方统一 customer(2026-10-14 联调口径) + partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + } + // 甲方相对方:fdStanding=甲(编号按名称反查,反查不到留空,§9-U6) + signers.add(signer("甲", partyAType, contractManage.getPartyA(), partyAArchive)); + // 乙方相对方:fdStanding=乙 + signers.add(signer("乙", partyBType, contractManage.getPartyB(), partyBArchive)); + return signers; + } + + /** + * 单个相对方:fdType 由签约主体落位规则决定(我方主体 inner,外部相对方 customer) + */ + private HcsSignerDTO signer(String fdStanding, String fdType, String partyName, CustomerArchive customerArchive) { + HcsSignerDTO signer = new HcsSignerDTO(); + // 签署地位:甲 / 乙 + signer.setFdStanding(fdStanding); + // 相对方名称:TMS 甲乙方名称 + signer.setFdName(partyName); + // 相对方编号:按名称反查 CustomerArchive 客商编码,反查不到留空(§9-U6) + signer.setFdNumber(customerArchive == null ? null : customerArchive.getCustomerCode()); + // 单位类型:inner 内部主体(我方签约主体)/ customer 外部相对方 + signer.setFdType(fdType); + return signer; + } + + /** + * 附件转传(可配开关):合同主文件 approved=true(挂 eopCdraftSign)+ 其它附件(挂 eopCdraftOther), + * 返回合规附件引用列表(挂载 C103-1 顶层 attachment,与 fdFieldJson 同级) + */ + private List transferAttachmentsIfEnabled(ContractManage contractManage) { + if (!hcsProperties.getAttachment().isTransferEnabled()) { + return List.of(); + } + List attachmentRefs = hcsAttachmentSupport.transferContractAttachments( + HcsSyncConstant.BIZ_TYPE_CONTRACT, contractManage.getId(), + contractManage.getContractFileJson(), contractManage.getAttachmentsJson(), hcsProperties.getOperatorLoginName()); + log.info("HCS合同附件转传完成:contractNo={},共 {} 个附件(随 C103-1 挂载)", contractManage.getContractNo(), attachmentRefs.size()); + return attachmentRefs; + } + + /** + * 校验 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 ? "无响应" : 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(); + } + + /** + * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口) + * + * @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() + ")"); + } + if (HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())) { + throw new ServiceException("合同特批审核中,禁止重推(等待合规特批结果后恢复)"); + } + 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); + log.error("HCS合同推送失败:{}", message, exception); + throw new ServiceException("HCS合同推送失败:" + message); + } + } + + /** + * 回填合规 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(HcsSyncConstant.truncateMsg(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(HcsSyncConstant.truncateMsg(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; + } + + /** + * 按名称反查客商档案(全称或简称精确匹配,查不到返回 null) + */ + private CustomerArchive queryCustomerArchive(String partyName) { + if (StringUtil.isBlank(partyName)) { + return null; + } + return customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyName).or().eq(CustomerArchive::getShortName, partyName)) + .eq(CustomerArchive::getIsDeleted, 0) + .last("limit 1")); + } + + /** + * 关联项目取值:合同所选项目对应合同系统项目ID(blade_project_apply.hcs_fd_id,C102-1 立项同步回填), + * 未选择项目、立项单不存在或未同步合规时返回 null 不传(fdProject 选填) + */ + private Map queryProjectFdId(Long projectId) { + if (projectId == null) { + return null; + } + ProjectApply projectApply = projectApplyMapper.selectById(projectId); + if (projectApply == null || StringUtil.isBlank(projectApply.getHcsFdId())) { + log.warn("HCS合同关联项目缺失合规项目ID:projectId=[{}] 未立项或未同步合规,fdProject 不传", projectId); + return null; + } + return Map.of("fdId", projectApply.getHcsFdId()); + } + + /** + * 币种名称映射: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, Object value) { + HcsXformFieldDTO field = new HcsXformFieldDTO(); + field.setName(name); + field.setLabel(label); + field.setValue(value); + return field; + } + + /** + * 时间模板字段(fdType=timestamp,v4 要求传 yyyy-MM-dd) + */ + 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(DATE_FORMATTER); + } else { + // BaseEntity.createTime 为 Date 类型 + text = DATE_FORMATTER.format(((java.util.Date) value).toInstant() + .atZone(java.time.ZoneId.systemDefault()).toLocalDate()); + } + 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) { + } +} 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..2b158e4e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java @@ -0,0 +1,391 @@ +/** + * 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.jackson.JsonUtil; +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.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.springblade.transport.pojo.vo.CustomerRiskVO; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3) + *

+ * 付款申请提交(submit)事务内同步调用(查询类同步): + * ① C101-1 收款方为外部客商时查风险,高风险(风险等级=2)/黑名单提示且必须上传「特批附件」, + * 并标记 hcs_spec_status=need 转付款特批链路(审批通过后扫描任务发起 C106-3,合规 paymentLegal/result 回调联动); + * 内部组织客商默认无风险不查询; + * ② 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); + } + // ② 额度校验(超额不拦截,转特批);一期暂不对接默认关,关闭后特批无自动触发 + if (!hcsProperties.getCredit().isCheckEnabled()) { + return; + } + checkCredit(payment); + } + + /** + * C101-1 收款方客商风险校验:仅外部客商查询,高风险/黑名单必须上传特批附件, + * 并标记 hcs_spec_status=need 转付款特批链路(不再直接阻断) + */ + private void checkRelativeRisk(PaymentApplication payment) { + CustomerArchive customerArchive = queryCustomerArchive(payment.getPayeeName()); + if (customerArchive == null) { + writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP, + "收款方[" + payment.getPayeeName() + "]反查不到客商编号,跳过风险校验(§9-U6)", null); + log.warn("HCS付款提交风险校验跳过:paymentNo={},收款方反查不到客商编号", payment.getPaymentNo()); + return; + } + // 内部组织类型客商默认无风险,不查 C101-1(与 queryCustomerRisk 展示口径一致) + if (isInternalCustomer(customerArchive)) { + writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP, + "收款方[" + payment.getPayeeName() + "]为内部组织客商,默认无风险跳过校验", null); + return; + } + HcsRiskQueryDTO param = new HcsRiskQueryDTO(); + // 【必填】客商编号:按收款方名称反查 CustomerArchive 客商编码(用哪个编码待联调,§9-U6) + param.setFdNumber(customerArchive.getCustomerCode()); + HcsResult result = hcsClient.getRelativeRisk(param); + if (result == null || !result.isSuccess()) { + log.error("HCS客商风险查询失败响应:{},paymentNo={},fdNumber={}", + result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo(), customerArchive.getCustomerCode()); + 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 risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus); + writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, risky ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS, + "收款方[" + payment.getPayeeName() + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel + ",高风险/黑名单=" + risky, + result.getTraceId()); + if (!risky) { + return; + } + // 高风险/黑名单:必须上传「特批附件」类型附件(提示阻断),并标记待特批(审批通过后推送 C106-3); + // 清空旧特批流程ID:特批单次有效,重新提交须重新发起 C106-3(否则会 C106-4 更新旧驳回单) + if (!hasSpecAttachment(payment.getAttachmentsJson())) { + throw new ServiceException("收款方[" + payment.getPayeeName() + "]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"); + } + payment.setHcsSpecFdId(null); + payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED); + } + + /** + * 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 ? "无响应" : 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(); + 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); + } + + /** + * 客商风险查询(C101-1,展示与提交校验共用,前端接入设计 §3.3): + * 内部组织类型客商默认无风险(不调 C101-1);外部客商按编号查询风险等级与黑名单状态并留痕; + * 高风险/黑名单时组装风险提示文案(风险项明细 C101-1 未返回,固定文案展示) + * + * @param customerCode 客商编号 + * @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip) + */ + public CustomerRiskVO queryCustomerRisk(String customerCode) { + if (StringUtil.isBlank(customerCode)) { + throw new ServiceException("客商编号不能为空"); + } + CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .eq(CustomerArchive::getCustomerCode, customerCode) + .eq(CustomerArchive::getIsDeleted, 0) + .last("limit 1")); + CustomerRiskVO riskVO = new CustomerRiskVO(); + riskVO.setCustomerCode(customerCode); + riskVO.setCustomerName(customerArchive == null ? customerCode : customerArchive.getFullName()); + // 内部组织类型客商默认无风险;档案不存在按外部客商查询(不静默放行) + boolean internal = isInternalCustomer(customerArchive); + riskVO.setCustomerKind(internal ? HcsSyncConstant.CUSTOMER_KIND_INTERNAL : "external"); + if (internal) { + riskVO.setRisky(Boolean.FALSE); + return riskVO; + } + HcsRiskQueryDTO param = new HcsRiskQueryDTO(); + // 【必填】客商编号(用哪个编码待联调,§9-U6) + param.setFdNumber(customerCode); + HcsResult result = hcsClient.getRelativeRisk(param); + if (result == null || !result.isSuccess()) { + log.error("HCS客商风险查询失败响应:{},fdNumber={}", + result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), customerCode); + throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg())); + } + HcsRiskVO risk = result.getData(); + String blackStatus = risk == null ? null : risk.getFdBlackStatus(); + String riskLevel = risk == null ? null : risk.getFdRiskLevel(); + boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus); + riskVO.setFdBlackStatus(blackStatus); + riskVO.setFdRiskLevel(riskLevel); + riskVO.setRiskLevelName(riskLevelName(blackStatus, riskLevel)); + riskVO.setRisky(risky); + if (risky) { + // 风险提示固定短文案(C101-1 未返回风险项明细,不拼接枚举清单):黑名单与高风险区分表述 + riskVO.setRiskTip("【" + riskVO.getCustomerName() + "】:" + + (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus) ? "被列入外部黑名单" : "为高风险客商")); + } + try { + HcsContractCheckLog checkLog = new HcsContractCheckLog(); + checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT); + checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK); + checkLog.setResult(risky ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS); + checkLog.setMsg("客商风险查询[" + customerCode + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel + ",高风险/黑名单=" + risky); + checkLog.setTraceId(result.getTraceId()); + checkLog.setCreateTime(LocalDateTime.now()); + hcsContractCheckLogMapper.insert(checkLog); + } catch (Exception exception) { + log.error("HCS客商风险查询留痕写入失败:fdNumber={}", customerCode, exception); + } + return riskVO; + } + + /** + * 按客商名称查询风险(合同提交校验用):按名称反查客商编号后转 {@link #queryCustomerRisk}; + * 反查不到客商编号时不阻断(risky=false),与推送链路 skip 口径一致(§9-U6) + * + * @param customerName 客商名称(合同甲/乙方名称) + * @return 风险展示信息 + */ + public CustomerRiskVO queryCustomerRiskByName(String customerName) { + String customerCode = queryCustomerCode(customerName); + if (StringUtil.isBlank(customerCode)) { + CustomerRiskVO riskVO = new CustomerRiskVO(); + riskVO.setCustomerName(customerName); + riskVO.setRisky(Boolean.FALSE); + return riskVO; + } + return queryCustomerRisk(customerCode); + } + + /** + * 内部组织客商判定(集团内我方签约主体,档案登记 customerKind=internal/内部组织); + * 展示、提交校验、付款校验、合同推送、signer 单位类型各链路统一复用本判定 + * + * @param customerArchive 客商档案 + * @return 是否内部组织客商 + */ + public static boolean isInternalCustomer(CustomerArchive customerArchive) { + return customerArchive != null + && (HcsSyncConstant.CUSTOMER_KIND_INTERNAL.equals(customerArchive.getCustomerKind()) + || "内部组织".equals(customerArchive.getCustomerKind())); + } + + /** + * 风险等级转义(C101-1 枚举 0=低风险/1=中风险/2=高风险):黑名单状态优先展示,无等级返回空 + */ + private String riskLevelName(String blackStatus, String riskLevel) { + if (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus)) { + return "黑名单"; + } + return switch (Objects.toString(riskLevel, "")) { + case "2" -> "高风险"; + case "1" -> "中风险"; + case "0" -> "低风险"; + default -> null; + }; + } + + /** + * 客商风险批量查询(C101-1,客商列表页展示用,前端接入设计 §3.3): + * 逐个复用 {@link #queryCustomerRisk}(内部客商默认无风险,外部客商查 C101-1,与付款页面展示口径一致); + * C101-1 仅支持单编号查询,逐个串行调用;展示场景单个失败跳过不阻断整页(强制校验仍在合同/付款链路) + * + * @param customerCodes 客商编号集合 + * @return 风险展示信息列表(查询失败的客商不在结果中) + */ + public List queryCustomerRiskBatch(List customerCodes) { + List riskList = new ArrayList<>(); + if (Func.isEmpty(customerCodes)) { + return riskList; + } + for (String customerCode : customerCodes) { + try { + riskList.add(this.queryCustomerRisk(customerCode)); + } catch (Exception exception) { + log.error("客商风险批量查询单个失败,跳过:customerCode={}", customerCode, exception); + } + } + return riskList; + } + + /** + * 按名称反查客商档案(内部/外部类型判断与客商编码获取共用;查不到返回 null) + */ + private CustomerArchive queryCustomerArchive(String customerName) { + if (StringUtil.isBlank(customerName)) { + return null; + } + return customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, customerName).or().eq(CustomerArchive::getShortName, customerName)) + .eq(CustomerArchive::getIsDeleted, 0) + .last("limit 1")); + } + + /** + * 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null) + */ + private String queryCustomerCode(String customerName) { + CustomerArchive customerArchive = queryCustomerArchive(customerName); + return customerArchive == null ? null : customerArchive.getCustomerCode(); + } + + /** + * 附件中是否已上传特批附件(付款附件结构 attachmentType=special_approval,即前端付款表单「特批附件」类型; + * 兼容 fileType=特批附件 结构;口径同合同提交校验 validateSpecExplanationOnSubmit) + */ + private boolean hasSpecAttachment(String attachmentsJson) { + if (StringUtil.isBlank(attachmentsJson)) { + return false; + } + try { + Object parsed = JsonUtil.parse(attachmentsJson, List.class); + if (!(parsed instanceof List attachments)) { + return false; + } + return attachments.stream().filter(Map.class::isInstance).map(Map.class::cast) + .anyMatch(attachment -> HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPECIAL + .equals(Objects.toString(attachment.get("attachmentType"), null)) + || HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION + .equals(Objects.toString(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD), null))); + } catch (Exception exception) { + log.error("HCS付款提交附件JSON解析失败:按未上传特批附件处理", exception); + return false; + } + } + + /** + * 校验留痕(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); + } + } +} 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..89bed20c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java @@ -0,0 +1,255 @@ +/** + * 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.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 ? "无响应" : 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(); + backfill(contractManage, pendingRows, param.getFdStages(), exeplan); + return String.format("履约规则同步成功(%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()); + // 【必填】操作人登录名:配置 thirdParty.hcs.operator-login-name(默认 srvadmin,§9-U2) + param.setFdLoginName(hcsProperties.getOperatorLoginName()); + // 【必填】计划整体操作类型:首次同步 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=先款后票 / invoiceFirst=先票后款 + 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("返回阶段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("同步成功"); + 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); + } + } + + /** + * 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/support/hcs/HcsImmediatePushSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java new file mode 100644 index 00000000..cc0e6106 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java @@ -0,0 +1,300 @@ +/** + * 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 jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.redis.cache.BladeRedis; +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.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * HCS 审批通过即时推送(前端接入设计 §6,2026-10-10 增补) + *

+ * 项目立项(C102)/合同(C103)/付款申请(C106 付款特批)审批通过(含变更通过)事务提交后立即触发单条推送,不再等待扫描周期; + * 执行体复用 {@link HcsProjectSyncSupport#repushProject(Long)} / {@link HcsContractSyncSupport#repushContract(Long)} / + * {@link HcsPaymentSyncSupport#repushPayment(Long)} + * (已含审批状态校验、失败 markFailed、高风险特批挂起判断),失败仅留痕,由 HcsPushTask 扫描兜底补偿 + * (ADR-0001 修订:审批代码埋点为一行触发调用,推送逻辑仍收敛在 hcs support 包)。 + *

+ * 触发模型:审批事务内调用则注册 afterCommit 回调(事务回滚不推送),无活动事务(MK 回调单条更新自动提交)直接提交; + * 自建 2 线程 daemon 池(工程未启用 @EnableAsync),HCS 推送可达 20s+ 不阻塞审批响应; + * Redis 单条锁防与扫描任务并发双推(TTL 120s,持有值比对释放,照 HcsPushTask 惯例), + * 极端双推由 HCS 侧判重/更新幂等吸收。与扫描任务同线程模型(无租户上下文),行为一致。 + *

+ * 例外:TMS 合同/项目立项的"通过"按钮路径由 Controller 在审批事务返回后分别调用 + * {@link #pushContractAndWait(Long)}、{@link #pushProjectAndWait(Long)} 同步推送 + * (审批响应会等待附件转传 + HCS 调用完成,与正式结算单审批一致), + * 失败原因经 {@link #pollSyncFailure()} 交给 Controller 拼进审批响应(照 KingdeeApprovalSyncListener); + * MK 回调路径仍异步,避免阻塞第三方回调。 + *

+ * 付款申请按"审批即发起特批"要求:即时推送不受 thirdParty.hcs.push.immediate-enabled 开关控制 + * ({@link #pushPaymentAndWait(Long)} / {@link #pushPaymentAfterCommit(Long)}), + * 审批通过即以 C106-3 发起特批,履约计划与履约记录等特批通过后补推。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsImmediatePushSupport { + + /** + * 审批通过状态集合(与 HcsPushTask.APPROVED_STATUSES 一致,含变更通过) + */ + private static final List APPROVED_STATUSES = List.of("approved", "change_approved"); + + /** + * 推送线程编号(线程名 hcs-immediate-push-N 便于日志定位) + */ + private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1); + + /** + * 当前审批请求线程的合规推送失败原因,供审批 Controller 取走提示前端 + */ + private static final ThreadLocal SYNC_FAILURE = new ThreadLocal<>(); + + private final HcsProperties hcsProperties; + private final HcsProjectSyncSupport hcsProjectSyncSupport; + private final HcsContractSyncSupport hcsContractSyncSupport; + private final HcsPaymentSyncSupport hcsPaymentSyncSupport; + private final ProjectApplyMapper projectApplyMapper; + private final ContractManageMapper contractManageMapper; + private final PaymentApplicationMapper paymentApplicationMapper; + private final BladeRedis bladeRedis; + + /** + * 推送线程池:2 线程 daemon,即时推送与定时扫描互不影响 + */ + private final ExecutorService pushExecutor = Executors.newFixedThreadPool(2, task -> { + Thread thread = new Thread(task, "hcs-immediate-push-" + THREAD_COUNTER.getAndIncrement()); + thread.setDaemon(true); + return thread; + }); + + /** + * 项目立项(C102)审批通过后即时推送:须在审批事务内调用(afterCommit 触发) + * + * @param id 项目立项单ID + */ + public void pushProjectAfterCommit(Long id) { + this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_PROJECT, id, () -> this.pushProjectNow(id), true); + } + + /** + * 合同(C103)审批通过后即时推送:须在审批事务内调用(afterCommit 触发) + * + * @param id 合同单ID + */ + public void pushContractAfterCommit(Long id) { + this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id), true); + } + + /** + * 付款申请审批通过后即时发起付款特批(C106-3,无需特批则直接推履约数据): + * 须在审批事务内调用(afterCommit 触发);按"审批即发起特批"要求不受 immediate-enabled 开关控制 + * + * @param id 付款申请单ID + */ + public void pushPaymentAfterCommit(Long id) { + this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_PAYMENT, id, () -> this.pushPaymentNow(id), false); + } + + /** + * 合同"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走 + * + * @param id 合同单ID + */ + public void pushContractAndWait(Long id) { + if (!hcsProperties.getPush().isImmediateEnabled()) { + return; + } + this.pushWithLock(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id)); + } + + /** + * 项目立项"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走 + * + * @param id 项目立项单ID + */ + public void pushProjectAndWait(Long id) { + if (!hcsProperties.getPush().isImmediateEnabled()) { + return; + } + this.pushWithLock(HcsSyncConstant.BIZ_TYPE_PROJECT, id, () -> this.pushProjectNow(id)); + } + + /** + * 付款申请"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走; + * 按"审批即发起特批"要求不受 immediate-enabled 开关控制 + * + * @param id 付款申请单ID + */ + public void pushPaymentAndWait(Long id) { + this.pushWithLock(HcsSyncConstant.BIZ_TYPE_PAYMENT, id, () -> this.pushPaymentNow(id)); + } + + /** + * 取走并清除当前审批请求的合规推送失败原因;null 表示推送成功、未触发或仍异步进行 + */ + public static String pollSyncFailure() { + String failure = SYNC_FAILURE.get(); + SYNC_FAILURE.remove(); + return failure; + } + + /** + * 提交时机注册:有活动事务注册 afterCommit 回调(回滚不推送),无事务(MK 回调单条更新自动提交)直接异步 + * + * @param requireSwitch 是否受 immediate-enabled 开关控制(项目/合同是,付款特批按"审批即发起"否) + */ + private void submitAfterCommit(String bizType, Long id, Runnable pushAction, boolean requireSwitch) { + if (requireSwitch && !hcsProperties.getPush().isImmediateEnabled()) { + return; + } + Runnable pushTask = () -> this.pushWithLock(bizType, id, pushAction); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + pushExecutor.execute(pushTask); + } + }); + } else { + pushExecutor.execute(pushTask); + } + } + + /** + * 单条锁防并发双推:抢不到即放弃(扫描兜底),持有值比对后释放 + */ + private void pushWithLock(String bizType, Long id, Runnable pushAction) { + String lockKey = HcsSyncConstant.PUSH_SINGLE_LOCK_KEY_PREFIX + bizType + ":" + id; + String lockHolder = UUID.randomUUID().toString(); + // 清理同线程残留的失败原因,避免线程复用串单(同步推送路径会写入) + SYNC_FAILURE.remove(); + Boolean locked = bladeRedis.getValueOps() + .setIfAbsent(lockKey, lockHolder, Duration.ofSeconds(HcsSyncConstant.PUSH_SINGLE_LOCK_TTL_SECONDS)); + if (!Boolean.TRUE.equals(locked)) { + log.info("HCS即时推送跳过(其他线程推送中):bizType={},id={}", bizType, id); + return; + } + try { + pushAction.run(); + } finally { + if (lockHolder.equals(bladeRedis.get(lockKey))) { + bladeRedis.del(lockKey); + } + } + } + + /** + * 项目立项推送执行体:非审批通过/已同步短路跳过,失败已由 Support markFailed,仅留痕交扫描补偿 + */ + private void pushProjectNow(Long id) { + try { + ProjectApply projectApply = projectApplyMapper.selectById(id); + if (projectApply == null || !APPROVED_STATUSES.contains(projectApply.getApprovalStatus()) + || HcsSyncConstant.HCS_STATUS_SYNCED.equals(projectApply.getHcsSyncStatus())) { + return; + } + hcsProjectSyncSupport.repushProject(id); + } catch (Exception exception) { + log.error("HCS项目立项即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception); + // 失败原因留在当前线程供审批 Controller 取走;异步路径无人取走,仅影响线程池自身 + String reason = exception.getMessage() == null ? "合规接口调用异常" : exception.getMessage(); + SYNC_FAILURE.set(reason.startsWith("HCS项目立项推送失败:") + ? reason.substring("HCS项目立项推送失败:".length()) : reason); + } + } + + /** + * 合同推送执行体:非审批通过/已同步/特批挂起中短路跳过,失败已由 Support markFailed,仅留痕交扫描补偿 + */ + private void pushContractNow(Long id) { + try { + ContractManage contractManage = contractManageMapper.selectById(id); + if (contractManage == null || !APPROVED_STATUSES.contains(contractManage.getApprovalStatus()) + || HcsSyncConstant.HCS_STATUS_SYNCED.equals(contractManage.getHcsSyncStatus())) { + return; + } + if (Arrays.asList(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(contractManage.getHcsSpecStatus())) { + log.info("HCS合同即时推送跳过(特批挂起中):id={},hcsSpecStatus={}", id, contractManage.getHcsSpecStatus()); + return; + } + hcsContractSyncSupport.repushContract(id); + } catch (Exception exception) { + log.error("HCS合同即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception); + // 失败原因留在当前线程供审批 Controller 取走;异步路径无人取走,仅影响线程池自身 + String reason = exception.getMessage() == null ? "合规接口调用异常" : exception.getMessage(); + SYNC_FAILURE.set(reason.startsWith("HCS合同推送失败:") + ? reason.substring("HCS合同推送失败:".length()) : reason); + } + } + + /** + * 付款推送执行体:非审批通过/已同步短路跳过;失败已由 Support markFailed,仅留痕交扫描/手动重推补偿 + */ + private void pushPaymentNow(Long id) { + try { + PaymentApplication payment = paymentApplicationMapper.selectById(id); + if (payment == null || !APPROVED_STATUSES.contains(payment.getApprovalStatus()) + || HcsSyncConstant.HCS_STATUS_SYNCED.equals(payment.getHcsSyncStatus())) { + return; + } + hcsPaymentSyncSupport.repushPayment(id); + } catch (Exception exception) { + log.error("HCS付款即时推送失败(可手动重推或扫描任务补偿):id={},{}", id, exception.getMessage(), exception); + // 失败原因留在当前线程供审批 Controller 取走;异步路径无人取走,仅影响线程池自身 + String reason = exception.getMessage() == null ? "合规接口调用异常" : exception.getMessage(); + SYNC_FAILURE.set(reason.startsWith("HCS付款申请推送失败:") + ? reason.substring("HCS付款申请推送失败:".length()) : reason); + } + } + + @PreDestroy + public void shutdown() { + pushExecutor.shutdown(); + } +} 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..067ab47b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java @@ -0,0 +1,361 @@ +/** + * 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.HcsAttachmentRefDTO; +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.HcsRefVO; +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.HcsFieldConstant; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +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.ContractManage; +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 ContractManageMapper contractManageMapper; + 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 关联字段确认后细化) + ContractRef contractRef = requireContractRef(application); + param.setFdContractNumber(contractRef.contractNo()); + // 合同 {fdId, fdName}:fdId 传合规合同ID(未推送不传),名称取结算单合同名称(v4 新增) + param.setFdContract(contractRef.contract()); + // 发票类型: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())) { + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, application.getId(), + application.getAttachmentsJson(), resolveCreatorCode(application)); + param.setFdAttIds(attachmentRefs.stream().map(HcsAttachmentRefDTO::getFdId).toList()); + } + 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 ? "无响应" : 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(); + } + + /** + * 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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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(HcsFieldConstant.INVOICE_FD_ID, HcsFieldConstant.INVOICE_FD_NUMBER)); + // 判重条件:单据编号精确匹配 + query.setConditions(Map.of(HcsFieldConstant.INVOICE_FD_NUMBER, 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 ? "无响应" : 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(); + if (page == null || page.getContent() == null || page.getContent().isEmpty()) { + return null; + } + return page.getContent().get(0).getFdId(); + } + + /** + * 应收结算链路回溯合同引用:开票申请 → 应收结算关联 → 正式结算单合同(§9-U10); + * fdContractNumber 必填,fdContract 随链路回填(fdId 为合规合同ID,未推送不传) + */ + private ContractRef requireContractRef(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())) { + HcsRefVO contract = new HcsRefVO(); + contract.setFdName(settlement.getContractName()); + if (settlement.getContractId() != null) { + ContractManage contractManage = contractManageMapper.selectById(settlement.getContractId()); + if (contractManage != null) { + contract.setFdId(contractManage.getHcsFdId()); + } + } + return new ContractRef(settlement.getContractNo(), contract); + } + } + throw new HcsException("C111 开票登记推送失败,应收结算链路回溯不到合同编号(applicationNo=" + application.getApplicationNo() + + ",§9-U10 待确认关联字段)"); + } + + /** + * 合同引用(fdContractNumber / fdContract) + */ + private record ContractRef(String contractNo, HcsRefVO contract) { + } + + /** + * 结算币种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(HcsSyncConstant.truncateMsg(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(HcsSyncConstant.truncateMsg(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-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..0c123fd8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsMasterDataSupport.java @@ -0,0 +1,207 @@ +/** + * 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); + } + }); + } + + /** + * 分页查询基础参数(offset/pageSize 必填,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-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..46059777 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java @@ -0,0 +1,327 @@ +/** + * 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.core.util.StrUtil; +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.HcsLegalResultDTO; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.constant.TransportBusiConstant; +import org.springblade.transport.event.KingdeeBillApprovedEvent; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.PaymentApplicationMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +/** + * HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约计划重推 C103-9 + 履约记录 C108-1/2 + 特批结果推送接收,设计文档 §5.0/§5.3/§5.5/§5.6) + *

+ * 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 特批通过后再推履约计划(付款审批款项金额累加行)→ 履约记录(审批通过新建 / 实际付款推进更新)。 + * 特批审核期间(need/pushing/processing)仅发起/等待 C106,不推送履约计划与履约记录;驳回不产生履约数据。 + * 特批流程ID 与履约记录ID 在各自 Support 内即时持久化;整体同步状态(hcs_sync_status)由本组件统一回写。 + * 特批发起后 TMS 挂起 currentNode="特批审核",合规 paymentLegal/result 回调联动付款审批状态 + * (通过恢复"审批通过"并补推履约数据 / 驳回联动审批驳回走重新提交闭环),人工确认等效兜底。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsPaymentSyncSupport { + + /** + * 审批通过状态集合 + */ + private static final List APPROVED_STATUSES = List.of("approved"); + + private final PaymentApplicationMapper paymentApplicationMapper; + private final ContractManageMapper contractManageMapper; + private final HcsProperties hcsProperties; + private final HcsSpecPaySyncSupport hcsSpecPaySyncSupport; + private final HcsExeStageSyncSupport hcsExeStageSyncSupport; + private final HcsPerfRecordSyncSupport hcsPerfRecordSyncSupport; + private final ApplicationEventPublisher eventPublisher; + + /** + * 推送付款申请到 HCS:特批(need 时发起 C106-3 后挂起)→ 特批通过后履约计划重推(C103-9 款项金额累加行)→ 履约记录(新建/更新)→ 回填整体同步状态 + * + * @param payment 付款申请单(approvalStatus=approved) + * @return 推送结果描述 + */ + public String pushPayment(PaymentApplication payment) { + StringBuilder result = new StringBuilder(); + // ① 结算付款特批:提交时高风险/黑名单(须特批附件)或额度超额(hcs_spec_status=need)且未发起时发起 C106-3; + // 发起后挂起等待特批结果,履约计划与履约记录由特批通过后的 pushAfterSpecApproved 补推(驳回不产生履约数据) + if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) { + String specResult = hcsSpecPaySyncSupport.pushSpecPay(payment); + // C106-4 更新路径不改状态,统一归一为特批审核中,避免下轮扫描重复发起 + if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) { + payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_PROCESSING); + payment.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE); + } + markSynced(payment, specResult); + return specResult; + } + // 特批审核中:仅等待合规回调/人工确认,不推送履约计划与履约记录 + if (HcsSyncConstant.SPEC_STATUS_PUSHING.equals(payment.getHcsSpecStatus()) + || HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(payment.getHcsSpecStatus())) { + String message = "付款特批审核中,待特批通过后推送履约数据"; + markSynced(payment, message); + return message; + } + // ② 履约计划重推(C103-9 行级更新):付款审批通过累加款项金额置 row_op=update 的计划行, + // 凭已存合规阶段ID 重推;独立于履约记录开关,推送失败抛出由付款链置 failed 重试兜底 + String stageResult = this.pushExeStageUpdate(payment); + if (StringUtil.isNotBlank(stageResult)) { + if (result.length() > 0) { + result.append(";"); + } + result.append(stageResult); + } + // ③ 履约记录:审批通过新建 C108-1 / 实际付款推进更新 C108-2(一期暂不对接默认关) + if (!hcsProperties.getPerfRecord().isSyncEnabled()) { + markSynced(payment, result.toString()); + return result.toString(); + } + if (result.length() > 0) { + result.append(";"); + } + result.append(hcsPerfRecordSyncSupport.pushPerfRecord(payment)); + markSynced(payment, result.toString()); + return result.toString(); + } + + /** + * 履约计划重推(C103-9):聚合合同下 row_op 非 none 的计划行(含付款审批累加款项金额的 update 行)凭已存 fdId 重推; + * 合同尚未推送合规时计划行随合同链路首推(首推即携带累加后金额),此处跳过 + * + * @param payment 付款申请单 + * @return 推送结果描述,无待同步行返回空串 + */ + private String pushExeStageUpdate(PaymentApplication payment) { + if (payment.getContractId() == null) { + return ""; + } + ContractManage contractManage = contractManageMapper.selectById(payment.getContractId()); + if (contractManage == null || StringUtil.isBlank(contractManage.getHcsFdId())) { + return ""; + } + String stageResult = hcsExeStageSyncSupport.syncExeStages(contractManage); + return "履约规则无待同步行".equals(stageResult) ? "" : stageResult; + } + + /** + * 特批通过后补推履约数据(C103-9 计划重推 + C108 履约记录):补推失败置 failed 留待人工重推兜底, + * 不阻断特批结果落库与金蝶补推 + * + * @param payment 付款申请单(hcs_spec_status=approved) + */ + private void pushAfterSpecApproved(PaymentApplication payment) { + try { + this.pushPayment(payment); + } catch (Exception exception) { + String message = StringUtil.isBlank(exception.getMessage()) ? "履约数据推送异常" : exception.getMessage(); + markFailed(payment, message); + log.error("付款特批通过后履约数据推送失败:paymentNo={},{}", payment.getPaymentNo(), message, exception); + } + } + + /** + * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口) + * + * @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); + } + } + + /** + * 特批结果人工确认(页面确认与合规 paymentLegal/result 回调等效,§9-U1 过渡兜底) + * + * @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 (!Arrays.asList(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() + ")"); + } + // 特批结果联动付款审批状态(通过恢复放行 / 驳回联动审批驳回),随 updateById 一并落库 + syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)); + payment.setHcsSpecStatus(result); + paymentApplicationMapper.updateById(payment); + // 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推 + if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)) { + this.pushAfterSpecApproved(payment); + } + publishKingdeeAutoSyncOnSpecApproved(payment, result); + log.info("HCS特批结果人工确认:paymentNo={},specFdId={},结果={}", payment.getPaymentNo(), payment.getHcsSpecFdId(), result); + return "特批结果已确认:" + result; + } + + /** + * 付款特批结果接收处理(合规 paymentLegal/result 回调通知特批审批结果): + * sourceSystem 须与 third-party.hcs.src-system 配置一致(不符抛出"来源系统标识错误"异常), + * requestId=TMS 付款申请ID、status=APPROVED/REJECTED; + * 特批结果联动付款审批状态:approved 恢复"审批通过"放行(后续金蝶推送放行), + * rejected 联动付款审批驳回(returned),走"重新编辑→提交→审批→重新特批"闭环 + * + * @param param 合规推送报文 + * @return 处理结果描述 + */ + public String handleLegalResult(HcsLegalResultDTO param) { + // 来源系统校验:仅接受 third-party.hcs.src-system 配置的来源,防伪造回传 + if (!StrUtil.equalsIgnoreCase(param == null ? null : param.getSourceSystem(), hcsProperties.getSrcSystem())) { + log.warn("付款特批结果推送来源系统标识错误:sourceSystem={}", param == null ? null : param.getSourceSystem()); + throw new ServiceException("来源系统标识错误"); + } + String requestId = param == null ? null : param.getRequestId(); + PaymentApplication payment = locatePayment(requestId); + if (payment == null) { + log.warn("付款特批结果推送未定位到付款申请:requestId={},reason={}", requestId, param == null ? null : param.getReason()); + return "未定位到付款申请(requestId=" + requestId + ")"; + } + String normalizedStatus = param.getStatus() == null ? "" : param.getStatus().trim(); + String status; + if (HcsSyncConstant.LEGAL_RESULT_APPROVED.equalsIgnoreCase(normalizedStatus)) { + status = HcsSyncConstant.SPEC_STATUS_APPROVED; + } else if (HcsSyncConstant.LEGAL_RESULT_REJECTED.equalsIgnoreCase(normalizedStatus)) { + status = HcsSyncConstant.SPEC_STATUS_REJECTED; + } else { + log.warn("付款特批结果状态未识别:paymentNo={},status={},reason={}", payment.getPaymentNo(), param.getStatus(), param.getReason()); + return "状态字段未识别(paymentNo=" + payment.getPaymentNo() + ")"; + } + syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)); + payment.setHcsSpecStatus(status); + paymentApplicationMapper.updateById(payment); + // 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推 + if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)) { + this.pushAfterSpecApproved(payment); + } + publishKingdeeAutoSyncOnSpecApproved(payment, status); + log.info("付款特批结果推送处理完成:paymentNo={},结果={},reason={}", payment.getPaymentNo(), status, param.getReason()); + return "特批结果已处理:" + status; + } + + /** + * 特批通过后自动补推金蝶:审批通过时因特批挂起已跳过同步(见 KingdeeApprovalSyncListener), + * 此处重新发布审批通过事件复用同一条自动推送链(开关、失败落库与重推兜底口径不变) + */ + private void publishKingdeeAutoSyncOnSpecApproved(PaymentApplication payment, String specStatus) { + if (!HcsSyncConstant.SPEC_STATUS_APPROVED.equals(specStatus)) { + return; + } + eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION, payment.getId())); + } + + /** + * 特批结果联动付款审批状态(handleLegalResult 与 confirmSpecResult 共用,仅改字段,随 updateById 落库): + * approved 恢复"审批通过"展示并放行(审批状态保持 approved,金蝶推送门禁解除); + * rejected 联动付款审批驳回(returned + "已驳回"),走重新编辑→提交→审批→重新特批闭环 + * (提交时重新校验收款方风险与特批附件) + */ + private void syncPaymentOnSpecResult(PaymentApplication payment, boolean approved) { + if (approved) { + payment.setCurrentNode("审批通过"); + } else { + payment.setApprovalStatus(TransportBusiConstant.RETURNED); + payment.setCurrentNode("已驳回"); + payment.setCurrentProcessor("待处理"); + } + } + + /** + * 按 TMS 付款申请ID 定位付款申请(合规推送 requestId) + */ + private PaymentApplication locatePayment(String requestId) { + if (StringUtil.isBlank(requestId)) { + return null; + } + try { + return paymentApplicationMapper.selectById(Long.valueOf(requestId.trim())); + } catch (NumberFormatException exception) { + return null; + } + } + + /** + * 回填整体同步状态置 synced + */ + public void markSynced(PaymentApplication payment, String message) { + payment.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED); + payment.setHcsRetryCount(0); + payment.setHcsSyncTime(LocalDateTime.now()); + payment.setHcsSyncMsg(HcsSyncConstant.truncateMsg(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(HcsSyncConstant.truncateMsg(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..7e7aa5a0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java @@ -0,0 +1,225 @@ +/** + * 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.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.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +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.springblade.transport.service.IContractExeStageService; +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 IContractExeStageService contractExeStageService; + 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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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)); + // 【必填】机制数据(附件):C108-1 必填,无附件传空列表(附件转传开关关闭或无附件时) + HcsMechanismsDTO mechanisms = buildMechanisms(payment, operatorLoginName); + if (mechanisms == null) { + mechanisms = new HcsMechanismsDTO(); + mechanisms.setAttachment(List.of()); + } + param.setMechanisms(mechanisms); + 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()); + // 【必填】申请编号:付款申请单号 + 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:TMS 付款申请单主键(方便溯源,v4 新增) + props.setFdBusinessId(payment.getId() == null ? null : String.valueOf(payment.getId())); + // 创建时间:createTime(毫秒时间戳,v4 新增) + props.setFdCreateTime(payment.getCreateTime() == null ? null : payment.getCreateTime().toInstant().toEpochMilli()); + // 表单模板ID:配置 perf-record.xform-id(环境值,§9-U9,未配置不传) + String xformId = StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId(); + props.setFdXformId(xformId); + // 【必填】表单模板ID(formData 顶层 fd_xform_id,v4 必填) + formData.setFdXformId(xformId); + formData.setDynamicProps(props); + return formData; + } + + /** + * 履约记录附件挂载(无附件或开关关闭返回 null) + */ + private HcsMechanismsDTO buildMechanisms(PaymentApplication payment, String operatorLoginName) { + if (!hcsProperties.getAttachment().isTransferEnabled() || StringUtil.isBlank(payment.getAttachmentsJson())) { + return null; + } + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_PAYMENT, payment.getId(), payment.getAttachmentsJson(), operatorLoginName); + if (attachmentRefs.isEmpty()) { + return null; + } + HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { + // 附件ID(C103-2 转传返回) + ref.setFdEntityKey(PERF_ATTACH_ENTITY_KEY); + return ref; + }).toList()); + return mechanisms; + } + + /** + * 匹配合同的付款履约计划行(优先按付款类型映射的款项性质精确匹配,无则取首条已同步付款行兜底) + */ + private HcsContractExeStage findPaymentStageRow(PaymentApplication payment) { + return contractExeStageService.findPaymentStageRow(payment.getContractId(), + HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType())); + } + + /** + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) + */ + private String resolveLoginName(PaymentApplication payment) { + return hcsProperties.getOperatorLoginName(); + } + + /** + * 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/HcsProjectSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java new file mode 100644 index 00000000..e24d8e16 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java @@ -0,0 +1,467 @@ +/** + * 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.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.dto.HcsBpmOperationDTO; +import org.springblade.hcs.dto.HcsFormSaveVO; +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.HcsProjectLegalResultDTO; +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.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.transport.constant.HcsFieldConstant; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.HcsContractCheckLogMapper; +import org.springblade.transport.mapper.ProjectApplyMapper; +import org.springblade.transport.pojo.entity.HcsContractCheckLog; +import org.springblade.transport.pojo.entity.ProjectApply; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +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; + private final HcsContractCheckLogMapper hcsContractCheckLogMapper; + + /** + * 推送项目立项到 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); + String addMessage = "项目立项新建成功:" + projectApply.getProjectCode(); + markSynced(projectApply, fdId, addMessage); + return addMessage; + } + updateProject(projectApply); + String updateMessage = "项目立项更新成功:" + projectApply.getProjectCode(); + markSynced(projectApply, projectApply.getHcsFdId(), updateMessage); + return updateMessage; + } + + /** + * 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 ? "无响应" : 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(); + } + + /** + * 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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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()); + + // erp请求id + Long id = projectApply.getId(); + props.setFdErpRequestId(String.valueOf(id)); + + // 项目简称:ProjectApply.projectShortName + props.setFdProSimpleName(projectApply.getProjectShortName()); + // 【必填】项目编号:ProjectApply.projectCode(判重候选键) + props.setFdNum(projectApply.getProjectCode()); + // 创建单位:固定取配置 thirdParty.hcs.creator-unit-id/name(HCS 组织主数据,§9-U2;任一未配置回退开题部门名称仅传 fdName,其他合同接口同用此配置) + HcsRefVO creatorUnit = hcsProperties.resolveCreatorUnit(projectApply.getBusinessDeptName()); + props.setFdCreatorUnit(creatorUnit); + // 项目负责人:负责人登录名 {fdLoginName}(§9-U2) + props.setFdHead(resolveUserRef(projectApply.getPrincipalUserId())); + // todo 承接部门:与创建单位一致,取配置 thirdParty.hcs.creator-unit-id/name(测试线联调固定广西物产城矿发展有限公司) + props.setFdTakerDept(creatorUnit); + // 承接人:项目经办人登录名 {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); + // 可用单位列表:与创建单位一致(同走 creator-unit 配置)(废弃) + // props.setFdAvailUnits(List.of(creatorUnit)); + + // 结算类型:TMS settle_method 字典文本映射 HCS 编码(1=先款后票,2=先票后款,3=先款后票+先票后款(同时存在)) + props.setFdSettlementType(mapSettlementType(projectApply.getSettlementMode())); + // 计划保证金:TMS fundDemand 与 HCS fd_pro_plan_deposit 单位同为万元,直传不换算(接口说明 §5.1) + props.setFdProPlanDeposit(projectApply.getFundDemand()); + // 是否自营项目:一期固定 "2"。是|1, 否|2 + props.setFdIsSelfPro("2"); + // TODO 待确认:项目收入目标 fd_pro_income_goal,TMS 无直接对应字段,一期不传(§5.1) + // 金蝶同步状态:固定 "1" 推送成功|1 + // props.setFdKingdeeSyncStatus("1"); + // 【必填】来源标识:fd_sources,配置 thirdParty.hcs.src-system(约定值 WLTMS,D14) + props.setFdSources(hcsProperties.getSrcSystem()); + + // 金蝶项目立项ID 来源系统主键 ID:TMS 项目立项单主键(D14,对账/幂等候选键) + // props.setFdKingdeeId(String.valueOf(projectApply.getId())); + + // 创建人:项目经办人登录名(扫描推送无登录上下文,取单据承办人,§9-U2) + props.setFdCreator(resolveUserRef(projectApply.getHandlerUserId())); + formData.setDynamicProps(props); + return formData; + } + + /** + * 结算方式字典文本映射 HCS 编码:先款后票=1、先票后款=2、先款后票+先票后款(同时存在)=3;未匹配时原值直传并告警 + */ + private String mapSettlementType(String settlementMode) { + if (StringUtil.isBlank(settlementMode)) { + return null; + } + String code = switch (settlementMode.trim()) { + case "先款后票" -> "1"; + case "先票后款" -> "2"; + case "先款后票+先票后款(同时存在)" -> "3"; + default -> null; + }; + if (code == null) { + log.warn("HCS项目立项结算类型未匹配编码,原值直传:settlementMode=[{}]", settlementMode); + return settlementMode; + } + return code; + } + + /** + * C102-3 项目立项查询判重(conditions={"fd_num": projectCode},§9-U4 查询能力联调确认) + */ + private String findExistFdId(ProjectApply projectApply) { + HcsPageQueryDTO query = new HcsPageQueryDTO(); + // C102-3 入参 columns 用下划线字段名 + query.setColumns(List.of(HcsFieldConstant.PROJ_FD_ID, HcsFieldConstant.PROJ_FD_NUM)); + // 判重条件:项目编号精确匹配 + query.setConditions(Map.of(HcsFieldConstant.PROJ_FD_NUM, Objects.toString(projectApply.getProjectCode(), ""))); + query.setOffset(0); + query.setPageSize(10); + query.setSorts(Map.of(HcsFieldConstant.PROJ_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 ? "无响应" : 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(); + if (page == null || page.getContent() == null || page.getContent().isEmpty()) { + return null; + } + return page.getContent().get(0).getFdId(); + } + + /** + * 项目金蝶同步结果接收处理(合规 projectLegal/result 推送,D902): + * sourceSystem 须为 (不符抛"来源系统标识错误"异常);projectCode=TMS 项目编号定位(requestId 仅 X-ERP-Request-Id 回显不可定位); + * success=true 回填金蝶项目ID/编号(kingdeeId/kingdeeNumber);success=false 记录失败原因;原文留痕 blade_hcs_check_log + * + * @param param 合规推送报文 + * @return 处理结果描述 + */ + public String handleLegalResult(HcsProjectLegalResultDTO param) { + // 来源系统校验:仅接受合规方约定的 ,防伪造回传 + if (!StrUtil.equalsIgnoreCase(param == null ? null : param.getSourceSystem(), hcsProperties.getSrcSystem())) { + log.warn("项目金蝶同步结果推送来源系统标识错误:sourceSystem={}", param == null ? null : param.getSourceSystem()); + throw new ServiceException("来源系统标识错误"); + } + String raw = JSONUtil.toJsonStr(param); + ProjectApply projectApply = locateProjectByCode(param.getProjectCode()); + if (projectApply == null) { + log.warn("项目金蝶同步结果推送未定位到项目:projectCode={},报文已留痕", param.getProjectCode()); + writeCallbackLog(null, HcsSyncConstant.CHECK_RESULT_SKIP, "项目金蝶同步结果推送未定位到项目:" + raw); + return "已留痕:未定位到项目(projectCode=" + param.getProjectCode() + ")"; + } + if (param.getSuccess() == null) { + log.warn("项目金蝶同步结果推送 success 字段缺失:projectCode={},报文已留痕", projectApply.getProjectCode()); + writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_SKIP, "项目金蝶同步结果推送 success 字段缺失:" + raw); + return "已留痕:success 字段缺失(projectCode=" + projectApply.getProjectCode() + ")"; + } + if (param.getSuccess()) { + // 成功:回填金蝶项目ID/编号,同步结果信息随 hcs_sync_msg 留痕 + projectApply.setKingdeeId(param.getKingdeeId()); + projectApply.setKingdeeNumber(param.getProjectNumber()); + projectApply.setHcsSyncMsg(HcsSyncConstant.truncateMsg("项目金蝶同步结果回调:金蝶同步成功,kingdeeId=" + param.getKingdeeId() + + ",projectNumber=" + param.getProjectNumber())); + appendKingdeeResultRecord(projectApply, "金蝶同步成功:金蝶项目ID=" + param.getKingdeeId() + + ",金蝶项目编号=" + param.getProjectNumber()); + projectApplyMapper.updateById(projectApply); + writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_PASS, "项目金蝶同步结果推送:" + raw); + log.info("项目金蝶同步结果推送处理完成:projectCode={},结果=success,kingdeeId={}", projectApply.getProjectCode(), param.getKingdeeId()); + return "金蝶同步结果已处理:success"; + } + // 失败:记录失败原因(金蝶同步在合规侧完成,TMS 不重推,失败重试由合规侧负责) + projectApply.setHcsSyncMsg(HcsSyncConstant.truncateMsg("项目金蝶同步结果回调:金蝶同步失败:" + param.getReason())); + appendKingdeeResultRecord(projectApply, "金蝶同步失败:" + param.getReason()); + projectApplyMapper.updateById(projectApply); + writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_FAIL, "项目金蝶同步结果推送:" + raw); + log.info("项目金蝶同步结果推送处理完成:projectCode={},结果=fail,reason={}", projectApply.getProjectCode(), param.getReason()); + return "金蝶同步结果已处理:fail"; + } + + /** + * 金蝶同步结果回填变更记录(changeRecordJson 追加一条;系统回调无登录用户,操作人记来源标识) + */ + private void appendKingdeeResultRecord(ProjectApply projectApply, String changeContent) { + List records = StringUtil.isBlank(projectApply.getChangeRecordJson()) + ? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(projectApply.getChangeRecordJson(), Object.class)); + Map record = new LinkedHashMap<>(); + record.put("changeDate", LocalDate.now().toString()); + record.put("handlerUserName", "合规系统回调"); + record.put("changeType", "金蝶同步结果"); + record.put("changeContent", changeContent); + record.put("status", projectApply.getApprovalStatus()); + record.put("statusName", HcsSyncConstant.approvalStatusName(projectApply.getApprovalStatus())); + records.add(record); + projectApply.setChangeRecordJson(JSONUtil.toJsonStr(records)); + } + + /** + * 按 ERP 项目编号定位项目(合规推送 projectCode,对应 C102 fd_num) + */ + private ProjectApply locateProjectByCode(String projectCode) { + if (StringUtil.isBlank(projectCode)) { + return null; + } + return projectApplyMapper.selectOne(Wrappers.lambdaQuery() + .eq(ProjectApply::getProjectCode, projectCode.trim()) + .last("limit 1")); + } + + /** + * 项目金蝶同步结果推送留痕(blade_hcs_check_log,biz_type=project,项目未定位到时 bizId 为空,异常不影响主流程) + */ + private void writeCallbackLog(ProjectApply projectApply, String result, String msg) { + try { + HcsContractCheckLog checkLog = new HcsContractCheckLog(); + if (projectApply != null) { + checkLog.setTenantId(projectApply.getTenantId()); + checkLog.setBizId(projectApply.getId()); + } + checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_PROJECT); + checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_KINGDEE); + checkLog.setResult(result); + checkLog.setMsg(StringUtil.left(msg, HcsSyncConstant.SYNC_MSG_MAX_LENGTH)); + checkLog.setCreateTime(LocalDateTime.now()); + hcsContractCheckLogMapper.insert(checkLog); + } catch (Exception exception) { + log.error("项目金蝶同步结果推送留痕写入失败:projectCode={}", projectApply == null ? null : projectApply.getProjectCode(), exception); + } + } + + /** + * 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口) + * + * @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 + */ + 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(HcsSyncConstant.truncateMsg(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(HcsSyncConstant.truncateMsg(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/fd_head/fd_taker/fd_creator 统一取配置,§9-U2) + */ + private String requireLoginName(ProjectApply projectApply, String scene) { + return resolveLoginName(projectApply.getHandlerUserId()); + } + + /** + * 按用户ID 解析登录名引用(查不到返回 null,调用方按字段必填性兜底) + */ + private HcsUserRefVO resolveUserRef(Long userId) { + String loginName = resolveLoginName(userId); + return StringUtil.isBlank(loginName) ? null : HcsUserRefVO.ofLoginName(loginName); + } + + /** + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin) + */ + private String resolveLoginName(Long userId) { + return hcsProperties.getOperatorLoginName(); + } + + /** + * TMS 万元 → HCS 元 + */ + private BigDecimal toYuan(BigDecimal wanAmount) { + return wanAmount == null ? null : wanAmount.multiply(WAN_TO_YUAN); + } +} 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..525cc3cd --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java @@ -0,0 +1,301 @@ +/** + * 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.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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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 attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, invoiceReceipt.getId(), + invoiceReceipt.getAttachmentsJson(), operatorLoginName); + if (!attachmentRefs.isEmpty()) { + HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { + // 附件ID(C103-2 转传返回) + ref.setFdEntityKey(RECEIPT_ATTACH_ENTITY_KEY); + // 绑定类型:仅发起传 ADD + ref.setFdBindType(add ? "ADD" : null); + return ref; + }).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 ? "无响应" : 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(); + } + + /** + * 合同引用解析:收票 → 应收结算关联 → 结算单合同 → 合同已推送传合规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(HcsSyncConstant.truncateMsg(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(HcsSyncConstant.truncateMsg(message)); + invoiceReceiptMapper.updateById(invoiceReceipt); + log.error("HCS收票单推送失败:invoiceNo={},retryCount={},{}", invoiceReceipt.getInvoiceNo(), invoiceReceipt.getHcsRetryCount(), message); + } + + /** + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) + */ + private String resolveLoginName(InvoiceReceipt invoiceReceipt) { + return hcsProperties.getOperatorLoginName(); + } + + /** + * 收票日期格式化(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/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..b3c31662 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReconSupport.java @@ -0,0 +1,146 @@ +/** + * 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.springblade.transport.constant.HcsFieldConstant; +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(HcsFieldConstant.PROJ_FD_ID, HcsFieldConstant.PROJ_FD_NUM, HcsFieldConstant.PROJ_FD_PRO_NAME)); + // 项目编号精确匹配 + query.setConditions(Map.of(HcsFieldConstant.PROJ_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(HcsFieldConstant.CONTRACT_FD_ID, HcsFieldConstant.CONTRACT_FD_SUBJECT, + HcsFieldConstant.CONTRACT_FD_NUMBER, HcsFieldConstant.CONTRACT_FD_CATE_HIERARCHY_ID)); + // 合同编号精确匹配(§9-U4 查询能力联调确认) + query.setConditions(Map.of(HcsFieldConstant.CONTRACT_FD_NUMBER, Objects.toString(contractNo, ""))); + query.setOffset(0); + query.setPageSize(50); + HcsResult result = hcsClient.contractList(query); + return requireSuccess("合同对账", result); + } + + /** + * 合同详情回查(C103-4,fdId 与 fdNumber 二选一) + *

+ * v4 已定义详情响应字段表(HcsContractDetailVO),级联/集合字段内部结构以实际返回为准(设计文档 §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(HcsFieldConstant.INVOICE_FD_ID, HcsFieldConstant.INVOICE_FD_NUMBER, + HcsFieldConstant.INVOICE_FD_BILL_DATE, HcsFieldConstant.INVOICE_FD_CONTRACT_NUMBER, + HcsFieldConstant.INVOICE_FD_INVOICE_AMOUNT, HcsFieldConstant.INVOICE_FD_STATUS)); + // 单据编号精确匹配(§9-U4 查询能力联调确认) + query.setConditions(Map.of(HcsFieldConstant.INVOICE_FD_NUMBER, 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; + } +} 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..6f4de144 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java @@ -0,0 +1,234 @@ +/** + * 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.HcsFormSaveVO; +import org.springblade.hcs.dto.HcsMechanismsDTO; +import org.springblade.hcs.dto.HcsResult; +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.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.PaymentApplicationMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +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 + * 并挂起 currentNode="特批审核"; + * 单据变更且特批流程存在时走 C106-4(顶层 fdId=hcs_spec_fd_id,附件不带 fdBindType)。 + * 特批结果:合规 paymentLegal/result 回调联动付款审批状态(通过放行/驳回联动审批驳回), + * 页面人工确认等效兜底(§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); + // TMS 流程挂起展示:等待合规 paymentLegal/result 回调联动付款审批状态(与合同链路 hangSpecReview 口径一致) + payment.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE); + paymentApplicationMapper.updateById(payment); + log.info("HCS结算付款特批发起成功:paymentNo={},specFdId={},挂起「{}」", payment.getPaymentNo(), specFdId, HcsSyncConstant.SPEC_REVIEW_NODE); + 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 ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), 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); + // 【必填】流程模板ID:公司流程模板(spec-pay.template-mapping 按租户配置,环境/公司相关) + param.setFdTemplateId(requireTemplateId(payment)); + 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 attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_PAYMENT, payment.getId(), payment.getAttachmentsJson(), operatorLoginName); + if (attachmentRefs.isEmpty()) { + return null; + } + HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); + boolean add = StringUtil.isBlank(payment.getHcsSpecFdId()); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { + // 附件ID(C103-2 转传返回) + ref.setFdEntityKey(SPEC_ATTACH_ENTITY_KEY); + // 绑定类型:仅发起传 ADD + ref.setFdBindType(add ? "ADD" : null); + return ref; + }).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 ? "无响应" : 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(); + } + + /** + * 付款特批流程模板ID(fdTemplateId 必填,spec-pay.template-id,每个环境/公司不同) + */ + private String requireTemplateId(PaymentApplication payment) { + String templateId = hcsProperties.getSpecPay().getTemplateId(); + if (StringUtil.isBlank(templateId)) { + throw new HcsException("C106 特批推送失败,未配置付款特批流程模板ID(thirdParty.hcs.spec-pay.template-id)"); + } + return templateId; + } + + /** + * 合规合同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(); + } + + /** + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) + */ + private String resolveLoginName(PaymentApplication payment) { + return hcsProperties.getOperatorLoginName(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java index 24d41f7e..02127d66 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java @@ -29,6 +29,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.transport.config.BfiProperties; import org.springblade.transport.service.ICustomerArchiveService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -47,6 +49,7 @@ import org.springframework.stereotype.Component; */ @Slf4j @Component +@RefreshScope @RequiredArgsConstructor public class CustomerKingdeeSyncTask { 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..874fc41f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java @@ -0,0 +1,352 @@ +/** + * 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.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.HcsContractSpecSyncSupport; +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.cloud.context.config.annotation.RefreshScope; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +/** + * HCS 合规推送定时扫描任务(设计文档 §5.0,ADR-0001) + *

+ * 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限), + * 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。 + *

+ * 五类任务各自独立调度、独立开关(Nacos thirdParty.hcs.push.{bizType}.*): + *

+ * bizType             说明                                        配置节点
+ * project             项目立项 C102                               thirdParty.hcs.push.project.*
+ * contract            合同 C103(内含风险校验/附件转传/履约规则) thirdParty.hcs.push.contract.*
+ * payment             付款特批 C106-3 + 履约记录 C108              thirdParty.hcs.push.payment.*
+ * invoice-application 开票登记 C111(含金蝶开票回填)              thirdParty.hcs.push.invoice-application.*
+ * invoice-receipt     收票单 C109                                 thirdParty.hcs.push.invoice-receipt.*
+ * 
+ * 每个任务配置项:sync-enabled 开关(默认关,灰度打开)、cron 调度(默认每 5 分钟,各任务分钟位错峰 + * 0/1/2/3/4,避免同刻并发扫描与 HCS 接口流量尖峰);batch-size/max-retry 为全局配置(thirdParty.hcs.push.*), + * 见 {@link HcsProperties.Push}。 + * 多实例部署时按任务隔离 Redis 分布式锁防重扫(setIfAbsent + 持有值比对释放,TTL 240s),任务间互不阻塞。 + * 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts/pushUnsyncedPayments 等方法,供手动端点(HcsSyncController)复用。 + * + * @author Chill + */ +@Slf4j +@Component +@RefreshScope +@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 PaymentApplicationMapper paymentApplicationMapper; + private final InvoiceApplicationMapper invoiceApplicationMapper; + private final InvoiceReceiptMapper invoiceReceiptMapper; + private final HcsProjectSyncSupport hcsProjectSyncSupport; + private final HcsContractSyncSupport hcsContractSyncSupport; + private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport; + private final HcsPaymentSyncSupport hcsPaymentSyncSupport; + private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport; + private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport; + private final BladeRedis bladeRedis; + + /** + * 项目立项(C102)定时推送:扫描审批通过且待同步/待补偿的项目,独立开关与 cron 调度 + */ + @Scheduled(cron = "${thirdParty.hcs.push.project.cron:0 */5 * * * ?}") + public void pushHcsProject() { + this.runOnce("项目立项", HcsSyncConstant.BIZ_TYPE_PROJECT, hcsProperties.getPush().getProject().isSyncEnabled(), this::pushUnsyncedProjects); + } + + /** + * 合同(C103)定时推送:扫描审批通过且待同步/待补偿的合同(含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步), + * 并对特批中(processing)的合同做 C103-4 结果查询兜底,独立开关与 cron 调度 + */ + @Scheduled(cron = "${thirdParty.hcs.push.contract.cron:0 1/5 * * * ?}") + public void pushHcsContract() { + this.runOnce("合同", HcsSyncConstant.BIZ_TYPE_CONTRACT, hcsProperties.getPush().getContract().isSyncEnabled(), this::syncHcsContracts); + } + + /** + * 合同链路组合动作:待同步合同推送 + 特批结果查询兜底(合规推送 contractLegal/result 未达时 C103-4 轮询联动) + */ + private String syncHcsContracts() { + String pushSummary = this.pushUnsyncedContracts(); + return pushSummary + ";" + hcsContractSpecSyncSupport.syncProcessingSpecResults(); + } + + /** + * 付款链(C106 特批 / C108 履约记录)定时推送:扫描审批通过且待同步/待补偿的付款申请,独立开关与 cron 调度 + */ + @Scheduled(cron = "${thirdParty.hcs.push.payment.cron:0 2/5 * * * ?}") + public void pushHcsPayment() { + this.runOnce("付款链", HcsSyncConstant.BIZ_TYPE_PAYMENT, hcsProperties.getPush().getPayment().isSyncEnabled(), this::pushUnsyncedPayments); + } + + /** + * 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填),独立开关与 cron 调度 + */ + @Scheduled(cron = "${thirdParty.hcs.push.invoice-application.cron:0 3/5 * * * ?}") + public void pushHcsInvoiceApplication() { + this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, hcsProperties.getPush().getInvoiceApplication().isSyncEnabled(), this::pushUnsyncedInvoiceApplications); + } + + /** + * 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,独立开关与 cron 调度 + */ + @Scheduled(cron = "${thirdParty.hcs.push.invoice-receipt.cron:0 4/5 * * * ?}") + public void pushHcsInvoiceReceipt() { + this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, hcsProperties.getPush().getInvoiceReceipt().isSyncEnabled(), this::pushUnsyncedInvoiceReceipts); + } + + /** + * 单任务执行模板:开关校验 → 分布式锁抢锁 → 执行推送 → 持有值比对后释放。 + * 锁按业务类型隔离,各任务可并行调度互不阻塞;TTL 见 {@link #PUSH_TASK_LOCK_TTL_SECONDS}。 + * + * @param taskName 任务中文名(用于日志定位) + * @param bizType 业务类型(同时作为锁 key 后缀) + * @param syncEnabled 该任务的独立开关(thirdParty.hcs.push.{bizType}.sync-enabled) + * @param pushAction 推送动作,返回结果汇总描述 + */ + private void runOnce(String taskName, String bizType, boolean syncEnabled, Supplier pushAction) { + if (!syncEnabled) { + // 开关未开启属常态(灰度),debug 级别避免轮询刷屏 + // log.debug("HCS{}推送任务未开启(thirdParty.hcs.push.{}.sync-enabled),本轮跳过", taskName, bizType); + return; + } + // 多实例部署防重扫:setIfAbsent 抢锁,持有值比对后释放 + String lockKey = HcsSyncConstant.PUSH_TASK_LOCK_KEY_PREFIX + bizType; + String lockHolder = UUID.randomUUID().toString(); + Boolean locked = bladeRedis.getValueOps() + .setIfAbsent(lockKey, lockHolder, Duration.ofSeconds(PUSH_TASK_LOCK_TTL_SECONDS)); + if (!Boolean.TRUE.equals(locked)) { + log.info("HCS{}推送任务其他实例执行中(lockKey={}),本轮跳过", taskName, lockKey); + return; + } + try { + log.info("HCS{}推送任务开始执行", taskName); + String summary = pushAction.get(); + log.info("HCS{}推送任务执行完成:{}", taskName, summary); + } finally { + if (lockHolder.equals(bladeRedis.get(lockKey))) { + bladeRedis.del(lockKey); + } + } + } + + /** + * 扫描并推送待同步/待补偿的项目立项(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; + } + + /** + * 扫描并推送待同步/待补偿的付款申请(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; + } + + /** + * 扫描并推送待同步/待补偿的开票申请(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; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java index 29f0e88a..e5e4fb87 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java @@ -26,6 +26,8 @@ import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IInvoiceApplicationService; import org.springblade.transport.service.IInvoiceReceiptService; import org.springblade.transport.service.IPaymentApplicationService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -47,6 +49,7 @@ import org.springframework.stereotype.Component; */ @Slf4j @Component +@RefreshScope @RequiredArgsConstructor public class PaymentKingdeeResultTask { diff --git a/blade-third-party-api/blade-hcs-api/pom.xml b/blade-third-party-api/blade-hcs-api/pom.xml new file mode 100644 index 00000000..ff5c4ad2 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springblade + blade-third-party-api + ${revision} + + + blade-hcs-api + ${project.artifactId} + jar + BladeX 合同合规系统(HCS)API 契约模块 + + + + org.springblade + blade-core-tool + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java new file mode 100644 index 00000000..122c083c --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java @@ -0,0 +1,81 @@ +/** + * 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 feign.Request; +import feign.RequestInterceptor; +import org.springblade.hcs.feign.HcsClientFallbackFactory; +import org.springblade.hcs.interceptor.HcsAuthInterceptor; +import org.springframework.context.annotation.Bean; + +import java.util.concurrent.TimeUnit; + +/** + * 合同合规系统(HCS)Feign 客户端配置 + *

+ * 仅通过 {@link org.springframework.cloud.openfeign.FeignClient#configuration} 引用,避免拦截器全局生效。 + * 按 D7 约定不配置 Retryer:写接口重试会造成 HCS 重复建单,重试职责归扫描任务(hcs_retry_count 补偿)。 + * + * @author Chill + */ +public class HcsFeignClientConfig { + + @Bean + public RequestInterceptor bladeFeignRequestInterceptor() { + // 空实现覆盖全局 BladeFeignRequestInterceptor,防止其覆盖 HCS 的认证凭据 + return template -> { + }; + } + + /** + * HCS 降级工厂:非 2xx 时还原远程错误体 msg/traceId,避免全局兜底吞成 null(配合 IHcsClient 的 fallbackFactory 声明) + */ + @Bean + public HcsClientFallbackFactory hcsClientFallbackFactory() { + return new HcsClientFallbackFactory(); + } + + /** + * HCS 认证拦截器:所有请求注入 Authorization + Key-Auth 两个静态认证头(v4 接口说明) + */ + @Bean + public RequestInterceptor hcsAuthInterceptor(HcsProperties properties) { + return new HcsAuthInterceptor(properties); + } + + @Bean + public feign.Logger.Level feignLoggerLevel() { + return feign.Logger.Level.FULL; + } + + /** + * 超时:connect 10s / read 60s(照 BfiFeignClientConfig,D7) + */ + @Bean + public Request.Options options() { + return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true); + } +} 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 new file mode 100644 index 00000000..dc2b4b2a --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java @@ -0,0 +1,348 @@ +/** + * 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 lombok.Data; +import org.springblade.hcs.dto.HcsRefVO; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * 合同合规系统(HCS)配置类 + *

+ * Nacos 配置段见 third-party-api.yaml 的 thirdParty.hcs 节点: + * 注解前缀按 Spring Boot 规范使用横线小写形式 third-party.hcs(照 BfiProperties 惯例), + * YAML 中的驼峰键 thirdParty.hcs.* 经宽松绑定归一后等价生效。 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "third-party.hcs") +public class HcsProperties { + /** + * 合同合规系统(HCS)接口基础地址,直连可配(网关/iPaaS 部署时改此处,代码不感知)。 + * v4 接口说明:地址带网关前缀,如 http://172.16.203.134:8000/gwzh/HTHG + */ + private String baseUrl; + /** + * Authorization 请求头整值(含 Basic 前缀,如 Basic d2Njazp...,凭据走 Nacos + 环境变量注入) + */ + private String authorization; + /** + * Key-Auth 请求头值(v4 接口说明:所有接口固定携带 Authorization + Key-Auth 两个头) + */ + private String keyAuth; + /** + * 来源系统标识(fd_sources,D14 约定值 TMS) + */ + private String srcSystem = "WLTMS"; + /** + * 操作人登录名(fdLoginName / curUser.fdLoginName,全部合同接口共用,§9-U2) + */ + private String operatorLoginName = "srvadmin"; + /** + * Mock 开关(thirdParty.hcs.mockEnabled):true 时 IHcsClient 切换为本地 JSON 桩 + * ({mockDir}/{方法名}.json,见 {@link org.springblade.hcs.mock.HcsClientMock}), + * 供联调前本地自测;生产环境必须为 false + */ + private boolean mockEnabled = false; + /** + * Mock 桩文件目录(与 mockEnabled 配套):存放各接口出参桩 {方法名}.json(源自 HCS 接口文档出参示例), + * 每次调用现读,改文件即生效;默认本机开发目录,其他环境经 Nacos 按需覆盖 + */ + private String mockDir = "F:/home/tms/mock/hcs"; + /** + * 创建单位 fdId(HCS 组织主数据固定值,fd_creator_unit 等组织引用,各合同接口共用) + */ + private String creatorUnitId = "1j56etj23w358wne0ow2k5214nk4f24213w0"; + /** + * 创建单位名称(与 creatorUnitId 配套,HCS 组织主数据固定值,各合同接口共用) + */ + private String creatorUnitName = "广西物产城矿发展有限公司"; + /** + * 推送任务配置(扫描首推/补偿) + */ + private Push push = new Push(); + /** + * 项目立项(C102)配置 + */ + private Project project = new Project(); + /** + * 合同(C103)配置 + */ + private Contract contract = new Contract(); + /** + * 履约规则(C103-9)配置 + */ + private ExeStage exeStage = new ExeStage(); + /** + * 客商风险校验(C101-1)配置 + */ + private Risk risk = new Risk(); + /** + * 额度校验(C106-1)配置 + */ + private Credit credit = new Credit(); + /** + * 结算付款特批(C106-3/4)配置 + */ + private SpecPay specPay = new SpecPay(); + /** + * 履约记录(C108-1/2)配置 + */ + private PerfRecord perfRecord = new PerfRecord(); + /** + * 开票登记(C111)配置 + */ + private Invoice invoice = new Invoice(); + /** + * 收票单(C109)配置 + */ + private ReceiptBill receiptBill = new ReceiptBill(); + /** + * 附件转传(C103-2)配置 + */ + private Attachment attachment = new Attachment(); + /** + * 主数据缓存(C103-6/7/12)配置 + */ + private MasterData masterData = new MasterData(); + + /** + * 创建单位引用解析:fdId+fdName 配置优先(third-party.hcs.creator-unit-id/name), + * 任一未配置回退单据部门名称仅传 fdName(各合同接口共用) + * + * @param fallbackName 单据自身部门名称(如开题部门名称) + * @return 创建单位引用 + */ + public HcsRefVO resolveCreatorUnit(String fallbackName) { + if (creatorUnitId == null || creatorUnitId.isEmpty() || creatorUnitName == null || creatorUnitName.isEmpty()) { + return HcsRefVO.ofName(fallbackName); + } + HcsRefVO ref = new HcsRefVO(); + ref.setFdId(creatorUnitId); + ref.setFdName(creatorUnitName); + return ref; + } + + @Data + public static class Push { + /** + * 审批通过即时推送总开关(thirdParty.hcs.push.immediate-enabled,默认关闭灰度打开;关闭时完全回落扫描链路) + */ + private boolean immediateEnabled = false; + /** + * 每类单据单轮扫描批量(全局) + */ + private int batchSize = 50; + /** + * 推送失败自动重试上限,超限仅人工重推(全局) + */ + private int maxRetry = 5; + /** + * 项目立项(C102)推送开关(thirdParty.hcs.push.project.sync-enabled,默认关闭,灰度打开) + */ + private Task project = new Task(); + /** + * 合同(C103)推送开关(thirdParty.hcs.push.contract.sync-enabled,默认关闭,灰度打开) + */ + private Task contract = new Task(); + /** + * 付款链(C106 特批 / C108 履约记录)推送开关(thirdParty.hcs.push.payment.sync-enabled,默认关闭,灰度打开) + */ + private Task payment = new Task(); + /** + * 开票登记(C111)推送开关(thirdParty.hcs.push.invoice-application.sync-enabled,默认关闭,灰度打开) + */ + private Task invoiceApplication = new Task(); + /** + * 收票单(C109)推送开关(thirdParty.hcs.push.invoice-receipt.sync-enabled,默认关闭,灰度打开) + */ + private Task invoiceReceipt = new Task(); + } + + /** + * 单类单据推送任务配置(thirdParty.hcs.push.{bizType}.sync-enabled; + * cron 有代码默认值错峰每 5 分钟,保持 @Scheduled 占位符形式,未纳入本类) + */ + @Data + public static class Task { + /** + * 推送开关,默认关闭(灰度打开) + */ + private boolean syncEnabled = false; + } + + @Data + public static class Project { + /** + * 立项 bpmOperation.templateCode(环境相关值,§9-U9,待合规方提供) + */ + private String templateCode = ""; + } + + @Data + public static class Contract { + /** + * TMS 合同类别 -> HCS 模板ID(fdTemplateId,§9-U7 待合规方提供模板清单) + */ + private Map templateMapping = new HashMap<>(); + /** + * TMS 审核状态 -> HCS 合同状态(xformFields CONTRACT_STATUS 必填,draft/approving/approved 等,§9-U3 待联调后配置) + */ + private Map statusMapping = new HashMap<>(); + /** + * TMS 币种码 -> HCS 币种名称(照金蝶 RMB->CNY 映射惯例) + */ + private Map currencyNameMapping = new HashMap<>(Map.of("RMB", "人民币")); + /** + * 合同 fdSepcailContract 固定 false(D15,配置项预留) + */ + private boolean specialContractEnabled = false; + /** + * 合同推送前是否强制客商风险校验 + */ + private boolean requireRiskCheck = true; + } + + @Data + public static class ExeStage { + /** + * 履约规则同步开关(C103-9 行级同步,一期暂不对接,默认关) + */ + private boolean syncEnabled = false; + /** + * 履约规则金额类型默认值(fdFundType) + */ + private String fundType = "byAmount"; + } + + @Data + public static class Risk { + /** + * 合同推送前风险校验开关 + */ + private boolean checkOnContract = true; + /** + * 付款提交前风险校验开关 + */ + private boolean checkOnPayment = true; + } + + @Data + public static class Credit { + /** + * 额度校验开关(C106-1,一期暂不对接,默认关;关闭后特批无自动触发) + */ + private boolean checkEnabled = false; + /** + * 付款提交校验是否要求合同已推送合规(hcs_fd_id 存在) + */ + private boolean requireContractSynced = true; + } + + @Data + public static class SpecPay { + /** + * 特批单据状态(fd_doc_status,示例 10,§9-U3 待联调) + */ + private String docStatus = "10"; + /** + * 付款用途字典值(fd_payment_purpose,§9-U3 待联调后配置) + */ + private String paymentPurpose = ""; + /** + * 付款性质字典值(fd_payment_nature,§9-U3 待联调后配置) + */ + private String paymentNature = ""; + /** + * 付款类型字典值(fd_payment_type,§9-U3 待联调后配置) + */ + private String paymentType = ""; + /** + * 付款特批流程模板ID(fdTemplateId,每个环境/公司不同,按环境在 Nacos 维护) + */ + private String templateId = ""; + } + + @Data + public static class Invoice { + /** + * TMS 发票类型 -> HCS 发票类型字典值(§9-U3 待联调后配置) + */ + private Map invoiceTypeMapping = new HashMap<>(); + /** + * TMS 币种码 -> HCS 币种 fdId(经 C103-12 币种表缓存取值,§9-U3 待联调后配置) + */ + private Map currencyIdMapping = new HashMap<>(); + /** + * 开票登记款项性质默认值(fdFundNature) + */ + private String fundNatureDefault = "progress"; + } + + @Data + public static class ReceiptBill { + /** + * TMS 发票类型 -> HCS 收票发票类型字典值(§9-U3 待联调后配置) + */ + private Map invoiceTypeMapping = new HashMap<>(); + } + + @Data + public static class PerfRecord { + /** + * 履约记录同步开关(C108-1/2,一期暂不对接,默认关) + */ + private boolean syncEnabled = false; + /** + * 履约记录表单模板ID(fd_xform_id,环境相关值,§9-U9 待合规方提供) + */ + private String xformId = ""; + } + + @Data + public static class Attachment { + /** + * 附件转传总开关 + */ + private boolean transferEnabled = true; + /** + * TMS 文件服务下载基址(拉取附件流用) + */ + private String fileDownloadBase = ""; + } + + @Data + public static class MasterData { + /** + * 模板/分类/币种列表缓存时长(分钟),启动/首次使用时拉取并缓存(设计文档 §5.9) + */ + private int ttlMinutes = 120; + } +} 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..83ca8ddb --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/constant/HcsConstant.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.constant; + +/** + * 合同合规系统(HCS)常量类 + * + * @author Chill + */ +public class HcsConstant { + + /** + * 接口调用成功 messageKey(统一出参 code,业务成败以 success 字段为准) + */ + public static final String SUCCESS_CODE = "return.optSuccess"; + + /** + * 网关认证请求头名称(整值可配,如 Basic d2Njazp...) + */ + public static final String HEADER_AUTHORIZATION = "Authorization"; + + /** + * 网关 Key-Auth 认证请求头名称(v4 接口说明:所有接口固定携带) + */ + public static final String HEADER_KEY_AUTH = "Key-Auth"; + + /** + * signer 相对方单位类型:甲方=inner(内部主体) + */ + public static final String SIGNER_FD_TYPE_INNER = "inner"; + + /** + * signer 相对方单位类型:乙方=supplier(外部相对方) + */ + public static final String SIGNER_FD_TYPE_SUPPLIER = "supplier"; + /** + * signer 相对方单位类型:客户=customer + */ + public static final String SIGNER_FD_TYPE_CUSTOMER = "customer"; +} 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..e497b40c --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCategoryVO.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-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; + /** + * 层级ID:由各级分类 fdId 以 x 包裹拼接(x+上级fdId+x+fdId+x,v4 新增) + */ + private String fdHierarchyId; + /** + * 创建人 + */ + private HcsRefVO fdCreator; + /** + * 创建人部门 + */ + private HcsRefVO fdCreatorDept; + /** + * 创建时间(毫秒时间戳) + */ + private Long fdCreateTime; +} diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java new file mode 100644 index 00000000..8185a17c --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.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.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)C107-1 新增认领记录入参(将收款流水认领到合同/履约项) + *

+ * 校验:fdCollectionFlow.fdId、fdContractNumber(须在合规本地存在)、fdClaimAmount 必填, + * 认领金额必须大于 0 且不能超过该流水收款金额;fdExePlanStageId 可选,若传须属于该合同。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsClaimRecordAddDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 收款流水(fdId 必填,为合规系统流水ID) + */ + private HcsRefVO fdCollectionFlow; + /** + * 认领合同 {fdId, fdName}(fdId 为合规合同ID,随编号一并回溯) + */ + private HcsRefVO fdContract; + /** + * 认领合同编号(必填,须在合规系统本地存在) + */ + private String fdContractNumber; + /** + * 对应履约项ID(可选,须属于该合同) + */ + private String fdExePlanStageId; + /** + * 认领款项类型(prepayment=预付款,progress=进度款,acceptance=验收款,retention=质保金) + */ + private String fdFundNature; + /** + * 认领人 {fdId, fdName} + */ + private HcsRefVO fdClaimer; + /** + * 认领人名称 + */ + private String fdClaimerName; + /** + * 认领人部门 {fdId, fdName} + */ + private HcsRefVO fdClaimerDept; + /** + * 认领人部门名称 + */ + private String fdClaimerDeptName; + /** + * 认领金额(必填,必须大于0,且不能超过收款金额) + */ + private BigDecimal fdClaimAmount; + /** + * 认领日期(毫秒时间戳) + */ + private Long fdClaimDate; + /** + * 备注 + */ + private String fdRemark; + /** + * 状态:0=已认领(默认),1=已作废 + */ + private Integer fdStatus; +} diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCollectionFlowVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCollectionFlowVO.java new file mode 100644 index 00000000..ebc3af2a --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCollectionFlowVO.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 + * is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for their own commercial purposes. + *

+ * Using this software signifies agreement to this license agreement and terms of use, + * and you expressly agree to be bound by the terms of use of this software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + *

+ * 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)C104-2 收款流水分页行(认领记录推送前按明细流水号定位合规侧收款流水ID) + *

+ * 合规侧收款流水由合规自 ERP 侧同步(fdDataSource 默认 erp),TMS 以明细流水号(fdDetailSerialNumber) + * 精确匹配取 fdId,供 C107-1 认领记录的 fdCollectionFlow.fdId 引用。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsCollectionFlowVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 合规收款流水ID(C107-1 fdCollectionFlow.fdId 引用) + */ + private String fdId; + /** + * 认领公告 + */ + private String fdClaimNotice; + /** + * 明细流水号(与 TMS blade_kingdee_receipt_flow.detail_serial_no 同源) + */ + private String fdDetailSerialNumber; + /** + * 收款金额 + */ + private BigDecimal fdCollectionAmount; + /** + * 认领状态:0=未认领,1=部分认领,2=认领完成 + */ + private Integer fdClaimStatus; + /** + * 已认领金额 + */ + private BigDecimal fdClaimedAmount; +} 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/HcsContractDetailVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDetailVO.java new file mode 100644 index 00000000..93c87dff --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDetailVO.java @@ -0,0 +1,519 @@ +/** + * 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)C103-4 合同详情出参(v4 字段表) + *

+ * 级联/集合类字段(对象/数组)内部结构 v4 标注以实际返回为准,暂以 Object/List<Object> 承接, + * 联调确认后细化。fdSepcailContract 为接口文档原文拼写(Special 笔误,必须沿用)。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsContractDetailVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 合同ID + */ + private String fdId; + /** + * 租户ID + */ + private String fdTenantId; + /** + * 可阅读标示 + */ + private String fdReaderFlag; + /** + * 不可阅读标记(可阅读者在文档发布前生效) + */ + private String fdNotReaderFlag; + /** + * 可阅读者(在文档发布前生效) + */ + private String fdRBPFlag; + /** + * 修改人 + */ + private String fdAlter; + /** + * 修改时间(毫秒时间戳) + */ + private Long fdAlterTime; + /** + * 描述 + */ + private String fdDesc; + /** + * 是否隐藏 + */ + private Boolean fdIsHidden; + /** + * 最后修改时间(毫秒时间戳) + */ + private Long fdLastModifiedTime; + /** + * 合同名称 + */ + private String fdSubject; + /** + * 合同分类(层级ID) + */ + private String fdCateHierarchyId; + /** + * 编号 + */ + private String fdNumber; + /** + * 状态:draft=草稿; approving=审批中; approved=已审批; tosign=待签订; signing=签订中; signerror=签订异常; signed=已签订等 + */ + private String fdStatus; + /** + * 合同发起ID + */ + private String fdDraftMainId; + /** + * 合同模板ID + */ + private String fdTemplateId; + /** + * 审批状态:approving=审批中; approved=已审批; approvalinvalid=审批作废 + */ + private String fdApprovalStatus; + /** + * 签订状态:tosign=待签订; signing=签订中; signerror=签订异常; signed=已签订; signinvalid=签订作废 + */ + private String fdSignStatus; + /** + * 签订异常信息 + */ + private String fdSignErrorMsg; + /** + * 进入待签订时间(毫秒时间戳) + */ + private Long fdToSignTime; + /** + * 签订发起时间(毫秒时间戳) + */ + private Long fdSignCreateTime; + /** + * 完成签订时间(毫秒时间戳) + */ + private Long fdSignedTime; + /** + * 归档状态:toarchive=待归档; archiving=归档中; archived=已归档; archiveerror=归档异常 + */ + private String fdArchiveStatus; + /** + * 变更状态:changing=变更中; changed=已变更; terminated=已终止 + */ + private String fdChangeStatus; + /** + * 履约状态:toplan=待履约; planing=履约中; planerror=履约异常; planed=已履约; planterminate=履约终止 + */ + private String fdPlanStatus; + /** + * 进入待履约时间(毫秒时间戳) + */ + private Long fdToPlanTime; + /** + * 完成履约时间(毫秒时间戳) + */ + private Long fdPlanedTime; + /** + * 关闭状态:toclose=待关闭; closing=关闭中; closed=已关闭 + */ + private String fdCloseStatus; + /** + * 进入待关闭时间(毫秒时间戳) + */ + private Long fdToCloseTime; + /** + * 关闭时间(毫秒时间戳) + */ + private Long fdClosedTime; + /** + * 签署提供者:psign=物理签署; esign=电子签署; qjsign=智能用印签署 + */ + private String fdSignProvider; + /** + * 签署实体ID + */ + private String fdSignEntityId; + /** + * 归档方式:auto=一键申请; review=流程申请 + */ + private String fdFileWay; + /** + * 归档实体ID + */ + private String fdArchiveEntityId; + /** + * 进入待归档时间(毫秒时间戳) + */ + private Long fdToArchiveTime; + /** + * 完成归档时间(毫秒时间戳) + */ + private Long fdArchivedTime; + /** + * 履约计划创建时机:draft=起草环节创建; toplan=履约环节创建 + */ + private String fdPlanCreateMoment; + /** + * 关闭方式:auto=一键关闭; review=流程关闭 + */ + private String fdCloseWay; + /** + * 关闭实体ID + */ + private String fdCloseEntityId; + /** + * 是否锁定 + */ + private Boolean fdIsLocked; + /** + * 锁定原因:invalid=合同作废; change=合同变更; termination=合同终止 + */ + private String fdLockReason; + /** + * 合同作废实体ID + */ + private String fdInvalidEntityId; + /** + * 上次合同变动发起实体ID + */ + private String fdLastChangeEntityId; + /** + * 合同终止发起实体ID + */ + private String fdTerminationEntityId; + /** + * 批量发起任务ID + */ + private String fdBatchTaskId; + /** + * 是否关联交易 + */ + private Boolean fdIsRelaDeal; + /** + * 是否补录 + */ + private Boolean fdIsRecord; + /** + * 履约计划(级联字段,结构以实际返回为准) + */ + private Object fdPlan; + /** + * 验收计划(级联字段,结构以实际返回为准) + */ + private Object fdAcptPlan; + /** + * 外部合同数据(级联字段,结构以实际返回为准) + */ + private Object fdOuterAttr; + /** + * 订单明细配置ID + */ + private String fdOrderDetailConfigId; + /** + * 收付款进度(结构以实际返回为准) + */ + private Object fdPayColProgress; + /** + * 开票进度(结构以实际返回为准) + */ + private Object fdInvoiceProgress; + /** + * 履约状态(新):0=正常; 1=低风险; 2=中风险; 3=高风险 + */ + private Integer fdPlanStatusNew; + /** + * 是否冻结 + */ + private Boolean fdIsFreeze; + /** + * 解冻表单关联ID + */ + private String fdUnfreezeFormId; + /** + * 合同履约流程状态:00/01/02/10/11/12/20/21/29/30/39 + */ + private String fdPlanFlowStatus; + /** + * 合同履约流程ID + */ + private String fdPlanFlowId; + /** + * 是否评分 + */ + private Boolean fdRated; + /** + * 完成审批时间(毫秒时间戳) + */ + private Long fdApprovedTime; + /** + * 是否同步金蝶 + */ + private Boolean fdIsSyncJd; + /** + * 同步金蝶状态:0/1/2 + */ + private Integer fdSyncJdStatus; + /** + * 特批合同(字段名原文拼写笔误,必须沿用) + */ + private Boolean fdSepcailContract; + /** + * 合同结算模式:open=开口合同; close=闭口合同。开口合同指合同签订时不明确约定总金额、总数量、具体收付款日期,按实际发生的供货量、业务量结算的合同 + */ + private String fdContractGwNature; + /** + * 合同类型:standard=标准合同; halfstandard=半标准合同; notstandard=非标准合同 + */ + private String fdContractCharacter; + /** + * 合同性质:general=普通合同; framework=框架协议 + */ + private String fdContractNature; + /** + * 引用范本ID + */ + private String fdModelId; + /** + * 合同属性:main=主合同; child=子合同; common=普通 + */ + private String fdContractAttribute; + /** + * 承办人 + */ + private HcsRefVO fdUnderTaker; + /** + * 承办人部门 + */ + private HcsRefVO fdUnderTakerDept; + /** + * 预计开始时间(毫秒时间戳) + */ + private Long fdEstimateStartTime; + /** + * 预计结束时间(毫秒时间戳) + */ + private Long fdEstimateEndTime; + /** + * 实际开始时间(毫秒时间戳) + */ + private Long fdStartTime; + /** + * 实际结束时间(毫秒时间戳) + */ + private Long fdEndTime; + /** + * 合同发起来源:inner=内部发起; outer=外部发起 + */ + private String fdContractSource; + /** + * 外部来源系统 + */ + private String fdOuterSystem; + /** + * 合同结算类型:payment=付款; payment_collection=收支双向; collection=收款; none=不结算 + */ + private String fdSettlementType; + /** + * 合同金额 + */ + private BigDecimal fdAmount; + /** + * 币种 + */ + private String fdCurrencyId; + /** + * 是否原始合同 + */ + private Boolean fdIsOrigin; + /** + * 原始合同ID + */ + private String fdOriginContractId; + /** + * 合同变更类型:change=合同变更; termination=合同终止 + */ + private String fdChangeType; + /** + * 是否含税 + */ + private Boolean fdIncludeTax; + /** + * 是否引用范本 + */ + private Boolean fdUseModel; + /** + * 引用范本名称 + */ + private String fdModelName; + /** + * 引用范本版本 + */ + private String fdModelVersion; + /** + * 是否固定期限 + */ + private Boolean fdIsFixedTerm; + /** + * 合同开始期限 + */ + private String fdStartTerm; + /** + * 合同结束期限 + */ + private String fdEndTerm; + /** + * 有效期限 + */ + private String fdValidTerm; + /** + * 项目立项ID + */ + private String fdProjectId; + /** + * 合同份数 + */ + private Integer fdServings; + /** + * 合同说明 + */ + private String fdContractDescription; + /** + * 合同的变更流程状态:changing=变更中; changed=已变更; noChangeProcess=无变更流程 + */ + private String fdChangeLbpmStatus; + /** + * 紧急程度:1=普通; 2=急件; 3=特急 + */ + private Integer fdUrgencyLevel; + /** + * 创建人 + */ + private HcsRefVO fdCreator; + /** + * 创建时间(毫秒时间戳) + */ + private Long fdCreateTime; + /** + * 创建者部门 + */ + private HcsRefVO fdCreatorDept; + /** + * 权限(级联字段,结构以实际返回为准) + */ + private Object fdPermissions; + /** + * 可阅读者(结构以实际返回为准) + */ + private List fdReaders; + /** + * 所有可阅读者(结构以实际返回为准) + */ + private List fdAllReaders; + /** + * 所有可阅读者ID(结构以实际返回为准) + */ + private List fdAllReaderIds; + /** + * 可维护者(结构以实际返回为准) + */ + private List fdEditors; + /** + * 所有可编辑者(结构以实际返回为准) + */ + private List fdAllEditors; + /** + * 协办人(结构以实际返回为准) + */ + private List fdCoTakers; + /** + * 合同时间线(结构以实际返回为准) + */ + private List fdTimeLines; + /** + * 合同关联(结构以实际返回为准) + */ + private List fdRelations; + /** + * 合同签约方及地位(结构以实际返回为准) + */ + private List fdStandingSigners; + /** + * 签印文件(结构以实际返回为准) + */ + private List fdSignFiles; + /** + * 业务明细(结构以实际返回为准) + */ + private List fdBusinessDetails; + /** + * 付款记录(结构以实际返回为准) + */ + private List fdPaymentRecords; + /** + * 收款记录(结构以实际返回为准) + */ + private List fdCollectionRecords; + /** + * 收票记录(结构以实际返回为准) + */ + private List fdInInvoices; + /** + * 开票记录(结构以实际返回为准) + */ + private List fdOutInvoices; + /** + * 预填写物理签印信息(结构以实际返回为准) + */ + private List fdPsignContents; + /** + * 审批人(结构以实际返回为准) + */ + private List fdApprovers; + /** + * 履约规则(结构以实际返回为准) + */ + private List fdExePlanRules; +} 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..56717899 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractDraftDTO.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; + +/** + * 合同合规系统(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-11 合同更新顶层必填,传 C103-1 返回的 fdContractId;C103-1 新建不传) + */ + private String fdId; + /** + * 合同模板ID(C103-6 模板列表获取,配置 template-mapping,§9-U7) + */ + private String fdTemplateId; + /** + * 外部系统标识(fd_sources,约定值 WLTMS,配置 thirdParty.hcs.src-system) + */ + private String fdOuterSystem; + /** + * 操作人登录名 + */ + private String fdLoginName; + /** + * 保存类型:submit(提交) + */ + private String saveType; + /** + * 合同字段(模板字段 + 签署方) + */ + private HcsContractFieldJsonDTO fdFieldJson; + /** + * 附件数据(与 fdFieldJson 同级;C103-1 必填,无附件传空列表,C103-11 更新不传;fdEntityKey 普通附件枚举待合规方给出,暂不传) + */ + private List attachment; +} 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..addf81ae --- /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 更新共用) + *

+ * 附件挂载在顶层报文 attachment(与 fdFieldJson 同级),见 HcsContractDraftDTO。 + * + * @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/HcsContractLegalResultDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractLegalResultDTO.java new file mode 100644 index 00000000..bfb4d3ea --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsContractLegalResultDTO.java @@ -0,0 +1,77 @@ +/** + * 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; + +/** + * 合同特批完成后合规系统回传 TMS 的结果报文(POST /hcs/contractLegal/result,iPaaS 网关 /gwzh/HTHG/contractLegal/result) + *

+ * 报文为"合同同步结果"结构:合规特批完成后同步金蝶,回调携带金蝶同步结果; + * 定位合同用 contractCode(ERP 合同编号),requestId 仅是 X-ERP-Request-Id 请求头原样回传、不能用于定位。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsContractLegalResultDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 合同类型:deliverer=投送人;gc=钢厂;sett_center=结算中心 + */ + private String contractType; + /** + * ERP 本次同步使用的合同编号(TMS blade_contract_manage.contract_no,定位合同用) + */ + private String contractCode; + /** + * 本次合同同步请求头 X-ERP-Request-Id,原样回传(不用于定位) + */ + private String requestId; + /** + * 金蝶同步成功传 true,失败传 false + */ + private Boolean success; + /** + * 金蝶同步失败原因;失败时建议填写 + */ + private String reason; + /** + * 金蝶实际返回的合同 ID;成功时用于保存留痕 + */ + private String kingdeeId; + /** + * 来源系统标识,约定传 WCCK(最长 64 字符) + */ + private String sourceSystem; +} 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..f3f98a61 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsCurrencyVO.java @@ -0,0 +1,86 @@ +/** + * 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; + /** + * 创建人 + */ + private HcsRefVO fdCreator; + /** + * 创建时间(毫秒时间戳) + */ + private Long fdCreateTime; +} 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..21dbc7ac --- /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..705b5207 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegAddDTO.java @@ -0,0 +1,107 @@ +/** + * 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; + /** + * 合同 {fdId, fdName}(v4 新增,fdId 为合规合同ID,随编号一并回溯) + */ + private HcsRefVO fdContract; + /** + * 发票类型(字典映射 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..0795fe05 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.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 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; + /** + * 票据日期(毫秒时间戳,金蝶开票完成后回填) + */ + private Long fdBillDate; +} 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/HcsLegalResultDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java new file mode 100644 index 00000000..b74880d9 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.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; + +/** + * 合同合规系统(HCS)特批结果回传报文 + *

+ * 合规系统审批完成后经 iPaaS 网关推送 TMS:合同走 /hcs/contractLegal/result, + * 付款走 /hcs/paymentLegal/result,通过 status=APPROVED、驳回 status=REJECTED,报文两接口共用。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsLegalResultDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 来源系统标识(约定 WLTMS,配置 thirdParty.hcs.src-system) + */ + private String sourceSystem; + /** + * 定位单据ID(付款特批=TMS 付款申请ID;合同特批=合规合同ID,与 id 同值,按 blade_contract_manage.hcs_fd_id 匹配) + */ + private String requestId; + /** + * 合规侧单据ID(合同特批回传携带,与 requestId 同值=合规合同ID;付款特批回传不传,忽略) + */ + private String id; + /** + * 审批结果:APPROVED 通过 / REJECTED 驳回 + */ + private String status; + /** + * 结果说明(驳回原因等,非必传) + */ + private String reason; +} 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..2ba5302f --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordPropsDTO.java @@ -0,0 +1,130 @@ +/** + * 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_stage_fd_id,接口必填,HCS 只认自己的阶段ID) + */ + @JsonProperty("fd_plan_id") + private String fdPlanId; + /** + * 金蝶单据号(溯源用) + */ + @JsonProperty("fd_jd_doc_no") + private String fdJdDocNo; + /** + * 合同ID(HCS 合同ID) + */ + @JsonProperty("fd_contract_id") + private String fdContractId; + /** + * 创建时间(毫秒时间戳,v4 新增) + */ + @JsonProperty("fd_create_time") + private Long fdCreateTime; + /** + * 业务ID(方便溯源用,v4 新增,TMS 传付款申请单ID) + */ + @JsonProperty("fd_business_id") + private String fdBusinessId; + /** + * 附件不可下载 + */ + @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/HcsProjectLegalResultDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectLegalResultDTO.java new file mode 100644 index 00000000..727c0a4b --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectLegalResultDTO.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; + +/** + * 项目金蝶同步完成后合规系统回传 TMS 的结果报文(D902,POST /hcs/projectLegal/result,iPaaS 网关 /gwzh/HTHG/projectLegal/result) + *

+ * 定位项目用 projectCode(ERP 项目编号,对应 C102 请求中的 fd_num), + * requestId 仅是 C102 请求头 X-ERP-Request-Id 原样回传、不能用于定位; + * 未知或过期的 requestId 合规侧返回失败。 + * + * @author Chill + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class HcsProjectLegalResultDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * ERP 项目编号(TMS blade_project_apply.project_code,对应 C102 请求中的 fd_num,定位项目用) + */ + private String projectCode; + /** + * C102 请求头 X-ERP-Request-Id 的值,原样返回(不用于定位) + */ + private String requestId; + /** + * 金蝶同步成功传 true,失败传 false + */ + private Boolean success; + /** + * 金蝶项目 ID;成功时用于回填保存 + */ + private String kingdeeId; + /** + * 金蝶项目编号;成功时用于回填保存 + */ + private String projectNumber; + /** + * 金蝶同步失败原因;失败时建议填写 + */ + private String reason; + /** + * 来源系统标识,约定传 WCCK(最长 64 字符) + */ + private String sourceSystem; +} 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..75310d50 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsProjectPropsDTO.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.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; + + /** + * erp请求id 传项目id + */ + @JsonProperty("fd_erp_request_id") + private String fdErpRequestId; + + /** + * 项目简称 + */ + @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,约定值 WLTMS,配置 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..6f5df361 --- /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(内部主体,客商档案内部组织),外部相对方=customer + */ + 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..993f57f2 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPaySaveDTO.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)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; + /** + * 流程模板ID(公司流程模板ID,发起必填;每个环境/公司不同,经 Nacos spec-pay.template-mapping 配置) + */ + private String fdTemplateId; + /** + * 特批表单数据 + */ + 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..8b738772 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsTemplateVO.java @@ -0,0 +1,109 @@ +/** + * 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; + /** + * 分类层级ID(v4 新增) + */ + private String fdCateHierarchyId; + /** + * 创建时间(毫秒时间戳) + */ + private Long fdCreateTime; + /** + * 创建人 + */ + private HcsRefVO fdCreator; + /** + * 创建部门 + */ + private HcsRefVO fdCreatorDept; + /** + * 是否禁止更新主数据权限 + */ + private Boolean fdDisableUpdateMainAuth; + /** + * 主数据授权时间点(如 afterapproved) + */ + private String fdAuthToMainTime; + /** + * 是否强制自动编号 + */ + private Boolean fdForceAutoNumber; + /** + * 模板扩展属性(含中文名) + */ + 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/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..d8479c0d --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsXformFieldDTO.java @@ -0,0 +1,68 @@ +/** + * 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; + /** + * 字段值(金额/日期等字符串传值;对象类字段如 fdProject 以 JSON 对象传值) + */ + private Object value; + /** + * 字段类型标记:时间字段传 timestamp + */ + private String fdType; + /** + * 是否系统字典值:字典类字段(如 fdSettlementType 合同结算类型)传 "true" + */ + private String fdIsSystem; +} 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/HcsClientFallbackFactory.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/HcsClientFallbackFactory.java new file mode 100644 index 00000000..76b817a2 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/HcsClientFallbackFactory.java @@ -0,0 +1,119 @@ +/** + * 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.FeignException; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.dto.HcsResult; +import org.springframework.cloud.openfeign.FallbackFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * HCS Feign 客户端降级工厂 + *

+ * HCS 返回非 2xx(如 500)时 Feign 抛 {@link FeignException},全局 BladeFeignFallback + * 对 HcsResult 返回值只会返回 null,导致远程错误信息丢失(调用侧只见"无响应/响应为空")。 + * 本工厂在降级时将错误体还原为失败的 {@link HcsResult}:HCS 错误体与 HcsResult 同构 + * (success/code/msg/traceId),直接反序列化取回远程 msg 与 traceId, + * 供调用侧日志与 hcs_sync_msg 等失败原因落库使用。 + *

+ * 通过 {@link IHcsClient} 的 fallbackFactory 声明并注册于 {@link org.springblade.hcs.config.HcsFeignClientConfig}, + * 所有 HCS 接口(含双层嵌套 HcsResult 外层)统一生效,调用侧零改动。 + * + * @author Chill + */ +public class HcsClientFallbackFactory implements FallbackFactory { + + @Override + public IHcsClient create(Throwable cause) { + return (IHcsClient) Proxy.newProxyInstance(IHcsClient.class.getClassLoader(), new Class[]{IHcsClient.class}, + (Object proxy, Method method, Object[] args) -> { + // Object 基础方法(toString/hashCode/equals)直接执行 + if (method.getDeclaringClass() != IHcsClient.class) { + return method.invoke(this, args); + } + // 附件下载等非 HcsResult 返回值无业务结果可还原,保持 null(与原全局兜底行为一致) + if (!HcsResult.class.isAssignableFrom(method.getReturnType())) { + return null; + } + return buildFailResult(cause); + }); + } + + /** + * 按异常构造失败的 HcsResult,优先还原远程错误体中的 msg/traceId + */ + private HcsResult buildFailResult(Throwable cause) { + HcsResult result = new HcsResult<>(); + result.setSuccess(false); + // HCS 非 2xx 时错误体随 FeignException 下发,结构与 HcsResult 一致 + if (cause instanceof FeignException feignException) { + result.setCode("errors.http." + feignException.status()); + HcsResult remote = parseRemoteResult(feignException.contentUTF8()); + if (remote != null && StringUtil.isNotBlank(remote.getMsg())) { + result.setMsg(remote.getMsg()); + result.setTraceId(remote.getTraceId()); + } else { + result.setMsg(resolveFallbackMessage(cause)); + } + return result; + } + // 非 HTTP 错误(熔断打开、连接超时等) + result.setCode("errors.feignFallback"); + result.setMsg(resolveFallbackMessage(cause)); + return result; + } + + /** + * 错误体反序列化为 HcsResult(非 JSON 或解析失败时返回 null) + *

+ * 网关层错误(如 404 页面)的响应体是纯文本/HTML 而非 JSON, + * 需先行排除,否则 JsonUtil.parse 内部会打印误导性的 ERROR 解析堆栈 + */ + private HcsResult parseRemoteResult(String body) { + if (StringUtil.isBlank(body) || !body.trim().startsWith("{")) { + return null; + } + try { + return JsonUtil.parse(body, HcsResult.class); + } catch (Exception exception) { + return null; + } + } + + /** + * 兜底消息:异常摘要非空用之,否则给可读默认值 + */ + private String resolveFallbackMessage(Throwable cause) { + if (StringUtil.isNotBlank(cause.getMessage())) { + return cause.getMessage(); + } + return "HCS 接口调用异常(" + cause.getClass().getSimpleName() + ")"; + } +} 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..916c36f0 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java @@ -0,0 +1,352 @@ +/** + * 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.HcsClaimRecordAddDTO; +import org.springblade.hcs.dto.HcsCollectionFlowVO; +import org.springblade.hcs.dto.HcsContractDetailQueryDTO; +import org.springblade.hcs.dto.HcsContractDetailVO; +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.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +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)接口(一期 25 个业务接口,D4) + *

+ * v4 接口说明:认证统一由 {@link org.springblade.hcs.interceptor.HcsAuthInterceptor} 注入 + * Authorization + Key-Auth 两个静态请求头,不再调用 getToken 取令牌; + * 各接口路径可用 thirdParty.hcs.*Url 配置覆盖(默认值即 v4 文档路径)。 + *

+ * 除附件上传(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, fallbackFactory = HcsClientFallbackFactory.class, primary = false) +public interface IHcsClient { + + 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/lcode/ext-eop-exeplan/extEopExeplanMainOpen/syncExeplan}"; + 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}"; + String CLAIM_RECORD_ADD_URL = "${thirdParty.hcs.claimRecordAddUrl:/openapi/eop-data/eopDataCollectionFlowClaimRecordOpen/add}"; + String COLLECTION_FLOW_LIST_URL = "${thirdParty.hcs.collectionFlowListUrl:/openapi/eop-data/eopDataCollectionFlowOpen/list}"; + + /** + * 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 二选一) + *

+ * v4 已定义详情响应字段表({@link HcsContractDetailVO});级联/集合字段内部结构以实际返回为准。 + * + * @param param 请求参数 + * @return 合同详情 + */ + @PostMapping(CONTRACT_DETAIL_URL) + HcsResult contractDetail(@RequestBody HcsContractDetailQueryDTO param); + + /** + * C103-9(新) 合同履约规则同步(行级增删改;2026-10-14 合规方确认改走 syncExeplan 统一同步路径) + * + * @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); + + /** + * C107-1 新增认领记录(将收款流水认领到合同/履约项) + * + * @param param 请求参数(fdCollectionFlow.fdId、fdContractNumber、fdClaimAmount 必填) + * @return 新增认领记录 fdId(必须持久化) + */ + @PostMapping(CLAIM_RECORD_ADD_URL) + HcsResult claimRecordAdd(@RequestBody HcsClaimRecordAddDTO param); + + /** + * C104-2 查询收款流水列表(认领记录推送前按明细流水号定位合规侧收款流水ID) + * + * @param param 分页查询参数(conditions.fdDetailSerialNumber 按明细流水号精确查询) + * @return 收款流水分页数据 + */ + @PostMapping(COLLECTION_FLOW_LIST_URL) + HcsResult> collectionFlowList(@RequestBody HcsPageQueryDTO 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..62499242 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/interceptor/HcsAuthInterceptor.java @@ -0,0 +1,72 @@ +/** + * 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.hcs.config.HcsProperties; +import org.springblade.hcs.constant.HcsConstant; + +/** + * 合同合规系统(HCS)请求拦截器(v4 接口说明,D2) + *

+ * 所有业务接口统一注入两个静态认证头,不再走 getToken/令牌缓存: + *

    + *
  • Authorization:整值可配(如 Basic d2Njazp...,含前缀);
  • + *
  • Key-Auth:网关密钥,整值可配。
  • + *
+ * 两个头均来自 Nacos + 环境变量注入,未配置时告警并放行(由对端返回 401 暴露问题)。 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +public class HcsAuthInterceptor implements RequestInterceptor { + + private final HcsProperties properties; + + @Override + public void apply(RequestTemplate template) { + String authorization = properties.getAuthorization(); + if (isBlank(authorization)) { + log.warn("HCS Authorization 请求头未配置(thirdParty.hcs.authorization),请求 {} 将无认证头", template.path()); + } else { + template.header(HcsConstant.HEADER_AUTHORIZATION, authorization); + } + String keyAuth = properties.getKeyAuth(); + if (isBlank(keyAuth)) { + log.warn("HCS Key-Auth 请求头未配置(thirdParty.hcs.key-auth),请求 {} 将无认证头", template.path()); + } else { + template.header(HcsConstant.HEADER_KEY_AUTH, keyAuth); + } + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} 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/blade-hcs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/blade-third-party-api/blade-hcs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..17faaaa5 --- /dev/null +++ b/blade-third-party-api/blade-hcs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springblade.hcs.config.ThirdPartyHcsAutoConfiguration 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/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md b/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md new file mode 100644 index 00000000..60225e94 --- /dev/null +++ b/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md @@ -0,0 +1,429 @@ +# 桂物合同合规系统(HCS)接入技术设计 + +> **日期**:2026-09-27 +> **依据**:《桂物合同集成相关接口说明-v4.docx》及其整理稿 [桂物合同集成接口逻辑梳理.md](../桂物合同集成接口逻辑梳理.md)、接口调试文件 [桂物合同集成接口-apifox导入.json](../桂物合同集成接口-apifox导入.json) +> **说明**:设计过程已明确排除《TMS业财融合-集成流程图文字版与业务流程梳理.md》,本文所有场景与规则仅以上述 v4 接口文档与 TMS 代码现状为依据 +> **关联决策**:[ADR-0001 定时扫描推送模式](../../adr/0001-hcs推送采用定时扫描模式.md)、[ADR-0002 履约规则独立数据表](../../adr/0002-履约规则独立数据表.md)、[ADR-0003 收款流水与认领记录不由TMS推送](../../adr/0003-收款流水与认领记录不由TMS推送.md) + +--- + +## 1. 决策记录(共识摘要) + +以下决策已经在设计评审中逐项确认: + +| # | 决策点 | 结论 | +|---|---|---| +| D1 | 客户端模块 | 新建 `blade-third-party-api/blade-hcs-api`,客户端 `IHcsClient`,配置前缀 `thirdParty.hcs.*`(照 blade-bfi-api 模式) | +| D2 | 认证 | v4 接口说明:所有接口固定携带 `Authorization`(整值可配)+ `Key-Auth` 两个静态请求头,由拦截器统一注入,不再调用 getToken 取令牌 | +| D3 | 网络 | 直连 `base-url` 可配置(Nacos),预留 iPaaS 前缀能力,代码不感知 | +| D4 | 范围 | 一期 20 个接口:C101-1、C102-1/2/3、C103-1/2/3/4/5/6/7/11/12、C106-3/4、C109-1/2、C111-1/2/3;排除 C104、C107、C103-9(新)/C103-10旧、C106-1、C108-1/2(范围调整见 §2.2) | +| D5 | TMS 角色 | TMS 是业务发起方:立项/合同/履约规则/特批/开票/收票/履约记录由 TMS 推送 | +| D6 | 落库模式 | 业务表加 `hcs_*` 集成字段;新建履约规则表 `blade_contract_exe_stage`;不建合规镜像库 | +| D7 | 可靠性 | 同步状态机 + `@Scheduled` 扫描首推/补偿 + 页面手动重推;Feign 无自动 Retryer;写前判重查询 | +| D8 | 交付物 | 本设计文档 + CONTEXT.md + ADR,落 `doc/docs/hcs合同/`(ADR 落 `doc/adr/`) | +| D9 | 触发模式 | 审批代码零埋点:approve/MK 回调只落 `approved + hcs_sync_status=unsynced`,扫描任务准实时首推(R3-Q1→(c))。付款申请/开票/收票的 MK 处理器由其他团队补齐,不在本设计范围 | +| D10 | 履约规则源头 | 新建 `blade_contract_exe_stage`,合同维度维护,C103-9 行级同步,合规 fdId 回填 | +| D11 | 履约记录来源 | 付款申请为主线:审批通过→C108-1 新增;实际付款推进→C108-2 更新 | +| D12 | 开票登记时机 | 审批通过即推 C111-2(票据日期空);金蝶开票完成后 C111-3 回填 | +| D13 | 收票合同关联 | 应付结算链路自动回溯;回溯不到收票表单加合同字段人工补录 | +| D14 | 来源标识 | `fd_sources="WLTMS"`(Nacos `src-system` 可配);`fd_kingdee_id`=TMS 业务单据主键 | +| D15 | 特批载体 | 付款申请表加 `hcs_credit_*`/`hcs_spec_*` 字段组;C106-1 提交时同步;超额转 C106-3;~~特批结果回传方式未决~~ **已决(2026-10-12):合规经 contractLegal/paymentLegal result 接口推送 TMS**;合同 `fdSepcailContract` 一期固定 false | + +--- + +## 2. 范围 + +### 2.1 接口范围(20 个,2026-10-09 调整:C103-9/C106-1/C108-1/2 暂不对接) + +| 场景组 | 接口 | TMS 触发业务 | +|---|---|---| +| 项目立项 | C102-1 新建 / C102-2 更新 / C102-3 查询 | ProjectApply 审批通过 / 变更审批通过 / 判重与回查 | +| 合同 | C103-1 发起 / C103-11 更新 / C103-3 列表 / C103-4 详情 | ContractManage 审批通过 / 变更审批通过 / 判重与回查 | +| 附件 | C103-2 上传(下载 C103-5 备用) | 各单据推送前附件转传 | +| 主数据 | C103-6 模板 / C103-7 分类 / C103-12 币种 | 下拉/映射(模板ID、币种ID 缓存) | +| 客商风险 | C101-1 | 合同推送前 + 付款提交前校验 | +| 额度与特批 | C106-3 特批发起 / C106-4 特批更新 | ~~额度超额自动转特批~~ 暂无自动触发(依赖 C106-1,见 §2.2) | +| 开票登记 | C111-1 查询 / C111-2 新增 / C111-3 更新 | InvoiceApplication 审批通过 / 金蝶开票完成 | +| 收票单 | C109-1 新建 / C109-2 更新 | InvoiceReceipt 审批通过 / 票据变更 | + +### 2.2 明确排除(No-s) + +- **2026-10-09 范围调整(暂不对接,代码开关默认关闭,接入时打开配置即可)**: + - **C103-9(新)履约计划保存 / C103-10旧 履约计划更新**:合同链路第⑤步履约规则同步跳过(`exe-stage.sync-enabled=false`),`blade_contract_exe_stage` 表与维护端点保留,仅不再上报 HCS。 + - **C106-1 额度校验**:付款提交校验只保留 C101-1 客商风险(`credit.check-enabled=false`);特批 C106-3 失去自动触发源(原依赖 fdIsCredit=false 置 need),暂无自动触发。 + - **C108-1/C108-2 履约记录**:付款链路跳过履约记录推送(`perf-record.sync-enabled=false`),付款单直接置 synced。 +- **C104 收款流水(C104-1/2/3)、C107 认领记录(C107-1/2/3)**:不由 TMS 推送,推送方与链路待定(见 ADR-0003)。 +- 附件下载 C103-5:仅在对账/排查需要时备用接入。 + +--- + +## 3. 总体架构 + +### 3.1 模块与包结构 + +``` +blade-third-party-api/blade-hcs-api/ (新建,照 blade-bfi-api 模式) +org.springblade.hcs +├── constant/HcsConstant.java # URI 常量(可被配置覆盖)、认证头名称 +├── config/HcsProperties.java # @ConfigurationProperties("thirdParty.hcs") +├── config/HcsFeignClientConfig.java # 覆盖全局拦截器、超时(connect 10s / read 60s) +├── interceptor/HcsAuthInterceptor.java # 所有请求注入 Authorization + Key-Auth 两个静态认证头(v4) +├── feign/IHcsClient.java # @FeignClient(name="HCS", url="${thirdParty.hcs.base-url}") +├── dto/ # 各接口请求/响应 DTO(HcsResult、HcsPageVO) +└── util/HcsPageUtil.java # queryAllPages 翻页工具(照 BfiUtil,默认上限 50 页) + +blade-service/blade-transport/ (业务侧扩展) +org.springblade.transport +├── support/hcs/ +│ ├── HcsProjectSyncSupport.java # 立项 C102 +│ ├── HcsContractSyncSupport.java # 合同 C103-1/11 + 前置:风险校验、附件转传、履约规则 +│ ├── HcsExeStageSyncSupport.java # 履约规则 C103-9 +│ ├── HcsCreditCheckSupport.java # C106-1 + C101-1(付款提交时同步校验) +│ ├── HcsSpecPaySyncSupport.java # 特批 C106-3/4 +│ ├── HcsInvoiceRegSyncSupport.java # 开票登记 C111 +│ ├── HcsReceiptBillSyncSupport.java # 收票单 C109 +│ └── HcsAttachmentSupport.java # 附件转传 + TMS→HCS 附件ID映射同步表(blade_hcs_attachment_sync) +├── task/HcsPushTask.java # @Scheduled 扫描首推/补偿(照 PaymentKingdeeResultTask 模式) +├── controller/HcsSyncController.java # 手动推送/重推、校验重放、对账查询端点 +└── entity/HcsContractExeStage.java # 履约规则实体(blade-transport-api) +``` + +依赖方向:`blade-transport` → `blade-hcs-api`(同 blade-transport → blade-bfi-api)。`blade-hcs-api` 不依赖业务模块。 + +### 3.2 认证设计(D2,v4 接口说明已变更) + +- **静态双请求头**:所有接口统一携带 `Authorization` 与 `Key-Auth` 两个请求头,由 `HcsAuthInterceptor` 注入,不再调用 getToken、不做令牌缓存。 +- 两个头的值均为整值可配(Nacos + 环境变量注入):`third-party.hcs.authorization`(含 `Basic ` 前缀,如 `Basic d2Njazp...`)、`third-party.hcs.key-auth`。前缀/密钥轮换只改配置,不改代码。 +- 未配置时拦截器告警并放行,由对端 401 暴露问题(便于环境配置缺失排查)。 + +### 3.3 超时与重试(D7) + +- Feign:connect 10s / read 60s(照 BfiFeignClientConfig),**不配 Retryer**——写接口重试会造成 HCS 重复建单。 +- 重试职责归扫描任务:失败落 `failed` 状态,下轮扫描按 `hcs_retry_count < max-retry` 自动重试,超限仅人工重推。 +- 幂等:写前判重(见 §5.1/§5.2),并依赖 `fd_kingdee_id`=TMS 主键作为对账/幂等候选键(HCS 侧是否按其幂等,联调确认,见 §9)。 + +### 3.4 字段标注规范(DTO 组装,照 `pushArBill` 惯例) + +所有 Support 组装 HCS 请求 DTO 时,**每个字段赋值的上一行必须以注释标注外部接口字段**,格式照 `KingdeeArBillSyncSupport#pushArBill` 的既有惯例: + +- 必填字段:`// 【必填】外部字段中文名`(层级用"主.子"表示,如"往来户.编码") +- 非必传字段:`// 外部字段中文名(非必传):TMS 取值来源说明` +- 取值来源统一跟在冒号后说明;字段中文名以《桂物合同集成接口逻辑梳理.md》各接口入参表的中文说明为准 +- 待联调确认的字段:`// TODO 待确认:...`,并标注对应设计文档 §9 未决事项编号 + +示例(C111-2 开票登记 DTO 组装): + +```java +HcsInvoiceRegAddDTO param = new HcsInvoiceRegAddDTO(); +// 【必填】合同编号:经应收结算链路回溯的合同编号 +param.setFdContractNumber(contractNo); +// 单据编号:开票申请单号(同租户唯一,判重候选键) +param.setFdNumber(application.getApplicationNo()); +// 【必填】结算币种:TMS 币种码映射 HCS 币种ID(currency-id-mapping,§9-U3) +param.setFdCurrencyId(currencyFdId); +// 开票金额(含税):本次开票金额 invoiceAmount +param.setFdInvoiceAmount(application.getInvoiceAmount()); +// TODO 待确认:款项性质,默认 progress,字典值联调后配置(§9-U3) +param.setFdFundNature(hcsProperties.getInvoice().getFundNatureDefault()); +``` + +§5 各场景的"字段映射"表即本规范的标注依据:表中"HCS 字段 / TMS 来源 / 备注"三列直接翻译成上述逐字段注释,一一对应,不得遗漏或凭空造字段。 + +--- + +## 4. 数据模型变更 + +SQL 脚本落 `tms-api/doc/sql/transport/`,命名照既有惯例 `blade_xxx_hcs_yyyymmdd.sql`。所有业务表统一追加的集成字段组: + +```sql +hcs_fd_id varchar(64) COMMENT '合规系统单据ID(fdId/fdContractId)', +hcs_sync_status varchar(16) DEFAULT 'unsynced' COMMENT '同步状态 unsynced/synced/failed', +hcs_retry_count int DEFAULT 0 COMMENT '推送失败重试次数', +hcs_sync_time datetime COMMENT '最近同步时间', +hcs_sync_msg varchar(512) COMMENT '最近同步结果信息/失败原因' +``` + +| 表 | 变更 | 用途 | +|---|---|---| +| `blade_project_apply` | +统一字段组 | C102 回填 fdId、状态 | +| `blade_contract_manage` | +统一字段组、`hcs_exeplan_id varchar(64)`、`hcs_risk_level varchar(8)`、`hcs_black_status varchar(8)`、`hcs_risk_time datetime` | C103 回填、履约计划ID、风险留痕 | +| `blade_payment_application` | +统一字段组、特批字段组:`hcs_credit_status varchar(16)`(pass/over)、`hcs_credit_msg varchar(512)`、`hcs_credit_time datetime`、`hcs_spec_fd_id varchar(64)`、`hcs_spec_status varchar(16)`(none/need/pushing/processing/approved/rejected) | C106-1 结果、C106-3 流程ID 与状态 | +| `blade_payment_application` | +履约记录字段:`hcs_perf_fd_id varchar(64)` | C108-1 返回 fdId | +| `blade_invoice_application` | +统一字段组 | C111 回填 | +| `blade_invoice_receipt` | +统一字段组、`hcs_contract_id bigint`、`hcs_contract_no varchar(64)`(人工补录兜底) | C109 回填、合同补录 | +| **`blade_contract_exe_stage`(新建)** | 见 §4.1 | 履约规则 | +| **`blade_hcs_check_log`(新建)** | id、biz_type、biz_id、check_type(risk/credit)、result、msg、trace_id、create_time | 风险/额度校验留痕(照 ops 日志惯例) | + +### 4.1 履约规则表 `blade_contract_exe_stage`(D10) + +```sql +CREATE TABLE blade_contract_exe_stage ( + id bigint PRIMARY KEY, + contract_id bigint NOT NULL COMMENT '合同ID(blade_contract_manage.id)', + perform_type varchar(16) NOT NULL COMMENT '履约类型 payment=付款/collection=收款', + fund_nature varchar(16) COMMENT '款项性质 prepayment/progress/acceptance/retention', + fund_amount decimal(18,2) COMMENT '计划金额(元)', + fund_date datetime COMMENT '计划资金日期', + invoice_rule varchar(16) COMMENT '开票规则(如 payFirst)', + invoice_cycle int 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) COMMENT '合规阶段ID(C103-9 返回,update/delete 必用)', + hcs_sync_status varchar(16) DEFAULT 'unsynced', + hcs_sync_msg varchar(512), + tenant_id varchar(12), create_user bigint, create_time datetime, + update_user bigint, update_time datetime, is_deleted tinyint DEFAULT 0 +); +``` + +维护入口:合同新建/变更表单的"履约规则"页签。合同审批通过置 `row_op=add`;后续编辑按行差异置 add/update/delete,由扫描任务随合同链路调 C103-9。 + +--- + +## 5. 场景设计 + +> 本节各"字段映射"表是 §3.4 标注规范的依据:Support 组装 DTO 时逐字段照表标注,字段中文名取自《桂物合同集成接口逻辑梳理.md》。 + +### 5.0 推送引擎(全部场景共用,D9) + +```mermaid +flowchart LR + A[业务操作\n提交/审批/变更/付款推进] -->|事务内| B[置 hcs_sync_status=unsynced\n(+特批/履约等业务标记)] + B --> C{HcsPushTask\n@Scheduled cron 可配} + C -->|扫 unsynced & failed(retry<max)| D[Support 判重查询] + D -->|已存在| E[回填 hcs_fd_id → synced] + D -->|不存在| F[调 HCS 写接口] + F -->|success| E + F -->|失败| G[failed + retry_count+1 + msg] + H[页面 手动推送/重推] --> F +``` + +- **审批零埋点**:`approve()` 与 `MkProcessBizHandlers.approveFromProcess` 只置状态;两审批入口(/approve 与 MK 回调)与未来新入口天然全覆盖。付款申请/开票/收票目前未注册 MK 处理器,**由其他团队补齐,不在本设计范围**——新处理器只要遵循既有约定置 `approvalStatus=approved`,HCS 扫描即自动覆盖,本设计无需任何配套改造。 +- **任务顺序**:项目 → 合同(内含:风险校验→附件转传→C103-1→履约规则 C103-9)→ 付款特批/履约记录 → 开票登记 → 收票单。每类批量 `push.batch-size`(默认 50)。 +- **多实例部署**:任务体加 Redis 分布式锁(照既有 BladeRedis 用法);若维持单实例部署则与既有金蝶任务一致。 +- **手动入口**:`HcsSyncController` 提供 `POST /hcs/sync/{bizType}/{id}`(单条重推,重置 unsynced)与批量端点,前端列表页加"推送合规/重推"按钮(照"同步金蝶"按钮模式)。 + +### 5.1 项目立项(C102-1/2/3 ← ProjectApply) + +**触发与链路**:审批通过(approve / MK 回调 `project-apply` 处理器)→ `unsynced`;变更审批通过(change_approved)→ 走 C102-2 更新。推送前判重:C102-3 `conditions={"fd_num": projectCode}`,命中则直接回填 `fdId` 置 synced(跳过新建)。 + +**字段映射**(dynamicProps): + +| HCS 字段 | TMS 来源(ProjectApply) | 备注 | +|---|---|---| +| fd_pro_name | projectName | | +| fd_pro_simple_name | projectShortName | | +| fd_num | projectCode | | +| fd_creator_unit | businessDeptName → `{fdName}` | ⚠️ HCS 组织 fdId 无法映射,先传名称(见 §9-U2) | +| fd_head | principalUserId → 用户登录名 `{fdLoginName}` | | +| fd_taker_dept | undertakeDeptName → `{fdName}` | 同上 | +| fd_taker | handlerUserName 对应用户 `{fdLoginName}` | | +| fd_pay_limit | fundLimit × 10000 | TMS 万元 → HCS 元 | +| fd_out_invoice_limit | receivableLimit × 10000 | 同上 | +| fd_pro_content | situationRemark | | +| fd_is_available | true | 固定 | +| fd_avail_units | [{fdName: businessDeptName}] | 同 fd_creator_unit | +| fd_settlement_type | settlementMode | ⚠️ 字典映射待联调(§9-U3) | +| fd_pro_plan_deposit | fundDemand × 10000 | ⚠️ 变更映射曾名"履约保证金",语义联调确认 | +| fd_is_self_pro / fd_pro_income_goal | 无直接对应 | 一期默认 "1" / 不传,联调确认 | +| fd_kingdee_sync_status | "1" | | +| fd_sources / fd_kingdee_id | "TMS" / ProjectApply.id | D14 | +| fd_creator | 当前操作人 `{fdLoginName}` | | + +顶层:`bpmOperation.templateCode` ← 配置 `thirdParty.hcs.project.template-code`(环境相关值,§9-U9);`saveOption.saveDraft=false`。**出参**:`data.fdId` → `hcs_fd_id`。 + +### 5.2 合同发起/更新(C103-1/11 ← ContractManage)+ 附件 + 履约规则 + +**触发与链路**:合同审批通过(含变更单 change_approved)→ `unsynced`。扫描任务按序执行: +1. **风险校验**(可配开关):C101-1 查甲方/乙方客商风险,结果写 `blade_hcs_check_log` + 合同 `hcs_risk_level/hcs_black_status`;黑名单且 `risk.block-blacklist=true` 时该单置 failed、msg 提示"客商黑名单,禁止推送"。 +2. **判重**:C103-3 `conditions={"fdNumber": contractNo}`(HCS 条件能力联调确认,§9-U4),命中回填跳过。 +3. **C103-1 起草**:成功回填 `data.fdContractId → hcs_fd_id`。 +4. **履约规则 C103-9**(§5.3)。 +5. 变更单(change_approved)→ C103-11 `updateDraft`,入参同发起;⚠️ HCS 侧如何定位已有合同 v4 未说明(§9-U5)。 + +**xformFields 字段映射**: + +| name | TMS 来源(ContractManage) | 备注 | +|---|---|---| +| fdNumber / CONTRACT_NO | contractNo | | +| fdSubject / CONTRACT_NAME | contractName | | +| fdAmount / AMOUNT | contractAmount | 字符串传值 | +| fdCurrencyId / CURRENCY | settlementCurrency → "人民币" | 配置 `currency-name-mapping: {"RMB":"人民币"}`(照金蝶 RMB→CNY 惯例) | +| fdSettlementType | settlementMode | ⚠️ 映射到"付款/收款"待联调(§9-U3) | +| fdSepcailContract | "false" | D15,配置项预留 | +| CONTRACT_TYPE | contractCategory | | +| PARTY_A / PARTY_B | partyA / partyB | TMS 存名称字符串 | +| SIGN_DATE | signDate | fdType=timestamp | +| fdEstimateStartTime / fdEstimateEndTime | startDate / endDate | fdType=timestamp | +| CREATE_TIME / fdCreateTime | createTime | fdType=timestamp | +| fd_syn_time | 推送时刻 | fdType=timestamp | +| CREATE_USER_NAME | handlerUserName(承办人) | | +| CONTRACT_STATUS | — | ⚠️ 无对应,联调确认是否必传(§9-U3) | + +**signer[](相对方)**:`{fdStanding:"甲", fdName:partyA, fdNumber:←按名称反查 CustomerArchive 编码}`、乙方同理。⚠️ TMS 合同只存甲乙方名称,编号反查不到时留空;`fdType`(inner/外部)判定规则联调确认(§9-U6)。 + +**顶层**:`fdTemplateId` ← `template-mapping[contractCategory]`(Nacos,需合规方提供模板ID清单,§9-U7);`fdOuterSystem` ← src-system;`fdLoginName` ← handlerUser 登录名;`saveType=submit`。 + +**合同附件**:⚠️ v4 的 C103-1 入参未定义附件字段——合同附件向 HCS 的传递方式**待合规方确认**(§9-U8);本设计先按 `HcsAttachmentSupport` 通用转传能力建设(合同主文件 `contractFileJson` 中 approved=true 的文件 + `attachmentsJson`),确认后按其要求挂载。 + +### 5.3 履约规则同步(C103-9新 ← blade_contract_exe_stage) + +- 数据源与表结构见 §4.1(ADR-0002)。 +- **行级同步算法**:扫描任务对 `hcs_sync_status=unsynced` 的合同聚合其规则行:`row_op=add` 的行入 fdStages(不带 fdId);`update/delete` 行必须带 `hcs_stage_fd_id`。整体 `fdEopExeplanOperateType=add`(或 update,随场景)。顶层 `fdContractId` 传 **HCS 合同ID(hcs_fd_id)**。 +- **回填**:`data.fdExeplanId` → `blade_contract_manage.hcs_exeplan_id`;`data.fdChangeStages[].fdId` 按行回填 `hcs_stage_fd_id`。**合规返回的 fdId 必须持久化**——后续行更新/删除全靠它。 +- fdStages 行字段:fdPerformType、fdFundNature、fdFundAmount(元)、fdFundDate(毫秒)、fdInvoiceRule、fdInvoiceCycle、fdFundType(默认 byAmount)、fdIsFinish、fdExtendContent="{}"。 + +### 5.4 客商风险 + 额度校验(C101-1 / C106-1 ← PaymentApplication 提交时) + +付款申请**提交(submit)事务内同步调用**(查询类同步,D15/R2-Q3): +1. **C101-1**:`fdNumber` ← 收款方客商编号(按 payeeName 反查 CustomerArchive;编号体系联调确认 §9-U6)。结果写 `blade_hcs_check_log`;黑名单且 `risk.block-blacklist=true` → 阻断提交。 +2. **C106-1**:`{fdContractId: 合同.hcs_fd_id, fdApplyAmount: appliedAmount}`。`fdIsCredit=true` → `hcs_credit_status=pass` 正常提交;`false` → `hcs_credit_status=over` + `hcs_credit_msg=fdMessage`,**不拦截提交**,标记 `hcs_spec_status=need`(走特批链路,D15/R2-Q2)。 + +> 校验依赖合同已推送(hcs_fd_id 存在);合同未推送时按 `credit.require-contract-synced` 配置决定阻断或跳过校验。 + +### 5.5 结算付款特批(C106-3/4 ← PaymentApplication) + +**触发**:审批通过且 `hcs_spec_status=need` → 扫描任务发起 C106-3;返回 `data.data.fdId`(双层嵌套)→ `hcs_spec_fd_id`,状态置 `processing`(特批中)。 + +**dynamicProps 映射**: + +| HCS 字段 | TMS 来源 | 备注 | +|---|---|---| +| fd_contract_name | ContractManage.hcs_fd_id | ⚠️ 传**合规合同ID**,非名称 | +| fd_contract_no | contractNo | | +| fd_doc_subject | "结算付款特批-" + paymentNo | | +| fd_doc_no / fd_id | paymentNo | 来源付款单标识 | +| fd_pay_amount | appliedAmount | | +| fd_total_amount | settlementAmount | | +| fd_payer / fd_payee | payerName / payeeName | | +| fd_receive_account_name / fd_receive_account / fd_bank_name | receiptAccountName / bankAccount / bankName | | +| fd_settle_currency | "人民币"(映射) | | +| fd_payment_purpose / fd_payment_nature / fd_payment_type | 配置映射(示例 2001 / 1 / 1) | 字典值联调确认(§9-U3) | +| fd_reason / fd_supplement_note | remark | | +| fd_doc_subject / fd_doc_status | 表单层主题 / 配置 "10" | | +| fd_spc_approval_attach | 固定 "fd_spc_approval_attach" | 特批附件标识 | + +`mechanisms.attachment`:`attachmentsJson` 经 C103-2 转传,特批附件 `fdEntityKey=fd_spc_approval_attach` 且带 `fdBindType=ADD`。更新(C106-4):单据变更且特批流程存在时调用,顶层 `fdId=hcs_spec_fd_id`,附件不带 fdBindType。 + +**特批结果**:v4 无结果查询/回调接口 → 一期由页面人工确认;**2026-10-12 已决**:合规审批完成后经 `POST /hcs/paymentLegal/result` 主动推送 TMS(`requestId`=付款申请ID,`status=APPROVED/REJECTED`),人工确认保留等效兜底(§9-U1 闭环)。 + +### 5.6 履约记录(C108-1/2 ← PaymentApplication,D11) + +- **新增**:付款申请审批通过 → 随推送链路调 C108-1:`fd_apply_amount`=appliedAmount、`fd_plan_date`=applyDate(毫秒)、`fd_paln_type`=paymentType、`fd_property`=款项性质(取关联履约规则行/结算性质)、`fd_id`=关联履约阶段 `hcs_stage_fd_id`(无关联则不传)、`fd_contract_id`=合同ID、`fd_xform_id` ← 配置(环境值)。附件经转传 `fdEntityKey=fd_plan_att`。返回 `data.fdId → hcs_perf_fd_id`。 +- **更新**:`paidAmount` 变化/付清 → C108-2:`formData.fdId=hcs_perf_fd_id` 必填,传 `fd_fact_amount`(实际金额)、`fd_contract_id` 等;响应无 data,success 即成功。 + +### 5.7 开票登记(C111-1/2/3 ← InvoiceApplication,D12) + +- **新增**:审批通过 → 扫描推 C111-2;**更新**:金蝶开票完成(`kingdeeStatus=invoiced`,由既有 PaymentKingdeeResultTask 开票回写时置"待更新")→ 扫描推 C111-3 回填 `fdBillDate`/`fdStatus`。 +- **映射**:fdNumber←applicationNo;fdRegisterDate←applicationDate(毫秒);fdInvoiceAmount←invoiceAmount(含税);fdInvoiceParty←issuerName(销方);fdInvoiceType←invoiceType 字典映射(electronic_normal/electronic_special,§9-U3);fdCurrencyId←RMB→币种fdId(启动时经 C103-12 缓存币种表,配置 `currency-id-mapping`);fdRemark←remark;fdThirdCreatorCode/DeptCode←申请人工号/部门编码;fdStatus=0;fdAttIds←附件转传。 +- ⚠️ **fdContractNumber 缺口**:InvoiceApplication 无合同字段,需经应收结算链路回溯(具体关联字段开发时确认,§9-U10)。 +- 判重:C111-1 `conditions={"fdNumber": applicationNo}`(联调确认)。 + +### 5.8 收票单(C109-1/2 ← InvoiceReceipt,D13) + +- **触发**:审批通过 → C109-1;票据变更/作废 → C109-2(顶层 `fdId=hcs_fd_id` 必填,只传变更字段,不带文档状态)。 +- **映射**:fd_invoice_code←invoiceNo;fd_bill_date←invoiceDate(`YYYY-MM-DD`);fd_invoicer←issuerName(销方);fd_buyer←receiverName(购方);fd_invoice_amt_tax_incl←invoiceAmount;fd_tax_rate←taxRate÷100(13→0.13);fd_invoice_type←字典映射(§9-U3);fd_remark←remark;fd_contract_subject/fd_contract_no←**应付结算链路回溯**(收票→应付结算 FormalSettlement.contractId/No),回溯不到→页面补录 `hcs_contract_id/hcs_contract_no`;fd_ap_bill_src_subject←收票认领应付单号(键联调确认 §9-U11);附件转传 `fdEntityKey=fd_attach`、`fdBindType=ADD`。 +- 返回 `data.data.fdId → hcs_fd_id`(双层嵌套)。 + +### 5.9 主数据接口(C103-6/7/12)与查询(C103-3/4) + +- **启动/首次使用时拉取并缓存**(Redis,照 BFI token 缓存惯例,TTL 可配):模板列表(供 template-mapping 校验)、合同分类树、币种表(fdId 映射)。提供管理端"刷新缓存"端点。 +- **C103-3/C103-4**:判重与对账回查(按 fdNumber/fdId);C103-4 的 data 结构 v4 未定义,对账功能联调后补充。 + +--- + +## 6. 附件转传设计(HcsAttachmentSupport,D 补充 R2-Q5) + +1. 输入:单据 `attachmentsJson`/`contractFileJson`(JSON 元素含 url/fileType/approved 等,去重键照 `attachmentKey` 惯例 url|link|fileUrl|downloadUrl|uid|originalName)。 +2. 下载:按元数据 URL 从文件服务拉取流(文件服务基址可配 `attachment.file-download-base`)。 +3. 上传:C103-2 multipart(fdFile、fdAnonymous=false、fdLoginName=操作人)→ 附件 `fdId`。 +4. 映射同步表:`blade_hcs_attachment_sync`(biz_type+biz_id+attachment_key 唯一,照金蝶附件同步表设计)落业务单据 → 合规 fdId 映射,转传前查命中即复用;仅落成功记录(失败整单重试,原因落单据 hcs_sync_msg)。 +5. 任一附件转传失败 → 整单推送失败进补偿重试。 + +--- + +## 7. 配置项清单(Nacos `third-party-api.yaml` → `thirdParty.hcs.*`) + +```yaml +thirdParty: + hcs: + base-url: http://172.16.203.134:8000/gwzh/HTHG # 网关前缀,直连/iPaaS 部署时改此处 + authorization: ${HCS_AUTHORIZATION:} # Authorization 头整值(含 Basic 前缀) + key-auth: ${HCS_KEY_AUTH:} # Key-Auth 头整值 + src-system: WLTMS # fd_sources + push: + sync-enabled: false # 总开关(默认关,灰度打开) + cron: "0 */5 * * * ?" + batch-size: 50 + max-retry: 5 + project: + template-code: "" # bpmOperation.templateCode(环境值) + contract: + template-mapping: {} # {TMS合同类别: HCS模板ID} + settlement-type-mapping: {} # {TMS结算方式: 付款|收款} + currency-name-mapping: {"RMB": "人民币"} + special-contract-enabled: false + require-risk-check: true + exe-stage: + sync-enabled: false # C103-9 履约规则同步开关(一期暂不对接,默认关) + fund-type: byAmount + risk: + check-on-contract: true + check-on-payment: true + block-blacklist: true + credit: + check-enabled: false # C106-1 额度校验开关(一期暂不对接,默认关;关闭后特批无自动触发) + require-contract-synced: true + spec-pay: + doc-status: "10" + payment-purpose: "" # 字典值联调后配置 + payment-nature: "" + payment-type: "" + perf-record: + sync-enabled: false # C108-1/2 履约记录同步开关(一期暂不对接,默认关) + xform-id: "" + invoice: + invoice-type-mapping: {"electronic_normal": "", "electronic_special": ""} + currency-id-mapping: {"RMB": ""} # {TMS币种码: HCS币种fdId} + fund-nature-default: progress + receipt-bill: + invoice-type-mapping: {} + attachment: + transfer-enabled: true + file-download-base: "" +``` + +--- + +## 8. 实施拆分(建议排期) + +| 阶段 | 内容 | 依赖 | +|---|---|---| +| P0 基础 | blade-hcs-api 骨架:Feign/拦截器/认证头/DTO(24 接口全量 DTO)/分页工具;Nacos 配置段;统一字段组 SQL | 合规方提供测试环境地址与凭据 | +| P1 立项与合同 | ProjectApply/ContractManage 字段+Support+扫描任务+手动端点;C102-3/C103-3 判重;C103-6/7/12 缓存 | P0;模板ID清单(§9-U7) | +| P2 履约规则 | blade_contract_exe_stage 表+表单维护+C103-9 行级同步 | P1(合同先推送) | +| P3 付款链 | C106-1/C101-1 提交校验+check_log;特批字段组+C106-3/4;C108-1/2 | P1(合同 fdId) | +| P4 开票与收票 | C111-1/2/3(含金蝶回写联动);C109-1/2(含合同回溯/补录) | P0;开票/收票关联字段确认 | +| P5 运维收尾 | 对账查询页、失败重推批量、监控埋点(traceId 落日志)、多实例锁 | 全部 | + +**全部阶段通用**:Support 组装 HCS 请求 DTO 时按 §3.4 规范逐字段标注外部字段中文名与 TMS 取值来源(照 `pushArBill` 惯例),代码评审以此为一票否决项。 + +--- + +## 9. 未决事项(需合规方/联调确认) + +| # | 事项 | 影响 | +|---|---|---| +| U1 | **特批流程结果回传方式**:v4 无查询/回调接口 → **已决(2026-10-12)**:合规经 `contractLegal/result`(合同)/`paymentLegal/result`(付款)主动推送 TMS | 推送接收端点已实现(blade.secure 放行,免登免授权);人工确认保留等效兜底 | +| U2 | 组织/人员引用体系:HCS `{fdId,fdName}` 的 fdId 与 TMS 部门ID 不同源;登录名是否同源(见原文登录名截图) | 立项/合同操作人字段解析方式 | +| U3 | 字典值映射:fd_settlement_type、fd_invoice_type、付款用途/性质/类型、CONTRACT_STATUS 是否必传 | 各映射配置 | +| U4 | C103-3/C102-3/C111-1 conditions 是否支持 fdNumber/fd_num/fdKingdeeId 精确查询;HCS 是否按 fd_kingdee_id 幂等 | 判重与防重方案 | +| U5 | C103-11 updateDraft 如何定位已有合同 | 合同更新链路 | +| U6 | signer 相对方编号体系与 fdType(inner/外部)判定;C101-1 的 fdNumber 用客商哪个编码 | 合同签署方、风险校验 | +| U7 | 合规模板ID清单(template-mapping 数据源) | 合同推送 | +| U8 | **合同附件向 HCS 的传递方式**(C103-1 未定义附件字段) | 附件转传挂载点 | +| U9 | 立项 bpmOperation.templateCode 环境值;C108 fd_xform_id 环境值 | 立项/履约记录推送 | +| U10 | 开票申请→合同/应收结算的关联字段确认 | C111 fdContractNumber | +| U11 | 收票 fd_ap_bill_src_subject 关联键(应付单号体系) | C109 | +| U12 | C104/C107 收款流水与认领的推送方及链路 | 范围外备忘(ADR-0003) | 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..f94142d8 --- /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`(约定值 WLTMS,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 | WLTMS | 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 接口客户端 | 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..c838e533 --- /dev/null +++ b/doc/docs/hcs合同/2026-09-28-HCS接入实施状态与确认清单.md @@ -0,0 +1,196 @@ +# 桂物合同合规系统(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);URI 均可被 `thirdParty.hcs.*Url` 覆盖 | ✅ | +| `interceptor/HcsAuthInterceptor` | 所有请求注入 `Authorization`(整值可配)+ `Key-Auth` 两个静态认证头(v4 接口说明,2026-10-01 变更;原 oauth2/getToken 与 basic 双模式已删除) | ✅ | +| `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。**2026-10-09 调整:一期暂不对接,`exe-stage.sync-enabled` 默认关,合同链路跳过本步骤** | ✅(开关默认关) | +| `HcsCreditCheckSupport` | 付款提交事务内同步校验:C101-1 黑名单阻断提交;C106-1 超额不拦截、置 over + spec=need 转特批(超额时 success 仍 true,以 fdIsCredit 为准)。**2026-10-09 调整:C106-1 一期暂不对接,`credit.check-enabled` 默认关,仅保留 C101-1;关闭后特批无自动触发** | ✅(C106-1 开关默认关) | +| `HcsSpecPaySyncSupport` | C106-3 发起(双层嵌套 fdId → hcs_spec_fd_id,状态 processing)/ C106-4 更新;特批附件 fdEntityKey=fd_spc_approval_attach、发起带 fdBindType=ADD。**C106-1 暂不对接后无自动置 need,仅历史数据/人工场景可达** | ✅ | +| `HcsPerfRecordSyncSupport` | C108-1 新建(fdId 回填 hcs_perf_fd_id)/ C108-2 更新(实际金额);关联履约阶段 fd_id;附件 fd_plan_att。**2026-10-09 调整:一期暂不对接,`perf-record.sync-enabled` 默认关,付款链路跳过履约记录直接置 synced** | ✅(开关默认关) | +| `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/contractLegal/result` | 合同特批结果回传接收(合规推送,blade.secure 放行,2026-10-12 替代 B103) | +| `POST /hcs/paymentLegal/result` | 付款特批结果回传接收(合规推送,blade.secure 放行) | +| `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 按约束明确不做 + +| 项 | 说明 | +|---|---| +| **C103-9 履约计划保存 / C103-10 履约计划更新(2026-10-09 范围调整)** | 一期暂不对接:合同链路第⑤步跳过(`exe-stage.sync-enabled=false`);`blade_contract_exe_stage` 表与维护端点保留,仅不上报 HCS;C103-10旧 合规方本已废弃 | +| **C106-1 额度校验(2026-10-09 范围调整)** | 一期暂不对接:付款提交校验只保留 C101-1(`credit.check-enabled=false`);特批 C106-3 失去自动触发源,暂无自动触发(历史 need 数据/人工场景仍可推) | +| **C108-1/C108-2 履约记录(2026-10-09 范围调整)** | 一期暂不对接:付款链路跳过履约记录推送(`perf-record.sync-enabled=false`),付款单直接置 synced | +| 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 | ~~特批流程结果回传方式~~ **已决(2026-10-12)**:合规经 `POST /hcs/contractLegal/result`、`POST /hcs/paymentLegal/result` 主动推送结果(报文 `{sourceSystem, requestId, status=APPROVED/REJECTED, reason}`) | 人工确认端点保留作等效兜底;原 B103 推测性回调端点 `/hcs/callback/spec-result` 已删除 | +| 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);作废推送缺接口依据 | +| `HcsAuthInterceptor` | Authorization/Key-Auth 头对全部接口(含 C103-2 附件上传/下载)是否同样生效;网关密钥轮换时仅改 Nacos 配置 | +| `HcsReconSupport#reconContractDetail` | C103-4 出参结构 | diff --git a/doc/docs/hcs合同/2026-10-09-项目立项与合同HCS前端接入设计.md b/doc/docs/hcs合同/2026-10-09-项目立项与合同HCS前端接入设计.md new file mode 100644 index 00000000..34e7d221 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-09-项目立项与合同HCS前端接入设计.md @@ -0,0 +1,314 @@ +# 项目立项与合同 HCS 前端接入设计(tms-erp-web) + +> **⚠️ 2026-10-13 失效说明**:C103-8 独立特批发起接口合规未提供,本文涉及的 `POST /hcs/contract/spec-apply`(发起特批)后端端点已删除;特批改随合同推送报文 `fdSepcailContract=true` 自动发起,前端只需展示特批状态(`hcs_spec_status`/`currentNode=特批审核`)与接收结果联动,无需"发起特批"按钮/弹窗。结果推送 `/hcs/contractLegal/result` 不受影响。 + +> **日期**:2026-10-09 +> **依据**:《桂物合同集成相关接口说明-v4.docx》、[TMS业财融合方案.docx](../TMS业财融合方案.docx)、[2026-09-27-桂物合同合规系统HCS接入技术设计.md](./2026-09-27-桂物合同合规系统HCS接入技术设计.md)、[2026-09-28-HCS接入实施状态与确认清单.md](./2026-09-28-HCS接入实施状态与确认清单.md) +> **涉及工程**:`tms-api`(后端补前端代理端点)、`tms-erp-web`(前端页面与 API 模块) + +--- + +## 1. 总体原则 + +| # | 原则 | +|---|---| +| P1 | **前端不直连 HCS**。所有 C 系列接口由 tms-api 代理:`Authorization`/`Key-Auth` 静态双头只存在于服务端,浏览器不可见,也无跨域问题 | +| P2 | **自动推送保留扫描兜底**。C102-1/2、C103-1/11 走"审批通过 → 置 unsynced → 扫描任务推送"链路(ADR-0001);2026-10-10 增补**审批通过即时推送**(§6),扫描任务降级为兜底补偿。前端只补**状态展示 + 手动重推 + 对账回查** | +| P3 | **附件主存储仍在 TMS OSS**。表单上传逻辑不改;HCS 转传由后端推送链路自动完成(HcsAttachmentSupport 已实现)。前端新增的是**手动转传**(特批材料、确定性上传)与**HCS 附件回下载** | +| P4 | **前端结算币种/合同分类不改**。两个下拉继续用本地字典(`currency_type`/`contractCategory`),存量值与交互零变化;C103-12 币种、C103-7 分类只在后端推送侧映射(`currency-name-mapping`/`template-mapping`,已实现),前端无触点 | +| P5 | UI 照**"同步金蝶"按钮模式**(参考 `customer-archive.vue`、`formal-settlement-table-panel.vue` 的 kingdeeSyncStatus 列 + 推送按钮),HCS 用 `hcsSyncStatus` 等同构字段 | + +--- + +## 2. 接口 → 前端触点映射总表 + +| HCS 接口 | 后端现状 | 面向前端端点 | 前端触点 | +|---|---|---|---| +| C101-1 客商风险等级 | Feign 已有,合同推送/付款提交时服务端调用 | **`GET /hcs/risk`(新增)** | 合同表单选甲乙方后展示风险标签;高风险+黑名单给"发起特批"入口 | +| C102-1 项目立项新增 | 扫描任务自动推送 | — | 无直接触点(列表展示同步结果) | +| C102-2 项目立项更新 | 变更审批通过自动推送 | — | 同上 | +| C102-3 项目立项查询 | `GET /hcs/recon/project` 已有 | 已有 | 立项列表"合规对账"弹窗 | +| C103-12 币种列表 | Feign + Redis 缓存已有(推送侧映射用) | **移出前端范围**(2026-10-09 确认:前端结算币种沿用本地字典,无触点) | — | +| C103-6 模板列表 | Feign + Redis 缓存已有 | **`GET /hcs/master-data/template`(新增)** | 管理端"合规主数据/映射"查看页(表单不选模板,template-mapping 在 Nacos) | +| ~~C103-7 分类列表~~ | Feign + 缓存已有 | **移出前端范围**(2026-10-09 确认:分类为推送侧 Nacos 映射的前置,前端无界面触点) | — | +| C103-1 合同同步 | 审批通过自动推送 | `POST /hcs/sync/contract/{id}` 已有 | 合同列表"推送合规"按钮 | +| C103-11 合同信息更新 | 变更审批通过自动推送 | 同上(重推入口) | 同上 | +| C103-2 附件上传 | Feign 已有,推送链路自动转传 | **`POST /hcs/attachment/upload`(新增)** | 合同/特批材料"上传到合同系统"手动操作,返回 fdId 落库 | +| C103-3 合同列表查询 | `GET /hcs/recon/contract` 已有 | 已有 | 合同列表"合规对账"弹窗 | +| C103-4 合同详情查询 | `GET /hcs/recon/contract/detail` 已有 | 已有 | 对账弹窗"查看合规详情" | +| C103-5 合同附件下载 | 仅 Feign 端点 | **`GET /hcs/attachment/download`(新增)** | 对账弹窗/合同详情中下载 HCS 侧附件 | +| C103-8 合同发起特批 | **本期正式接入**(业务闭环见集成流程梳理:高风险客商签约必走;结果经 contractLegal/result 推送,2026-10-12 替代 B103) | **`POST /hcs/contract/spec-apply`(新增)+ `POST /hcs/contractLegal/result`(结果推送接收,新增)** | 合同详情"发起特批"、特批状态展示、结果推送后自动恢复展示 | + +--- + +## 3. 后端补充端点设计(tms-api) + +全部挂在 `HcsSyncController`(`/hcs` 前缀,与既有端点同文件),复用 `HcsMasterDataSupport`、`HcsAttachmentSupport`、`IHcsClient`,不新建 Support。 + +### 3.1 主数据查询(走 Redis 缓存,未命中自动拉取) + +``` +GET /hcs/master-data/template +返回 R>:直接序列化 +``` + +仅模板一个端点;币种/分类已在 `HcsMasterDataSupport` 内部缓存供推送映射,不暴露前端。 + +### 3.2 附件代理 + +``` +POST /hcs/attachment/upload (multipart:fdFile + bizType 可选) + → 服务端包装 fdLoginName=当前登录名、fdAnonymous=false,调 C103-2 + → 返回 R<{ fdId }>;同时写 HcsAttachmentSupport 的 Redis 映射缓存(attachmentKey → fdId), + 使后续 C103-1 推送链路命中缓存不再重传 + → 文件限制与 TMS OSS 现行一致:pdf/bmp/jpeg/png/jpg/doc/docx/ppt/pptx/xlsx/xls/eml/msg/zip,≤50M + +GET /hcs/attachment/download?fdId=... + → 调 C103-5,文件流透传(Content-Disposition attachment,文件名从附件元数据或 fdId 兜底) + → axios 需 responseType: 'blob' +``` + +### 3.3 客商风险查询(C101-1) + +``` +GET /hcs/risk?customerCode=... + → 调 IHcsClient.relativeRisk(C101-1) + → 返回 R:{ fdId, fdNumber, fdBlackStatus, fdRiskLevel } + → 结果写 blade_hcs_check_log(biz_type=risk,留痕与合同推送链路同表) +``` + +> 前端仅用于**展示与提示**,不作为安全边界:强制校验仍在合同推送链路(`risk.block-blacklist`)与付款提交事务内。 + +### 3.4 合同发起特批(C103-8)+ 结果推送接收(contractLegal/result)——本期正式接入 + +**业务闭环**(依据《TMS业财融合-集成流程图文字版与业务流程梳理.md》§3.3/§8): + +``` +合同审批通过 → C101-1 查甲乙方风险 + ├─ 非高风险 → 常规链路:C103-2 附件转传 → C103-1 推送(现状不变) + └─ 高风险 → 合同链路挂起(hcs_spec_status=need) + → TMS 内部特批审批(按原审批流) + → 前端补特批申请材料 + 内部审批流程附件 → C103-8 发起特批(hcs_spec_status=processing) + → 合同系统集团审批(流程在合同系统预配置) + → 合规推送特批结果 TMS:POST /hcs/contractLegal/result(hcs_spec_status=approved/rejected) + → approved → 恢复"审批通过"展示(C103-1 已起草,无需再推送)→ 完成签订;rejected → 前端提示,重新发起 +``` + +**数据模型**:`blade_contract_manage` 追加特批字段组(照付款申请 `hcs_spec_*` 惯例),SQL 落 `doc/sql/transport/`: + +```sql +hcs_spec_fd_id varchar(64) COMMENT '合规特批流程ID(C103-8 返回)', +hcs_spec_status varchar(16) DEFAULT 'none' COMMENT '特批状态 none/need/processing/approved/rejected', +hcs_spec_time datetime COMMENT '最近特批动作时间', +hcs_spec_msg varchar(512) COMMENT '特批结果/失败原因', +hcs_spec_files_json text COMMENT '特批申请材料+内部审批附件(含C103-2 fdId)' +``` + +**端点设计**: + +``` +POST /hcs/contract/spec-apply (body:{ contractId, attachments: [{fdId, name}] }) + 校验:合同审批通过 && hcs_spec_status ∈ {need, rejected} && 特批材料非空 + 组装报文调 C103-8(报文模式预估照 C106-3:fd_contract_id=hcs_fd_id、fd_contract_no、 + fd_doc_subject="合同特批-"+contractNo、特批附件 mechanisms.attachment 带 fdBindType=ADD) + → 返回 data.fdId → hcs_spec_fd_id,状态置 processing;失败置 need 并记 hcs_spec_msg + → C103-8 v4 未含报文,联调前按上述预估实现并加 TODO 标注(§7-U1) + +POST /hcs/contractLegal/result (合规系统推送特批结果,2026-10-12 替代未建设的 B103) + 免登免授权端点(blade.secure skip-url + auth permitAll 放行),iPaaS 网关注册映射 /gwzh/HTHG/contractLegal/result → 本端点 + body(HcsLegalResultDTO):{ sourceSystem="WLTMS", requestId=TMS合同ID, status=APPROVED|REJECTED, reason } + 处理:按 requestId 定位合同 → status 大小写不敏感识别 → 回写 hcs_spec_status + 原文(含 reason)留痕 blade_hcs_check_log + → approved 恢复 currentNode="审批通过"(C103-1 已起草,无需再推送);rejected 联动审批驳回,reason 随原文见 hcs_spec_msg + → 未定位到合同/状态未识别仅留痕返回,不报错 +``` + +**状态展示与操作权限**: + +| hcs_spec_status | 前端表现 | +|---|---| +| none | 无特批相关 UI | +| need(高风险待特批) | 合同详情警告条 +【发起特批】按钮可用;推送合规按钮禁用并提示"高风险客商,需先完成特批" | +| processing | "特批中"标签(橙);发起按钮禁用 | +| approved | "特批通过"标签(绿);推送恢复 | +| rejected | "特批驳回"标签(红)+ 驳回原因;可重新发起 | + +### 3.5 权限 + +`/hcs/**` 补 `@PreAuth`(菜单权限码,随菜单 SQL 一起落 `doc/sql/transport/`): + +| 权限码 | 控制点 | +|---|---| +| `hcs_sync_push` | 推送合规/重推按钮 | +| `hcs_sync_status` | 合规状态列/筛选(登录可见即可时不建) | +| `hcs_recon` | 合规对账弹窗 | +| `hcs_attachment` | 上传到合同系统 / 合规附件下载 | +| `hcs_master_data` | 主数据/映射查看页 | +| `hcs_spec_apply` | 发起特批按钮 | + +--- + +## 4. 前端设计(tms-erp-web) + +### 4.1 新增 API 模块 `src/api/business/hcs.js` + +集中一个文件,不拆散到各业务 api(对齐 `api/payment/paymentApplication.js` 的超时覆盖惯例): + +```js +import request from '@/axios'; + +const baseUrl = '/blade-transport/hcs'; + +// 合规推送涉及 HCS 远程多次调用(风险校验+附件转传+起草),可达 20s+,覆盖全局 10s 超时 +export const repush = (bizType, id) => + request({ url: `${baseUrl}/sync/${bizType}/${id}`, method: 'post', timeout: 60000 }); + +export const getTemplates = () => + request({ url: `${baseUrl}/master-data/template`, method: 'get' }); + +export const getRisk = customerCode => + request({ url: `${baseUrl}/risk`, method: 'get', params: { customerCode } }); + +export const uploadAttachment = formData => + request({ url: `${baseUrl}/attachment/upload`, method: 'post', data: formData, timeout: 60000 }); + +export const downloadAttachment = fdId => + request({ url: `${baseUrl}/attachment/download`, method: 'get', params: { fdId }, responseType: 'blob' }); + +export const reconProject = projectCode => + request({ url: `${baseUrl}/recon/project`, method: 'get', params: { projectCode } }); +export const reconContract = contractNo => + request({ url: `${baseUrl}/recon/contract`, method: 'get', params: { contractNo } }); +export const reconContractDetail = params => + request({ url: `${baseUrl}/recon/contract/detail`, method: 'get', params }); + +export const applyContractSpec = data => + request({ url: `${baseUrl}/contract/spec-apply`, method: 'post', data }); +``` + +### 4.2 结算币种(C103-12)——前端不改 + +前端"结算币种"下拉继续用本地字典 `currency_type`(`loadSettlementDictionaries()` 现状不动),存储值、编辑回显、下游运单/结算取值全部维持现状。C103-12 币种列表仅在后端推送 C103-1 时经 `currency-name-mapping`(`RMB→人民币`)映射,已实现,无前端工作。 + +### 4.3 合同模板(C103-6) + +- **表单"合同类别"保留本地字典 `contractCategory`**:它是 TMS 业务主数据,推送侧靠 Nacos `template-mapping` 转模板ID。C103-7 已移出前端范围(2026-10-09 确认)。 +- 新增**管理端"合规主数据"查看页**(`views/business/hcs-master-data.vue`,模板列表 + "刷新缓存"按钮,`POST /hcs/master-data/refresh` 已有)。菜单挂系统管理或合规管理目录下。 + +### 4.4 客商风险等级(C101-1) + +合同表单(新增/变更)选择甲方/乙方后: + +- 调 `getRisk(customerCode)`,在甲乙方字段旁展示风险标签:无风险/低/中/高 + 黑名单红标(`fdBlackStatus === 'yes'`)。 +- 高风险或黑名单时,表单顶部警告条:"该客商为高风险/黑名单客商,按集团规定不可签约,如需签约请发起特批",并提供【发起特批】按钮(走 §4.7)。 +- 项目立项表单不做强制校验(业务流程:风险校验在合同签订与付款环节),立项页不加。 +- 失败静默(HCS 不可达不阻塞填写,强校验在后端推送/付款链路)。 + +### 4.5 附件上传到合同系统(C103-2) + +- **表单附件流程不变**:仍传 TMS OSS,推送时后端自动转传(已实现,含 7 天 fdId 映射缓存)。 +- 新增**手动转传入口**:合同详情页附件区(`contract-attachment-section.vue`)加"上传到合同系统"操作(仅已审批通过的合同显示): + 1. 选中附件行 → `uploadAttachment(formData)`(前端从 OSS url 拉文件或直接复用原始 File); + 2. 返回 `fdId` 写入附件行 `hcsFdId` 字段(后端 `update-attachments` 已有,行结构加一字段即可); + 3. 已转传的行显示"已上传合规"标记。 +- 特批材料(C103-8 占位)也走此端点先取得 fdId。 + +### 4.6 合同附件下载(C103-5) + +- 对账弹窗(§4.7)中展示 HCS 侧附件列表(来自 C103-4 详情),点击下载调 `downloadAttachment(fdId)`,blob 落地文件。 +- `contract-attachment-section.vue` 的行内"下载"仍走 TMS OSS url,不改。 + +### 4.7 列表页合规状态 + 推送/对账(照"同步金蝶"模式) + +`project-apply.vue` 与 `contract-manage.vue` 各加: + +1. **状态列**:`合规状态`(prop `hcsSyncStatus`)——`unsynced 待推送`(灰)/`synced 已推送`(绿)/`failed 推送失败`(红);失败时 tooltip 显示 `hcsSyncMsg`;已推送的可附 `hcsFdId`。 + - 列配置加在 `src/option/business/project-apply.js`、`contract-manage.js`(或页面内 column 定义,与两页现状一致)。 + - 筛选区加"合规状态"下拉(unsynced/synced/failed,复用 settlement 模块 kingdeeSyncStatusOptions 的写法)。 +2. **操作按钮**:行操作"推送合规"(`repush(bizType, id)`,成功后刷新行;按钮文案照 customer-archive 的"推送中…"防重态);权限码 `hcs_sync_push`。 +3. **对账弹窗**:行操作/工具栏"合规对账"——立项按 `projectCode` 调 `reconProject`,合同按 `contractNo` 调 `reconContract`;弹窗内 JSON 树/描述列表展示 HCS 返回,附"查看合规详情"(C103-4,合同页专属)与 HCS 附件下载(§4.6)。权限码 `hcs_recon`。 + +### 4.8 合同发起特批(C103-8,本期接入) + +- **入口**:合同详情页【发起特批】按钮,仅 `hcsSpecStatus ∈ {need, rejected}` 时可用;权限码 `hcs_spec_apply`。 +- **交互**:弹窗选择/确认特批申请材料(先经 §4.5 `uploadAttachment` 取 fdId,弹窗内表格展示已传材料,含内部审批流程附件)→ 确认调 `applyContractSpec({ contractId, attachments })` → 成功后详情刷新为"特批中"。 +- **状态展示**:合同列表与详情按 §3.4 状态表展示特批标签(need/processing/approved/rejected);`need` 时列表"推送合规"按钮禁用并 tooltip 提示。 +- **结果推送**:合规推送(`/hcs/contractLegal/result`)由后端接收并自动联动状态,前端无需轮询;详情页打开时实时拉取最新状态即可,`rejected` 时展示 `hcsSpecMsg` 驳回原因。 + +--- + +## 5. 实施拆分 + +| 阶段 | tms-api | tms-erp-web | 依赖 | +|---|---|---|---| +| F1 基础 | `GET /hcs/master-data/template` + `@PreAuth`/菜单 SQL | `api/business/hcs.js`;管理端主数据页 | 无 | +| F2 状态与推送 | 无(端点已有) | 两列表页:状态列/筛选 + 推送合规按钮 | F1(hcs.js) | +| F3 对账与附件 | `/hcs/risk`、`/hcs/attachment/upload`、`/hcs/attachment/download` | 对账弹窗 + 合规附件下载 + 手动转传 + 附件行 `hcsFdId` | F2 | +| F4 特批 | 合同特批字段组 SQL;`POST /hcs/contract/spec-apply` + `POST /hcs/contractLegal/result`(合规结果推送接收,2026-10-12 替代 B103);合同推送链路加 need 挂起判断 | 发起特批弹窗 + 特批状态标签 + 按钮联动 | C103-8 报文(§7-U1,可先按预估模式实现联调替换);结果推送报文已定(requestId/status/reason) | +| F5 即时推送(2026-10-10 增补,纯后端) | `HcsImmediatePushSupport` + 4 处审批入口触发(§6.3)+ 开关配置 | 无 | 无(复用既有 Support,扫描兜底保留) | + +--- + +## 6. 审批通过即时推送(2026-10-10 增补,后端链路增强) + +### 6.1 目标与原则 + +| # | 原则 | +|---|---| +| I1 | **秒级触达**:项目立项(C102-1/2)、合同(C103-1/11)审批通过(含变更通过)事务提交后立即异步推送,不再等扫描周期(原最长 ≤5 分钟) | +| I2 | **ADR-0001 修订**:审批代码埋点由"零"放宽为"一行触发调用"(仅传 id,不含任何推送逻辑);推送逻辑仍全部收敛在 hcs support 包,扫描任务保留为**兜底**(失败重试、批量补偿、停机窗口漏单) | +| I3 | **复用既有链路**:执行体直接调 `repushProject(id)` / `repushContract(id)`(已含 approved 校验、状态重置、失败 markFailed、高风险挂起判断),不新写报文与状态机 | + +### 6.2 新增组件:HcsImmediatePushSupport(仅 1 个类) + +位置 `org.springblade.transport.support.hcs`,职责只有一个:**事务提交后异步触发一次单条推送**。 + +```java +public void pushProjectAfterCommit(Long id) // 项目 C102(MK 回调路径,异步) +public void pushContractAfterCommit(Long id) // 合同 C103(MK 回调路径,异步) +public void pushProjectAndWait(Long id) // 项目 C102(本地"通过"按钮,Controller 同步调用) +public void pushContractAndWait(Long id) // 合同 C103(本地"通过"按钮,Controller 同步调用) +``` + +执行模型: + +1. **afterCommit 触发**:审批方法在事务内调用本组件 → `TransactionSynchronizationManager.registerSynchronization` 注册 afterCommit 回调,审批事务回滚则不推送。MK 回调路径若未包事务(单条 updateById 自动提交)则直接提交异步,语义等价。 +2. **异步执行**:自建 2 线程 daemon 池(线程名 `hcs-immediate-push`,`@PreDestroy` 释放)。工程未启用 `@EnableAsync`,不为此新增全局配置;HCS 推送含风险校验+附件转传+起草可达 20s+,绝不能阻塞审批响应(例外:本地「通过」按钮走 `pushProjectAndWait/pushContractAndWait`,事务提交后同步执行,失败原因回传前端)。 +3. **执行体**(与 HcsPushTask 同线程模型,无租户上下文,与定时扫描行为一致): + - 开关:`thirdParty.hcs.push.immediate-enabled`(默认 false,灰度打开) + - 短路:查库 `approvalStatus ∉ {approved, change_approved}` 或 `hcs_sync_status = synced` → 跳过(已被扫描/人工重推处理过) + - 单条锁:Redis `setIfAbsent("blade:third:hcs:push:lock:{bizType}:{id}", TTL 120s)`,抢不到直接放弃(扫描兜底),持有值比对后释放(照 HcsPushTask 惯例) + - 推送:`repushProject(id)` / `repushContract(id)`;异常仅 `log.error` 吞掉(Support 内部已 markFailed,扫描按 `hcs_retry_count < max-retry` 自动补偿,超限人工重推,D7 不变) + +### 6.3 触发点(4 处,各加 1 行) + +| 入口 | 位置 | 触发调用 | +|---|---|---| +| 本地审批 | `ProjectApplyController.approve()`(审批事务返回后同步推送) | `pushProjectAndWait(id)`,失败原因经 `pollSyncFailure()` 以 `data=false` 回传前端 | +| 本地审批 | `ContractManageController.approve()`(审批事务返回后同步推送) | `pushContractAndWait(id)`,失败原因同上 | +| MK 回调 | `MkProcessBizHandlers.ProjectHandler.approveFromProcess()`(`updateById` 返回 true 后) | `pushProjectAfterCommit(id)` | +| MK 回调 | `MkProcessBizHandlers.ContractHandler.approveFromProcess()`(同上) | `pushContractAfterCommit(id)` | + +### 6.4 并发与失败语义 + +| 场景 | 语义 | +|---|---| +| 即时推送 vs 扫描任务并发 | 单条锁 + synced 短路 + HCS 侧判重(C102-3 / C103 判重、更新幂等)三重吸收;极端双推为更新语义,无脏数据 | +| 即时推送失败 | `markFailed` → 扫描下轮自动补偿 → 超限人工重推(与现状完全一致,零新增重试机制) | +| 高风险合同 | `pushContract` 既有 `hcs_spec_status ∈ {need, processing}` 挂起判断不变,即时推送同样被挂起,特批通过后恢复推送 | +| 开关关闭 / 组件异常 | 完全回落扫描链路,行为与现状一致 | + +### 6.5 配置 + +| 配置项 | 默认 | 说明 | +|---|---|---| +| `thirdParty.hcs.push.immediate-enabled` | false | 即时推送总开关,灰度打开;普通 `@Value` 重启生效即可(扫描兜底使风险可控,无需 @RefreshScope) | + +--- + +## 7. 未决事项 + +| # | 事项 | 影响 | 当前处理 | +|---|---|---|---| +| U1 | ~~C103-8 / B103 报文 v4 未提供~~ → **C103-8 报文仍待联调;结果回传已决(2026-10-12)**:B103 不建设,合规经 `contractLegal/result`/`paymentLegal/result` 推送结果(报文已定) | C103-8 发起报文字段 | 发起仍按 C106-3 模式预估(代码留 TODO);结果接收已按定稿报文实现(`HcsLegalResultDTO`) | +| U2 | **C103-5 返回流是否带文件名**(v4 仅写"文件流") | 下载文件命名 | 后端以附件元数据/`fdId` 兜底命名,联调校准 | +| U3 | 合规状态列/菜单权限是否需要按租户差异化 | 菜单 SQL | 先按统一菜单落地 | diff --git a/doc/docs/hcs合同/2026-10-10-C102-1与C103-1接口页面字段取值说明.md b/doc/docs/hcs合同/2026-10-10-C102-1与C103-1接口页面字段取值说明.md new file mode 100644 index 00000000..038b97f0 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-10-C102-1与C103-1接口页面字段取值说明.md @@ -0,0 +1,134 @@ +# C102-1 项目立项新建 / C103-1 合同新建 —— 页面字段取值说明 + +> 数据组装代码: +> - C102-1:`HcsProjectSyncSupport#addProject` / `buildFormData`(blade-service/blade-transport/.../support/hcs/HcsProjectSyncSupport.java) +> - C103-1:`HcsContractSyncSupport#draftContract` / `buildDraftParam`(blade-service/blade-transport/.../support/hcs/HcsContractSyncSupport.java) +> +> 触发链路:审批通过(approved)自动即时推送,扫描任务兜底;页面"推送合规/重推"按钮可手动触发。 +> +> 必填标记图例:✅必填(HCS 接口文档 required)|⚠️TMS必传(文档未强校验,TMS 业务必传)|选填|固定(代码写死)|不传 + +--- + +## 一、C102-1 项目立项新建 + +数据源:**项目立项单页面**(ProjectApply / blade_project_apply),整单审批通过后推送。 + +### 1. 表头固定项(非页面字段) + +| HCS 入参 | 取值 | 页面字段 | 必填 | 说明 | +| --- | --- | --- | --- | --- | +| bpmOperation | 流程操作节点 | — | ✅ | 节点整体必填 | +| bpmOperation.operationType | `drafter_submit` | — | ✅ | 固定,以起草人身份直接提交 | +| bpmOperation.templateCode | 配置 `thirdParty.hcs.project.template-code` | — | ✅ | 立项表单模板编码,未配置直接失败 | +| checkAuth | `false` | — | 选填 | 固定 | +| curUser.fdLoginName | 经办人登录名 | 经办人 handlerUserId | ✅ | ⚠️ 当前代码临时写死 `srvadmin`(requireLoginName 内原逻辑被注释,联调需恢复) | +| formData | 表单数据节点 | — | ✅ | 节点整体必填 | +| saveOption.saveDraft | `false` | — | ✅ | 固定,直接提交 | + +### 2. 表单业务字段(formData.dynamicProps) + +HCS 文档 required 仅列:`fd_pro_name`、`fd_is_available`、`fd_sources`;其余为选填。 + +| HCS 字段 | HCS 含义 | 页面字段(ProjectApply) | 必填 | 取值 / 转换规则 | +| --- | --- | --- | --- | --- | +| fd_pro_name | 项目名称 | 项目名称 projectName | ✅ | 原值 | +| fd_pro_simple_name | 项目简称 | 项目简称 projectShortName | 选填 | 原值 | +| fd_num | 项目编号 | 项目编号 projectCode | ⚠️TMS必传 | 原值;HCS 文档未列 required,但为 C102-3 判重键,TMS 侧必传 | +| fd_creator_unit | 创建单位 | —(无页面字段) | 选填 | 固定取配置 `creator-unit-id/name`;任一未配置时回退"开题部门名称 businessDeptName"仅传 fdName | +| fd_head | 项目负责人 | 负责人 principalUserId | 选填 | 按用户ID查登录名,传 `{fdLoginName: 登录名}` | +| fd_taker_dept | 承接部门 | 承接部门 undertakeDeptName | 选填 | 传部门名称 | +| fd_taker | 承接人 | 经办人 handlerUserId | 选填 | 登录名 `{fdLoginName}` | +| fd_pay_limit | 付款额度 | 付款额度(万元) fundLimit | 选填 | × 10000 换算为元 | +| fd_out_invoice_limit | 开票额度 | 开票额度(万元) receivableLimit | 选填 | × 10000 换算为元 | +| fd_pro_content | 项目建设内容 | 项目建设内容/情况说明 situationRemark | 选填 | 原值 | +| fd_settlement_type | 结算类型 | 结算方式 settlementMode | 选填 | 字典原值直传(字典映射待联调 §9-U3) | +| fd_pro_plan_deposit | 计划保证金 | 资金需求(万元) fundDemand | 选填 | × 10000 换算为元 | +| fd_is_available | 是否可用 | — | ✅ | 固定 `true` | +| fd_avail_units | 可用单位列表 | — | 选填 | 与创建单位一致(同走 creator-unit 配置) | +| fd_is_self_pro | 是否自营项目 | — | 选填 | 固定 `"1"`(联调确认) | +| fd_kingdee_sync_status | 金蝶同步状态 | — | 选填 | 固定 `"1"` | +| fd_sources | 来源标识 | — | ✅ | 配置 `src-system`(约定值 WLTMS) | +| fd_kingdee_id | 来源系统主键 | — | 选填 | TMS 立项单主键 id(对账/幂等候选键) | +| fd_creator | 创建人 | 经办人 handlerUserId | 选填 | 登录名(扫描推送无登录上下文,取单据承办人) | +| fd_pro_income_goal | 项目收入目标 | — | 不传 | TMS 无对应字段,一期不传 | + +--- + +## 二、C103-1 合同发起(合同新建) + +数据源:**合同新建页面**(ContractManage / blade_contract_manage),合同审批通过后推送。 +推送前置:① 客商风险校验(C101-1,甲乙双方)→ ② 附件转传(C103-2,仅缓存不挂载)→ ③ 判重(C103-3)→ ④ C103-1 发起。 + +### 1. 顶层字段 + +HCS 文档 required:`fdTemplateId`、`fdOuterSystem`、`fdLoginName`、`saveType`、`fdFieldJson` 全部必填。 + +| HCS 入参 | HCS 含义 | 页面字段(ContractManage) | 必填 | 取值 / 转换规则 | +| --- | --- | --- | --- | --- | +| fdTemplateId | 合同模板ID | 合同类别 contractCategory | ✅ | 经配置 `contract.template-mapping[合同类别]` 映射为 HCS 模板ID(§9-U7),未配置直接失败 | +| fdOuterSystem | 外部系统标识 | — | ✅ | 配置 `src-system`(WLTMS) | +| fdLoginName | 操作人登录名 | 经办人 handlerUserId | ✅ | 按用户ID查登录名(§9-U2),解析不到直接失败 | +| saveType | 保存类型 | — | ✅ | 固定 `submit` | +| fdFieldJson | 合同字段节点 | — | ✅ | 节点整体必填(内含 xformFields + signer) | + +### 2. xformFields 模板字段 + +HCS 文档未对 xformFields 单项列 required;下表"✅"为 TMS 按合同模板必填项标注(缺失会被 HCS 模板校验拦截)。 + +| HCS 字段 | HCS 含义 | 页面字段(ContractManage) | 必填 | 取值 / 转换规则 | +| --- | --- | --- | --- | --- | +| fdNumber / CONTRACT_NO | 合同编号 | 合同编号 contractNo | ✅ | 原值(双字段同值传) | +| fdSubject | 合同标题 | 合同名称 contractName | ✅ | 原值 | +| CONTRACT_NAME | 合同名称 | 合同名称 contractName | 选填 | 原值(与 fdSubject 同值) | +| fdAmount / AMOUNT | 合同金额 | 合同金额 contractAmount | ✅ | 原值,字符串传值(双字段同值传) | +| fdCurrencyId / CURRENCY | 币种 | 结算币种 settlementCurrency | 选填 | 经 `currency-name-mapping` 映射(默认 RMB→人民币),未命中传原值 | +| fdSettlementType | 合同结算类型 | 结算方式 settlementMode | 选填 | 经 `settlement-type-mapping` 映射(付款/收款,待联调 §9-U3),未配置不传 | +| fdSepcailContract | 特批流程 | — | 选填 | 固定 `false`(配置项预留,原文拼写笔误必须沿用) | +| CONTRACT_TYPE | 合同类型 | 合同类别 contractCategory | 选填 | 字典原值 | +| PARTY_A | 甲方名称 | 甲方 partyA | 选填 | 名称原值 | +| PARTY_B | 乙方名称 | 乙方 partyB | 选填 | 名称原值 | +| SIGN_DATE | 签订日期 | 签订日期 signDate | ✅ | `yyyy-MM-dd`,fdType=timestamp | +| fdEstimateStartTime | 预计开始时间 | 开始日期 startDate | 选填 | fdType=timestamp | +| fdEstimateEndTime | 预计结束时间 | 结束日期 endDate | 选填 | fdType=timestamp | +| CREATE_TIME / fdCreateTime | 创建时间 | —(系统字段 createTime) | 选填 | `yyyy-MM-dd HH:mm:ss`,fdType=timestamp | +| fd_syn_time | 同步时间 | — | 选填 | 推送时刻 | +| CREATE_USER_NAME | 承办人名称 | 经办人 handlerUserName | 选填 | 名称原值 | + +### 3. signer 相对方(签署方) + +HCS 文档未对 signer 单项列 required,均按选填处理;TMS 固定传甲、乙两方。 + +| HCS 字段 | 甲方 | 乙方 | 必填 | +| --- | --- | --- | --- | +| fdStanding | `甲` | `乙` | 选填(TMS 固定传) | +| fdName | 页面"甲方" partyA | 页面"乙方" partyB | 选填(TMS 必传,业务标识) | +| fdNumber | 按甲方名称反查客商档案 CustomerArchive.customerCode(全称/简称匹配) | 按乙方名称反查 | 选填(反查不到留空,§9-U6) | +| fdType | — | — | 不传(inner/外部判定规则待联调 §9-U6) | + +### 4. 不传项说明 + +| HCS 字段 | 原因 | +| --- | --- | +| CONTRACT_STATUS 合同状态 | TMS 无直接对应,一期不传(是否必传待联调 §9-U3) | +| 附件字段 | C103-1 入参未定义附件字段;附件已先经 C103-2 转传并缓存合规ID,挂载方式待合规方确认(§9-U8) | + +--- + +## 三、必填项汇总(推送前自检清单) + +**C102-1 必填**:templateCode(配置)、curUser.fdLoginName、fd_pro_name(页面:项目名称)、fd_is_available(固定)、fd_sources(配置);fd_num 建议按必传对待(判重键)。 + +**C103-1 必填**:fdTemplateId(页面:合同类别 → template-mapping 必须有映射)、fdOuterSystem(配置)、fdLoginName(页面:经办人,必须能解析登录名)、saveType(固定)、fdFieldJson;模板字段中合同编号(fdNumber)、合同标题(fdSubject)、合同金额(fdAmount)、签订日期(SIGN_DATE)按模板必填传。 + +## 四、两个接口取值差异速览 + +| 维度 | C102-1 项目立项 | C103-1 合同 | +| --- | --- | --- | +| 页面数据源 | 项目立项单 ProjectApply | 合同单 ContractManage | +| 金额单位换算 | 万元 × 10000 转元(付款/开票额度、资金需求) | 原值字符串直传(合同金额) | +| 字典/映射 | 结算方式原值直传 | 合同类别→模板ID、币种→名称映射、结算方式→结算类型映射 | +| 人员取值 | 负责人/经办人 → 登录名(fdLoginName 结构) | 经办人 → 登录名(顶层 fdLoginName)+ 经办人名称原值 | +| 相对方 | 无 | 甲乙方名称 + 反查客商编号 | +| 操作人 curUser | ⚠️ 临时写死 srvadmin | 承办人真实登录名(解析不到直接失败) | +| 必填校验 | required 集中在表头 + 3 个业务字段 | 顶层 5 项全部必填,模板字段按模板校验 | diff --git a/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md b/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md new file mode 100644 index 00000000..a11b92af --- /dev/null +++ b/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md @@ -0,0 +1,120 @@ +# HCS 接口 500 错误信息还原设计(Feign 降级工厂) + +> 日期:2026-10-10 +> 涉及模块:`blade-third-party-api/blade-hcs-api`、`blade-service/blade-transport` +> 背景:HCS(合同合规系统)接口返回 HTTP 500 时,TMS 侧只见"无响应/响应为空",远程真实失败原因丢失,`hcs_sync_msg` 无法落库有效信息。 + +--- + +## 一、问题现象 + +真实日志(C102-1 项目立项新建推送,projectCode=LX-2026-000064): + +```text +CustomFeignLogger : <==== [IHcsClient#projectAdd(HcsProjectAddDTO)] .../km-project1/V1/projectApproval/add | Status: 500 | Body: {"success":false,"code":"errors.noRecord","msg":"地址本数据标识{\"fdName\":\"桂物物流集团\"}在系统中查找不到对应的组织机构数据!","traceId":"1k4ikrub...","errStacks":[...]} +BladeFeignFallback : BladeFeignFallback:[org.springblade.hcs.feign.IHcsClient.projectAdd] serviceId:[HCS] message:[[500 Internal Server Error] during [POST] ...] +HcsProjectSyncSupport : HCS项目立项新建失败响应:无响应,projectCode=LX-2026-000064 +``` + +现象:HCS 明确返回了业务错误(地址本组织机构不存在),但业务代码只拿到"无响应",`hcs_sync_msg` 只能落"响应为空",排查必须翻 Feign 日志。 + +## 二、根因分析 + +结合报文与 `blade-core-cloud`(4.10.0.BASE-SNAPSHOT)反编译,失败链路如下: + +``` +HCS 返回 HTTP 500(body 含真实业务错误) + → Feign 对非 2xx 抛 FeignException(body 锁在异常 contentUTF8() 中) + → BladeSentinelInvocationHandler 捕获,进入降级 + → @FeignClient 未声明 fallback/fallbackFactory + → 落到全局 org.springblade.core.cloud.feign.BladeFallbackFactory + → BladeFeignFallback 代理对非 R 返回值一律返回 null + → 调用侧 HcsProjectSyncSupport#addProject 拿到 result == null + → 打印"无响应"、抛 HcsException("...响应为空") +``` + +关键依据(javap 反编译 `BladeFeignSentinel$Builder$1` 字节码): + +1. `@FeignClient.fallback()` 有值 → 从 Feign 子上下文取该 bean,构造 `BladeSentinelInvocationHandler`; +2. 否则 `@FeignClient.fallbackFactory()` 有值 → 同样取自定义工厂(**本方案接入点**); +3. 两者皆无 → `new BladeFallbackFactory(target)` 全局兜底(即吞异常返回 null 的根因)。 + +即:**声明专属 fallbackFactory 后,全局兜底不再介入**。 + +## 三、备选方案否决记录 + +### 3.1 在调用侧捕获 HcsException —— 不可行 + +`HcsException` 是 `HcsProjectSyncSupport` 在拿到 null **之后**抛的,此时远程消息早已丢失。真正的 `FeignException` 在到达调用方之前就被 `BladeSentinelInvocationHandler → BladeFeignFallback` 吞掉并转成 null,任何 try/catch 都拦不到它——没有可捕获的异常内容。 + +### 3.2 照搬 BFI 的 commonCall + BfiExceptionTranslator 方式 —— 不可行 + +BFI 同类报错能正常拿到原因,但其成立前提与 HCS 不同: + +| 对比项 | BFI(可行) | HCS(不可行) | +| --- | --- | --- | +| 业务失败时 HTTP 状态 | **200** | **500** | +| Feign 行为 | body 正常反序列化返回 | 抛 FeignException,body 进异常对象 | +| 错误体到达 service 层? | 到达 → `BfiServiceImpl#commonCall` 手动解析 `message` 后抛 `BfiException` | **到不了**,service 层只拿到 null | + +而 `BfiExceptionTranslator` 实际是 `@RestControllerAdvice`,职责是把**我们自己 Controller 往外抛的** `BfiException` 转成 `R.fail` 给前端——它在出站 Feign 链路之外,`@ExceptionHandler` 无从触发。HCS 失败时 service 层连异常都收不到,service 层没有可写解析逻辑的位置,Rest Controller Advice 也拦不到 Feign 出站降级。 + +结论:两者错误发生在不同环节。HCS 错误体唯一能拦住的位置是 **Feign 降级层**。 + +### 3.3 关闭熔断兜底(feign.circuitbreaker.enabled=false)—— 不采纳 + +异常虽可直接抛到调用侧,但该配置全局生效会波及服务内所有 Feign 客户端,且 32 个 HCS 调用点都要补 try/catch 解析,改动面大。 + +## 四、采用方案:HcsClientFallbackFactory + +### 4.1 改动清单(共 3 处) + +| # | 文件 | 改动 | +| --- | --- | --- | +| 1 | `blade-hcs-api/.../hcs/feign/HcsClientFallbackFactory.java` | 新增降级工厂(约 120 行) | +| 2 | `blade-hcs-api/.../hcs/feign/IHcsClient.java` | `@FeignClient` 增加 `fallbackFactory = HcsClientFallbackFactory.class` | +| 3 | `blade-hcs-api/.../hcs/config/HcsFeignClientConfig.java` | 注册 `HcsClientFallbackFactory` 为 Feign 子上下文 bean(`BladeFeignSentinel` 经 `FeignClientFactory.getInstance` 取用) | + +### 4.2 降级工厂处理规则 + +`create(cause)` 返回 JDK 动态代理(24 个接口方法无需逐一实现): + +| 场景 | 处理 | +| --- | --- | +| 方法返回类型是 `HcsResult`(含双层嵌套 `HcsResult>` 外层) | 构造失败 `HcsResult`:`cause` 为 `FeignException` 时,`contentUTF8()` 取错误体——HCS 错误体与 `HcsResult` 同构(success/code/msg/traceId),`JsonUtil.parse` 直接还原远程 `msg`/`traceId`;非 JSON body 降级为异常摘要;`status` 记入 `code`(errors.http.500 样式) | +| 非 `FeignException`(熔断打开、连接超时等) | `msg` 取异常摘要,空则给 `HCS 接口调用异常(异常类名)`,`code=errors.feignFallback` | +| 非 `HcsResult` 返回类型(附件下载 `Response` 等) | 返回 null,与原全局兜底行为一致 | +| Object 基础方法(toString/hashCode/equals) | 直接反射执行 | + +### 4.3 还原后的完整链路 + +``` +HCS 500 → FeignException(错误体) + → BladeSentinelInvocationHandler → HcsClientFallbackFactory.create(cause) + → 返回 HcsResult{success=false, msg=远程msg, traceId=远程traceId} + → 各 support 现有判断 result.isSuccess()==false + → 日志:HCS项目立项新建失败响应:msg=地址本数据标识{...}在系统中查找不到对应的组织机构数据!, traceId=1k4ikrub... + → 抛 HcsException(远程msg) + → markFailed → retryCount+1 → hcs_sync_msg = 远程错误原因 + → 手动重推场景:repushProject 转 ServiceException → BladeX 全局 RestExceptionTranslator → 前端收到远程原因 +``` + +## 五、影响范围 + +- 调用点:`IHcsClient` 共 32 处调用,全部位于 `blade-transport` 的 10 个 `support/hcs` 类中,**零改动**——各处已有的 `result == null || !result.isSuccess()` 防御判断继续有效(null 分支保留为极端兜底)。 +- 成功路径:降级工厂仅在异常时被调用,正常 2xx 响应不经过它,零影响。 +- 全局兜底:仅 HCS 客户端声明了专属 fallbackFactory,其余 Feign 客户端(BFI 等)行为不变。 +- 重试策略:不变,仍由扫描任务按 `hcs_retry_count` 补偿(D7 约定,Feign 层无 Retryer)。 + +## 六、验证与回归建议 + +- 编译验证:`mvn clean compile -pl blade-third-party-api/blade-hcs-api,blade-service/blade-transport -am -DskipTests` 通过。 +- 建议回归场景: + 1. **本案例复现**:HCS 地址本不含"桂物物流集团"时推送项目立项,确认日志与 `hcs_sync_msg` 出现远程 msg 及 traceId; + 2. 修正 `thirdParty.hcs.creator-unit-name` 为 HCS 地址本准确名称后手动重推,确认成功链路不受影响; + 3. HCS 停机/连接超时场景,确认失败原因为可读的连接类摘要(而非"无响应"); + 4. 其余 HCS 接口(合同 C103、开票登记 C111 等)抽查推送失败场景,确认 msg 还原一致生效。 + +## 七、遗留事项(与本设计无关,另案处理) + +- 本案例的业务根因:`fd_creator_unit`/`fd_avail_units` 传值"桂物物流集团"在 HCS 地址本不存在,需修正 `thirdParty.hcs.creator-unit-name` 配置或由合规方在地址本新建该组织后重推。 diff --git a/doc/docs/hcs合同/2026-10-11-hcs合同推送流程梳理.md b/doc/docs/hcs合同/2026-10-11-hcs合同推送流程梳理.md new file mode 100644 index 00000000..9eb25194 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-11-hcs合同推送流程梳理.md @@ -0,0 +1,299 @@ +# 新建合同推送 HCS(合同合规系统)流程梳理 + +> 依据代码现状整理,核心类:`HcsContractSyncSupport`(blade-service/blade-transport/.../support/hcs)。 +> 对接接口:C101-1 风险校验、C103-2 附件上传、C103-3 合同判重、C103-1 合同发起、C103-11 合同更新、C103-8 特批、C103-9 履约计划、C108-1/C108-2 履约记录;特批结果接收 `/hcs/contractLegal/result`(合规推送,替代未建设的 B103,见 §9.4)。 +> "必填" 标注来源:代码注释中标注的 v4 接口文档要求(HCS 侧),以及 TMS 推送前强校验(缺失直接失败)。 + +--- + +## 一、触发入口(共 3 个) + +| 入口 | 触发时机 | 代码位置 | 说明 | +| --- | --- | --- | --- | +| 审批通过即时推送 | 合同审批通过(含变更通过)事务提交后 | `ContractManageServiceImpl#approve` → 置 `hcsSyncStatus=unsynced` → `HcsImmediatePushSupport#pushContractAfterCommit` | 受开关 `thirdParty.hcs.push.immediate-enabled` 控制(默认关闭,走扫描兜底);afterCommit 异步执行(2 线程 daemon 池),Redis 单条锁防与扫描任务并发双推 | +| 定时扫描兜底 | 扫描周期到 | `HcsPushTask#pushUnsyncedContracts` | 扫描条件:`approvalStatus in (approved, change_approved)` 且 `hcsSyncStatus in (unsynced, failed)` 且 `hcsRetryCount < maxRetry`,按 `batchSize` 限量 | +| 页面手动重推 | 用户点"推送合规/重推"按钮 | `HcsSyncController` → `HcsContractSyncSupport#repushContract(id)` | 前置校验:仅审批通过可推、特批挂起中不可推;重置为待同步并清零重试计数后立即推送 | + +三条入口最终都收敛到 `HcsContractSyncSupport#pushContract(ContractManage)`。 + +--- + +## 二、推送主流程(pushContract 内部步骤) + +``` +⓪ 特批挂起门禁 + hcsSpecStatus ∈ {need, processing} → 跳过推送,等待 C103-8 特批结果回传 +① 客商风险校验(C101-1,开关 require-risk-check) + 甲方、乙方各查一次;按名称反查 CustomerArchive 客商编码,反查不到记 skip 留痕不阻断 + 仅留痕 hcs_risk_* 并返回高风险/黑名单标志(风险等级=2 或任一方黑名单),不再阻断推送 + (2026-10-11 变更:原"黑名单挂起终止推送"逻辑取消,改为起草成功后自动转特批,见 §九) +② 附件转传(C103-2,开关 attachment.transfer-enabled) + 合同主文件(approved=true)+ 其它附件上传合规,取得合规附件ID列表 +③ 判重(C103-3,仅首推 hcsFdId 为空时) + conditions = {"fdNumber": 合同编号};命中 → 回填已有合规合同ID,置 synced,结束 +④ 合同发起/更新 + 首推且未判重命中 → C103-1 发起(返回 fdContractId,必须持久化) + 已有 hcsFdId(变更单)→ C103-11 更新(顶层 fdId 定位) +⑤ 履约计划同步(C103-9,开关 exe-stage.sync-enabled,一期默认关闭,详见 §十) + 仅依赖 hcs_fd_id,先于特批挂起执行(2026-10-14 调整); + 失败抛出走下轮凭 hcs_fd_id 更新补偿,不被特批门禁阻塞 +⑥ 自动特批挂起(高风险/黑名单 且 hcsSpecStatus ≠ approved 时) + 报文随 fdSepcailContract=true 在合规侧发起特批流程(2026-10-13 起,无独立 C103-8 发起接口) + → 置 hcsSpecStatus=processing + currentNode="特批审核"(TMS 流程挂起展示, + 等待合规推送 contractLegal/result 或 C103-4 定时查询联动;发起异常仅留痕) +``` + +结果回填: + +- **成功** `markSynced`:`hcs_fd_id` 回填、`hcs_sync_status=synced`、重试清零、记录 `hcs_sync_time/hcs_sync_msg` +- **失败** `markFailed`:`hcs_sync_status=failed`、重试次数 +1、记录失败原因,交扫描任务下轮补偿 + +--- + +## 三、C103-1 / C103-11 报文顶层字段(`HcsContractDraftDTO`) + +| HCS 字段 | 说明 | TMS 取值 | HCS 必填 | +| --- | --- | --- | --- | +| `fdId` | 合同ID | `contractManage.hcsFdId`(C103-1 返回值) | **必填**(C103-11 更新时);C103-1 新建不传 | +| `fdTemplateId` | 合同模板ID | `template-mapping[contractCategory]` 配置映射 | **必填**;TMS 强校验:未配置映射直接抛异常失败(§9-U7) | +| `fdOuterSystem` | 外部系统标识 | 配置 `thirdParty.hcs.src-system`(约定值 WLTMS) | **必填** | +| `fdLoginName` | 操作人登录名 | 合同承办人:`handlerUserId` → `UserCache.getUser().getAccount()` | **必填**;TMS 强校验:承办人未维护或登录名解析不到直接失败 | +| `saveType` | 保存类型 | 固定 `submit`(提交) | **必填** | +| `fdFieldJson` | 合同字段 | xformFields + signer + attachment(见下) | **必填** | + +--- + +## 四、fdFieldJson.xformFields 字段映射(`buildXformFields`) + +值一律字符串传值;时间字段格式 `yyyy-MM-dd` 且带 `fdType=timestamp`。 + +| HCS 字段(name) | 中文名 | TMS 来源字段 | HCS 必填 | 备注 | +| --- | --- | --- | --- | --- | +| `fdId` | 合同ID | `hcsFdId` | **必填**(更新时) | 新建不传 | +| `fdNumber` | 合同编号 | `contractNo` | **必填** | fd 模板字段;同时用作 C103-3 判重条件 | +| `CONTRACT_NO` | 合同编号 | `contractNo` | — | CONTRACT 模板字段(与 fdNumber 同值双传) | +| `fdSubject` | 合同标题 | `contractName` | **必填** | fd 模板字段 | +| `CONTRACT_NAME` | 合同名称 | `contractName` | — | CONTRACT 模板字段 | +| `fdAmount` | 合同金额 | `contractAmount` | **必填** | 字符串传值(`toPlainString`) | +| `AMOUNT` | 合同金额 | `contractAmount` | — | CONTRACT 模板字段 | +| `fdCurrencyId` | 币种 | `settlementCurrency` 经 `currency-name-mapping` | — | 默认 RMB→人民币;映射未命中传原值并告警 | +| `CURRENCY` | 币种 | 同上 | — | CONTRACT 模板字段 | +| `fdSettlementType` | 合同结算类型 | `settlement-type-mapping[settlementMode]` | **必填** | 枚举:payment=付款 / payment_collection=收支双向 / collection=收款 / none=不结算;未配置映射则不传并告警(§9-U3 待联调) | +| `fdSepcailContract` | 特批流程 | 固定 `false` | — | 文档原文拼写笔误必须沿用(D15);开关 `contract.special-contract-enabled` 预留 | +| `CONTRACT_TYPE` | 合同类型 | `contractCategory` | — | | +| `PARTY_A` | 甲方名称 | `partyA` | — | TMS 存名称字符串 | +| `PARTY_B` | 乙方名称 | `partyB` | — | | +| `SIGN_DATE` | 签订日期 | `signDate` | **必填** | yyyy-MM-dd,fdType=timestamp | +| `fdEstimateStartTime` | 预计开始时间 | `startDate` | 期限为指定日期时必填 | fdType=timestamp | +| `fdEstimateEndTime` | 预计结束时间 | `endDate` | 期限为指定日期时必填 | fdType=timestamp | +| `fdStartTerm` | 合同开始期限 | 固定 `specifiedDate`(指定日期) | **必填** | 日期取 `startDate`;startDate 为空则传空 | +| `fdEndTerm` | 合同结束期限 | 固定 `specifiedDate`(指定日期) | **必填** | 日期取 `endDate`;endDate 为空则传空 | +| `CREATE_TIME` | 创建时间 | `createTime` | **必填** | yyyy-MM-dd,fdType=timestamp;CONTRACT 模板字段 | +| `fdCreateTime` | 创建时间 | `createTime` | **必填** | fd 模板字段(与 CREATE_TIME 同值双传) | +| `fd_syn_time` | 同步时间 | 推送时刻(`LocalDateTime.now()`) | **必填** | yyyy-MM-dd,fdType=timestamp | +| `CREATE_USER_NAME` | 承办人名称 | `handlerUserName` | **必填** | | +| `CONTRACT_STATUS` | 合同状态 | `status-mapping[approvalStatus]` | **必填**(v4) | draft/approving/approved 等;未配置映射则不传并告警(§9-U3) | +| `fdServings` | 合同份数 | `copyCount` | **必填** | 字符串传值 | +| `fdDesc` | 合同说明 | `remark` | 选填 | | + +**v4 选填字段暂不传(待联调)**:`fdCoTakers` 协办人、`fdUnderTaker`/`fdUnderTakerDept` 承办人及部门(组织架构对象传值待确认)、`fdRelations` 关联合同、`fdProject` 关联项目(可经立项 hcs_fd_id 关联,对象传值待确认)、`fdContractGwNature` 结算模式、`fdContractAttribute` 合同属性、`fdUrgencyLevel` 紧急程度。 + +--- + +## 五、fdFieldJson.signer 签署方(相对方) + +固定传两条:甲方(`fdStanding=甲`)、乙方(`fdStanding=乙`)。 + +| HCS 字段 | 说明 | TMS 取值 | HCS 必填 | 备注 | +| --- | --- | --- | --- | --- | +| `fdStanding` | 签署地位 | 固定 甲 / 乙 | — | | +| `fdName` | 相对方名称 | `partyA` / `partyB` | — | | +| `fdNumber` | 相对方编号 | 按名称反查 `CustomerArchive.customerCode`(全称或简称精确匹配) | — | 反查不到留空不阻断(§9-U6) | +| `fdType` | 单位类型(inner=内部单位) | 暂不传 | — | inner/外部判定规则待联调(§9-U6) | + +--- + +## 六、fdFieldJson.attachment 附件(C103-2 转传后挂载) + +| HCS 字段 | 说明 | TMS 取值 | HCS 必填 | 备注 | +| --- | --- | --- | --- | --- | +| 列表本体 | 附件数据 | C103-2 转传返回的合规附件ID列表 | **必填**(C103-1 时,无附件传空列表) | C103-11 更新传 null 不重复挂载 | +| `fdId` | 附件ID | C103-2 返回 | **必填** | | +| `fdBindType` | 操作类型 | 固定 `ADD`(新增) | **必填** | | + +附件来源:合同主文件 `contractFileJson`(仅 approved=true 的)+ 其它附件 `attachmentsJson`;操作人登录名同 `fdLoginName`;受开关 `attachment.transfer-enabled` 控制。 + +--- + +## 七、依赖配置项(`thirdParty.hcs.*`) + +| 配置 | 用途 | 缺失影响 | +| --- | --- | --- | +| `src-system` | 顶层 fdOuterSystem | 报文缺系统标识 | +| `push.immediate-enabled` | 审批通过即时推送开关(默认 false) | 关闭时仅走扫描兜底 | +| `push.max-retry` / `push.batch-size` | 扫描重试上限 / 批量大小 | — | +| `contract.template-mapping` | contractCategory → HCS 模板ID | **缺失直接推送失败** | +| `contract.settlement-type-mapping` | settlementMode → 结算类型枚举 | 未配置不传 fdSettlementType 并告警 | +| `contract.status-mapping` | approvalStatus → 合同状态 | 未配置不传 CONTRACT_STATUS 并告警 | +| `contract.currency-name-mapping` | 币种名称映射(默认 RMB→人民币) | 未命中传原值 | +| `contract.require-risk-check` | 推送前客商风险校验开关 | — | +| `risk.block-blacklist` | 黑名单阻断付款提交(付款链路);合同链路不再阻断推送,高风险/黑名单转自动特批(§九) | — | +| `attachment.transfer-enabled` | 附件转传开关 | 关闭时不挂附件 | +| `exe-stage.sync-enabled` | C103-9 履约计划同步开关(一期默认关) | — | +| `exe-stage.fund-type` | C103-9 行金额类型缺省值(默认 byAmount) | — | +| `operator-login-name` | C103-9 报文操作人登录名(默认 srvadmin,非承办人) | — | +| `perf-record.sync-enabled` | C108-1/2 履约记录同步开关(一期默认关) | — | +| `perf-record.xform-id` | 履约记录表单模板ID(待合规方提供) | 未配置不传 fd_xform_id | + +--- + +## 八、TMS 侧留痕字段(`blade_contract_manage`) + +| 字段 | 含义 | +| --- | --- | +| `hcs_fd_id` | 合规合同ID(C103-1 返回的 fdContractId,判重命中也回填) | +| `hcs_exeplan_id` | 履约计划ID(C103-9 返回的 fdExeplanId,整体 update 定位用) | +| `hcs_sync_status` | unsynced / synced / failed | +| `hcs_retry_count` | 推送重试次数 | +| `hcs_sync_time` / `hcs_sync_msg` | 最近推送时间 / 结果描述 | +| `hcs_risk_level` / `hcs_black_status` / `hcs_risk_time` | 风险校验结果(甲乙双方取先查到的非空风险等级;任一方黑名单即为是) | +| `hcs_spec_status` / `hcs_spec_time` / `hcs_spec_msg` | 特批状态(none/need/processing/approved/rejected)及留痕 | +| `hcs_spec_fd_id` | 特批流程ID(C103-8 返回) | +| `hcs_spec_files_json` | 特批申请材料JSON(含 C103-2 合规附件ID) | +| `current_node` | 挂起展示节点:自动特批后置"特批审核",合规推送特批通过恢复"审批通过",驳回置"已驳回" | + +风险校验明细另写 `blade_hcs_check_log`(甲方/乙方各一条,含 traceId)。 + +--- + +## 九、外部客商高风险/黑名单管控(2026-10-11 增补) + +**管控口径**:内部组织类型客商(`CustomerArchive.customerKind=internal`)默认无风险,不调 C101-1;外部客商(external,含档案不存在兜底按外部处理)命中 **高风险(fdRiskLevel=2)或黑名单(fdBlackStatus=yes)** 时进入管控。甲/乙任一方命中即整单管控。 + +### 9.1 签约前风险查询(展示用) + +`GET /hcs/risk?customerCode=` → `HcsCreditCheckSupport#queryCustomerRisk` → `CustomerRiskVO`: + +| 字段 | 说明 | +| --- | --- | +| `customerKind` | internal / external | +| `fdBlackStatus` / `fdRiskLevel` | C101-1 原值透传 | +| `risky` | 高风险/黑名单判定结果 | +| `riskTip` | 风险提示文案(前端展示在基本信息备注后) | + +提示文案固定模板:`【公司名称】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷`(C101-1 未返回风险项明细,联调后如有明细字段再替换)。结果留痕 `blade_hcs_check_log`。 + +### 9.2 提交校验(特批说明附件必传) + +`ContractManageServiceImpl` 三个提交入口(`submit` / `toTemporary` / `submitFormal`)统一调 `validateSpecExplanationOnSubmit`: + +- 甲/乙方按名称反查客商编号(反查不到不阻断,与推送链路 skip 口径一致)→ 调 C101-1 实时查风险; +- 任一方高风险/黑名单 → `attachmentsJson` 必须存在 `attachmentType=spec_explanation` 的附件,否则抛"合同相对方存在高风险/黑名单客商,必须上传「特批说明」类型附件后再提交审批"。 +- **前端配合**:特批说明附件上传时元数据须带 `attachmentType: "spec_explanation"` 字段。 + +### 9.3 审批通过后自动特批链路 + +``` +TMS 审批通过(approved / change_approved) + → 即时推送 / 扫描兜底 → pushContract + → ① 风险校验留痕(risky 标志,不阻断) + → ④ C103-1 起草成功(hcs_sync_status=synced,回填 hcs_fd_id) + → ⑤ C103-9 履约计划同步(先于特批挂起;失败单据落 failed,修复后重推不被特批门禁阻塞) + → ⑥ 置 hcs_spec_status=processing + currentNode="特批审核"(TMS 流程挂起,合规侧随报文 fdSepcailContract=true 走特批) +``` + +- 原设计"黑名单挂起不推送、人工发起特批"存在死锁(挂起时无 `hcs_fd_id`,C103-8 报文必填合规合同ID 永远失败),本次变更为 **先 C103-1 起草、后 C103-8 特批**,特批通过即合规侧放行签约,无需恢复推送; +- 特批已通过(approved,单次有效)的合同推送/变更不重复发起特批; +- C103-8 发起失败保持 need 仅留痕,人工经 `/hcs/contract/spec-apply`(详情页"发起特批"按钮)重发。 + +### 9.4 特批结果联动(合规推送 contractLegal/result / 人工确认等效) + +**结果接收机制(2026-10-12 变更)**:原计划的 B103 推送合规方未建设,改为合规系统审批完成后经 iPaaS 网关调用 TMS 接收回传结果: + +| 接口 | 定位方式 | 处理 | +| --- | --- | --- | +| `POST /hcs/contractLegal/result`(公开端点,网关注册映射 `/gwzh/HTHG/contractLegal/result`) | `requestId` = TMS 合同ID | 合同特批结果联动 | +| `POST /hcs/paymentLegal/result`(公开端点,网关注册映射 `/gwzh/HTHG/paymentLegal/result`) | `requestId` = TMS 付款申请ID | 付款特批回写 `hcs_spec_status`(不联动付款审批状态) | + +报文两接口共用(`HcsLegalResultDTO`):`{sourceSystem, requestId, status, reason}`;先校验 `sourceSystem` 与 `third-party.hcs.src-system` 配置一致(不符抛出"来源系统标识错误"异常,不落库不留痕);`status=APPROVED`(通过)/ `REJECTED`(驳回,reason 为驳回原因如"特批说明材料不完整,请补充"),大小写不敏感识别,其他值仅留痕不落状态。报文原文(含 reason)全量留痕 `blade_hcs_check_log`;未定位到单据或状态未识别时仅留痕返回,不报错。 + +| 特批结果 | 合同联动 | +| --- | --- | +| approved | `currentNode` 恢复"审批通过"(C103-1 已起草,hcs_sync_status 保持 synced,无需再推送) | +| rejected | `approvalStatus=rejected` + `currentNode="已驳回"`,走重新编辑 → 提交 → 审批 → 重新特批闭环(重新推送时 C103-11 更新后再次自动特批),reason 随原文见 `hcs_spec_msg` | + +> 等效兜底:页面人工确认(`POST /hcs/contract/spec/confirm`,合同)与(`POST /hcs/spec-pay/confirm`,付款)与合规推送等效,二者均以先到为准。原推测性接收端点 `POST /hcs/callback/spec-result`(B103 候选键解析)已删除。 + +--- + +## 十、履约计划(C103-9)与履约记录(C108-1/C108-2)同步 + +> 业务定位:涉及收付款的合同须在合规侧建立履约计划,付款履约记录凭计划阶段ID(`hcs_stage_fd_id`)关联阶段。 +> 一期 `exe-stage.sync-enabled=false`、`perf-record.sync-enabled=false` 暂不上报 HCS(2026-10-09 范围调整),但 `blade_contract_exe_stage` 表、维护端点与审批标记逻辑均在运行。 + +### 10.1 数据模型与维护入口 + +`blade_contract_exe_stage` 是 TMS 侧的**合同履约计划行项目表**:合同的收付款资金计划逐行存储(如"预付款 30 万 / 进度款 50 万 / 质保金 20 万"各一行),是 C103-9 推送 HCS 的数据源。单独建表而非挂合同主表 JSON 的原因: + +- **行级同步**:C103-9 是行级 add/update/delete 操作,每行需独立 `row_op` 标记、独立回填合规阶段ID、独立同步状态 +- **删除不可逆**:已同步行必须软删后凭 `hcs_stage_fd_id` 上报 delete,直接物理删会在合规侧留孤儿数据 +- **重试粒度**:单行失败(如 add 未匹配到返回阶段ID)可单独保持 failed 下轮重试,不拖累整单 +- **一期攒数据**:开关关闭不上报,但表与维护端点照常运行,打开后可凭已有数据补推 + +行字段(`HcsContractExeStage`): + +| 字段 | 说明 | +| --- | --- | +| `perform_type` | 履约类型 payment=付款 / collection=收款 | +| `fund_nature` | 款项性质 prepayment=预付 / progress=进度 / acceptance=验收 / retention=质保 | +| `fund_amount` / `fund_date` | 计划金额(元)/ 计划资金日期 | +| `invoice_rule` / `invoice_cycle` | 开票规则 payFirst=先款后票 / invoiceFirst=先票后款;账期(天) | +| `fund_type` / `is_finish` | 金额类型(空取配置 `exe-stage.fund-type`)/ 是否完成 | +| `row_op` | 行级同步标记 none / add / update / delete | +| `hcs_stage_fd_id` | 合规阶段ID(C103-9 返回,行 update/delete 全靠它,必须持久化) | +| `hcs_sync_status` / `hcs_sync_msg` | 行级同步状态与结果 | + +> `fund_type`(金额类型)是 HCS 报文要求的**金额口径声明**(byAmount=按固定金额),一期三级兜底恒为 byAmount(建表默认 → 行值为空取 `exe-stage.fund-type` → 推送组装时再兜底);其他口径(如按比例)值域 HCS 文档未给出,待联调后放开,代码不做枚举校验。 + +维护端点 `ContractExeStageController`(列表 / 批量提交 / 删除,`ContractExeStageServiceImpl`): + +- 校验:`performType` 必填且仅 payment/collection,计划金额非负 +- 新行置 `row_op=add` 待随合同链路首推;已同步行(有 `hcs_stage_fd_id`)编辑置 update、未同步行保持 add +- 删除:已同步行软删 + `row_op=delete`(须凭合规阶段ID 上报删除,不可物理删除);未同步行直接物理删除 + +### 10.2 同步时机 + +- 合同审批通过(`ContractManageServiceImpl#approve`):未同步行(`hcs_stage_fd_id is null`)统一置 `row_op=add` +- 执行位置:`pushContract` 第⑤步(先于特批挂起,2026-10-14 调整);前置强约束:合同须已有 `hcs_fd_id`(先 C103-1 起草、后 C103-9);开关关闭时直接跳过 + +### 10.3 C103-9 报文与回填(`HcsExeStageSyncSupport`) + +顶层(`HcsExeplanSyncDTO`):`fdContractId`=`hcs_fd_id`(合规合同ID,非 TMS 主键)、`fdOuterSystem`=`src-system`、`fdLoginName`=配置 `operator-login-name`(默认 srvadmin)、`fdEopExeplanOperateType` 首次 add / 已同步 update(看 `hcs_exeplan_id`,已同步则同时传 `fdExeplanId`)。 + +行级 `fdStages[]`(对应待同步行): + +| HCS 字段 | TMS 取值 | 备注 | +| --- | --- | --- | +| `fdId` | `hcs_stage_fd_id` | add 行不传;update/delete 必传 | +| `fdEopExeplanOperateType` | `row_op` | add / update / delete | +| `fdPerformType` | `perform_type` | 必填 | +| `fdFundNature` / `fdFundAmount` / `fdFundDate` | 同名字段 | 资金日期转毫秒时间戳 | +| `fdInvoiceRule` / `fdInvoiceCycle` | 同名字段 | 账期(天) | +| `fdFundType` | `fund_type`,空取 `exe-stage.fund-type` | 默认 byAmount | +| `fdIsFinish` | `is_finish` | boolean | +| `fdExtendContent` | 固定 `"{}"` | | + +取数与回填: + +- 取数 `selectSyncRows`:`row_op <> 'none'` 的行,**含软删的 delete 行**(原生 SQL 绕过逻辑删除过滤) +- 成功回填三件事:`fdExeplanId` → `blade_contract_manage.hcs_exeplan_id`;`fdChangeStages[]` 各行合规 fdId → `hcs_stage_fd_id`;成功行清 `row_op=none` 并置行级 synced +- `fdChangeStages` 与请求行对应关系 v4 未明确(待联调):行数一致按请求行序对应;不一致按 履约类型|款项性质|金额|资金日期 组合键匹配;add 行匹配不到保持 failed 待下轮重试 +- 失败抛异常走合同 `markFailed`,下轮凭 `hcs_fd_id` 整体重推补偿(§二⑥) + +### 10.4 履约记录(付款链路,C108-1/C108-2) + +`HcsPerfRecordSyncSupport`,随付款推送链路执行(`HcsPaymentSyncSupport`,开关 `perf-record.sync-enabled`,一期默认关): + +- 付款审批通过 → C108-1 新建,返回 fdId 回填付款申请 `hcs_perf_fd_id`(立即持久化) +- 实际付款推进(`paidAmount` 变化置待同步)→ C108-2 更新,`formData.fdId`=`hcs_perf_fd_id` 必填(响应无 data,success=true 即成功) +- 关联履约阶段:按合同 + 付款类型匹配**已同步**的计划行(`hcs_stage_fd_id` 非空),无关联不传 fd_id —— 履约计划同步是履约记录关联的前置 diff --git a/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md b/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md new file mode 100644 index 00000000..a618c1bf --- /dev/null +++ b/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md @@ -0,0 +1,294 @@ +# 合同特批与付款(结算付款)特批流程梳理 + +> 梳理日期:2026-10-12 +> 涉及模块:`blade-service/blade-transport`(业务编排/状态留痕)、`blade-third-party-api/blade-hcs-api`(Feign 客户端 + DTO + 配置) +> 关联设计文档:`doc/docs/hcs合同/2026-10-11-hcs合同推送流程梳理.md`(§九)、`2026-09-27-桂物合同合规系统HCS接入技术设计.md`(§5.4/§5.5) +> 关联提交:`8e6e263`(外部客商高风险黑名单管控与合同自动特批) +> 付款申请全链路(前端提交 → 风险/特批 → 同步金蝶 → 结果回写)专项梳理:`doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md` +> +> **2026-10-12 变更**:原计划的 B103 推送合规方未建设,特批结果改为合规审批完成后经 iPaaS 主动推送 TMS:合同 `POST /hcs/contractLegal/result`、付款 `POST /hcs/paymentLegal/result`(报文 `{sourceSystem, requestId, status=APPROVED/REJECTED, reason}`);原推测性回调端点 `/hcs/callback/spec-result` 及 `HcsFieldConstant.SPEC_CB_*` 候选键解析已删除。见 §2.5/§4.4/§五。 +> **2026-10-13 变更**:C103-8 独立特批发起接口合规未提供(联调 404),合同特批改为随 C103-1/C103-11 报文携带 `fdSepcailContract=true` 发起(高风险/黑名单判定后置入报文);TMS 挂起"特批审核"(processing)后新增**定时 C103-4 结果查询兜底**(随合同推送任务执行,`fdApprovalStatus=approved/approvalinvalid` 联动恢复/驳回);特批附件改随 C103-1 附件面板 `eopCdraftSpecial` 挂载;附件类型判定值与业务字典对齐为「特批附件」。**C103-8 全链路代码已删除**(`IHcsClient#addSpecContract`/`HcsSpecContractSaveDTO`/`applySpec`/`POST /hcs/contract/spec-apply`)。**contractLegal/result 报文已按合规方最新字段表修正**:`{contractType, contractCode, requestId, success, reason, kingdeeId, sourceSystem=WCCK}`(金蝶同步结果结构,非原假设的 status=APPROVED/REJECTED),按 `contractCode`=合同编号定位,`success=true` 联动放行(kingdeeId 留痕)/`false` 联动驳回走重新提交闭环;付款 `paymentLegal/result` 仍为旧结构,待合规方字段表后同步修正。 +> **2026-10-13 付款链路变更**:按最新需求,付款链路黑名单**不再直接阻断**,与高风险统一走特批:提交时收款方为**外部客商**(`CustomerArchive.customerKind≠internal`)才查 C101-1,高风险(`fdRiskLevel=2`)/黑名单(`fdBlackStatus=yes`)→ 提示且**必须上传「特批附件」**(付款附件结构 `attachmentType=special_approval`,缺失抛异常)+ 标记 `hcs_spec_status=need`(不再依赖 C106-1 额度超额这一唯一触发源);审批通过后扫描任务发起 C106-3,TMS 挂起 `currentNode="特批审核"`;合规审批完成后**回调 `paymentLegal/result`** 通知结果,`APPROVED` 恢复"审批通过"放行、`REJECTED` 联动付款审批驳回(returned)走重新提交闭环,页面人工确认等效兜底;特批审核中(need/pushing/processing)**禁止推送金蝶**(`doSyncKingdee` 门禁);前端付款表单同步增加收款方 C101-1 风险查询提示与特批附件提交校验。`risk.block-blacklist` 配置及 Nacos 项已删除。见 §三/§4.4。 +> **2026-10-23 变更(付款特批即时发起 + 下游门禁)**:付款审批通过即发起付款特批,不再等待定时扫描,也不受 `push.immediate-enabled` 开关控制——`POST /payment-application/approve` 在审批事务提交返回后同步调用 `HcsImmediatePushSupport#pushPaymentAndWait`(失败原因经 `pollSyncFailure()` 以 `data=false` 警告返回,审批结果不受影响),MK 审批回调经 `pushPaymentAfterCommit` afterCommit 异步发起。特批审核期间(need/pushing/processing)`pushPayment` 只发起/等待 C106,**不再推送履约计划 C103-9 与履约记录 C108**;特批通过(合规回调或人工确认)后由 `pushAfterSpecApproved` 补推履约数据,再发布金蝶同步事件;驳回不产生履约数据。审批响应区分「特批待发起 / 已发起特批 / 合规推送失败(保留重推)/ 已同步金蝶」。见 §1.3/§3.2/§4.2/§4.4/§五;相关提交 `3ddf0448`(下游门禁与补推)、`1b7b8ae5`(付款即时推送能力)、`6369ec64`(审批入口接线与响应提示)。 + +--- + +## 一、总览:特批的业务含义 + +本文所述"特批"**不是 TMS 本地审批流**(本地审批走 `approvalStatus`/`currentNode`),而是**向集团合规系统 HCS 发起的特殊审批流程**:TMS 侧仅负责发起、留痕、挂起展示并接收结果,审批动作本身在 HCS 内完成。 + +### 1.1 触发条件(仅两类,与金额阈值、超权限无关) + +| 触发线 | HCS 接口 | 判定条件 | 触发的特批 | 作用对象 | +| --- | --- | --- | --- | --- | +| 外部客商高风险/黑名单 | C101-1 风险查询 | `fdRiskLevel=2`(高风险)或 `fdBlackStatus=yes`(黑名单) | 合同特批(C103-1/C103-11 报文 `fdSepcailContract=true` 发起 / contractLegal/result 结果回调 + C103-4 定时查询兜底) | `ContractManage`(合同) | +| 收款方外部客商高风险/黑名单 | C101-1 风险查询 | 同上判定(仅收款方,`customerKind=internal` 内部客商不查) | 结算付款特批(提交校验特批附件 + 标记 need → 审批通过即发起 C106-3 / C106-4 更新 / paymentLegal/result 回调联动付款审批状态) | `PaymentApplication`(付款申请) | +| 合同额度超额 | C106-1 额度校验 | `fdIsCredit=false`(超额) | 结算付款特批(同上链路) | `PaymentApplication`(付款申请) | + +补充规则: + +- **内部组织客商不查**:`CustomerArchive.customerKind=internal` 默认无风险(`HcsSyncConstant.CUSTOMER_KIND_INTERNAL`),合同链路(甲/乙方)与付款链路(收款方)口径一致。 +- **甲/乙任一方命中即整单管控**(`HcsSyncConstant.java:151-176`)。 +- **付款链路高风险/黑名单不再阻断**(2026-10-13 起):提示且必须上传「特批附件」,标记 need 走 C106-3 特批;原 `risk.block-blacklist` 阻断配置已删除。 + +### 1.2 与普通审批的关系 + +特批与合同起草**同报文发起**(高风险/黑名单客商推送时 C103-1/C103-11 报文带 `fdSepcailContract=true`,合规侧随起草自动走特批流程;C103-8 独立发起接口合规未提供,相关代码已删除)。特批期间 TMS 用 `hcs_spec_status=processing` + `currentNode="特批审核"` 挂起展示,推送门禁与手动重推均拦截;特批通过即放行,驳回则联动本地审批驳回(合同与付款链路均已联动)。付款链路审批通过即发起 C106-3(`/approve` 同步、MK 回调异步,扫描任务兜底),特批审核中(need/pushing/processing)**禁止推送金蝶**,**履约计划 C103-9 与履约记录 C108 也等待特批通过后补推**。 + +### 1.3 特批状态机(两条链路共用 `HcsSyncConstant.SPEC_STATUS_*`) + +``` +none(默认) ──高风险/黑名单随 C103-1/C103-11 带 fdSepcailContract=true──▶ processing(挂起"特批审核")──结果回调/查询──▶ approved / rejected +none(默认) ──付款提交高风险/黑名单须特批附件 / 额度超额──▶ need ──审批通过即发起 C106-3(/approve 同步、MK 回调异步,扫描兜底)──▶ processing(挂起"特批审核")──paymentLegal/result 回调──▶ approved / rejected +need(提交校验后的待审批中间态;审批通过即由即时推送发起 C106-3,扫描任务仅作兜底;已存在特批流程ID时走 C106-4 更新,更新成功后同样归一为 processing) +``` + +- 合同特批 `approved` **单次有效**:再次推送时若已 approved 不重复发起。 +- 付款特批结果**联动付款审批状态**(2026-10-13 起):`approved` 恢复"审批通过"放行(金蝶推送门禁解除)并补推履约数据;`rejected` 联动付款审批驳回(`approvalStatus=returned` + `currentNode="已驳回"`),走"重新编辑→提交→审批→重新特批"闭环。 +- 付款特批审核期间(need/pushing/processing)**不推送履约计划 C103-9 与履约记录 C108**(2026-10-23 起):发起 C106-3 后本轮 `hcs_sync_status` 回填 `synced`,避免扫描重复发起;特批通过后由 `pushAfterSpecApproved` 补推。 + +--- + +## 二、合同特批(fdSepcailContract 随起草发起 / contractLegal/result 结果回调 + C103-4 查询兜底) + +### 2.1 触发条件 + +外部客商高风险(风险等级=2)或黑名单,甲方或乙方任一方命中。风险判定:`HcsContractSyncSupport#checkRelativeRisk`(`HcsContractSyncSupport.java:167-183`,`risky` 判定在 :178)。 + +### 2.2 阶段 A:提交校验(特批附件必传) + +入口 `ContractManageController`(`@RequestMapping("/contract-manage")`): + +| 接口 | Service 方法 | +| --- | --- | +| `POST /contract-manage/submit` | `ContractManageServiceImpl#submit` | +| `POST /contract-manage/to-temporary` | `#toTemporary` | +| `POST /contract-manage/submit-formal` | `#submitFormal` | +| `POST /contract-manage/submit-change`(变更提交/变更驳回重提交) | `#submitChange` | + +四处均调用 `validateSpecExplanationOnSubmit`: + +1. `isRiskyCustomer` → `HcsCreditCheckSupport#queryCustomerRiskByName` → `queryCustomerRisk` **实时调 C101-1**(`hcsClient.getRelativeRisk`);内部组织客商直接返回不命中;名称反查不到客商编号不阻断。 +2. 命中且 `attachmentsJson` 中无 `fileType=特批附件`(业务字典 `contract_attachment_type`,2026-10-13 与字典对齐,原设计值「特批说明」字典中不存在)附件 → 抛出:"合同相对方存在高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"。 + +### 2.3 阶段 B:审批通过 → 推送(报文带特批标记)→ 挂起 + +审批通过:`POST /contract-manage/approve` → `ContractManageServiceImpl#approve`:置 `hcsSyncStatus=unsynced`(推送零埋点 ADR-0001);审批事务提交返回后由 `ContractManageController#approve` 同步调用 `hcsImmediatePushSupport.pushContractAndWait(id)`,失败原因经 `pollSyncFailure()` 以警告文案(`data=false`)回传前端。 + +合同推送共 3 个入口,收敛到 `HcsContractSyncSupport#pushContract`: + +| 入口 | 位置 | 说明 | +| --- | --- | --- | +| 即时推送 | `HcsImmediatePushSupport#pushContractAfterCommit` | afterCommit 异步(2 线程 daemon 池),开关 `push.immediate-enabled` 默认 false;`pushContractNow` 对特批挂起中(processing)短路跳过 | +| 审批按钮同步推送 | `ContractManageController#approve` → `HcsImmediatePushSupport#pushContractAndWait` | 审批事务提交返回后同步推送(响应等待附件转传 + HCS 调用),失败原因经 `pollSyncFailure()` 拼入响应 `data=false` | +| 定时扫描 | `HcsPushTask#pushHcsContract`(cron 默认 `0 1/5 * * * ?`) | `pushUnsyncedContracts`:`approvalStatus ∈ {approved, change_approved}` 且 `hcs_sync_status ∈ {unsynced, failed}` 且重试未超限;同轮附带特批结果查询兜底(见 §2.5) | +| 手动重推 | `HcsSyncController POST /hcs/sync/contract/{id}` → `repushContract` | 特批审核中(processing)不可推 | + +`pushContract` 内部步骤: + +``` +⓪ 特批挂起门禁(仅 processing 拦截;need 存量单放行自愈) +① 风险校验仅留痕(回写 hcs_risk_level / hcs_black_status / hcs_risk_time,返回 risky 标志) +②③④ 附件转传 C103-2 / 判重 / C103-1 起草或 C103-11 更新——risky=true 时报文带 fdSepcailContract=true, + 合规侧随起草/更新自动走特批流程;markSynced 回填 hcs_fd_id +⑤ risky 时 hangSpecReview:置 hcs_spec_status=processing + currentNode="特批审核"(先起草后挂起,同推送事务内完成) +⑥ 履约规则 C103-9 行级同步(一期默认关) +``` + +`hangSpecReview` 要点: + +1. 终态守卫:`hcs_spec_status` 已是 approved/rejected(合规秒批/秒驳回调先于挂起执行)不再覆盖; +2. 置 `processing` + `currentNode="特批审核"` + `hcs_spec_msg` 说明,等待结果联动(§2.5)。 + +> 设计要点:C103-8 独立发起接口合规未提供(联调 404),特批**随 C103-1/C103-11 报文** `fdSepcailContract=true` 发起(字段名原文如此,Special 拼写笔误必须沿用);原 `autoApplySpecIfNeeded`/`applySpec` 及人工端点 `POST /hcs/contract/spec-apply` 已随 C103-8 删除。 + +### 2.4 特批附件挂载(随 C103-1 附件面板) + +`HcsAttachmentSupport#transferContractAttachments`:主文件(approved=true)→ `eopCdraftSign`(用印)、 +**`fileType=特批附件` → `eopCdraftSpecial`(特批面板,2026-10-13 起)**、其它附件 → `eopCdraftOther`。 +特批附件随起草报文一并挂载,合规特批流程直接引用,无独立转传步骤。 + +### 2.5 阶段 D:结果联动(两条等效路径 + 定时查询兜底) + +| 路径 | 接口/机制 | 实现 | +| --- | --- | --- | +| 合规回调(主) | `POST /hcs/contractLegal/result`(`HcsSyncController`,blade.secure 放行,网关注册 `/gwzh/HTHG/contractLegal/result`) | `handleLegalResult(HcsContractLegalResultDTO)`:`sourceSystem` 须为 **WCCK**(不符抛"来源系统标识错误")→ `contractCode`=TMS 合同编号定位(`requestId` 仅 X-ERP-Request-Id 回显,不可定位)→ `success=true` 联动恢复"审批通过"(kingdeeId 随留痕保存)/ `success=false` 联动审批驳回走重新提交闭环 → 原文留痕 | +| 定时查询兜底 | `HcsPushTask#pushHcsContract` 每轮附带 → `HcsContractSpecSyncSupport#syncProcessingSpecResults` | 扫描 `hcs_spec_status=processing` 的合同,C103-4 合同详情 `fdApprovalStatus`:`approved` 联动恢复 / `approvalinvalid` 联动驳回(特批驳回的合规侧取值待联调确认);回调先到则状态已非 processing,天然跳过 | +| 人工确认 | `POST /hcs/contract/spec/confirm` | `confirmSpecResult`(仅 processing 可确认) | + +联动 `syncContractOnSpecResult`: + +- 通过(回调 success=true / 查询 approved)→ `currentNode="审批通过"`(C103-1 已起草,无需再推送,流程放行); +- 驳回(回调 success=false / 查询 approvalinvalid)→ `approvalStatus=rejected` + `currentNode="已驳回"`,走"重新编辑 → 提交 → 审批 → 重新特批"闭环。 + +`markSpec` 回写 `hcs_spec_status`/`hcs_spec_time`/`hcs_spec_msg`,回调原文留痕。 + +### 2.6 TMS 侧留痕字段(`blade_contract_manage`,`ContractManage.java:45`) + +| 字段 | 位置 | 含义 | +| --- | --- | --- | +| `hcs_fd_id` | :213 | HCS 合同 ID(C103-1 起草回填) | +| `hcs_risk_level` / `hcs_black_status` / `hcs_risk_time` | :233/:236/:239 | C101-1 风险留痕 | +| `hcs_spec_fd_id` | :242 | HCS 特批流程 ID(原 C103-8 回填;接口删除后新流程不再产生,仅存量数据可能有值) | +| `hcs_spec_status` | :246 | `none/need/processing/approved/rejected` | +| `hcs_spec_time` / `hcs_spec_msg` | :249/:253 | 特批时间/信息 | +| `hcs_spec_files_json` | :256 | 特批材料(含 C103-2 合规附件 ID) | +| `current_node` | — | 挂起展示:`特批审核` / `审批通过` / `已驳回` | + +--- + +## 三、付款特批(付款申请提交触发侧) + +### 3.1 入口与触发(2026-10-13 起两类触发源) + +`PaymentApplicationController`(`/payment-application`)`POST /submit` → `PaymentApplicationServiceImpl#submit`,事务内调 `HcsCreditCheckSupport#checkOnSubmit(entity)`(`HcsCreditCheckSupport.java:94-108`),依次执行两项校验: + +**① C101-1 收款方风险校验**(开关 `risk.check-on-payment` 默认 true,:95-98) + +- `checkRelativeRisk`(:110-148):按收款方 `payeeName` 反查 `CustomerArchive`: + - 反查不到客商档案 → skip 留痕、不阻断(:111-117); + - **内部组织客商(`customerKind=internal`)默认无风险,不调 C101-1**(:119-124); + - 外部客商按客商编号实时调 C101-1,高风险(`fdRiskLevel=2`)或黑名单(`fdBlackStatus=yes`)判定 risky(:136); +- **risky 时两步管控(不再直接阻断)**: + 1. **特批附件必传**:`attachmentsJson` 中无 `attachmentType=special_approval`(前端付款表单「特批附件」类型,兼容 `fileType=特批附件` 旧结构)附件 → 抛"收款方[xxx]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"(`hasSpecAttachment`,:295-313); + 2. **标记待特批**:`hcs_spec_status=need` 并清空旧 `hcs_spec_fd_id`(特批单次有效,重新提交须重新发起 C106-3,:150-151)——审批通过后**即时发起 C106-3**(2026-10-23 起,扫描任务兜底)。 + +**② C106-1 合同额度校验**(开关 `credit.check-enabled` 默认 false,**一期未对接**,:100-103) + +- `checkCredit`:要求合同已推送(有 `hcs_fd_id`)→ `hcsClient.contractCreditCheck`; +- `fdIsCredit=true`(额度内)→ `hcs_credit_status=pass`; +- **超额不拦截提交** → `hcs_credit_status=over` + `hcs_credit_msg` + **`hcs_spec_status=need`**。 + +### 3.2 后续流转(审批通过 → 即时发起特批 → 挂起 → 特批通过后补推 / 回调联动) + +- 审批通过:`#approve` 置 `hcs_sync_status=unsynced`(零埋点 ADR-0001); +- **审批生效后立即发起特批(2026-10-23 起,不再依赖扫描周期)**: + - `/approve` 入口:`PaymentApplicationController#approve` 在 `service.approve` 返回后同步调用 `HcsImmediatePushSupport#pushPaymentAndWait`,审批响应等待 C106-3 返回;失败以 `data=false` 警告返回(审批已生效),单据保留 `failed` 可重推; + - MK/OA 回调入口:`MkProcessBizHandlers.PaymentHandler#approveFromProcess` 调 `HcsImmediatePushSupport#pushPaymentAfterCommit`(afterCommit 异步,不阻塞第三方回调); + - 两个入口均**不受 `push.immediate-enabled` 开关控制**(与项目/合同的即时推送不同,付款特批按"审批即发起"落地); +- C106-3 成功 → `processing` + **挂起 `currentNode="特批审核"`**(`HcsSpecPaySyncSupport#pushSpecPay`),本轮 `hcs_sync_status` 回填 `synced`,扫描任务不会重复发起; +- **特批审核中(need/pushing/processing)不推送下游数据**:`HcsPaymentSyncSupport#pushPayment` 仅发起/等待 C106 后返回,履约计划 C103-9 与履约记录 C108 均不推送;金蝶继续由 `doSyncKingdee` 门禁拦截(见 §4.4); +- 特批通过(合规回调 `paymentLegal/result` 或人工确认)→ `pushAfterSpecApproved` 补推 C103-9 + C108,再发布金蝶同步事件;特批驳回 → 仅联动付款审批驳回(`approvalStatus=returned` + `currentNode="已驳回"`),**不产生履约数据**,走"重新编辑→提交→审批→重新特批"闭环(提交时重新校验风险与特批附件); +- 补推履约数据失败 → `hcs_sync_status=failed`(重试计数+1),不阻断金蝶补推,由手动重推或扫描任务兜底。 + +### 3.3 TMS 侧留痕字段(`blade_payment_application`,`PaymentApplication.java:41`) + +| 字段 | 位置 | 含义 | +| --- | --- | --- | +| `hcs_fd_id` / `hcs_sync_status` / `hcs_retry_count` | :125/:127/:129 | HCS 同步主状态 | +| `hcs_credit_status` | :135 | `pass`(额度内)/ `over`(超额) | +| `hcs_credit_msg` / `hcs_credit_time` | :137/:139 | 额度校验信息 | +| `hcs_spec_fd_id` / `hcs_spec_status` | :141/:142 | 特批流程 ID / `none/need/pushing/processing/approved/rejected` | +| `current_node` | :111 | 特批挂起展示:`特批审核`(C106-3 发起后)/ `审批通过`(特批通过恢复)/ `已驳回`(特批驳回联动) | +| `hcs_perf_fd_id` | :145 | 履约记录(C108,默认关,不在本文范围) | + +### 3.4 与合同特批的关联 + +- 付款特批报文中的合同取 `ContractManage.hcs_fd_id` 传值(`requireContractFdId`,`HcsSpecPaySyncSupport.java:210-215`),额度校验也是按合同维度(C106-1 `fdContractId`); +- 二者共用 `HcsSyncConstant.SPEC_STATUS_*` 状态机与 `blade_hcs_check_log` 留痕表(biz_type=`payment`,check_type=`risk`/`credit`/`spec`)。 + +--- + +## 四、结算付款特批(C106-3 发起 / C106-4 更新) + +### 4.1 概念澄清 + +代码库中**不存在独立的"结算单付款"特批实体**。"结算付款特批"是**付款申请(PaymentApplication)的 HCS 特批流程在 HCS 侧的表单名称**——单据主题固定 `结算付款特批-{paymentNo}`(`HcsSpecPaySyncSupport.java:129/158`)。即: + +- §三(付款特批)= **触发侧**:提交时高风险/黑名单或 C106-1 超额置 `need`; +- 本节(结算付款特批)= **推送侧**:审批通过即发起 C106-3 / 更新 C106-4。 + +佐证:`PaymentApplication.paymentType` 含 `settlement_payment`(`TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT`,:35),BFI 申请事由即称"结算付款申请"(`PaymentApplicationServiceImpl.java:1207-1213`);`FormalSettlementPayment`(正式结算付款记录,`blade_formal_settlement_payment`)**没有任何 hcs/特批字段**,仅在付款完成后联动生成,不参与特批。 + +### 4.2 推送链路 + +| 入口 | 位置 | 说明 | +| --- | --- | --- | +| 即时推送(2026-10-23 新增) | `HcsImmediatePushSupport#pushPaymentAndWait`(`/approve` 同步)/ `#pushPaymentAfterCommit`(MK 回调 afterCommit 异步) | 审批通过即发起 C106-3;**不受 `push.immediate-enabled` 开关控制**;失败留痕,由扫描/手动重推兜底 | +| 定时扫描(兜底) | `HcsPushTask#pushHcsPayment`(:163,cron 默认 `0 2/5 * * * ?`,开关 `push.payment.sync-enabled` 默认 false) | `pushUnsyncedPayments`(:285-309):`approvalStatus=approved` 且 `hcs_sync_status ∈ {unsynced, failed}` 且重试未超限 → `HcsPaymentSyncSupport#pushPayment` | +| 手动重推 | `POST /hcs/sync/payment/{id}`(`HcsSyncController.java:111`)→ `repushPayment`(:168);批量 `POST /hcs/sync/payment`(:129) | 特批审核中(processing)重推只返回等待提示,不重复发起;特批通过后重推为幂等补推 | + +`HcsPaymentSyncSupport#pushPayment`(`HcsPaymentSyncSupport.java:84`,2026-10-23 调整编排): + +1. **① 结算付款特批**:`hcs_spec_status=need` 调 `HcsSpecPaySyncSupport#pushSpecPay`(C106-3 成功后 `processing` + 挂起 `currentNode="特批审核"`;C106-4 更新路径同样归一为 `processing`),随后 `markSynced` 并返回,**本轮不推下游**; +2. `pushing/processing`:仅回写"付款特批审核中,待特批通过后推送履约数据",不推履约计划与履约记录; +3. 特批通过(`none/approved`):② 履约计划重推 C103-9(付款审批款项金额累加行)→ ③ 履约记录 C108(默认关)→ `markSynced`; +4. 推送失败统一 `markFailed`(重试计数+1),由手动重推或扫描任务兜底。 + +> 特批通过后补推入口 `pushAfterSpecApproved`(`HcsPaymentSyncSupport.java:152`):在合规回调/人工确认将 `hcs_spec_status` 落库为 `approved` 后调用,补推失败仅置 `failed`,不阻断后续金蝶补推。 + +> `need` 触发源(2026-10-13 起):提交时收款方外部客商高风险/黑名单(须特批附件)或 C106-1 额度超额(`credit.check-enabled=false` 未对接时仅前者可达)。 + +### 4.3 C106-3/4 实现(`HcsSpecPaySyncSupport`) + +`pushSpecPay`(:86-108): + +- `hcs_spec_fd_id` 为空 → **C106-3 发起**:`hcsClient.addSpecApplyPay`(:92,`IHcsClient.java:259`)双层嵌套 `data.data.fdId` → 回填 `hcs_spec_fd_id` + 状态 `processing`(:94-96); +- 非空 → **C106-4 更新**:`updateSpecApplyPay`(:102,`IHcsClient.java:268`,顶层 `fdId` 定位,内层 `success` 判断)。 + +报文组装 `buildSpecPayParam`(:113-166):`HcsSpecPaySaveDTO` → `HcsSpecPayFormDataDTO` → `HcsSpecPayPropsDTO`,关键字段: + +| HCS 字段 | 取值 | +| --- | --- | +| `fdDocSubject` | `结算付款特批-{paymentNo}`(:129/:158) | +| `fdContractName` | 合同 `hcs_fd_id`(:123) | +| `fdDocNo` / `fdId` | `paymentNo`(:127/:131) | +| `fdPayAmount` | `appliedAmount` 申请金额(:133) | +| `fdTotalAmount` | `settlementAmount` 结算金额(:135) | +| `fdPayer` / `fdPayee` | 付款方/收款方(:137-138) | +| 收款账户/账号/开户行 | :140-142 | +| `fdSettleCurrency` | 默认"人民币"(:144) | +| `fdPaymentPurpose`/`fdPaymentNature`/`fdPaymentType` | 来自 `thirdParty.hcs.spec-pay.*` 配置(:146-148) | +| `fdReason` / `fdSupplementNote` | `remark`(:150-151) | +| `fdSpcApprovalAttach` | `fd_spc_approval_attach`(:155) | + +操作人 = 付款申请创建人登录名(`resolveLoginName`,:220-229)。 + +附件 `buildMechanisms`(:171-193):`attachmentsJson` 经 `HcsAttachmentSupport#transferAttachments`(C103-2)转传;**发起时带 `fdBindType=ADD`、更新时不带**(:181-189)。 + +### 4.4 结果确认 + +特批结果由合规系统审批完成后**回调 `paymentLegal/result` 通知 TMS**,并联动付款审批状态(2026-10-13 起): + +- `POST /hcs/paymentLegal/result`(`HcsSyncController`,blade.secure 放行,网关注册 `/gwzh/HTHG/paymentLegal/result`)→ `HcsPaymentSyncSupport#handleLegalResult`(:229):先校验 `sourceSystem`(不符抛"来源系统标识错误"异常),`requestId`=付款申请ID 定位,`status=APPROVED` → 特批通过 + 恢复 `currentNode="审批通过"` 放行(金蝶推送门禁解除);`status=REJECTED` → 特批驳回 + **联动付款审批驳回**(`approvalStatus=returned` + `currentNode="已驳回"`,`syncPaymentOnSpecResult` :280),走"重新编辑→提交→审批→重新特批"闭环; +- **特批通过后补推(2026-10-23 起)**:`handleLegalResult`/`confirmSpecResult` 将 `hcs_spec_status=approved` 落库后调用 `pushAfterSpecApproved`(:152)补推履约计划 C103-9 与履约记录 C108,随后 `publishKingdeeAutoSyncOnSpecApproved`(:267)发布金蝶同步事件;补推失败置 `failed`,不影响金蝶补推; +- 等效兜底:页面人工确认 `POST /hcs/spec-pay/confirm` → `confirmSpecResult`(:195,仅 `processing` 可确认,与回调同样联动付款审批状态并补推履约数据); +- 与合同特批(contractLegal/result + C103-4 定时查询兜底)不同,付款特批**仅回调通知**(合规未提供 C106 结果查询接口),无定时查询兜底,回调未达时靠人工确认。 + +--- + +## 五、推送时机与结果接收机制汇总 + +| 机制 | 合同特批(fdSepcailContract/contractLegal/result/C103-4 兜底) | 付款/结算付款特批(C106-3/4/paymentLegal/result) | +| --- | --- | --- | +| 定时扫描 | `HcsPushTask#pushHcsContract`(默认 `0 1/5 * * * ?`,同轮附带特批结果查询兜底 `syncProcessingSpecResults`) | `HcsPushTask#pushHcsPayment`(默认 `0 2/5 * * * ?`,2026-10-23 起仅作即时发起的兜底);Redis 任务锁 TTL 240s、分钟位错峰 | +| 即时推送 | `HcsImmediatePushSupport#pushContractAfterCommit`(afterCommit 异步,开关默认 false,单条锁 TTL 120s) | `HcsImmediatePushSupport#pushPaymentAndWait`(`/approve` 同步)/ `#pushPaymentAfterCommit`(MK 回调 afterCommit 异步)——**审批即发起,不受 `push.immediate-enabled` 控制**(2026-10-23 起),单条锁 TTL 120s | +| 下游履约数据 | C103-9 履约规则随合同推送同步(一期默认关) | **特批审核中不推**;特批通过后 `pushAfterSpecApproved` 补推 C103-9(付款审批款项金额累加行)+ C108 履约记录(默认关) | +| 结果推送接收 | `POST /hcs/contractLegal/result`(blade.secure 放行,sourceSystem=WCCK,contractCode=合同编号定位,success 联动审批状态,原文留痕) | `POST /hcs/paymentLegal/result`(blade.secure 放行,requestId=付款申请ID,回写特批状态并**联动付款审批状态**:approved 恢复放行并补推履约数据 / rejected 联动审批驳回;报文仍为旧结构待合规方字段表确认) | +| 人工确认 | `POST /hcs/contract/spec/confirm` | `POST /hcs/spec-pay/confirm` | +| 手动发起/重推 | `POST /hcs/sync/contract/{id}`(spec-apply 已随 C103-8 删除,2026-10-13) | `POST /hcs/sync/payment/{id}`、`POST /hcs/sync/payment` | + +--- + +## 六、依赖配置项(`thirdParty.hcs.*`,`HcsProperties.java`) + +| 配置 | 默认 | 说明 | +| --- | --- | --- | +| `contract.require-risk-check` | true | 提交时特批附件强校验 | +| `risk.check-on-contract` | — | 合同链路 C101-1 查询开关 | +| `risk.check-on-payment` | true | 付款提交 C101-1 校验开关(收款方外部客商高风险/黑名单须特批附件并标记待特批) | +| `credit.check-enabled` | **false** | C106-1 额度校验,一期未对接(付款特批第二触发源关闭,高风险/黑名单触发不受影响) | +| `contract.special-contract-enabled` | false | C103-1/C103-11 报文 `fdSepcailContract` 强制 true(默认关;高风险/黑名单判定自动带 true,本开关可强制全量走特批,联调验证用) | +| `push.immediate-enabled` | false | 审批通过后即时推送(项目/合同受控;付款特批按"审批即发起"**不受该开关控制**,2026-10-23 起) | +| `push.contract.sync-enabled` / `push.contract.cron` | — / `0 1/5 * * * ?` | 合同定时扫描 | +| `push.payment.sync-enabled` / `push.payment.cron` | — / `0 2/5 * * * ?` | 付款定时扫描 | +| `spec-pay.payment-purpose/nature/type` | — | C106 报文付款用途/性质/类型 | + +--- + +## 七、留痕与常量速查 + +- **留痕表**:`blade_hcs_check_log`(`HcsContractCheckLog.java:44`):`biz_type=contract/payment`、`check_type=risk/credit/spec`、`result`、`msg`(≤500)、`trace_id`。 +- **关键常量**(`HcsSyncConstant`):`SPEC_STATUS_NONE/NEED/PROCESSING/APPROVED/REJECTED`(:119-149)、`RISK_LEVEL_HIGH="2"`(:156)、`ATTACHMENT_TYPE_SPEC_EXPLANATION="特批附件"`(:181,2026-10-13 与业务字典对齐)、`SPEC_REVIEW_NODE="特批审核"`(:208)。 +- **合规推送结果状态**(`HcsSyncConstant`):`LEGAL_RESULT_APPROVED="APPROVED"` / `LEGAL_RESULT_REJECTED="REJECTED"`(原 B103 回调候选键常量 `HcsFieldConstant.SPEC_CB_*` 已随旧回调端点删除)。 +- **特批附件挂载键**:合同特批附件随 C103-1 附件面板 `eopCdraftSpecial`(2026-10-13 起);付款 C106-3/4 特批材料仍用 `fd_spc_approval_attach`。 diff --git a/doc/docs/hcs合同/2026-10-22-HcsPushTask定时任务逻辑与流程梳理.md b/doc/docs/hcs合同/2026-10-22-HcsPushTask定时任务逻辑与流程梳理.md new file mode 100644 index 00000000..97e6ee15 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-22-HcsPushTask定时任务逻辑与流程梳理.md @@ -0,0 +1,210 @@ +# HcsPushTask 定时任务逻辑与流程梳理 + +> 代码:`blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java` +> 定位:HCS(合同合规系统)合规推送的**兜底定时扫描**。审批代码零埋点——扫描只认「审批通过 + 待同步/失败未超限」的单据,与审批入口(/approve、MK 回调)无关;失败由下轮扫描自动补偿,超限仅人工重推(D7)。 + +--- + +## 1. 总体机制 + +| 机制 | 说明 | +| --- | --- | +| 调度 | 五类任务独立 `@Scheduled`,cron 走 Nacos `thirdParty.hcs.push.{bizType}.cron`,默认每 5 分钟、分钟位 0/1/2/3/4 错峰,避免同刻并发与 HCS 接口流量尖峰 | +| 开关 | 每任务独立开关 `thirdParty.hcs.push.{bizType}.sync-enabled`(默认关,灰度打开);未开启时本轮直接跳过 | +| 防重 | 按业务类型隔离的 Redis 分布式锁(`blade:third:hcs:push:task:lock:{bizType}`),setIfAbsent 抢锁 + 持有值比对释放,TTL 240s;多实例部署时同任务只有一台执行,任务间互不阻塞 | +| 批量 | `batch-size`(单轮扫描条数上限)、`max-retry`(重试上限)为全局配置 `thirdParty.hcs.push.*` | +| 复用 | 扫描与推送逻辑(pushUnsyncedXxx)同时供手动端点 HcsSyncController 复用(页面"推送合规/重推"按钮) | + +--- + +## 2. 五类任务总览 + +| bizType | 任务 | 默认 cron | 覆盖合规接口 | 推送 Support | +| --- | --- | --- | --- | --- | +| project | 项目立项 | `0 */5 * * * ?` | C102-1/2/3 | HcsProjectSyncSupport | +| contract | 合同(含特批兜底) | `0 1/5 * * * ?` | C101-1、C103-1/11/3/4/9、C103-2 附件 | HcsContractSyncSupport + HcsContractSpecSyncSupport | +| payment | 付款链(特批+履约) | `0 2/5 * * * ?` | C106-3/4、C108-1/2 | HcsPaymentSyncSupport(编排)→ HcsSpecPaySyncSupport / HcsPerfRecordSyncSupport | +| invoice-application | 开票登记 | `0 3/5 * * * ?` | C111-1/2/3、C103-2 附件 | HcsInvoiceRegSyncSupport | +| invoice-receipt | 收票单 | `0 4/5 * * * ?` | C109-1/2、C103-2 附件 | HcsReceiptBillSyncSupport | + +--- + +## 3. 统一扫描条件(状态口径) + +各任务扫描 SQL 口径一致(payment/invoice 两类只认 `approved`,不含变更通过): + +1. **审批状态**:`approvalStatus IN (approved, change_approved)`(合同/项目);付款、开票、收票仅 `approved`; +2. **同步状态**:`hcs_sync_status IN (unsynced, failed)`; +3. **重试门槛**:`hcs_retry_count IS NULL OR < max-retry`; +4. `limit batch-size` 控制单轮量。 + +逐条推送:成功由 Support 内 `markSynced` 回填(`hcs_fd_id` 合规ID、`synced`、清零重试、记 `hcs_sync_time/msg`);异常由任务统一 `markFailed`(`failed`、重试+1、失败原因截断 500 字落 `hcs_sync_msg`)。 + +### 同步状态机(hcs_sync_status) + +``` +unsynced(审批通过初始态) + │ 扫描推送成功 + ├──→ synced(终态,回填 hcs_fd_id) + │ 推送异常 + └──→ failed(重试+1) + │ 未超限:下轮扫描自动补偿 + ├──→ synced + └──→ 超限:停留 failed,仅人工重推(重推重置 unsynced + 清零计数) +``` + +--- + +## 4. 各任务流程与接口明细 + +### 4.1 项目立项(C102) + +**流程**(HcsProjectSyncSupport.pushProject): + +1. 无合规ID(首推)→ **C102-3 列表判重**(conditions=`fd_num=项目编号`),命中直接回填 fdId 置 synced,跳过新建; +2. 未命中 → **C102-1 新建**(模板编码取配置 `project.template-code`,操作 drafter_submit 直接提交,金额万元×10000 转元),回填 fdId; +3. 已有合规ID(变更单 change_approved)→ **C102-2 更新**(formData.fdId 必填)。 + +| 接口 | 用途 | +| --- | --- | +| C102-3 | 项目立项列表查询(判重) | +| C102-1 | 项目立项新建 | +| C102-2 | 项目立项更新 | + +**附带回调**(非本任务,同 Support):合规 `projectLegal/result` 推送(D902)→ 回填金蝶项目ID/编号(kingdeeId/kingdeeNumber),原文留痕 `blade_hcs_check_log`。 + +### 4.2 合同(C103)—— 最复杂链路 + +任务为组合动作:`pushUnsyncedContracts()` + `hcsContractSpecSyncSupport.syncProcessingSpecResults()`(特批结果查询兜底)。 + +**推送流程**(HcsContractSyncSupport.pushContract,按序): + +| 步骤 | 动作 | 接口 | +| --- | --- | --- | +| ⓪ 门禁 | 特批中(hcsSpecStatus=processing)的合同跳过推送,等待结果恢复;need 不拦截(重推时随报文自愈转特批) | — | +| ① 风险校验 | 开关 `contract.require-risk-check`;甲乙双方各查一次 C101-1(**不跳过内部组织客商**,仅名称反查不到编号记 skip;等级取甲方优先非空值、黑名单任一方为是),留痕 hcs_risk_* 字段与 check_log;**仅留痕不阻断**,高风险/黑名单触发后续自动特批。注:提交环节的阻断校验在合同/付款 submit 时已完成(见 §6 C101-1 全景),此步是推送链路的复核留痕 | **C101-1** 客商风险查询 | +| ② 附件转传 | 开关 `attachment.transfer-enabled`;合同主文件→eopCdraftSign、特批附件→eopCdraftSpecial、其它→eopCdraftOther;转传命中映射表(blade_hcs_attachment_sync)直接复用 | **C103-2** 附件上传 | +| ③ 判重 | conditions=`fdNumber=合同编号`,命中回填 fdId 置 synced | **C103-3** 合同列表查询 | +| ④ 起草/更新 | 首推 **C103-1 合同发起**(fdFieldJson 模板字段 + signer 相对方 + 顶层 attachment 挂载;高风险/黑名单或强制开关时报文带 `fdSepcailContract=true` 在合规侧走特批);变更单走 **C103-11 合同更新**(顶层 fdId 定位) | **C103-1** / **C103-11** | +| ⑤ 履约计划 | 开关 `exe-stage.sync-enabled`;C103-9 行级同步(add/update/delete 标记),仅依赖 hcs_fd_id、失败不受特批门禁阻塞 | **C103-9** 履约规则同步(exeplanAdd) | +| ⑥ 挂起特批 | 风险客商(且特批无终态)→ hcsSpecStatus=processing、currentNode="特批审核",等待结果 | — | + +**特批结果闭环**(HcsContractSpecSyncSupport,三条入口等效): + +1. 合规推送 `contractLegal/result`:success=true → 恢复"审批通过"(金蝶合同ID回填 kingdee_id,特批 approved);false → 联动合同审批驳回(rejected),走重新提交闭环; +2. **定时 C103-4 兜底**(本任务每轮执行):扫描特批中(processing 且已有 hcs_fd_id)的合同(limit 50),查合同详情 fdApprovalStatus——`approved` 联动通过、`approvalinvalid` 联动驳回、其余等待中跳过; +3. 人工确认(页面按钮,与推送等效)。 + +| 接口 | 用途 | +| --- | --- | +| C101-1 | 客商风险等级/黑名单查询(甲乙各一次) | +| C103-2 | 合同附件上传(multipart) | +| C103-3 | 合同列表查询(判重) | +| C103-1 | 合同发起(新建,返回 fdContractId 必须持久化) | +| C103-11 | 合同更新(变更单) | +| C103-9 | 履约规则/计划行级同步 | +| C103-4 | 合同详情查询(特批结果兜底,fdId 定位) | + +**附带回调**(非本任务):`contractKingdee/result` 推送(D903)→ 回填金蝶合同ID,仅留痕不联动审批。 + +### 4.3 付款链(C106-3 特批 + C108 履约) + +**流程**(HcsPaymentSyncSupport.pushPayment,编排组件): + +1. **① 结算付款特批**:仅当 `hcs_spec_status=need`(提交时高风险/黑名单须特批附件,或额度超额标记)时触发——未发起走 **C106-3 特批发起**(返回双层嵌套 data.data.fdId → hcs_spec_fd_id,状态置 processing),已发起走 **C106-4 特批更新**;发起后挂起 currentNode="特批审核"; +2. **② 履约记录**:开关 `perf-record.sync-enabled`(**一期默认关**,关闭时直接置 synced)——无记录ID走 **C108-1 新建**(回填 hcs_perf_fd_id),实际付款推进走 **C108-2 更新**。 + +特批结果入口(非扫描,同 Support):合规 `paymentLegal/result` 推送(requestId=付款申请ID,APPROVED/REJECTED)或人工确认——通过恢复"审批通过"放行,驳回联动付款审批驳回(returned)。 + +| 接口 | 用途 | +| --- | --- | +| C106-3 | 结算付款特批流程发起 | +| C106-4 | 结算付款特批流程更新 | +| C108-1 | 履约记录新建 | +| C108-2 | 履约记录更新 | + +### 4.4 开票登记(C111) + +**流程**(HcsInvoiceRegSyncSupport.pushInvoiceRegistration): + +1. 无合规ID → **C111-1 列表判重**(conditions=`fdNumber=申请单号`),命中回填; +2. 未命中 → **C111-2 新增**(合同编号经"开票申请→应收结算关联→正式结算单"链路回溯,回溯不到直接失败;币种按 RMB 取 `invoice.currency-id-mapping`;附件经 C103-2 转传);若金蝶已开票完成(kingdeeStatus=invoiced)则紧接着回填票据日期; +3. 已有合规ID(金蝶开票完成后回写置待同步)→ **C111-3 更新**(回填 fdBillDate/fdStatus=已开票)。 + +| 接口 | 用途 | +| --- | --- | +| C111-1 | 开票登记列表查询(判重) | +| C111-2 | 开票登记新增 | +| C111-3 | 开票登记更新(票据日期/状态回填) | +| C103-2 | 附件转传 | + +### 4.5 收票单(C109) + +**流程**(HcsReceiptBillSyncSupport.pushReceiptBill): + +1. 无合规ID → **C109-1 新建**(税率 TMS 整数÷100 转小数;合同主题/编号经"收票→结算关联→结算单合同"回溯,回溯不到取页面人工补录 hcs_contract_id/hcs_contract_no;附件挂 fdEntityKey=fd_attach;返回**双层嵌套** data.data.fdId)→ 立即持久化 hcs_fd_id → 任务层 markSynced; +2. 已有合规ID → **C109-2 更新**(顶层 fdId 必填,只传变更字段、不带文档状态)。 + +注:收票单是唯一在任务循环内显式调用 `markSynced` 的(Support 内只持久化 fdId),其余任务由 Support 内部回填。 + +| 接口 | 用途 | +| --- | --- | +| C109-1 | 收票单新建 | +| C109-2 | 收票单更新 | +| C103-2 | 附件转传 | + +--- + +## 5. 状态字段速查 + +| 字段 | 取值 | 含义 | +| --- | --- | --- | +| approvalStatus | draft/reviewing/withdrawn/**approved**/rejected/change_reviewing/change_approved/change_rejected/voided | TMS 审批状态;扫描只认 approved、change_approved(付款/开票/收票仅 approved) | +| hcs_sync_status | **unsynced / synced / failed** | 合规推送整体状态(任务补偿的依据) | +| hcs_retry_count | 整数 | 失败重试次数,≥ max-retry 停止自动补偿 | +| hcs_fd_id | — | 合规侧单据ID(项目/合同/开票登记),有值走更新链路 | +| hcs_spec_status | none/need/pushing/**processing**/approved/rejected | 特批状态;need 触发付款 C106-3,processing 挂起"特批审核"并阻塞合同重推 | +| hcs_spec_fd_id | — | 合规特批流程ID(付款 C106-3 返回) | +| hcs_perf_fd_id | — | 合规履约记录ID(C108-1 返回) | +| hcs_risk_level / hcs_black_status / hcs_risk_time | — | 合同客商风险校验留痕(C101-1 结果) | +| kingdee_id / kingdee_number | — | 金蝶同步结果回填(合规 result 回调) | + +--- + +## 6. C101-1 客商风险校验全景(最新口径) + +C101-1(`getRelativeRisk`,入参 fdNumber=客商编号,返回 fdRiskLevel 0=低/1=中/2=高、fdBlackStatus yes/no)在链路中共有 4 类调用场景。风险判定口径统一:**风险等级=2 或 黑名单=yes 即风险客商**;但「内部组织客商跳过查询」仅适用于场景 1/2/4(HcsCreditCheckSupport 统一入口),**场景 3(合同推送链路)未做内部组织跳过,甲乙双方均直接调 C101-1**,仅名称反查不到客商编号时记 skip。 + +| # | 场景 | 入口 | 开关 | 行为 | +| --- | --- | --- | --- | --- | +| 1 | **合同提交校验**(阻断) | ContractManageServiceImpl.validateSpecExplanationOnSubmit(新增/变更/重新提交等 4 处调用) | 无开关,始终执行 | 甲、乙方各自独立实时查(`isRiskyCustomer(partyA) \|\| isRiskyCustomer(partyB)`,queryCustomerRiskByName 名称反查编号,反查不到记 risky=false 不阻断);风险客商**必须已上传「特批附件」**,否则阻断提交(attachmentsJson 与 contractFileJson 均认可,历史已传兼容放行) | +| 2 | **付款提交校验**(提示+转特批) | PaymentApplicationServiceImpl 提交事务内 → HcsCreditCheckSupport.checkOnSubmit | `risk.check-on-payment` | 收款方反查档案:反查不到/内部组织 → skip 留痕不阻断;外部客商查询 C101-1,风险客商**必须上传「特批附件」**(attachmentType=special_approval,阻断提示),通过后置 `hcs_spec_status=need`、**清空旧 hcs_spec_fd_id**(特批单次有效,重新提交须重新发起 C106-3) | +| 3 | **合同推送留痕**(不阻断) | HcsPushTask 合同链路步骤① → HcsContractSyncSupport.checkRelativeRisk | `contract.require-risk-check` | **甲乙双方必各调一次 C101-1(此链路不跳过内部组织客商)**,仅名称反查不到编号时记 skip;结果留痕 hcs_risk_level(甲乙取甲方优先的非空值)/hcs_black_status(任一方黑名单即 yes)/hcs_risk_time 与 check_log;风险客商 → C103-1/C103-11 报文带 `fdSepcailContract=true` + 挂起"特批审核"(见 §4.2) | +| 4 | **页面展示查询** | HcsSyncController:`GET /hcs/risk?customerCode=`(单个)、`GET /hcs/risk/batch`(客商列表页批量,单个失败跳过)、`GET /hcs/risk/by-name`(按名称,合同前端校验用) | — | HcsCreditCheckSupport.queryCustomerRisk:内部客商直接 risky=false;外部客商查询后组装 CustomerRiskVO(fdBlackStatus/fdRiskLevel/riskLevelName/risky/riskTip);riskTip 为固定短文案(C101-1 不返回风险项明细):黑名单="被列入外部黑名单"、高风险="为高风险客商" | + +各场景均留痕 `blade_hcs_check_log`(check_type=risk,result=pass/fail/skip)。 + +### 6.1 风险客商管控闭环(合同/付款两条线) + +``` +提交时(阻断门禁):C101-1 查甲乙/收款方 → 风险客商须传「特批附件」方可提交 + │ +合同线:推送 C103-1/C103-11 报文 fdSepcailContract=true(合规侧走特批流程) + │ TMS 挂起"特批审核"(hcs_spec_status=processing) + └→ 结果:contractLegal/result 推送 或 C103-4 定时查询兜底 + approved → 恢复"审批通过";驳回 → 联动合同审批驳回 + +付款线:提交置 hcs_spec_status=need + │ 审批通过后扫描任务发起 C106-3(挂起"特批审核") + └→ 结果:paymentLegal/result 推送 或 人工确认 + APPROVED → 恢复放行;REJECTED → 联动付款审批驳回(returned) +``` + +--- + +## 7. 与本任务相关的其他链路(简述) + +- **即时推送**(HcsImmediatePushSupport):审批通过瞬间单条推送,Redis 单条锁(TTL 120s)防与扫描任务并发双推;失败落 failed 由本任务兜底。 +- **手动重推**(各 Support.repushXxx / HcsSyncController):重置 unsynced + 清零计数后立即推送;合同特批中(processing)禁止重推。 +- **主数据**(HcsMasterDataSupport):C103-6 模板 / C103-7 分类 / C103-12 币种列表,Redis 缓存,非本任务调度。 +- **额度校验 C106-1**:付款提交时同步调用(开关 `credit.check-enabled`,一期默认关);以 `fdIsCredit` 为准判定,通过置 hcs_credit_status=pass,超额**不拦截**提交、置 over 并标记 `hcs_spec_status=need` 转特批链路;校验依赖合同已推送合规(hcs_fd_id),未推送时按 `credit.require-contract-synced` 配置阻断或跳过。 diff --git a/doc/docs/hcs合同/2026-10-22-付款审批履约计划款项金额累加与金蝶付款申请携带履约计划id业务流程梳理.md b/doc/docs/hcs合同/2026-10-22-付款审批履约计划款项金额累加与金蝶付款申请携带履约计划id业务流程梳理.md new file mode 100644 index 00000000..3225c4be --- /dev/null +++ b/doc/docs/hcs合同/2026-10-22-付款审批履约计划款项金额累加与金蝶付款申请携带履约计划id业务流程梳理.md @@ -0,0 +1,155 @@ +# 付款审批履约计划款项金额累加与金蝶付款申请携带履约计划id 业务逻辑与流程梳理 + +> 需求:① 审批通过后,更新合同履约计划并自动推送合同系统(HCS),款项金额=申请付款金额,多次付款累加更新;② 审批通过自动推送金蝶付款申请,报文携带关联的履约计划id(进度款计划id),用于合同系统关联履约付款数据与履约计划。 +> 关键口径:HCS 接口文档(桂物合同集成接口-apifox导入-KeyAuth.json)中履约计划行金额字段 `fdFundAmount` 的定义即**「款项金额,必填」**,本地 `blade_contract_exe_stage.fund_amount` 与之映射——累加直接落在该字段,无需新增数据库列。 +> 代码改动日期:2026-10-22,`mvn compile` 通过。 + +--- + +## 1. 业务逻辑总览 + +| # | 规则 | 说明 | +| --- | --- | --- | +| 1 | 款项金额=申请付款金额,多次付款累加 | 付款申请**审批通过**时,按 `coalesce(fund_amount, 0) + applied_amount` 原子累加到匹配的履约计划行(SQL 原子累加,防多笔付款并发审批丢失更新) | +| 2 | 款项性质匹配 | `settlement`(正式结算付款)→ 进度款行 `progress`;`progress_advance`/`project_advance`(预付款)→ 预付款行 `prepayment`(`HcsSyncConstant.fundNatureOfPayment`) | +| 3 | 仅精确性质行累加 | 匹配不到该性质的已同步付款计划行时**跳过累加**(日志留痕),兜底匹配到的异性质行不累加,避免预付款金额误计入进度款计划 | +| 4 | 已同步行重推、未同步行随首推 | 计划行已有合规阶段ID(hcs_stage_fd_id)→ 置 `row_op=update` + 待同步,随付款链凭 fdId 重推 C103-9;未同步行保持 `row_op=add`,合同链路首推时即携带累加后金额 | +| 5 | 金蝶报文携带履约计划id | A103-1/A103-3 付款申请分录备注 `e_remark` 携带 `履约计划id:{hcs_stage_fd_id}`(进度款计划行的合规阶段ID,合同系统凭此关联);原 `remark` 有值时拼接其后 | +| 6 | 无关联不传、异常不阻断 | 无匹配计划行时不携带履约计划id(与 C108 履约记录 §5.6「无关联则不传」同口径);计划行查询异常仅告警,不阻断金蝶推送 | + +--- + +## 2. 业务流程 + +### 2.1 问题1:付款审批 → 履约计划款项金额累加 → 推送合同系统 + +``` +付款申请审批通过(/approve 或 MK 审批流回调,同一事务内) + │ + ├─① approve():approvalStatus=approved、hcs_sync_status=unsynced(HCS 零埋点,ADR-0001) + │ + ├─② accumulatePaidAmountOnApprove()(事务内落库) + │ 按 contractId + performType=payment + fundNature(按付款类型映射)匹配已同步计划行 + │ ├─ 精确命中 → fund_amount 原子累加申请金额 + │ │ ├─ 行已同步 → row_op=update、hcs_sync_status=unsynced(待重推 C103-9) + │ │ └─ 行未同步 → 保持 row_op=add(合同链路首推即携带累加金额) + │ └─ 未命中/性质不一致 → 跳过累加,仅日志 + │ + └─③ HCS付款链定时任务 pushHcsPayment(cron 默认 0 2/5 * * * ?,开关 thirdParty.hcs.push.payment.sync-enabled) + 扫描「approved + unsynced/failed 未超限」的付款申请 → HcsPaymentSyncSupport.pushPayment + ① 特批(如需 C106-3) + ② 履约计划重推 C103-9(新增):聚合合同下 row_op≠none 的计划行凭已存 fdId 行级更新 + │ ├─ 整体操作类型:有 fdExeplanId → update(首次 add 的判断逻辑不变) + │ ├─ 合同尚未推送合规(hcs_fd_id 空)→ 跳过(计划行随合同链路首推) + │ └─ 推送失败 → 抛出,由付款链统一 markFailed → 下轮自动重试 + ③ 履约记录 C108-1/2(原有,开关 thirdParty.hcs.push.perf-record…… 独立) + ④ 成功 markSynced 回填整体状态 +``` + +### 2.2 问题2:付款审批 → 自动推送金蝶付款申请(携带履约计划id) + +``` +付款申请审批通过事务提交 + │ KingdeeBillApprovedEvent(BIZ_PAYMENT_APPLICATION) + ▼ +KingdeeApprovalSyncListener(AFTER_COMMIT,失败不影响审批结果) + │ 前置门禁:付款特批未完成(need/pushing/processing)→ 跳过,特批通过后由 HcsPaymentSyncSupport 重新发布事件补推 + ▼ +syncKingdee → doSyncKingdee(单据级事务,幂等预检 + 特批门禁) + ├─ 正式结算付款(settlement_payment)→ A103-1 应付下推(前置 A102-4 校验应付单已生成) + └─ 预付款(progress_advance/project_advance)→ A103-3 直接保存 + ▼ +分录备注 e_remark = 「履约计划id:{hcs_stage_fd_id}」(+ 原备注) + ├─ 计划行匹配:findPaymentStageRow(contractId, fundNatureOfPayment(paymentType)) + │ 优先按款项性质精确匹配已同步付款行 → 无则取首条已同步付款行兜底(与 C108 口径一致) + └─ 无关联不传 / 查询异常仅告警不阻断推送 + ▼ +成功:回填 kingdeeBillId/No、置 synced,联带锁单(lockBusiData)+ 附件上传(A902) +失败:markKingdeeSyncFailed → 定时重推任务(KingdeeAutoSyncRetryTask)与手动「同步金蝶」兜底(重推同样携带履约计划id) +``` + +### 2.3 履约记录(C108)——合同系统的履约付款数据 + +履约记录是付款申请在合同系统的**履约付款数据载体**:每笔审批通过的付款申请在 HCS 生成一条履约记录,凭 `fd_id`(来源履约计划项ID = 计划行 hcs_stage_fd_id)挂在对应履约计划行下,与 §2.1 履约计划款项金额累加、§2.2 金蝶付款申请携带计划id 共同完成「合同系统关联履约付款数据和履约计划」。 + +**生命周期**(HcsPerfRecordSyncSupport.pushPerfRecord): + +``` +付款申请审批通过(approve 置 hcs_sync_status=unsynced) + │ 付款链定时任务扫描(HcsPaymentSyncSupport.pushPayment 第③步,开关 third-party.hcs.push.perf-record.sync-enabled,默认关) + ▼ +无 hcs_perf_fd_id → C108-1 新建 + │ 返回 fdId 立即回填 hcs_perf_fd_id 并持久化(后续更新全靠它) + ▼ +实际付款推进:金蝶付款结果回写任务(A103-2,PaymentKingdeeResultTask → applyPayApplyResult) + │ paidAmount 变化且已有 hcs_perf_fd_id → 置 hcs_sync_status=unsynced + ▼ +下轮付款链扫描 → C108-2 更新(formData.fdId 必填,响应 success 即成功,幂等可重试) +``` + +**报文字段映射**(formData 动态属性,§5.6): + +| 报文字段 | 取值 | 说明 | +| --- | --- | --- | +| fdApplyAmount | appliedAmount | 申请金额(本笔申请付款金额,字符串传值) | +| fdFactAmount | paidAmount | 实际金额(金蝶实付回写后更新) | +| fdApplyNo【必填】 | paymentNo | 付款申请单号 | +| fdPlanDate【必填】 | applyDate | 计划日期(毫秒时间戳) | +| fdPalnType | paymentType | 计划类型(原文拼写 fd_paln_type 必须沿用) | +| fdProperty | 计划行 fundNature | 款项性质(无关联计划行不传) | +| fd_id | 计划行 hcs_stage_fd_id | **来源履约计划项ID**:履约记录与履约计划的挂接键(无关联不传) | +| fdContractId【必填】 | 合同 hcs_fd_id | HCS 合同ID | +| fdBusinessId | payment.id | TMS 付款申请主键(溯源,v4) | +| fdCreateTime / fdXformId | createTime / 配置 | 创建时间戳、表单模板ID(perf-record.xform-id,未配置不传) | + +**附件转传**(C108-1):开关 `third-party.hcs.attachment.transfer-enabled` 开启且付款申请有附件时,经 C103-2 转传后挂 `fd_plan_att` 实体键;无附件传空列表。 + +**状态与重试**:履约记录与特批、履约计划重推共用付款申请的 `hcs_sync_status` 状态机(unsynced→synced/failed→下轮重试,超限人工重推);C108-2 更新按 fdId 幂等,重试无副作用。手动入口:付款申请页「推送合规/重推」按钮(HcsPaymentSyncSupport.repushPayment,重置 unsynced 清零计数立即推送)。 + +--- + +## 3. 字段与状态口径 + +| 字段 | 口径 | +| --- | --- | +| `blade_contract_exe_stage.fund_amount` | 语义修正为**款项金额**(HCS `fdFundAmount` 文档定义):付款审批通过按申请付款金额累加更新;承运商合同默认进度款计划行金额本就留空,首次累加即等于申请付款金额 | +| `row_op` | `none`(已同步无变更)/ `add`(新行/未同步行)/ `update`(已同步行编辑或款项金额累加)/ `delete`(软删行待上报) | +| `hcs_sync_status`(计划行) | 累加后已同步行置 `unsynced`,`hcs_sync_msg=「付款审批款项金额累加,待重推C103-9」`;C103-9 推送成功后清除标记置 synced | +| `hcs_stage_fd_id` | 合规阶段ID(C103-9 返回),行级 update/delete 必传;金蝶备注携带的「履约计划id」即此值——合同系统(HCS)只认自己的阶段ID,不认 TMS 本地主键 | +| 付款类型→款项性质 | `settlement_payment→progress`;`progress_advance`/`project_advance→prepayment` | + +--- + +## 4. 代码改动清单(2026-10-22) + +| 文件 | 改动 | +| --- | --- | +| `service/impl/ContractExeStageServiceImpl.java` | 新增 `findPaymentStageRow(contractId, fundNature)`(性质精确匹配→首条付款行兜底)与 `accumulatePaidAmountOnApprove(payment)`(原子累加 + 标记重推) | +| `service/IContractExeStageService.java` | 上述两方法接口声明 | +| `service/impl/PaymentApplicationServiceImpl.java` | `approve()` 审批通过事务内调用累加;新增 `bfiExeStageRemark()`,A103-1/A103-3 分录备注携带履约计划id | +| `support/hcs/HcsPaymentSyncSupport.java` | `pushPayment()` 编排插入第②步 `pushExeStageUpdate()`:调 `HcsExeStageSyncSupport.syncExeStages` 重推 C103-9 款项金额累加行 | +| `support/hcs/HcsPerfRecordSyncSupport.java` | C108 计划行匹配收敛到 `IContractExeStageService.findPaymentStageRow`(原"取第一条付款行"升级为先按性质精确匹配) | +| `constant/HcsSyncConstant.java` | 新增 `FUND_NATURE_PREPAYMENT` 常量与 `fundNatureOfPayment()` 映射 | +| `pojo/entity/HcsContractExeStage.java`、`blade-hcs-api/HcsExeplanStageDTO.java`、`HcsExeStageSyncSupport.java` | `fundAmount/fdFundAmount` 注释语义修正为「款项金额」(照 HCS 接口文档) | + +--- + +## 5. 配置与开关(全部复用既有配置,无新增) + +| 配置 | 作用 | 默认 | +| --- | --- | --- | +| `thirdParty.hcs.push.payment.sync-enabled` / `.cron` | 付款链任务(含新增的履约计划重推)开关与调度 | 关 / 每 5 分钟 | +| `thirdParty.hcs.push.perf-record.sync-enabled`(hcsProperties.perfRecord) | C108 履约记录开关(关闭时付款链仅推特批+履约计划重推) | 关 | +| `bfi.auto-sync.payment`(BfiProperties) | 审批通过自动推金蝶付款申请开关 | — | +| `thirdParty.hcs.exe-stage.fund-type` | C103-9 金额类型兜底值 | byAmount | + +--- + +## 6. 边界与联调注意事项 + +1. **预付款不误计**:预付款类型匹配不到预付款计划行时跳过累加(不落入进度款行);若业务要求预付款也强制累加,需另行确认落行规则。 +2. **款项金额不含原计划金额**:累加基于 0 起算(默认承运商进度款计划行金额留空,天然吻合);业务自维护了计划金额的行,累加值不会叠加在计划金额上——语义为「累计申请付款金额」,如需「计划金额+累加」口径需再确认。 +3. **重试自愈**:C103-9 履约计划重推失败 → 付款申请整体置 failed → 下轮付款链重试(C108 走 C108-2 幂等更新,无副作用);重试超限后页面「推送合规/重推」人工兜底。 +4. **竞态窗口**:付款审批早于合同首次 C103-9 同步完成时(极少见),计划行仍为 add 未同步——本次只累加金额不置 update,由合同链路首推携带累加后金额,不会产生重复行。 +5. **金蝶备注长度**:`e_remark` 为金蝶非必传文本字段,拼接「履约计划id + 原备注」后长度上限需联调确认(超长会被金蝶强校验拦截时,可改为只送履约计划id)。 +6. **HCS 侧回显**:款项金额经 C103-9 行级 update 覆盖 HCS 履约计划行金额,HCS 页面展示口径变化需与合规侧确认(原计划金额被累计付款金额覆盖属本需求的预期行为)。 diff --git a/doc/docs/hcs合同/2026-10-22-付款申请作废对履约计划款项金额与履约记录的影响分析与补齐方案.md b/doc/docs/hcs合同/2026-10-22-付款申请作废对履约计划款项金额与履约记录的影响分析与补齐方案.md new file mode 100644 index 00000000..c9a6f572 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-22-付款申请作废对履约计划款项金额与履约记录的影响分析与补齐方案.md @@ -0,0 +1,72 @@ +# 付款申请作废对履约计划款项金额与履约记录的影响分析与补齐方案 + +> 结论先行:**目前付款申请作废不会同步修改 HCS 侧数据**——履约计划行款项金额不回减、履约记录不删除不更新,且作废单因状态不在扫描范围,即使本地置了重推标记也没有推送驱动。本文梳理现状、影响与补齐方案(截至 2026-10-22,配套提交 dcda231c)。 +> 关联文档:`2026-10-22-付款审批履约计划款项金额累加与金蝶付款申请携带履约计划id业务流程梳理.md` + +--- + +## 1. 作废现有处理范围(PaymentApplicationServiceImpl.voidBill) + +| 处理 | 说明 | +| --- | --- | +| ✅ 门禁 | 仅审批通过(approved)可作废;金蝶锁单中(lockBusiData 未解锁)禁止作废 | +| ✅ 本地回滚 | 汇票占用释放、联动作废预结算预付记录、删除项目预付核销行、刷新结算单已付汇总 | +| ✅ 状态 | approvalStatus=voided、currentNode=已作废 | +| ❌ 履约计划行款项金额 | 不回减、不重推 C103-9 | +| ❌ 履约记录(C108) | 不删除、不更新 | +| ❌ 金蝶付款申请单 | 不联动金蝶删单(既有行为,另行评估) | + +--- + +## 2. 影响分析 + +| # | 数据 | 作废后状态 | 影响 | +| --- | --- | --- | --- | +| 1 | 履约计划行款项金额(本地 fund_amount) | 审批时累加的申请金额**永久留存** | 本地计划行金额虚高 | +| 2 | HCS 履约计划行(fdFundAmount) | 已推送的累加金额**不会被修正** | 合规侧计划金额虚高,且本地后续修正也无法触达(见 §3) | +| 3 | 履约记录(C108) | 已新建的记录**继续挂在 HCS 计划行下**,实付金额口径也不再更新 | 合规侧展示已作废付款的履约付款数据 | + +--- + +## 3. 连锁问题:作废单没有重推驱动 + +付款链定时任务扫描条件为 `approvalStatus = approved`(不含 voided/returned)。作废后该单永远不再进入扫描: + +- 本地计划行即使置 `row_op=update`,也不会被付款链推送; +- 只能等**同合同下一笔付款审批**(pushPayment 第②步会捎带重推该合同全部待同步计划行)、或合同页手动重推时捎带; +- 长期无付款的合同,错误金额将一直停留在 HCS。 + +--- + +## 4. 补齐方案 + +### 4.1 履约计划款项金额回减(建议实施) + +1. **回减**:`voidBill` 事务内,按审批累加的同一匹配规则(合同 + performType=payment + 款项性质精确匹配)找到计划行,`fund_amount = coalesce(fund_amount,0) - applied_amount` 原子回减(防并发);已同步行置 `row_op=update` + 待同步。 +2. **重推驱动**:作废单进不了付款链扫描,需新增轻量事件(如 `HcsPaymentVoidedEvent`)+ AFTER_COMMIT 监听器,事务提交后直接调 `HcsExeStageSyncSupport.syncExeStages(contract)` 重推 C103-9(复用既有行级 update 凭 fdId 更新链路,失败仅告警落库待补偿)。 +3. **回减为负保护**:若计划行金额已不足回减(如同行金额被业务编辑过),按 0 兜底并在日志/同步信息中标注,避免推送负数金额。 + +### 4.2 履约记录(受接口限制,待外部确认) + +C108 仅有新建(C108-1)/更新(C108-2)接口,**无删除/作废接口**: + +- 程序上无法清除 HCS 侧已生成的履约记录; +- 可选过渡口径(需与合规侧确认):a) 合规侧人工删除;b) HCS 扩展作废接口后 TMS 对接;c) C108-2 更新金额为 0/备注作废标识(语义是否被合规侧接受待确认)。 + +--- + +## 5. 待确认事项 + +| # | 事项 | 确认方 | +| --- | --- | --- | +| 1 | 作废是否需要回减履约计划款项金额(还是保留作为"历史发生额") | 业务 | +| 2 | C108 履约记录作废/删除的合规侧处理方式(人工/接口扩展/更新为0) | 合规侧 | +| 3 | 金蝶付款申请单是否需要联动作废/删单(现状:不联动) | 业务+财务 | +| 4 | 已作废单若金蝶侧已实付(先付款后作废的极端场景),回减与实付口径冲突如何处理 | 业务 | + +--- + +## 6. 临时兜底(方案实施前的人工处理) + +1. 发现作废单导致计划行金额虚高:合同履约规则页编辑该行金额(自动置 update),随后同合同任一付款审批或合同手动重推捎带 C103-9 修正; +2. HCS 侧履约记录:联系合规管理员人工删除。 diff --git a/doc/docs/hcs合同/2026-10-22-收款认领推送金蝶完成后同步合同合规系统(C103-9履约计划-C107-1认领记录-C108-1履约流水)业务流程梳理.md b/doc/docs/hcs合同/2026-10-22-收款认领推送金蝶完成后同步合同合规系统(C103-9履约计划-C107-1认领记录-C108-1履约流水)业务流程梳理.md new file mode 100644 index 00000000..47071b63 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-22-收款认领推送金蝶完成后同步合同合规系统(C103-9履约计划-C107-1认领记录-C108-1履约流水)业务流程梳理.md @@ -0,0 +1,187 @@ +# 收款认领推送金蝶完成后同步合同合规系统(C103-9 履约计划 + C107-1 认领记录 + C108-1 履约流水)业务流程梳理 + +> 需求:金蝶认领推送(A301-3)完成后,把认领记录推送给合同合规系统(HCS)——① C103-9 更新合同履约计划(认领金额累加到收款计划行并重推);② C107-1 新增认领记录;③ C108-1 新增履约流水。开关复用既有配置(exe-stage / perf-record),不单独新增。 +> 关键口径:C107-1 的 `fdCollectionFlow.fdId` 为**合规系统自己的收款流水ID**(非 TMS 本地主键),经 C104-2 按明细流水号定位(合规侧流水由合规自 ERP 同步,与 TMS A301-2 镜像同源);C107-1/C108-1 引用的履约项ID 均为 C103-9 返回的阶段ID(hcs_stage_fd_id)。 +> 代码改动日期:2026-10-22,`mvn compile` 通过。 + +--- + +## 1. 业务逻辑总览 + +| # | 规则 | 说明 | +| --- | --- | --- | +| 1 | 触发点唯一:A301-3 推送成功之后 | 挂在 `pushClaimLocked`(金蝶认领推送事务模板)返回成功后执行,手动「推送金蝶」按钮与定时任务两条路共用;金蝶推送事务已提交,合规推送失败不影响金蝶推送结果 | +| 2 | 开关复用既有配置(不新增) | `exe-stage.sync-enabled`(履约计划同步,与合同链 C103-9 同开关)门禁 C103-9/C107-1;`perf-record.sync-enabled`(履约记录同步,与付款链 C108 同开关)门禁 C108-1。exe-stage 关闭时整个认领链不执行、零开销 | +| 3 | 按合同分组推送 | 有效认领分摊行(`blade_receipt_claim_settlement` status=1 且未删)经结算单 `contract_id` 归集,逐合同推送;**单合同失败不影响其余合同** | +| 4 | 编排顺序:C103-9 → C107-1 → C108-1 | 先更新履约计划(认领金额累加重推),因为 C107-1 的 `fdExePlanStageId` 与 C108-1 的 `fd_plan_id` 都引用 C103-9 返回的阶段ID(与付款链 C103-9→C108 同口径) | +| 5 | 认领金额累加口径 | `coalesce(fund_amount, 0) + 认领金额` 原子累加到收款计划行(防并发丢更新);已同步行置 `row_op=update` 凭阶段ID 重推 C103-9,未同步行保持 `add` 待合同链路首推(首推即携带累加后金额)——与付款审批累加逻辑完全对称 | +| 6 | 只推未作废认领 | 认领主单 `claim_status=claimed` 才推;作废认领的分摊行在作废时已置 status=0,天然排除。且已推送金蝶的认领禁止作废(voidClaim 闸门),已同步合规的认领不存在后续被作废的场景 | +| 7 | 合同未推送合规降级 | `hcs_fd_id` 为空时只累加本地计划行金额,不推 C103-9/C107-1 的合同引用/C108-1,随合同链路首推补齐 | +| 8 | 失败只留痕不重试 | 认领链任何一步失败仅 error 日志,**无自动重试、无失败状态落库**;且金蝶推送 `push_status=pushed` 为终态闸门,重推金蝶不会再触发合规链(见 §6 边界) | + +--- + +## 2. 业务流程 + +### 2.1 全链路(业务视角) + +``` +金蝶认领公告同步(A301-2)→ 流水落库 blade_kingdee_receipt_flow + ▼ +用户认领(选应收正式结算单 + 分摊金额)→ 认领主单 + 分摊行落库,流水累计认领金额 + ▼ +推送金蝶 A301-3(手动「推送金蝶」按钮 / 定时任务 ReceiptClaimPushTask,10分钟) + │ 闸门:公告id存在 + 未推送过(pushed 终态)+ 认领完成(claimed)+ 公告对账一致 + ▼ +A301-3 推送成功(push_status=pushed、回填金蝶收款单号)★事务已提交 + ▼ +HCS 收款认领链(开关 exe-stage.sync-enabled 门禁 C103-9/C107-1、perf-record.sync-enabled 门禁 C108-1,失败仅日志) + ├─① 归集:有效分摊行 × 结算单 → 按合同分组(合同id → 认领主单id → 金额) + └─② 逐合同推送(单合同失败 continue 下一合同) + ├─ C103-9 更新履约计划:认领金额累加收款计划行 → 已同步行重推 + ├─ C104-2 定位合规收款流水ID(明细流水号精确匹配,一次定位全程共用;失败仅跳过 C107-1) + ├─ C107-1 认领记录 × N(按「认领主单 × 合同」分组,每组合一条) + └─ C108-1 履约流水 × 1(perf-record 开关开启时;须有已同步收款计划行提供 fd_plan_id) +``` + +### 2.2 代码视角(HcsClaimSyncSupport.pushClaimToHcs) + +``` +ReceiptFlowServiceImpl.pushClaimLocked(flowId, announce) + │ transactionTemplate.execute(doPushClaimToKingdee) ← 金蝶推送事务 + │ result.isSuccess() ? + ▼ yes +hcsClaimSyncSupport.pushClaimToHcs(flowId) + ├─ exe-stage.sync-enabled 关闭 → return(静默,整个认领链不执行) + ├─ try doPushClaimToHcs(flowId) catch → log.error(不外抛) + │ + ├─ 查询数据(流水重读 + 认领主单 claimed + 分摊行 status=1/is_deleted=0) + ├─ 结算单批量 selectBatchIds → 合同id 归集(LinkedHashMap 保序) + ├─ locateHcsCollectionFlowId:C104-2 按 detail_serial_no 查 fdId + │ └─ 异常/查不到 → null,后续 C107-1 跳过(C103-9/C108-1 照常) + │ + └─ for each contractId: + ├─ accumulateClaimedAmountOnPush(contractId, 合同累计认领金额) + │ ├─ findCollectionStageRow(progress 精确匹配 → 首条已同步收款行兜底) + │ ├─ fund_amount 原子累加(setSql coalesce) + │ └─ 行已同步 → row_op=update + unsynced("收款认领金额累加,待重推C103-9") + ├─ 合同已推送合规(hcs_fd_id 非空)→ syncExeStages 重推 C103-9(无待同步行返回提示非失败) + ├─ C107-1 × 认领主单数:claimRecordAdd(流水引用+合同+阶段ID+认领人+金额+日期+备注,fdStatus=0) + └─ C108-1 × 1:perfRecordAdd(perf-record 开关开启时;须有已同步收款计划行提供 fd_plan_id,否则跳过) +``` + +--- + +## 3. 接口清单(本次涉及 HCS 接口) + +| 接口 | Feign 方法 | 路径 | 触发时机 | 说明 | +| --- | --- | --- | --- | --- | +| C103-9 履约规则同步 | `exeplanAdd` | `/openapi/eop-exeplan/lcode/ext-eop-exeplan/extEopExeplanMainOpen/syncExeplan` | 认领金额累加后(合同已推送合规时) | 复用既有 `HcsExeStageSyncSupport.syncExeStages`,聚合 row_op≠none 行整体 update | +| C104-2 查询收款流水列表 | `collectionFlowList`(**新增**) | `/openapi/eop-data/eopDataCollectionFlowOpen/list` | C107-1 推送前一次 | conditions `fdDetailSerialNumber`=流水明细流水号 → 取合规 `fdId` | +| C107-1 新增认领记录 | `claimRecordAdd` | `/openapi/eop-data/eopDataCollectionFlowClaimRecordOpen/add` | 每个认领主单 × 合同一条 | 必填:fdCollectionFlow.fdId、fdContractNumber、fdClaimAmount;fdStatus 固定 0 | +| C108-1 履约记录新建 | `perfRecordAdd` | `/openapi/sys-modeling/apis/data/km-pay-receive/V1/dataSync/add` | 每合同一条 | 必填:fd_apply_no、fd_contract_id、fd_plan_date、**fd_plan_id**(接口新版必填,取收款计划行阶段ID) | + +> Feign 降级工厂与本地 Mock 均为通用代理实现,新增方法零改动;本地联调可在 mock 目录放 `collectionFlowList.json` / `claimRecordAdd.json` / `perfRecordAdd.json` 桩文件(有桩走桩、无桩透传真实接口)。 + +### 3.1 C107-1 请求字段映射 + +| HCS 字段 | TMS 取值 | 说明 | +| --- | --- | --- | +| `fdCollectionFlow.fdId` | C104-2 按流水 `detail_serial_no` 定位的合规流水ID | 【必填】合规侧收款流水,非 TMS 主键 | +| `fdContract` | 合同 `hcs_fd_id` + `contract_name` | 合规合同引用 | +| `fdContractNumber` | 合同 `contract_no` | 【必填】须在合规本地存在 | +| `fdExePlanStageId` | 收款计划行 `hcs_stage_fd_id`(无已同步行不传) | 对应履约项 | +| `fdFundNature` | 收款计划行 `fund_nature`(无行不传) | 认领款项类型 | +| `fdClaimer` / `fdClaimerName` | 认领主单 `claimer_name`(仅名称引用) | TMS 与合规用户不同源(§9-U2) | +| `fdClaimerDept` / `fdClaimerDeptName` | 认领主单 `claimer_dept_name` | 同上 | +| `fdClaimAmount` | 该认领主单分摊到本合同的金额合计 | 【必填】>0 | +| `fdClaimDate` | 认领主单 `claim_date`(毫秒时间戳) | | +| `fdRemark` | 认领主单 `remark` | | +| `fdStatus` | 固定 0(已认领) | 作废=1 本期未接 | + +### 3.2 C108-1 请求字段映射(dynamicProps) + +| HCS 字段 | TMS 取值 | 说明 | +| --- | --- | --- | +| `fd_apply_no` | 流水 `receipt_notice_no`(兜底 `detail_serial_no`) | 【必填】申请编号=收款通知单号 | +| `fd_apply_amount` / `fd_fact_amount` | 该合同累计认领金额(字符串传值) | 申请=实际(认领即到账口径) | +| `fd_plan_date` | 名下认领主单最近 `claim_date`(毫秒) | 【必填】履约日期 | +| `fd_paln_type` | 固定 `collection` | 履约类型(原文拼写 paln) | +| `fd_property` | 收款计划行 `fund_nature` | 履约性质 | +| `fd_id` / `fd_plan_id` | 收款计划行 `hcs_stage_fd_id` | 履约计划id【必填】,无已同步行整条跳过 | +| `fd_jd_doc_no` | 流水 `kingdee_bill_no`(A301-3 返回收款单号) | 金蝶单据号溯源 | +| `fd_contract_id` | 合同 `hcs_fd_id` | 【必填】 | +| `fd_business_id` | 流水主键 `id` | 溯源 | +| `fd_xform_id` | 配置 `thirdParty.hcs.perf-record.xform-id`(未配置不传) | 表单模板 | +| `mechanisms.attachment` | 空列表 | C108-1 必填节点;认领附件暂不转传 | + +--- + +## 4. 代码改动清单(2026-10-22) + +| 文件 | 改动 | +| --- | --- | +| `support/hcs/HcsClaimSyncSupport.java`(新增) | 认领链编排:归集分组 → C103-9 累加重推 → C104-2 定位 → C107-1 × N → C108-1 × 1;开关守卫 + 异常内吞(不影响金蝶推送) | +| `service/impl/ReceiptFlowServiceImpl.java` | `pushClaimLocked` 金蝶推送事务成功后挂钩 `pushClaimToHcs`(手动/定时共用);注入 support | +| `feign/IHcsClient.java` | 新增 C104-2 `collectionFlowList` 与路径常量 | +| `dto/HcsCollectionFlowVO.java`(新增) | C104-2 分页行出参 | +| `dto/HcsPerfRecordPropsDTO.java` | 补充 `fd_plan_id`(fd_plan_id)、`fdJdDocNo`(fd_jd_doc_no)——接口文档新版必填/新增字段 | +| `config/HcsProperties.java` | 无改动(认领链复用既有 `exe-stage` / `perf-record` 开关,未新增配置项) | +| `service/IContractExeStageService.java` + Impl | 新增 `findCollectionStageRow`(progress 精确匹配→首条收款行兜底)与 `accumulateClaimedAmountOnPush`(原子累加+标记重推) | +| `doc/nacos/third-party-api.yaml`、`blade-dev.yaml` | 无改动(复用既有 exe-stage / perf-record 开关,未新增配置项) | + +> 无数据库表结构变更、无新增定时任务、无新增页面。 + +--- + +## 5. 操作过程 + +### 5.1 前置条件(联调/生产启用前逐项确认) + +| # | 条件 | 不满足时的表现 | +| --- | --- | --- | +| 1 | 合同已推送合规(`blade_contract_manage.hcs_fd_id` 非空)且 C103-9 计划行已同步 | 只累加本地金额不推合规;C108-1 跳过(缺 fd_plan_id) | +| 2 | 合同下有**已同步**收款履约计划行(performType=collection,优先 progress) | 金额不累加、C107-1 不带履约项、C108-1 跳过 | +| 3 | 合规侧已存在同明细流水号的收款流水(合规自 ERP 同步) | C104-2 查不到 → 仅 C107-1 跳过 | +| 4 | HCS 配置可用(baseUrl/鉴权头/xform-id 等,与付款链共用) | 对应接口失败,error 日志留痕 | +| 5 | 开关:`exe-stage.sync-enabled=true`(C108-1 另需 `perf-record.sync-enabled=true`) | exe-stage 关→整链不执行;perf-record 关→仅跳过 C108-1 | + +### 5.2 开关配置(复用既有配置,无新增配置项) + +```yaml +thirdParty: + hcs: + # 履约规则行级同步开关:门禁认领链 C103-9 更新履约计划 + C107-1 认领记录(当前环境已开) + exe-stage: + sync-enabled: ${HCS_EXE_STAGE_SYNC_ENABLED:true} + # 履约记录同步开关:门禁认领链 C108-1 履约流水(一期暂不对接,默认关) + perf-record: + sync-enabled: ${HCS_PERF_RECORD_SYNC_ENABLED:false} +``` + +与合同链 C103-9、付款链 C108 共用同一开关,语义一致:exe-stage 开=履约计划同步对接;perf-record 开=履约记录同步对接。代码内配置键为 `third-party.hcs.exe-stage.sync-enabled` / `third-party.hcs.perf-record.sync-enabled`(横线小写),YAML 驼峰经宽松绑定等价生效。 + +### 5.3 业务操作步骤 + +1. 收款流水页「同步流水」(A301-2)拉取认领公告; +2. 流水认领(选应收正式结算单、分摊金额); +3. 「推送金蝶」手动推送,或等定时任务(10 分钟一轮)自动推送; +4. A301-3 成功后系统自动推合规,无需人工操作;日志关键字 `HCS收款认领链` / `HCS认领记录推送` / `HCS履约流水`。 + +### 5.4 失败排查 + +- 看应用日志 `HCS收款认领链推送失败`(含 contractId)与 `HCS收款流水定位失败`,失败原因随 HCS 响应 msg/traceId 记录; +- **无自动重试**:修复问题后无法通过「重推金蝶」重新触发(pushed 终态闸门拦截);数据修正需人工在合规侧补录,或后续迭代增加按流水的合规重推入口。 + +--- + +## 6. 边界与注意事项 + +1. **失败无重试、无状态落库**(本期 shortcut):认领链失败仅日志,金蝶推送终态后不可重触发;如需闭环,后续给流水加 hcs 同步状态列 + 定时补偿(照付款链模式)。 +2. **认领作废与合规数据天然隔离**:已推送金蝶的认领禁止作废(`voidClaim` 闸门:「该认领已推送金蝶,不允许作废」),而认领链仅在推送金蝶成功后触发——已同步合规的认领不可能再被作废,无需 C107-3 作废同步;作废只发生在推送金蝶前(认领链未触发),仅在本地回退(认领主单置 voided、分摊行置无效、流水累计金额/认领状态重算、结算单收款汇总刷新)。若未来放开「推送后作废」,需先持久化 C107-1/履约流水 fdId 再接 C107-3。 +3. **认领附件未转传**:C108-1 的 mechanisms.attachment 传空列表;需附件时接 C103-2 转传(照付款链 HcsAttachmentSupport 模式)。 +4. **款项性质无来源**:认领分摊不区分款项性质,累加固定落在 progress(精确匹配)→ 首条收款行(兜底);若合同收款计划含预付款行,认领金额不会计入预付款行——业务要求分性质拆分时需认领页增加性质字段。 +5. **C103-9 覆盖口径与付款链一致**:认领金额累加覆盖 HCS 计划行款项金额(原计划金额被累计认领金额覆盖属预期行为),需与合规侧确认展示口径。 +6. **金额口径**:C107-1/C108-1 金额为「分摊/累计认领金额」(2 位小数,随认领校验规则);同一流水多次认领后一次性推送金蝶,认领链按推送时点的全量有效分摊归集,不区分历史认领主单先后。 +7. **合规流水定位依赖明细流水号一致**:C104-2 按 `fdDetailSerialNumber` 精确匹配(与 A301-2 detailid 同源),合规侧同步口径变化(如明细流水号为空)会导致 C107-1 全部跳过。 +8. **并发安全**:金蝶推送侧已有行锁 + 终态闸门防双推,认领链单实例单流水触发一次;计划行累加为 SQL 原子操作,多流水并发认领不丢更新。 diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md new file mode 100644 index 00000000..808af093 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md @@ -0,0 +1,714 @@ +# HCS 合同管理操作手册 + +> **文档日期**:2026-10-23 +> **适用系统**:运输板块数字化管理平台(TMS) +> **适用对象**:合同经办人、合同负责人、合同审批人、合规管理员 +> **文档目的**:介绍合同新增、审批、变更、附件归档及 HCS 合规同步的完整操作方法 +> **截图说明**:本文截图取自测试环境,合同编号、客商名称、金额等均为测试数据;不同环境因角色权限、功能开关不同,页面按钮和状态可能略有差异,以实际页面为准。 + +--- + +## 1. 先了解合同管理与 HCS + +### 1.1 合同管理负责什么 + +合同管理模块用于管理运输业务合同的完整生命周期: + +1. **起草合同**:录入合同基本信息、计费方案、结算规则、付款比例及附件。 +2. **合同审批**:支持临时合同审批和正式合同审批。 +3. **合规同步**:合同审批通过后,将合同推送至 HCS 合同合规系统;正式合同及合同变更是合规同步的主要场景。 +4. **合同变更**:正式合同信息发生变化时,通过变更流程修改并重新审批。 +5. **附件归档**:上传用印材料、双章归档文件、特批附件等材料。 + +### 1.2 关键名词 + +| 名词 | 说明 | +| --- | --- | +| HCS | 桂物合同合规系统,负责合同合规校验、客商风险校验、合同特批及金蝶同步 | +| 合规状态 | 合同推送 HCS 的结果,分为“待推送”“已推送”“推送失败” | +| 合规合同ID | 合同成功推送 HCS 后,由 HCS 返回的合同唯一标识 | +| 金蝶合同ID | 合同在 HCS 完成合规及金蝶同步后,回写到 TMS 的金蝶合同编号 | +| 特批 | 高风险或黑名单客商合同,需经集团特批后才能完成合规流程 | +| 临时合同 | 用于提前生效的合同,审批通过后有 **30 天** 临时效力 | +| 正式合同 | 完成正式审批、可长期履约的合同,是 HCS 合规同步与归档的主要对象 | +| 合同变更 | 正式合同内容发生变化时发起的变更审批流程 | + +### 1.3 使用前必须知道的规则 + +- 页面中带 `*` 的字段为必填项;保存草稿时可先填写部分信息,提交临时合同或正式合同前需补全。 +- 合同编号可不填写,系统会按合同负责部门配置的规则自动生成;如业务要求手工编号,可直接填写。 +- 临时合同审批通过后有效期为 **30 天**(自提交临时合同审批之日起算),到期前应及时转为正式合同。 +- 高风险、黑名单客商合同,**提交审批前必须上传“特批附件”**,否则系统会阻止提交。 +- 合同审批通过后,系统会将合同推送 HCS;正式合同与合同变更是合规同步、归档的主要场景。是否自动推送由环境开关决定,也可在页面手动推送。 +- 合同已推送 HCS 后,如需修改合同信息,必须走“变更”流程,**不要重复新增合同**,否则会在合规系统产生重复合同。 +- 合同列表默认按创建时间倒序排列。 + +--- + +## 2. 业务流程总览 + +### 2.1 合同全生命周期 + +```mermaid +flowchart TD + A["新增合同"] --> B["保存草稿"] + B --> C["提交临时合同"] + C --> D["临时合同审批"] + D -->|驳回| B + D -->|通过| E["临时合同生效
30 天临时效力"] + B --> F["提交正式合同"] + E -->|转正式合同| F + F --> G["正式合同审批"] + G -->|驳回| E + G -->|通过| H["推送 HCS 合规系统"] + H --> I{"客商风险校验"} + I -->|正常| J["合规状态:已推送
回填合规合同ID"] + I -->|高风险/黑名单| K["特批中
等待合规特批结果"] + K -->|通过| J + K -->|驳回| L["合同审批驳回
修改后重新提交"] + J --> M["签订 / 用印 / 双章归档"] + M --> N["发起变更 → 变更审批 → HCS 更新"] +``` + +### 2.2 一句话理解流程 + +1. **先起草**:新增合同,填写基本信息、计费结算、附件。 +2. **再审批**:可先提交临时合同快速生效,也可以直接提交正式合同。 +3. **后合规**:合同审批通过后,系统自动/手动推送 HCS,高风险客商进入特批。 +4. **有变化走变更**:正式合同内容变化时发起变更,变更审批通过后更新 HCS。 + +> 上图和流程以正式合同办理为主线。临时合同审批通过后,系统同样会置为待推送并尝试推送 HCS;正式合同与合同变更审批通过是合规同步、归档和金蝶回写的关键节点。 + +--- + +## 3. 进入合同管理 + +登录系统后,在左侧菜单依次点击: + +**合同管理 → 合同管理** + +![合同列表](images/01-合同列表.png) + +> 合同管理列表页 + +列表页由上到下分为四部分: + +1. **搜索区**:按合同编号、名称、项目、状态等条件查询。 +2. **到期标签**:全部、30天内到期、90天内到期、90天以上到期、已到期。 +3. **工具栏**:新增、批量导出、批量推送合规。 +4. **合同列表**:展示合同及操作入口。 + +--- + +## 4. 查询合同 + +### 4.1 搜索条件 + +在搜索区填写条件后点击“查询”;点击“重置”清空条件。搜索条件超过一行时,可点击“展开/收起”查看全部条件。 + +![合同列表筛选](images/03-合同列表筛选.png) + +> 合同搜索区 + +| 搜索项 | 说明 | +| --- | --- | +| 签约类型 | 新签、续签、补充协议 | +| 合同类别 | 客户合同、承运商合同、补充协议(对应新增页面的“合同类型”) | +| 所属项目 | 选择合同关联的项目 | +| 是否同步合规 | 是 / 否 | +| 合同名称 | 支持模糊查询 | +| 是否同步金蝶 | 是 / 否 | +| 合同编号 | 支持模糊查询 | +| 合规状态 | 待推送 / 已推送 / 推送失败 | +| 生效类型 | 未生效 / 临时生效 / 正式生效 / 已过期 | +| 归档状态 | 未归档 / 已归档 | +| 审核状态 | 草稿 / 审批中 / 审批通过 / 已驳回 / 已撤回 / 变更审批中 / 变更审批通过 / 变更驳回 | +| 合同负责部门 | 选择部门 | +| 合同阶段 | 草稿 / 临时合同 / 正式合同 / 已终止 | + +### 4.2 到期标签 + +列表上方提供 5 个到期标签,标签后的数字为对应合同数量: + +- **全部**:全部合同。 +- **30天内到期**:合同结束日期在未来 30 天内。 +- **90天内到期**:合同结束日期在未来 31~90 天内。 +- **90天以上到期**:合同结束日期在 90 天以后。 +- **已到期**:合同结束日期已过。 + +点击标签即可筛选对应合同。 + +### 4.3 列表字段 + +| 字段 | 说明 | +| --- | --- | +| 合同编号 / 合同名称 | 合同唯一编号和名称,点击名称可打开详情 | +| 所属项目 / 合同负责部门 | 合同归属项目及负责部门 | +| 合同类别 / 签约类型 | 合同业务分类及签约方式 | +| 甲方 / 乙方 | 合同双方 | +| 合同金额 | 合同含税金额 | +| 合同阶段 | 草稿 / 临时合同 / 正式合同 / 已终止 | +| 审核状态 | 当前审批状态 | +| 归档状态 | 未归档 / 已归档 | +| 合规状态 | 待推送 / 已推送 / 推送失败;鼠标悬停可查看失败原因 | +| 金蝶合同ID | 合规同步完成后回写的金蝶合同编号 | +| 当前节点 / 当前处理人 | 当前审批节点及处理人 | +| 创建人 / 创建时间 | 合同创建信息 | +| 操作 | 查看、编辑、复制、变更、附件上传、推送合规等 | + +### 4.4 操作列说明 + +![合同列表状态与操作列](images/02-合同列表状态与操作列.png) + +> 合同列表中的状态与操作列(示例) + +操作列会根据合同状态、当前登录人权限动态显示: + +| 合同状态 | 常见操作 | +| --- | --- | +| 草稿 | 查看、编辑、复制、删除 | +| 临时合同 - 审批中 | 查看、撤回、通过/驳回(审批人)、流程 | +| 临时合同 - 审批通过 | 查看、复制、转正式合同、流程 | +| 临时合同 - 已驳回 | 查看、编辑、复制、重新提交转正 | +| 正式合同 - 审批中 | 查看、撤回、通过/驳回(审批人)、流程 | +| 正式合同 - 审批通过 / 变更审批通过 | 查看、复制、变更、附件上传、流程、推送合规(未同步时) | +| 正式合同 - 变更审批中 | 查看、通过/驳回(审批人)、撤回变更、流程 | +| 正式合同 - 变更驳回 | 查看、编辑变更内容、重新提交变更 | + +> 说明: +> - “复制”会以当前合同为模板打开新增页面,便于快速新建相似合同。 +> - 只有草稿状态的合同可以删除。 +> - 只有审批通过且未同步合规的合同显示“推送合规”。 + +--- + +## 5. 新增合同 + +### 5.1 新增入口 + +在合同列表页点击左上角 **新增** 按钮,进入“新增合同管理”页面。 + +![新增合同-基本信息](images/04-新增合同-基本信息.png) + +> 新增合同页面 + +### 5.2 填写基本信息 + +按照页面从上到下依次填写: + +| 字段 | 填写说明 | +| --- | --- | +| 签约类型 `*` | 选择新签、续签或补充协议 | +| 合同编号 | 不填写时按部门规则自动生成;也可手工录入 | +| 合同名称 `*` | 最多 100 个字符 | +| 合同类型 `*` | 选择客户合同、承运商合同或补充协议 | +| 项目 `*` | 选择合同关联的项目 | +| 合同负责部门 `*` | 选择负责该合同的部门 | +| 甲方 `*` | 选择甲方客商 | +| 乙方 `*` | 选择乙方客商 | +| 签订日期 `*` | 选择合同签订日期 | +| 申请日期 | 提交正式合同时系统自动填充为当天,无需手工填写 | +| 合同期限 `*` | 填写合同开始日期和结束日期 | +| 回款账期 | 填写回款账期天数 | +| 合同金额 `*` | 填写合同金额,不能小于 0 | +| 是否范本 | 选择“是/否” | +| 原件合同编号 | 如有原件合同,填写原件编号 | +| 是否电子章 | 选择“是/否” | +| 开票周期 | 填写开票周期天数 | +| 结算币种 `*` | 选择人民币等结算币种 | +| 合同格式 `*` | 选择合同格式 | +| 一式 | 填写合同份数 | +| 是否需要加盖法人章 `*` | 选择“是/否” | +| 结算方式 `*` | 选择先票后款、先款后票、月结等 | +| 经办人 | 自动带出当前登录人,不可修改 | +| 合同主要内容 | 填写合同主要条款,最多 1000 字 | +| 其他事项说明 | 填写补充说明,长度以页面提示为准 | + +> 提示:保存草稿时系统仅校验合同名称、合同类型、结算币种等基础信息;提交临时合同或正式合同时会校验更多必填项,请以页面提示为准。 + +### 5.3 上传用印材料 + +在“用印材料”区域: + +1. 点击表格下方的 **上传附件**。 +2. 选择附件类型:用印材料、双章归档文件、其他文件。 +3. 上传文件,支持 pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip,单个文件不超过 50MB。 +4. 上传完成后可在表格中查看文件名、大小、上传人、上传时间。 +5. 需要下载时,勾选附件后点击 **批量下载**。 + +> 说明:附件类型选择“双章归档文件”并上传后,合同审批通过时归档状态会自动变为“已归档”。 + +### 5.4 填写计费与结算 + +继续向下填写以下区域: + +![新增合同-计费结算与附件](images/05-新增合同-计费结算与附件.png) + +> 计费、结算、付款比例与附件区域 + +**计费信息** + +- 费用生成模式:系统生成 / 账单导入生成。 +- 点击 **添加** 新增计费方案,填写方案名称、运输方式。 + +**结算单规则** + +- 分为“预结算配置”和“正式结算配置”两个页签。 +- 每个页签可设置是否自动生成结算单(开启 / 关闭)。 + +**付款比例设置** + +- 点击 **添加**,填写付款笔数、付款比例上限(%)、备注。 + +**附件(佐证材料等)** + +- 用于上传佐证材料、特批附件等。 +- 附件类型:特批附件、其他文件。 +- 高风险/黑名单客商合同必须在此处上传类型为 **特批附件** 的文件。 + +### 5.5 选择提交方式 + +页面底部提供 4 个按钮: + +| 按钮 | 适用场景 | 提交后状态 | +| --- | --- | --- | +| 关闭 | 放弃本次填写 | 不保存 | +| 保存 | 信息尚未填全,先暂存 | 合同阶段为“草稿”,审核状态为“草稿” | +| 提交临时合同 | 需要合同尽快生效 | 合同阶段为“临时合同”,进入临时合同审批 | +| 提交正式合同 | 合同信息完整,直接走正式审批 | 直接进入正式合同审批 | + +> 说明: +> - 提交临时合同前,至少需填写签约类型、合同名称、合同类型、所属项目、合同负责部门、甲方、乙方、合同金额。 +> - 提交正式合同前,需完整填写合同期限、签订日期、结算方式、合同格式、合同金额等字段,并完成计费方案、结算规则、付款比例校验。 +> - 无论选择哪种提交方式,高风险/黑名单客商合同都必须先上传“特批附件”。 + +--- + +## 6. 合同审批与状态 + +### 6.1 审批人操作 + +审批人在合同列表操作列进行审批: + +| 操作 | 说明 | +| --- | --- | +| 通过 | 审批通过;系统会尝试推送合规(是否即时推送由环境配置决定),请稍候 | +| 驳回 | 审批驳回,合同退回经办人修改 | +| 撤回 | 提交人可在审批中撤回;普通审批撤回后为“已撤回”,可修改后重新提交 | +| 流程 | 查看审批流程;如提示“流程实例为空”,说明该合同未生成外部流程实例,以审核状态为准 | + +> 重要:点击“通过”后,系统会尝试推送 HCS(是否即时推送由环境配置决定),页面会显示“正在推送合规,请稍候…”。如果提示 **“审批通过,但合规推送失败:原因”**,表示合同审批已通过,但合规推送失败,请按第 11 章处理,不要重复审批;如未开启即时推送,可在合同列表手动点击“推送合规”。 + +### 6.2 合同阶段说明 + +| 合同阶段 | 含义 | +| --- | --- | +| 草稿 | 已保存但未提交审批 | +| 临时合同 | 正在走临时合同审批,或临时合同已审批通过(30 天临时效力) | +| 正式合同 | 正在走正式合同审批,或正式合同已审批通过 | +| 已终止 | 合同已终止(当前版本前端暂未开放终止入口) | + +### 6.3 审核状态说明 + +| 审核状态 | 含义 | +| --- | --- | +| 草稿 | 尚未提交审批 | +| 审批中 | 临时合同或正式合同审批中 | +| 审批通过 | 审批完成 | +| 已驳回 | 审批被驳回,需修改后重新提交 | +| 已撤回 | 提交人主动撤回,可修改后重新提交 | +| 变更审批中 | 合同变更审批中 | +| 变更审批通过 | 合同变更审批完成 | +| 变更驳回 | 合同变更被驳回,可修改后重新提交变更 | + +### 6.4 其他状态说明 + +| 状态 | 说明 | +| --- | --- | +| 归档状态:未归档 | 尚未上传双章归档文件 | +| 归档状态:已归档 | 已上传双章归档文件,合同审批通过后自动置为已归档 | +| 合规状态:待推送 | 合同已审批通过,等待推送 HCS | +| 合规状态:已推送 | 合同已成功提交至 HCS,已回填合规合同ID | +| 合规状态:推送失败 | 推送 HCS 失败,鼠标悬停合规状态可查看失败原因 | +| 当前节点:特批审核 | 高风险/黑名单合同正在等待合规特批结果 | + +--- + +## 7. 临时合同转正式合同 + +### 7.1 适用条件 + +- 合同阶段为“临时合同”,审核状态为“审批通过”。 +- 临时合同被驳回时,可先编辑修改,再点击“重新提交转正”。 + +### 7.2 操作步骤 + +1. 在合同列表找到临时合同。 +2. 点击操作列的 **转正式合同**。 +3. 在确认弹窗中点击“确定”。 +4. 系统提示“已提交正式合同审批”。 + +### 7.3 提交后的变化 + +- 合同阶段变为“正式合同”。 +- 审核状态变为“审批中”。 +- 当前节点为“正式合同审批”。 +- 申请日期自动更新为当天。 +- 正式审批通过后,系统推送 HCS 合规系统。 + +> 临时合同有效期为 30 天,请及时转正式合同,避免临时效力到期影响业务。 + +--- + +## 8. 查看合同详情 + +在合同列表点击 **合同名称** 或操作列的 **查看**,进入合同详情页。 + +![合同详情-基本信息与合规信息](images/06-合同详情-基本信息与合规信息.png) + +> 合同详情:基本信息与合规信息 + +详情页包含以下区域: + +### 8.1 基本信息 + +展示签约类型、合同编号、合同名称、合同类型、项目、甲方、乙方、合同期限、合同负责部门、经办人、归档状态、签订日期、申请日期、合同格式、结算币种、结算方式、开票周期、回款账期、合同金额、是否范本、原件合同编号、是否电子章、合同主要内容、其他事项说明等。 + +**风险客商提示**:如果甲方或乙方属于高风险/黑名单客商,基本信息下方会显示红色提示,列明风险原因,并提示“提交前须上传特批附件”。 + +### 8.2 合规信息 + +合规信息区域展示: + +| 字段 | 说明 | +| --- | --- | +| 合规同步状态 | 待推送 / 已推送 / 推送失败 | +| 合规合同ID | HCS 返回的合同唯一标识 | +| 客商风险 | 是否黑名单、风险等级 | +| 特批状态 | 待特批 / 特批中 / 特批通过 / 特批驳回 | +| 特批说明 | 特批原因或处理说明 | + +### 8.3 用印材料与附件 + +向下滚动可查看用印材料、计费信息、结算单规则、付款比例设置、附件(佐证材料等)。 + +![合同详情-附件佐证材料](images/07-合同详情-附件佐证材料.png) + +> 合同详情:计费、结算、付款比例与佐证材料 + +### 8.4 变更记录 + +详情页底部展示合同的变更记录,包括变更日期、经办人、变更类型、变更内容、变更原因、状态。 + +![合同详情-变更记录](images/08-合同详情-变更记录.png) + +> 合同详情:附件与变更记录 + +--- + +## 9. 附件上传与归档 + +### 9.1 适用条件 + +只有**正式合同**且审核状态为“审批通过”或“变更审批通过”时,才显示“附件上传”操作。 + +### 9.2 操作步骤 + +1. 在合同列表找到正式合同。 +2. 点击操作列 **附件上传**。 + +![附件上传弹窗](images/11-附件上传弹窗.png) + +> 附件上传弹窗 + +3. 选择 **附件位置**: + - **用印材料**:可上传用印材料、双章归档文件、其他文件。 + - **附件(佐证材料等)**:可上传特批附件、其他文件。 +4. 选择 **附件类型**。 +5. 将文件拖入上传区域,或点击上传区域选择文件。 +6. 点击 **确定** 保存。 + +### 9.3 上传限制 + +- 支持 pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip 格式。 +- 单个文件不超过 50MB。 +- 上传人、上传时间由系统自动记录。 + +### 9.4 归档规则 + +- 上传附件类型为 **双章归档文件** 后,合同归档状态会在审批通过或附件保存后变为“已归档”。 +- 双章归档文件属于用印材料,请在“附件位置”中选择“用印材料”。 + +--- + +## 10. 合同变更 + +### 10.1 适用条件 + +只有**正式合同**,且审核状态为“审批通过”“变更审批通过”或“变更驳回”时,才允许发起变更。 + +### 10.2 发起变更 + +1. 在合同列表找到正式合同。 +2. 点击操作列 **变更**(变更驳回后可点击“重新提交变更”)。 +3. 进入“合同变更”页面。 + +![合同变更-基本信息](images/09-合同变更-基本信息.png) + +> 合同变更页面 + +4. 修改需要变更的内容:基本信息、用印材料、计费信息、结算单规则、付款比例、附件等。 +5. 页面会展示风险客商提示;如存在高风险/黑名单客商,变更提交前同样必须上传“特批附件”。 + +### 10.3 填写变更原因与材料 + +![合同变更-原因与材料](images/10-合同变更-原因与材料.png) + +> 合同变更:变更原因与变更材料 + +1. 在“变更原因”中填写本次变更的原因,最多 500 字。 +2. 在“变更材料”区域上传相关附件。 +3. 点击页面底部 **提交**。 +4. 在确认弹窗中点击“确定”。 + +### 10.4 变更审批 + +- 提交后审核状态变为“变更审批中”。 +- 审批人可执行“通过”“驳回”“撤回变更”。 +- 审批通过后,审核状态变为“变更审批通过”,系统按合规合同ID更新 HCS;如该合同尚未推送过,则按首次推送处理。 +- 审批驳回后,审核状态变为“变更驳回”,可点击“重新提交变更”重新填写变更原因后再次提交。 + +> 说明: +> - 变更审批通过后,变更附件会合并到合同附件中,变更原因、变更内容会被清空,变更记录保留。 +> - 页面中的“终止合同”选项当前版本暂未开放,请选择“合同信息变更”。 + +--- + +## 11. HCS 合规推送 + +### 11.1 什么时候推送 + +合同满足以下条件后进入合规推送流程: + +- 审核状态为“审批通过”或“变更审批通过”; +- 合规状态为“待推送”或“推送失败”。 + +> 说明:临时合同审批通过后系统同样会置为待推送并尝试推送;正式合同与合同变更审批通过是合规同步的主要场景,也是归档、金蝶回写的关键节点。 + +系统支持两种触发方式: + +1. **自动推送**:审批通过后系统会尝试即时推送;同时后台定时任务每 5 分钟扫描待推送/推送失败的合同进行补偿。即时推送和定时任务是否启用均由环境配置决定。 +2. **手动推送**:在合同列表点击“推送合规”或“批量推送合规”。 + +### 11.2 系统会推送什么 + +![待推送与推送失败列表](images/14-待推送与推送失败列表.png) + +> 列表中的待推送、推送失败状态 + +推送 HCS 时,系统会依次完成: + +| 步骤 | 内容 | +| --- | --- | +| ① 客商风险校验 | 校验甲乙双方是否高风险/黑名单(C101-1) | +| ② 附件转传 | 将合同附件转传到合规系统(C103-2) | +| ③ 合同判重 | 按合同编号等信息判断合规系统是否已存在(C103-3) | +| ④ 起草 / 更新 | 首次推送起草合同(C103-1);已有合规合同ID时更新合同(C103-11) | +| ⑤ 履约计划同步 | 同步合同履约计划/履约规则(C103-9),系统自动创建默认履约计划,无需人工维护 | +| ⑥ 特批 | 高风险/黑名单客商合同自动进入特批流程 | + +### 11.3 手动推送单条合同 + +1. 在合同列表找到“待推送”或“推送失败”的合同。 +2. 点击操作列 **推送合规**。 + +![手动推送合规确认](images/15-手动推送合规确认.png) + +> 手动推送合规确认 + +3. 在确认弹窗中点击“确定”。 +4. 页面显示“正在推送合规,请稍候…”,请勿重复点击。 +5. 推送完成后列表自动刷新,合规状态变为“已推送”或“推送失败”。 + +### 11.4 批量推送合规 + +1. 在合同列表勾选需要推送的合同。 +2. 点击工具栏 **批量推送合规**。 +3. 在确认弹窗中点击“确定”。 + +> 批量推送要求选中的合同全部满足:审核状态为“审批通过”或“变更审批通过”,且合规状态不是“已推送”。如存在不符合条件的合同,系统会提示“选中数据中有 N 条不符合推送条件”,请重新选择。 +> +> 特批中的合同请勿勾选,需等待特批结果后再处理,系统会拒绝重推。 + +### 11.5 推送成功后的结果 + +推送成功后: + +- 合规状态变为“已推送”。 +- 详情页“合规信息”中回填“合规合同ID”。 +- 金蝶合同ID由合规系统/金蝶同步完成后回写,可在合同列表查看;如暂时为空,可稍后刷新。 + +> 注意:“已推送”表示合同已成功提交至合规系统;如客商为高风险/黑名单,特批状态可能仍为“特批中”,需等待特批结果。 + +### 11.6 推送失败的处理 + +1. 鼠标悬停在列表的合规状态“推送失败”上,查看失败原因。 +2. 也可进入合同详情,在“合规信息”中查看失败说明。 +3. 根据原因修正数据(如合同类别未配置合规模板、附件异常、客商信息异常等)。 +4. 重新点击“推送合规”。 + +系统默认会对推送失败的合同自动重试,最多重试 5 次;超过重试次数后,需要人工排查并手动重推(手动重推会将重试次数清零)。 + +> 特批中的合同不允许重复推送,请等待特批结果后再操作。 + +--- + +## 12. 合同特批(高风险/黑名单客商) + +### 12.1 什么情况需要特批 + +当合同甲方或乙方为外部客商,且命中以下任一情况时,需要走特批流程: + +- 客商为黑名单; +- 客商风险等级为高风险。 + +### 12.2 提交前:上传特批附件 + +1. 在新增/编辑合同页面,滚动到“附件(佐证材料等)”区域。 +2. 点击上传,附件类型选择 **特批附件**。 +3. 上传特批申请材料及内部审批流程附件。 + +> 未上传特批附件时提交,系统会提示:**“合同相对方存在高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批”**。 + +### 12.3 查看特批状态 + +高风险/黑名单合同详情页会显示: + +- 红色“风险客商提示”,列明风险原因; +- 合规信息中的“客商风险”“特批状态”“特批说明”。 + +![特批待发起](images/12-特批待发起.png) + +> 高风险客商合同详情(测试环境截图,图中“特批说明”的 C103-8 接口报错为测试环境历史数据) + +特批状态说明: + +| 特批状态 | 含义 | +| --- | --- | +| 待特批 | 历史数据停留在待发起状态,正常新流程不会长期停留在此状态 | +| 特批中 | 合规系统正在审批特批,当前节点显示“特批审核” | +| 特批通过 | 特批完成,可继续后续流程 | +| 特批驳回 | 特批未通过,合同审批会被联动驳回 | + +### 12.4 特批的正常流程 + +1. 合同上传特批附件后提交审批。 +2. 合同审批通过并推送 HCS。 +3. 风险校验命中高风险/黑名单,合同自动进入“特批中”,当前节点为“特批审核”。 +4. 合规系统完成特批后,将结果推送回 TMS;系统定时查询也会补偿获取结果。 +5. 特批通过:合同恢复为“审批通过”;如合规侧已完成金蝶同步,金蝶合同ID会同时回填。 +6. 特批驳回:合同审批状态联动变为“已驳回”,需修改合同后重新提交审批、重新推送。 + +### 12.5 人工确认特批结果 + +特批结果通常由合规系统自动回写。如结果未自动回传,合规管理员可在合同详情页人工确认: + +1. 打开合同详情,确认特批状态为“特批中”。 +2. 点击“合规信息”右上角的 **确认特批结果**。 + +![确认特批结果](images/13-确认特批结果.png) + +> 确认特批结果 + +3. 在输入框中输入 `approved`(通过)或 `rejected`(驳回)。 +4. 点击“确定”。 + +> 人工确认仅允许在“特批中”状态操作,结果只支持 `approved` 和 `rejected`。确认后合同状态会按结果联动变化,请确认结果无误后再提交。 + +### 12.6 关于“发起特批”按钮 + +合同详情页右上角的 **发起特批** 是旧版独立发起入口。当前合规接口已改为随合同推送自动发起特批,正常流程**无需点击该按钮**;点击后如提示接口不存在,不影响合同数据。如特批状态长时间停留在“待特批”,请联系系统管理员重新推送合同。 + +--- + +## 13. 常见问题 + +**Q1:审批通过后,合规状态一直显示“待推送”怎么办?** + +A:可能当前环境未开启自动推送,或后台扫描任务尚未执行。可先点击操作列“推送合规”手动推送;仍无法推送时联系系统管理员检查推送开关和任务状态。 + +**Q2:推送失败了怎么办?** + +A:将鼠标悬停在“推送失败”上查看原因,或在合同详情“合规信息”中查看。修正数据后点击“推送合规”重推。系统默认自动重试 5 次,超限后必须人工重推。 + +**Q3:提交时提示必须上传“特批附件”怎么办?** + +A:说明合同甲方或乙方命中高风险/黑名单。请在“附件(佐证材料等)”区域上传附件,并将附件类型选择为“特批附件”,然后重新提交。 + +**Q4:临时合同和正式合同有什么区别?** + +A:临时合同用于提前生效,审批通过后有 30 天临时效力;正式合同用于长期履约,是 HCS 合规同步和归档的主要对象。临时合同审批通过后应及时点击“转正式合同”。 + +**Q5:为什么列表里没有“转正式合同”?** + +A:只有合同阶段为“临时合同”,且审核状态为“审批通过”时才显示;临时合同被驳回时显示“重新提交转正”。 + +**Q6:为什么没有“附件上传”按钮?** + +A:只有正式合同,且审核状态为“审批通过”或“变更审批通过”时才允许上传附件。 + +**Q7:金蝶合同ID为什么为空?** + +A:金蝶合同ID由合规系统完成金蝶同步后回写,可能存在延迟。推送成功后可稍后刷新查看;长时间为空请联系系统管理员。 + +**Q8:合同推送 HCS 后还能修改吗?** + +A:不能直接编辑。请通过“变更”流程修改,变更审批通过后系统会更新 HCS,避免产生重复合同或数据不一致。 + +**Q9:变更页面里的“终止合同”为什么不能提交?** + +A:当前版本“终止合同”选项暂未开放,请选择“合同信息变更”完成信息调整;如确需终止合同,请联系系统管理员。 + +**Q10:上传“双章归档文件”后,归档状态为什么没有变化?** + +A:请确认附件位置选择的是“用印材料”,附件类型为“双章归档文件”,并确认合同已审批通过。保存后刷新列表查看;如仍未变化,请联系系统管理员。 + +**Q11:点击“流程”提示“流程实例为空,无法查看流程”怎么办?** + +A:说明该合同没有关联外部审批流程实例。请以列表/详情中的审核状态和当前节点为准;如需查看审批流转,请联系系统管理员。 + +**Q12:批量推送合规提示“选中数据中有 N 条不符合推送条件”怎么办?** + +A:批量推送只允许“审批通过/变更审批通过”且未同步的合同。请取消勾选不符合条件的合同,或单独对符合条件的合同发起推送。 + +--- + +## 14. 操作权限速查 + +| 操作 | 所需条件 | +| --- | --- | +| 新增合同 | 具有合同管理新增权限 | +| 编辑合同 | 草稿、已撤回、已驳回、变更驳回状态 | +| 删除合同 | 草稿状态 | +| 复制合同 | 具有合同管理复制权限,复制后为新增草稿 | +| 转正式合同 | 临时合同 + 审批通过 | +| 变更 | 正式合同 + 审批通过 / 变更审批通过 | +| 重新提交变更 | 正式合同 + 变更驳回 | +| 附件上传 | 正式合同 + 审批通过 / 变更审批通过 | +| 推送合规 | 审批通过 / 变更审批通过 + 未同步 | +| 通过 / 驳回 | 审批中 / 变更审批中 + 审批权限 | +| 撤回 | 审批中(撤回变更仅限变更审批中) | + +--- + +## 15. 注意事项 + +1. 合同编号不填写时按部门规则自动生成;手工编号请确保唯一。 +2. 合同金额不能小于 0,金额类字段请按实际合同金额填写。 +3. 高风险/黑名单客商合同必须上传“特批附件”,新增、变更、重新提交时均需保留。 +4. 临时合同有效期为 30 天,请及时转正式合同。 +5. 合同审批通过后会自动进入合规推送,请耐心等待,不要重复点击审批或推送。 +6. 已推送 HCS 的合同需要修改时,必须走“变更”流程,禁止重复新建合同。 +7. 推送失败时先查看失败原因,修正后再重推;特批中的合同不要重推。 +8. 合同列表默认按创建时间倒序,审批、归档、合规状态请以页面实时显示为准。 +9. 本文截图取自测试环境,正式环境的数据和按钮以实际权限配置为准。 diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png new file mode 100644 index 00000000..7553fe86 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/02-合同列表状态与操作列.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/02-合同列表状态与操作列.png new file mode 100644 index 00000000..1c4ec31c Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/02-合同列表状态与操作列.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/03-合同列表筛选.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/03-合同列表筛选.png new file mode 100644 index 00000000..d59436f0 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/03-合同列表筛选.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/04-新增合同-基本信息.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/04-新增合同-基本信息.png new file mode 100644 index 00000000..7d412bee Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/04-新增合同-基本信息.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/05-新增合同-计费结算与附件.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/05-新增合同-计费结算与附件.png new file mode 100644 index 00000000..8b7c13ff Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/05-新增合同-计费结算与附件.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/06-合同详情-基本信息与合规信息.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/06-合同详情-基本信息与合规信息.png new file mode 100644 index 00000000..f1e41822 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/06-合同详情-基本信息与合规信息.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/07-合同详情-附件佐证材料.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/07-合同详情-附件佐证材料.png new file mode 100644 index 00000000..2ac9e6a0 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/07-合同详情-附件佐证材料.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/08-合同详情-变更记录.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/08-合同详情-变更记录.png new file mode 100644 index 00000000..5039d0bd Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/08-合同详情-变更记录.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png new file mode 100644 index 00000000..d678cc82 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/10-合同变更-原因与材料.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/10-合同变更-原因与材料.png new file mode 100644 index 00000000..b9548596 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/10-合同变更-原因与材料.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/11-附件上传弹窗.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/11-附件上传弹窗.png new file mode 100644 index 00000000..ca69250e Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/11-附件上传弹窗.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/12-特批待发起.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/12-特批待发起.png new file mode 100644 index 00000000..d2b444f0 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/12-特批待发起.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/13-确认特批结果.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/13-确认特批结果.png new file mode 100644 index 00000000..ef3baffb Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/13-确认特批结果.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/14-待推送与推送失败列表.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/14-待推送与推送失败列表.png new file mode 100644 index 00000000..8a698514 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/14-待推送与推送失败列表.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/15-手动推送合规确认.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/15-手动推送合规确认.png new file mode 100644 index 00000000..e990dc4e Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/15-手动推送合规确认.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md new file mode 100644 index 00000000..2f8efae9 --- /dev/null +++ b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md @@ -0,0 +1,841 @@ +# HCS 合同管理操作手册(纯操作版) + +> 整理日期:2026-10-23 +> 适用对象:合同经办人、合同负责人、合同审批人、合规管理员、实施运维。 +> 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。 +> 第 1–3 节为业务操作(合同、项目立项、付款特批);第 4 节为实施运维使用的五类 HCS 定时任务逻辑、接口与配置。 +> 完整字段说明、状态字典与流程背景见同目录《2026-10-23-HCS合同管理操作手册》;接口与代码细节见同目录 HCS 相关梳理文档。 + +--- + +合同系统 + sit环境 + https://contract-sit.gxwuzi.com/ 13737128880 Mk13737128880@ + + + +## 1. 全局须知 + +TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS)。HCS 完成客商风险校验、合同合规校验、特批和金蝶同步,并把合规合同ID、金蝶合同ID、特批结果回写到 TMS。 + +### 1.1 列表里的三个合规状态 + +| 状态值 | 页面显示 | 含义 | 还会变吗 | +|---|---|---|---| +| unsynced | 待推送 | 审批已通过,尚未成功推送 HCS | 会(推送成功变“已推送”) | +| synced | 已推送 | HCS 已受理,已回填合规合同ID | 会(特批状态、金蝶合同ID可能继续变化) | +| failed | 推送失败 | 推送失败,鼠标悬停合规状态可查看原因 | 会(自动重试或手动重推成功后变“已推送”) | + +### 1.2 特批状态 + +| 状态值 | 页面显示 | 含义 | 后续 | +|---|---|---|---| +| need | 待特批 | 尚未进入特批流程,多见于存量数据或推送未完成 | 重新推送成功后转“特批中” | +| processing | 特批中 | HCS 正在审批特批,当前节点显示“特批审核” | 等待 HCS 回传、定时查询或人工确认 | +| approved | 特批通过 | 特批完成 | 合同恢复“审批通过”,继续后续流程 | +| rejected | 特批驳回 | 特批未通过 | 合同审批联动“已驳回”,修改后重新提交、重新推送 | + +> 合同特批与付款特批使用同一组状态值。付款申请页面没有独立“特批状态”列,审批通过后看列表“当前节点”:显示“特批审核”即为特批中;通过后恢复“审批通过”,驳回则审核状态变为“已驳回”。 + +### 1.3 三条推送路径 + +| 路径 | 触发时机 | 前提 | 说明 | +|---|---|---|---| +| 自动即时推送 | 审批人点“通过”后 | `thirdParty.hcs.push.immediate-enabled=true` | 审批响应会等待推送结果;失败提示“审批通过,但合规推送失败:xxx” | +| 定时扫描推送 | 每 5 分钟 | `thirdParty.hcs.push.contract.sync-enabled=true` | 扫“待推送/推送失败未超限”的合同;同时兜底查询特批结果 | +| 手动推送 | 随时 | 合同已审批通过且未同步 | 列表“推送合规”或工具栏“批量推送合规” | + +三条路径共用同一套推送逻辑;HCS 侧有判重,重复触发不会重复生成合同。 + +- **项目立项**:审批通过后即时推送(受 `push.immediate-enabled` 控制),另有定时扫描和列表手动/批量推送。 +- **付款申请**:审批通过后**无论 `push.immediate-enabled` 是否开启**都会立即发起付款特批/推送履约数据;定时扫描只做失败补偿。 +- **开票登记、收票单**:没有即时推送,只能靠定时扫描或手动接口。 + +### 1.4 硬规则 + +- 只有审核状态为“审批通过”或“变更审批通过”的合同可以推送;其他状态请先完成审批。 +- 高风险/黑名单客商合同,**提交审批前必须上传“特批附件”**,否则提交会被拦截。 +- 特批中(processing)的合同**不允许重推**,请等待特批结果。 +- 合同推送 HCS 后不能直接编辑;需要修改时必须走“变更”,变更审批通过后更新 HCS。 +- 合同类别必须在 Nacos 配置 HCS 模板映射,否则每次推送都会失败。 +- 推送失败默认自动重试,最多 5 次;超过后只能人工重推。人工重推会把失败次数清零。 +- 当前版本临时合同审批通过后也会进入推送范围(合规状态会变为待推送/已推送),不要因为合同是临时合同就忽略合规状态。 +- 项目立项只有审核状态为“审批通过/变更审批通过”才能推送;变更审批通过后按已有合规项目ID更新 HCS。 +- 付款申请收款方为高风险/黑名单客商时,提交前必须上传“特批附件”,否则提交被拦截。 +- 付款申请审批通过后系统自动发起付款特批;特批中“当前节点”为“特批审核”,特批未通过前不推送履约数据、不同步金蝶。 +- 付款特批驳回后付款申请变“已驳回”,修改后重新提交审批会重新发起特批。 + +### 1.5 推送与环境前提 + +**推送成功必需:** + +1. 合同已审批通过或变更审批通过。 +2. 合同类别已配置 HCS 模板ID(`thirdParty.hcs.contract.template-mapping`)。 +3. HCS 连接配置可用(`baseUrl`、`authorization`、`key-auth`)。 +4. 合同如有附件,TMS 文件服务可被 HCS 侧下载(`attachment.file-download-base` 已配置)。 +5. 合同不处于“特批中”状态。 + +**风险校验需要:** + +- 甲乙双方名称应能在客商档案中匹配到(全称或简称)。匹配不到不会阻断推送,但会**跳过该方风险校验**,可能导致高风险/黑名单客商未被识别、未触发特批。 + +**项目立项推送需要:** + +1. 项目审核状态为“审批通过”或“变更审批通过”。 +2. 项目编号不为空(HCS 以项目编号判重)。 +3. `thirdParty.hcs.project.template-code` 已配置。 +4. HCS 连接配置可用。 + +**付款特批需要:** + +1. 付款申请关联合同已推送合规(合同有合规合同ID)。 +2. `thirdParty.hcs.spec-pay.template-id` 已配置,付款用途/性质/类型字典值正确。 +3. 收款方为高风险/黑名单时,已上传“特批附件”。 + +--- + +## 2. 一次性准备(实施/管理员操作) + +| # | 事项 | 操作人 | 操作内容 | 不做的后果 | +|---|---|---|---|---| +| 1 | HCS 连接配置 | 运维 | Nacos `thirdParty.hcs.baseUrl`、`authorization`、`key-auth` | 所有推送失败或认证被拒 | +| 2 | 来源与操作人 | 运维 | `thirdParty.hcs.src-system`(约定 WLTMS)、`operator-login-name` | 合规侧来源/操作人不符合约定 | +| 3 | 合同类别模板映射 | 运维 | `thirdParty.hcs.contract.template-mapping`,为**每个合同类别**配置 HCS 模板ID | 未映射的类别推送必失败,提示“合同类别未配置合规模板ID” | +| 4 | 附件下载基址 | 运维 | `thirdParty.hcs.attachment.file-download-base`;保持 `transfer-enabled=true` | 合同有附件时转传失败 | +| 5 | 币种映射 | 运维 | `thirdParty.hcs.contract.currency-name-mapping`(如 RMB→人民币) | 币种传错或为空 | +| 6 | 风险校验开关 | 运维 | `thirdParty.hcs.contract.require-risk-check` 保持 `true` | 关闭后高风险合同不会触发特批 | +| 7 | 履约计划同步 | 运维 | `thirdParty.hcs.exe-stage.sync-enabled`,一期默认 `false` | 不推送 C103-9 履约计划,不影响合同主流程 | +| 8 | 推送开关 | 运维 | 按需开启 `push.immediate-enabled`(项目/合同即时)、`push.project/contract/payment.sync-enabled`(定时扫描) | 只能靠手动按钮推送;付款审批即时特批不受 `push.immediate-enabled` 控制 | +| 9 | 客商档案 | 业务 | 确保合同甲乙方名称与客商档案全称或简称一致 | 风险校验取不到客商,无法判定高风险/黑名单 | +| 10 | 菜单与按钮权限 | 管理员 | 为经办人、审批人、合规管理员配置对应角色权限 | 页面看不到新增、审批或推送合规入口 | +| 11 | 项目立项模板编码 | 运维 | `thirdParty.hcs.project.template-code`(环境值,向合规方获取) | 项目立项推送必失败 | +| 12 | 付款特批模板与字典 | 运维 | `thirdParty.hcs.spec-pay.template-id`、`payment-purpose`、`payment-nature`、`payment-type` | 付款特批发起失败 | +| 13 | 付款履约记录 | 运维 | `thirdParty.hcs.perf-record.sync-enabled`(一期默认 `false`);开启时同时配置 `perf-record.xform-id` | 开启但未配模板ID时履约记录推送失败 | +| 14 | 付款额度校验 | 运维 | 如需“超额自动转特批”,开启 `thirdParty.hcs.credit.check-enabled` | 关闭时超额付款不会自动进入特批 | + +> 模板映射的中文键在 Nacos YAML 中必须使用括号写法,例如 `"[承运商合同]": "模板ID"`,否则配置无法正确绑定。 +> +> 当前 Nacos 样例只映射了“承运商合同”。客户合同、补充协议如果也要推送,必须在上线前逐类确认并补齐映射。 + +--- + +## 3. 日常操作流程 + +### 3.1 查找与筛选合同 + +**页面:合同管理 → 合同管理** + +![合同管理列表:顶部为到期标签与工具栏,列表含合同阶段、审核状态、合规状态、金蝶合同ID、当前节点/处理人](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png) + +![合同搜索区:可按合同编号、合同名称、项目、负责部门、审核状态、合规状态等条件筛选](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/03-合同列表筛选.png) + +1. 找“待推送”合同:审核状态选 **审批通过** 或 **变更审批通过**,合规状态选 **待推送**。 +2. 找“推送失败”合同:合规状态选 **推送失败**,鼠标悬停合规状态查看失败原因。 +3. 支持按合同编号、合同名称、项目、合同负责部门、签约类型、合同类别、生效类型、归档状态、是否同步合规/金蝶筛选。 +4. 列表上方的到期标签(全部、30天内到期、90天内到期、90天以上到期、已到期)可直接筛选。 +5. 点击合同名称进入详情;操作列按合同状态显示可用操作。 + +> 说明: +> - 列表默认按创建时间倒序排列。 +> - 特批中的合同合规状态仍是“已推送”,要通过“特批状态”或当前节点“特批审核”识别,不能只看合规状态。 + +### 3.2 新增合同并提交审批 + +**页面:合同管理 → 合同管理 → 新增** + +![新增合同:填写签约类型、合同名称、合同类型、项目、甲乙方、合同期限、金额、结算方式等基本信息](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/04-新增合同-基本信息.png) + +![新增合同下部:计费信息、结算单规则、付款比例设置和佐证材料附件](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/05-新增合同-计费结算与附件.png) + +1. 点左上角 **新增**。 +2. 填写基本信息。合同编号可不填,系统按合同负责部门规则自动生成。 +3. 填写计费信息、结算单规则、付款比例设置。 + - 提交**正式合同**前,这三块必须完整并通过校验。 + - 提交**临时合同**只校验基础必填项。 +4. 上传附件: + - 用印材料区域可上传“用印材料 / 双章归档文件 / 其他文件”。 + - 佐证材料区域可上传“特批附件 / 其他文件”。 + - 高风险/黑名单客商合同必须上传类型为 **特批附件** 的文件。 +5. 选择提交方式: + +| 按钮 | 适用场景 | 提交后状态 | +|---|---|---| +| 保存 | 信息未填全,先暂存 | 合同阶段“草稿”、审核状态“草稿”,不推送 HCS | +| 提交临时合同 | 需要合同尽快生效 | 合同阶段“临时合同”、审核状态“审批中” | +| 提交正式合同 | 信息完整,直接走正式审批 | 合同阶段“正式合同”、审核状态“审批中” | + +**成功标志**:列表出现该合同,状态与上表一致。 + +**常见拦截**: + +- 提示必填项缺失 → 补齐后重新提交。 +- 提示付款比例/结算规则校验不通过 → 按页面提示调整计费、结算或付款比例。 +- 提示必须上传“特批附件” → 在“附件(佐证材料等)”区域上传,附件类型选“特批附件”。 + +### 3.3 临时合同转正式合同 + +**页面:合同管理 → 合同管理** + +前提:合同阶段为“临时合同”,审核状态为“审批通过”。 + +1. 在列表找到该临时合同。 +2. 点操作列 **转正式合同**。 +3. 在确认弹窗点“确定”。 +4. 提示“已提交正式合同审批”。 + +**提交后的变化**: + +- 合同阶段变为“正式合同”,审核状态变为“审批中”,当前节点“正式合同审批”。 +- 申请日期自动更新为当天。 +- 正式审批通过后进入 HCS 推送。 + +> 临时合同审批被驳回时,操作列显示“重新提交转正”;编辑修改后再提交即可。临时合同有效期为 30 天,请及时转正式合同。 + +### 3.4 审批通过与推送 HCS + +**页面:合同管理 → 合同管理(审批人操作)** + +![合同列表状态与操作列:审批中显示通过/驳回/撤回,审批通过且未同步的合同显示“推送合规”](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/02-合同列表状态与操作列.png) + +**审批人操作:** + +1. 在操作列点 **通过**。 +2. 如当前环境开启即时推送,页面显示“正在推送合规,请稍候…”遮罩,请等待完成,不要重复点击。 +3. 根据提示判断结果: + +| 提示 | 含义 | 处理 | +|---|---|---| +| 审批通过 | 审批完成;即时推送成功,或当前未开启即时推送 | 查看合规状态确认;未推送则手动推或等待扫描 | +| 审批通过,但合规推送失败:xxx | 审批已生效,但 HCS 推送失败 | 按失败原因修复后点“推送合规”重推,**不要重复审批** | + +**手动推送单条:** + +![手动推送合规确认:列表行内“推送合规”,确认后显示“正在推送合规,请稍候…”](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/15-手动推送合规确认.png) + +1. 在列表找到“待推送”或“推送失败”的合同。 +2. 点操作列 **推送合规**。 +3. 在确认弹窗点“确定”。 +4. 等待“正在推送合规,请稍候…”结束,列表自动刷新。 + +**批量推送:** + +1. 勾选需要推送的合同。 +2. 点工具栏 **批量推送合规**。 +3. 在确认弹窗点“确定”。 +4. 系统逐条推送,单条失败不影响其他合同。 + +> 批量推送要求选中合同全部满足:审核状态为“审批通过/变更审批通过”,且合规状态不是“已推送”。否则提示“选中数据中有 N 条不符合推送条件”,请重新勾选。 + +**成功标志**:合规状态变为 **已推送**,详情页回填 **合规合同ID**。 + +> 高风险/黑名单合同推送成功后,合规状态同样是“已推送”,但特批状态会进入“特批中”,这是正常流程,不是推送失败。 + +### 3.5 查看推送结果 + +**页面:合同管理 → 合同详情** + +![合同详情:基本信息下方显示合规信息,包括合规同步状态、合规合同ID、客商风险、特批状态、特批说明](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/06-合同详情-基本信息与合规信息.png) + +在详情页“合规信息”区域查看: + +| 字段 | 说明 | +|---|---| +| 合规同步状态 | 待推送 / 已推送 / 推送失败;失败时可悬停查看原因 | +| 合规合同ID | HCS 返回的合同唯一标识,推送成功后回填 | +| 客商风险 | 是否黑名单、风险等级 | +| 特批状态 | 待特批 / 特批中 / 特批通过 / 特批驳回 | +| 特批说明 | 特批原因或处理说明 | + +**推送成功的判断标准**:合规状态为“已推送”,且“合规合同ID”不为空。 + +> 金蝶合同ID由 HCS 完成金蝶同步后回写,可能存在延迟;暂时为空可稍后刷新列表查看。“已推送”只代表 HCS 已受理,不代表特批和金蝶同步已经完成。 + +### 3.6 推送失败的处理与重推 + +**页面:合同管理 → 合同管理 / 合同详情** + +![待推送与推送失败列表:合规状态列显示“待推送/推送失败”,失败原因可悬停查看](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/14-待推送与推送失败列表.png) + +1. 鼠标悬停列表中的“推送失败”,或进入详情查看失败说明。 +2. 按下表排查并修正数据/配置。 +3. 修正后点操作列 **推送合规**,确认后等待结果。 +4. 手动重推会把失败次数清零;如仍失败,按新的失败原因继续处理。 + +**自动重试规则**: + +- 合同扫描任务开启时,每 5 分钟自动重试“推送失败且未超过 5 次”的合同。 +- 超过 5 次后不再自动重试,必须人工重推。 +- 手动重推没有次数限制,但根因不修复会反复失败。 + +**常见失败原因**: + +| 失败提示/现象 | 原因 | 处理 | +|---|---|---| +| 合同类别 [xxx] 未配置合规模板ID | Nacos 模板映射缺该类合同 | 运维在 `contract.template-mapping` 补齐后重推 | +| 附件转传失败 | 下载基址为空、附件文件不存在、文件超限或 HCS 附件接口异常 | 检查 `attachment.file-download-base`、附件是否可下载,修正后重推 | +| C101-1 客商风险查询失败 | HCS 风险接口异常/无响应 | 联系 HCS 侧确认接口,恢复后重推;客商档案匹配不到不会报此错,而是跳过风险校验 | +| C103-3 合同判重查询失败 | HCS 查重接口异常/网络超时 | 稍后重推;持续失败联系 HCS 侧 | +| C103-1 合同发起失败 | HCS 返回业务校验错误(模板、必填字段、主数据等) | 按返回的 msg 修正数据;必要时联系合规方核对模板 | +| C103-11 合同更新失败 | 变更合同更新 HCS 失败 | 按返回 msg 处理,修正后重推 | +| 合同特批审核中,禁止重推 | 合同处于“特批中” | 等待特批结果,不要重推 | +| 网络超时/连接失败 | HCS 服务不可用或网络抖动 | 恢复后重推;扫描开关开启时系统也会自动重试 | + +> 如果怀疑 HCS 侧其实已经生成合同,仍可直接重推:本地没有合规合同ID时会先走判重(C103-3),命中后回填已有ID;已有合规合同ID时走更新(C103-11)。两种路径都不会重复生成合同。 + +### 3.7 高风险/黑名单合同特批 + +**页面:合同详情 → 合规信息** + +![高风险客商合同详情:红色风险客商提示,合规信息显示客商风险、特批状态、特批说明](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/12-特批待发起.png) + +**触发条件**:合同甲方或乙方为外部客商,且客商为黑名单或风险等级为高风险。 + +**提交前:** + +1. 在新增/编辑合同的“附件(佐证材料等)”区域上传特批申请材料。 +2. 附件类型必须选择 **特批附件**。 +3. 未上传时提交,系统提示“合同相对方存在高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批”。 + +**正常流程:** + +1. 合同审批通过并推送 HCS。 +2. 风险校验(C101-1)命中高风险/黑名单,合同自动进入特批流程。 +3. TMS 特批状态变为 **特批中**,当前节点显示 **特批审核**。 +4. HCS 完成特批后,通过结果推送或 TMS 定时查询回传结果。 +5. 特批通过:合同恢复“审批通过”;如 HCS 已完成金蝶同步,金蝶合同ID会同时回填。 +6. 特批驳回:合同审批状态联动变为“已驳回”,修改合同后重新提交审批、重新推送。 + +**人工确认特批结果(结果未自动回传时):** + +![确认特批结果:在合规信息右上角点击“确认特批结果”,输入 approved 或 rejected](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/13-确认特批结果.png) + +1. 打开合同详情,确认特批状态为 **特批中**。 +2. 点“合规信息”右上角 **确认特批结果**。 +3. 输入 `approved`(通过)或 `rejected`(驳回)。 +4. 点“确定”。 + +> 人工确认仅允许在“特批中”状态操作,结果只支持 `approved` 和 `rejected`。确认后合同状态会按结果联动变化,请核对无误后再提交。 + +**关于“发起特批”按钮:** + +- 详情页右上角的 **发起特批** 是旧版独立入口,当前合规接口已改为随合同推送自动发起特批,正常流程**无需点击**。 +- 点击后如提示接口不存在,不影响合同数据,请忽略。 +- 如特批状态长时间停留在“待特批”,请联系系统管理员重新推送合同。 + +> 特批中的合同不允许重推,请等待特批结果。特批驳回后的合同必须修改并重新提交审批,不能直接重推。 + +### 3.8 合同变更并更新 HCS + +**页面:合同管理 → 合同管理 → 变更** + +![合同变更页面:在正式合同基础上修改基本信息、附件、计费、结算和付款比例](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png) + +![合同变更下部:填写变更原因(最多 500 字)并上传变更材料](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/10-合同变更-原因与材料.png) + +前提:合同阶段为“正式合同”,审核状态为“审批通过”“变更审批通过”或“变更驳回”。 + +1. 在列表点操作列 **变更**(变更驳回后点“重新提交变更”)。 +2. 修改需要变更的内容:基本信息、用印材料、计费信息、结算单规则、付款比例、附件等。 +3. 填写 **变更原因**,最多 500 字。 +4. 在“变更材料”区域上传相关附件。 +5. 点页面底部 **提交**,在确认弹窗点“确定”。 +6. 审核状态变为 **变更审批中**。 + +**审批人操作:** + +- 通过:审核状态变为 **变更审批通过**,系统按合规合同ID更新 HCS(C103-11);如该合同从未推送过,则按首次推送处理。 +- 驳回:审核状态变为 **变更驳回**,可点“重新提交变更”重新填写并提交。 +- 撤回变更:仅变更审批中可撤回,撤回后恢复为“审批通过”。 + +**变更结果:** + +- 变更附件合并到合同附件,变更原因、变更内容被清空。 +- 变更记录保留,可在详情页底部查看。 + +![合同详情变更记录:展示变更日期、经办人、变更类型、变更内容、变更原因和状态](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/08-合同详情-变更记录.png) + +> 页面中的“终止合同”选项当前版本暂未开放,请选择“合同信息变更”。已推送 HCS 的合同禁止直接编辑,必须走变更流程,否则会产生重复合同。 + +### 3.9 附件上传与归档 + +**页面:合同管理 → 合同管理 → 附件上传** + +前提:合同阶段为“正式合同”,审核状态为“审批通过”或“变更审批通过”。 + +![附件上传弹窗:先选择附件位置(用印材料/佐证材料),再选择附件类型并上传](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/11-附件上传弹窗.png) + +1. 在列表点操作列 **附件上传**。 +2. 选择 **附件位置**: + - **用印材料**:可上传用印材料、双章归档文件、其他文件。 + - **附件(佐证材料等)**:可上传特批附件、其他文件。 +3. 选择 **附件类型**。 +4. 将文件拖入上传区域,或点击选择文件。 +5. 点 **确定** 保存。 + +**上传限制**: + +- 支持 pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip。 +- 单个文件不超过 50MB。 +- 上传人、上传时间由系统自动记录。 + +**归档规则**: + +- 上传附件类型为 **双章归档文件**,且附件位置选择“用印材料”时,合同审批通过或附件保存后归档状态变为 **已归档**。 +- 其他附件类型不会改变归档状态。 + +![合同详情附件区:可查看、下载用印材料和佐证材料](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/07-合同详情-附件佐证材料.png) + +### 3.10 项目立项同步 HCS + +**页面:项目管理(`/business/project-apply`)** + +项目立项审批通过后,TMS 把项目信息推送到 HCS(C102)。项目没有客商风险特批;首次推送先按项目编号判重,变更审批通过后更新已推送项目。 + +**前提:** + +- 项目审核状态为“审批通过”或“变更审批通过”。 +- 项目编号不为空(HCS 以项目编号判重)。 +- Nacos `thirdParty.hcs.project.template-code` 已配置。 +- HCS 连接配置可用。 + +**推送方式:** + +| 方式 | 触发时机 | 说明 | +|---|---|---| +| 审批通过自动推送 | 审批人点“通过” | 受 `push.immediate-enabled` 控制;开启时页面显示“正在推送合规,请稍候…” | +| 定时扫描 | 开关 `push.project.sync-enabled=true` | 每 5 分钟扫“待推送/推送失败未超限”的项目 | +| 单条手动推送 | 列表行内“推送合规” | 审批通过且合规状态不是“已推送”时显示 | +| 批量推送 | 勾选后工具栏“批量推送合规” | 选中项必须全部满足推送条件,否则整批提示不符合条数 | + +> 若启用 MK 审批,列表显示“流程”而不是“通过”,在外部审批页通过后同样会触发推送。 + +**审批人操作:** + +1. 在项目列表操作列点 **通过**。 +2. 如开启即时推送,等待“正在推送合规,请稍候…”结束,不要重复点击。 +3. 根据提示判断: + +| 提示 | 含义 | 处理 | +|---|---|---| +| 通过成功 | 审批完成;即时推送成功,或当前未开启即时推送 | 查看合规状态;未推送则手动推或等待扫描 | +| 审批通过,但合规推送失败:xxx | 审批已生效,HCS 推送失败 | 按原因修复后点“推送合规”,**不要重复审批** | + +**手动推送:** + +1. 合规状态筛选 **待推送** 或 **推送失败**;失败原因可悬停合规状态查看。 +2. 点行内 **推送合规** → 在确认弹窗点“确定” → 等待遮罩结束。 +3. 成功提示可能为: + - `项目立项新建成功:项目编号`:首次推送并在 HCS 新建成功。 + - `项目立项更新成功:项目编号`:已有合规项目ID,按变更更新成功。 + - `判重命中,回填已有合规项目ID`:HCS 已存在相同项目编号,直接回填,不重复新建。 + +**成功标志:** 合规状态变为 **已推送**。列表中的“金蝶项目ID”“金蝶项目编号”由 HCS 金蝶同步结果回填,可能延迟;推送成功不代表金蝶项目已创建。 + +**项目变更:** + +1. 审核状态为“审批通过/变更审批通过”的项目,点行内 **变更**。 +2. 修改后提交,审批人点 **通过**。 +3. 已有合规项目ID时更新 HCS(C102-2);从未推送过的项目按首次推送处理(先判重,再新建)。 + +**常见失败处理:** + +| 提示 | 原因 | 处理 | +|---|---|---| +| 立项表单模板编码未配置 | `project.template-code` 为空 | 运维补齐配置后重推 | +| C102-3 项目立项判重查询失败 | HCS 查询接口异常/超时 | 稍后重推;持续失败联系 HCS 侧 | +| C102-1 项目立项新建失败 | HCS 返回模板、必填字段或主数据校验错误 | 按返回 msg 修正项目数据后重推 | +| C102-2 项目立项更新失败 | 变更数据不符合 HCS 校验 | 按返回 msg 修正后重新提交变更 | +| 网络超时/连接失败 | HCS 服务不可用或网络抖动 | 恢复后重推;扫描开启时也会自动重试 | + +> 项目推送没有 HCS 特批环节;但 HCS 可能存在同名项目编号,手动重推会先判重,不会重复创建。失败超过 5 次后不再自动重试,需人工重推(会清零次数)。 + +### 3.11 付款特批 + +**页面:付款管理 → 付款申请(`/payment/payment-application`)** + +付款特批由付款申请审批通过后**自动发起**,页面没有独立“发起特批”按钮。是否需要特批,在提交和审批时由收款方风险、合同额度共同决定。 + +**触发条件:** + +- 收款方为外部客商,C101-1 返回 **高风险** 或 **黑名单**:提交前必须上传“特批附件”,提交后标记为待特批。 +- 申请金额超出合同额度(C106-1,`credit.check-enabled=true`):不拦截提交,自动转特批。 +- 收款方为内部组织客商时不查风险,也不会因风险触发特批。 + +**提交前:** + +1. 新增或编辑付款申请,选择收款方。 +2. 基本信息出现“风险客商提示”时,在“附件”区新增一行,**附件类型选择“特批附件”**,上传特批材料。 +3. 提交审批;未上传特批附件会被拦截: + +> 收款方[xxx]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批 + +**审批人操作:** + +1. 在付款申请列表操作列点 **通过**(启用 MK 审批时为“流程”,在外部审批页通过)。 +2. 审批生效后,系统立即发起付款特批(C106-3),**不受 `push.immediate-enabled` 开关控制**,无需人工发起。 +3. 根据审批响应提示判断: + +| 提示 | 含义 | 后续 | +|---|---|---| +| 审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶 | 特批已发起 | 列表“当前节点”显示“特批审核”,等待结果 | +| 审批通过,付款特批待发起,可在详情页重推合规 | 特批尚未发起(并发跳过或前置条件不满足) | 当前页面没有重推按钮;联系实施检查模板、合同合规ID后重推 | +| 审批通过,但发起付款特批/推送合规失败:xxx,可稍后重推合规 | 审批已生效,特批发起或推送失败 | 按原因修复后联系实施重推,**不要重复审批** | +| 审批通过,已同步金蝶:xxx / 审批通过,但自动同步金蝶失败:xxx | 未走特批,直接同步金蝶的结果 | 金蝶失败按提示原因处理 | +| 审批通过 | 无特批、也没有即时同步结果 | 查看“当前节点”和金蝶单据状态 | + +**如何判断特批进度:** + +付款申请列表没有独立“特批状态”列,查看 **当前节点**: + +| 当前节点 | 审核状态 | 含义 | +|---|---|---| +| 特批审核 | 审批通过 | 特批中,等待 HCS 回传或人工确认 | +| 审批通过 | 审批通过 | 特批已通过,或本单无需特批 | +| 已驳回 | 已驳回 | 特批驳回,需要修改后重新提交 | + +> 特批中审核状态仍显示“审批通过”,不能只看审核状态判断流程是否结束。 + +**特批通过后:** + +1. 当前节点恢复“审批通过”。 +2. 系统自动补推履约计划(C103-9)和履约记录(C108,若开启),并自动同步金蝶。 +3. 金蝶单据号、金蝶单据状态稍后回填,可刷新列表查看。 + +**特批驳回后:** + +1. 审核状态变为 **已驳回**,当前节点“已驳回”。 +2. 点 **编辑** 修改付款申请,必要时重新上传特批附件。 +3. 重新 **提交** → 审批人再次 **通过**,系统会重新发起一张特批单,不复用旧特批单。 + +**特批中注意:** + +- 特批未通过前不推送履约数据、不同步金蝶,这是正常等待,不是推送失败。 +- 不要重复审批;特批中审核状态已是“审批通过”,列表不会再显示“通过”。 +- 特批中请先等待结果;确需作废或终止,先联系实施确认 HCS 侧特批单的处理,避免状态冲突。 + +**结果一直未回传:** + +HCS 正常情况下通过 `paymentLegal/result` 回传结果。长时间停留在“特批审核”时联系实施,用 `POST /hcs/spec-pay/confirm` 人工确认结果(`approved`/`rejected`);当前付款页面没有人工确认按钮。 + +**实施前置条件:** + +- 关联合同已推送合规,合同有合规合同ID。 +- `thirdParty.hcs.spec-pay.template-id` 已配置。 +- `spec-pay.payment-purpose`、`payment-nature`、`payment-type` 字典值正确。 + +任一条件不满足时,审批仍会成功,但特批发起失败;按审批提示或日志修复后重推。 + +--- + +## 4. 定时任务与接口(运维视角) + +HCS 推送由五个独立定时任务和一组手动接口组成。五个任务各有自己的开关与 cron,互不影响;定时任务、手动接口和审批即时推送共用同一套推送逻辑。 + +### 4.1 五类定时任务总览 + +| 任务 | bizType | 扫描对象 | 推送链路(HCS 接口) | 独立开关 | 默认 cron | 手动触发 | +|---|---|---|---|---|---|---| +| 项目立项 C102 | `project` | 项目立项:审批通过/变更审批通过,待推送/失败未超限 | C102-3 判重 → C102-1 新建 / C102-2 更新 | `push.project.sync-enabled`(默认 `false`) | `0 */5 * * * ?` | `POST /hcs/sync/project/{id}`、`/hcs/sync/project`、`/hcs/sync/project/batch` | +| 合同 C103 | `contract` | 合同:审批通过/变更审批通过,待推送/失败未超限;同轮补查特批结果 | C101-1 风险 → C103-2 附件 → C103-3 判重 → C103-1 起草 / C103-11 更新 → C103-9 履约规则(开关)→ 高风险挂特批 | `push.contract.sync-enabled`(默认 `false`) | `0 1/5 * * * ?` | `POST /hcs/sync/contract/{id}`、`/hcs/sync/contract`、`/hcs/sync/contract/batch` | +| 付款链 C106/C108 | `payment` | 付款申请:审批通过,待推送/失败未超限 | C106-3/C106-4 特批(`hcs_spec_status=need` 时)→ C103-9 履约规则重推 → C108-1/C108-2 履约记录(开关) | `push.payment.sync-enabled`(默认 `false`) | `0 2/5 * * * ?` | `POST /hcs/sync/payment/{id}`、`/hcs/sync/payment` | +| 开票登记 C111 | `invoice-application` | 开票申请:审批通过,待推送/失败未超限 | C111-1 判重 → C111-2 新增 → C111-3 更新(金蝶已开票/已有ID) | `push.invoice-application.sync-enabled`(默认 `false`) | `0 3/5 * * * ?` | `POST /hcs/sync/invoice-application/{id}`、`/hcs/sync/invoice-application` | +| 收票单 C109 | `invoice-receipt` | 收票单:审批通过,待推送/失败未超限 | C109-1 新建 / C109-2 更新 | `push.invoice-receipt.sync-enabled`(默认 `false`) | `0 4/5 * * * ?` | `POST /hcs/sync/invoice-receipt/{id}`、`/hcs/sync/invoice-receipt` | + +> - **即时推送**:项目立项、合同审批通过后立即推送(受 `push.immediate-enabled` 控制);付款申请审批通过后立即发起特批/推送履约数据(**不受**该开关控制);开票登记、收票单没有即时推送,只能靠定时扫描或手动接口。 +> - 五个扫描开关默认全部 `false`,按业务灰度打开;手动接口不受开关限制。 +> - 只有合同扫描任务额外承担“特批结果兜底查询”,其他任务没有这个动作。 + +### 4.2 公共执行框架 + +五个任务都按同一套模板执行: + +1. **开关校验**:`push.{bizType}.sync-enabled=false` 时直接跳过本轮,属正常灰度状态。 +2. **抢任务锁**:按 bizType 抢 Redis 锁 `blade:third:hcs:push:task:lock:{bizType}`,TTL 240 秒;抢不到说明其他实例正在执行,本轮跳过。 +3. **扫描单据**: + - 项目立项/合同:审核状态为 `approved` / `change_approved`; + - 付款申请/开票申请/收票单:审核状态仅 `approved`; + - 合规状态为 `unsynced` / `failed`; + - 失败次数为空或小于 `push.max-retry`(默认 5); + - 单轮最多 `push.batch-size`(默认 50)条。 +4. **逐条推送**:单条失败只把该条置 `failed`、失败次数 +1、记录原因,不影响同批其他单据;下一轮自动重试未超限单据。 +5. **结果回写**:成功置 `synced`、失败次数清零、记录同步时间和结果说明;项目立项、合同、开票申请、收票单同时回填合规ID。 +6. **任务日志**:每轮输出“扫描 X 条,成功 Y 条,失败 Z 条(重试上限 5)”;抢锁失败输出“其他实例执行中,本轮跳过”。 + +> 手动接口和定时任务的区别:手动单条/批量/扫一轮接口**不检查开关,也不抢任务锁**,适合开关关闭时临时补推;不要和定时任务同一时刻执行,避免并发重复调用(HCS 侧有判重/更新幂等,但会增加无效请求)。 + +### 4.3 项目立项任务(C102) + +**扫描条件**:审核状态为 `approved` / `change_approved`,合规状态为 `unsynced` / `failed`,失败次数为空或小于 `push.max-retry`;单轮最多 `push.batch-size` 条。 + +**推送逻辑**: + +1. 项目没有合规项目ID(`hcs_fd_id` 为空):先调 C102-3 判重,查询条件 `fd_num=项目编号`。命中则回填已有合规ID并置 `synced`,不再新建;未命中调 C102-1 新建,成功后回填合规项目ID。 +2. 项目已有合规项目ID(通常是变更审批通过):调 C102-2 更新。 +3. C102-1 依赖 `project.template-code`;未配置时整条推送失败,提示“立项表单模板编码未配置”。 +4. C102-1 以 `drafter_submit` 方式直接提交(不存草稿);付款额度、开票额度由万元乘 10000 转为元,计划保证金仍按万元直传。 + +**金蝶结果回调**:HCS 完成金蝶同步后调 `projectLegal/result`,按 `projectCode`(TMS 项目编号)定位;`success=true` 回填金蝶项目ID/编号,`success=false` 记录失败原因。该回调**不改** HCS 同步状态和重试次数,也不触发 TMS 重推。 + +**接口**: + +| 接口 | 作用 | 关键行为 | +|---|---|---| +| `POST /hcs/sync/project/{id}` | 单条重推 | 仅 `approved` / `change_approved` 可推;先重置 `unsynced`、失败次数清零,再立即推送 | +| `POST /hcs/sync/project` | 按扫描逻辑推一轮 | 最多 `batch-size` 条;**不检查开关、不抢任务锁** | +| `POST /hcs/sync/project/batch` | 按勾选ID批量重推 | 请求体为ID数组;逐条重置并推送,单条失败不影响其他,返回成功数与失败明细 | +| `POST /hcs/projectLegal/result` | HCS 回调金蝶同步结果 | 报文含 `projectCode`、`success`、`kingdeeId`、`projectNumber`、`reason`、`sourceSystem`;只回填金蝶ID/编号或失败原因 | + +### 4.4 合同任务(C103) + +**扫描条件**:审核状态为 `approved` / `change_approved`,合规状态为 `unsynced` / `failed`,失败次数未超限;单轮最多 `batch-size` 条。除推送外,任务每轮还会补查特批结果。 + +**推送逻辑**: + +1. **特批挂起门禁**:`hcs_spec_status=processing` 的合同跳过推送,等待特批结果;手动重推直接报“合同特批审核中,禁止重推”。 +2. **客商风险 C101-1**(`contract.require-risk-check=true`):甲乙双方分别查询;客商档案匹配不到或为内部组织时跳过该方,不阻断;任一方高风险或黑名单只做标记,**不阻断推送**,推送成功后自动转特批。 +3. **附件转传 C103-2**(`attachment.transfer-enabled=true`):附件转传失败会导致整次推送失败。 +4. **首次推送**:无合规合同ID时先调 C103-3 判重(`fdNumber=合同编号`);命中直接回填已有ID并置已推送,未命中调 C103-1 起草;起草成功后立即回填合规合同ID,再继续后续步骤。 +5. **变更推送**:已有合规合同ID时调 C103-11 更新。 +6. **履约规则 C103-9**(`exe-stage.sync-enabled=true`):按履约计划行级新增/更新/删除;无可同步行时返回“履约规则无待同步行”;同步失败会把整条合同置 `failed`。由于第 4 步已先回填合规ID,下一轮重推会走 C103-11 更新,不会重复起草。 +7. **高风险挂起特批**:风险命中时置 `hcs_spec_status=processing`、当前节点“特批审核”,等待 HCS 特批结果。 + +**特批结果兜底(随合同任务执行)**: + +- 每轮推送完待同步合同后,另查 `hcs_spec_status=processing` 且已有合规合同ID的合同,固定最多 50 条。 +- 逐条调 C103-4 查询合同详情:`fdApprovalStatus=approved` → 特批通过,当前节点恢复“审批通过”;`approvalinvalid` → 特批驳回,合同审批状态联动 `rejected`、当前节点“已驳回”;其他状态继续等待。 +- 单条查询失败只记日志,下一轮继续;HCS 主动回调 `contractLegal/result` 是并行的另一条结果通道。 +- 该兜底只随合同定时任务执行,受 `push.contract.sync-enabled` 控制;手动 `POST /hcs/sync/contract` **不补查**特批结果,需用“确认特批结果”接口人工兜底。 + +合同链路涉及的 HCS 接口:C101-1 风险查询、C103-2 附件上传、C103-3 判重、C103-1 起草、C103-11 更新、C103-9 履约规则、C103-4 特批结果查询。 + +**接口**: + +| 接口 | 作用 | 关键行为 | +|---|---|---| +| `POST /hcs/sync/contract/{id}` | 单条重推 | 仅 `approved` / `change_approved`;`processing` 拒绝;先重置 `unsynced`、失败次数清零 | +| `POST /hcs/sync/contract` | 按扫描逻辑推一轮 | **不检查开关、不抢任务锁、不补查特批结果** | +| `POST /hcs/sync/contract/batch` | 按勾选ID批量重推 | 请求体为ID数组;逐条重置并推送,返回成功数与失败明细 | +| `POST /hcs/contract/spec/confirm` | 人工确认合同特批结果 | 参数 `id`、`result`(`approved`/`rejected`);仅 `processing` 可确认 | +| `POST /hcs/contractLegal/result` | HCS 回调特批+金蝶同步结果 | 报文含 `contractType`、`contractCode`、`requestId`、`success`、`reason`、`kingdeeId`、`sourceSystem`;`sourceSystem` 必须匹配;按 `contractCode` 定位;`success=true` → 特批通过并回填 `kingdeeId`;`false` → 特批驳回并联动合同审批驳回 | +| `POST /hcs/contractKingdee/result` | HCS 回调金蝶最终同步结果 | 报文同上;只回填 `kingdeeId` 或失败原因,**不改**审批状态和特批状态 | + +### 4.5 付款链任务(C106/C108) + +**扫描条件**:付款申请审核状态仅 `approved`(不含变更状态),合规状态为 `unsynced` / `failed`,失败次数未超限;单轮最多 `batch-size` 条。 + +**特批从哪来**:付款提交时,收款方为高风险/黑名单(`risk.check-on-payment=true`,须先上传特批附件)或额度超额(`credit.check-enabled=true`)会置 `hcs_spec_status=need`。审批通过时由审批接口(或 MK 审批回调)即时发起 C106-3,**不受 `push.immediate-enabled` 控制**;扫描任务负责失败补偿和存量 `need` 数据。 + +**推送逻辑**: + +1. `hcs_spec_status=need`:无特批流程ID(`hcs_spec_fd_id`)时调 C106-3 发起特批,已有特批流程ID时调 C106-4 更新。C106-3 依赖 `spec-pay.template-id` 与关联合同的合规合同ID;成功后回填特批流程ID、置 `processing`、当前节点“特批审核”。 +2. 关联合同已推送合规(合同 `hcs_fd_id` 非空)时,调 C103-9 重推履约规则;无待同步行自动跳过。该动作**不受** `exe-stage.sync-enabled` 控制,与合同任务不同。 +3. `perf-record.sync-enabled=false`:直接置整体 `synced` 结束;`true`:无履约记录ID调 C108-1 新建,已有调 C108-2 更新。 +4. 整体合规状态 `synced` 只代表本轮推送完成,**不代表特批已通过**;特批中(`processing`)的付款也可能显示已推送。 + +**特批结果**:HCS 通过 `paymentLegal/result` 回调,或用 `/hcs/spec-pay/confirm` 人工确认。通过后恢复“审批通过”并重新触发金蝶自动同步;驳回后付款审批状态置 `returned`(已驳回),需修改后重新提交审批、重新发起特批。 + +付款链路涉及的 HCS 接口:C106-3 特批发起、C106-4 特批更新、C103-9 履约规则、C108-1/C108-2 履约记录。 + +**接口**: + +| 接口 | 作用 | 关键行为 | +|---|---|---| +| `POST /hcs/sync/payment/{id}` | 单条重推 | 仅 `approved` 可推;先重置 `unsynced`、失败次数清零 | +| `POST /hcs/sync/payment` | 按扫描逻辑推一轮 | 不检查开关、不抢任务锁;**无批量 ID 接口** | +| `POST /hcs/spec-pay/confirm` | 人工确认付款特批结果 | 参数 `id`、`result`(`approved`/`rejected`);仅 `processing` 可确认 | +| `POST /hcs/paymentLegal/result` | HCS 回调付款特批结果 | 报文含 `sourceSystem`、`requestId`、`status`、`reason`;`requestId` 为 TMS 付款申请ID;`status=APPROVED/REJECTED`;通过恢复审批并触发金蝶同步,驳回置 `returned` | + +### 4.6 开票登记任务(C111) + +**扫描条件**:开票申请审核状态仅 `approved`,合规状态为 `unsynced` / `failed`,失败次数未超限;单轮最多 `batch-size` 条。 + +**推送逻辑**: + +1. 没有合规开票登记ID时,先调 C111-1 判重(`fdNumber=开票申请编号`);命中直接回填已有ID并置已推送,未命中调 C111-2 新增。 +2. C111-2 必须能通过应收结算链路回溯到合同编号,回溯不到整条推送失败;币种映射 `invoice.currency-id-mapping.RMB` 必须非空,否则整条推送失败;发票类型映射缺失只记警告并留空字段,是否被拒取决于 HCS 必填校验。 +3. 新增成功后,若金蝶状态为已开票(`kingdeeStatus=invoiced`),立即调 C111-3 更新票据日期/状态;已有合规ID时同样走 C111-3。 +4. 金蝶开票完成的结果回写会把 `hcs_sync_status` 重置为 `unsynced`,下一轮扫描自动执行 C111-3,不需要人工重推。 +5. 附件在推送时经 C103-2 转传(受 `attachment.transfer-enabled` 控制)。 + +开票登记没有 HCS 结果回调接口,TMS 不接收 C111 的结果推送;以整体同步状态和 `hcs_sync_msg` 为准。 + +**接口**: + +| 接口 | 作用 | 关键行为 | +|---|---|---| +| `POST /hcs/sync/invoice-application/{id}` | 单条重推 | 仅 `approved` 可推;先重置 `unsynced`、失败次数清零 | +| `POST /hcs/sync/invoice-application` | 按扫描逻辑推一轮 | 不检查开关、不抢任务锁;**无批量 ID 接口** | + +### 4.7 收票单任务(C109) + +**扫描条件**:收票单审核状态仅 `approved`,合规状态为 `unsynced` / `failed`,失败次数未超限;单轮最多 `batch-size` 条。 + +**推送逻辑**: + +1. 没有合规收票单ID时调 C109-1 新建,成功后回填 `hcs_fd_id`;已有合规ID时调 C109-2 更新。 +2. 合同关联按“收票单 → 结算关系 → 正式结算 → 合同”回溯;回溯不到时取页面人工补录的 `hcs_contract_id` / `hcs_contract_no`;两者都没有时只记警告、合同字段传空,**不阻断推送**。 +3. 附件挂载键为 `fd_attach`,新建时携带 `fdBindType=ADD`,更新时不带。 +4. 税率按 HCS 口径除以 100 转换;发票类型映射来自 `receipt-bill.invoice-type-mapping`,未映射时该字段为空。 + +收票单没有 HCS 结果回调接口,TMS 不接收 C109 的结果推送。 + +**接口**: + +| 接口 | 作用 | 关键行为 | +|---|---|---| +| `POST /hcs/sync/invoice-receipt/{id}` | 单条重推 | 仅 `approved` 可推;先重置 `unsynced`、失败次数清零 | +| `POST /hcs/sync/invoice-receipt` | 按扫描逻辑推一轮 | 不检查开关、不抢任务锁;**无批量 ID 接口** | + +### 4.8 配置项 + +> 下表键名省略公共前缀 `thirdParty.hcs.`。例如 `push.immediate-enabled` 的完整键为 `thirdParty.hcs.push.immediate-enabled`。 + +**公共连接与推送配置** + +| 配置键 | 默认/样例 | 作用 | 配错的后果 | +|---|---|---|---| +| `baseUrl` | 测试网关地址 | HCS 接口基础地址 | 所有推送失败 | +| `authorization` | 环境值 | HCS Authorization 请求头 | 认证被拒 | +| `key-auth` | 环境值 | HCS Key-Auth 请求头 | 认证被拒 | +| `src-system` | `WLTMS` | 来源系统标识 | 合规侧来源不符;回调 `sourceSystem` 校验失败 | +| `operator-login-name` | `srvadmin`(环境可改) | 操作人登录名 | 合规侧操作人异常 | +| `creator-unit-id/name` | 环境固定值 | 创建单位 | 合同创建单位错误 | +| `mock-enabled` | `false`(开发样例 `true`) | Mock 模式开关 | 生产误开会导致推送走本地桩,不真正调用 HCS | +| `push.immediate-enabled` | `false`(开发样例 `true`) | 审批通过即时推送 | 只影响项目立项和合同;关闭后等扫描或手动。付款审批即时特批不受此开关控制 | +| `push.batch-size` | `50` | 单轮扫描条数上限 | 积压数据需多轮处理 | +| `push.max-retry` | `5` | 失败自动重试上限 | 超限后仅能人工重推 | +| `attachment.transfer-enabled` | `true` | 附件转传总开关 | 关闭后合同/开票附件不转传 | +| `attachment.file-download-base` | 空 | TMS 文件下载基址 | 有附件时转传失败 | +| `master-data.ttl-minutes` | `120` | 模板/分类/币种缓存时长 | 主数据变更最长延迟 120 分钟生效 | + +**五个任务开关与 cron** + +| 任务 | 开关(默认) | cron 键 | 默认 cron | +|---|---|---|---| +| 项目立项 | `push.project.sync-enabled`(`false`) | `push.project.cron` | `0 */5 * * * ?` | +| 合同 | `push.contract.sync-enabled`(`false`) | `push.contract.cron` | `0 1/5 * * * ?` | +| 付款链 | `push.payment.sync-enabled`(`false`) | `push.payment.cron` | `0 2/5 * * * ?` | +| 开票登记 | `push.invoice-application.sync-enabled`(`false`) | `push.invoice-application.cron` | `0 3/5 * * * ?` | +| 收票单 | `push.invoice-receipt.sync-enabled`(`false`) | `push.invoice-receipt.cron` | `0 4/5 * * * ?` | + +> 五个开关默认全关,按业务灰度打开;cron 不配置时使用代码默认值(每 5 分钟,分钟位 0–4 错峰)。 + +**各任务业务配置** + +| 配置键 | 默认/样例 | 影响任务 | 说明 | +|---|---|---|---| +| `project.template-code` | `template_15x0og` | 项目立项 C102-1 | 未配置时推送失败 | +| `contract.template-mapping` | 样例仅 `[承运商合同]` | 合同 C103-1 | 合同类别未映射必失败;中文键必须写成 `"[承运商合同]"` | +| `contract.currency-name-mapping` | `RMB→人民币`(另一环境样例 `CNY`) | 合同 C103-1 | 币种传错或为空;上线前与合规方确认取值 | +| `contract.require-risk-check` | `true` | 合同 C101-1 | 关闭后不做风险校验,高风险合同不会触发特批 | +| `exe-stage.sync-enabled` | `false` | 合同 C103-9 | 关闭时合同任务不推履约规则 | +| `exe-stage.fund-type` | `byAmount` | 合同 C103-9 | 金额类型传错会被合规校验拒绝 | +| `spec-pay.template-id` | 环境值 | 付款 C106-3 | 未配置时付款特批发起失败 | +| `spec-pay.doc-status` | `10` | 付款 C106-3 | 单据状态不符合合规要求 | +| `spec-pay.payment-purpose` / `payment-nature` / `payment-type` | 开发样例 `2001` / `1` / `1`,生产样例为空 | 付款 C106-3 | 字典值缺失或错误会导致特批发起失败 | +| `perf-record.sync-enabled` | `false` | 付款 C108 | 关闭时付款只更新整体状态,不推履约记录 | +| `perf-record.xform-id` | 空 | 付款 C108 | 开启履约记录但未配置模板ID时字段为空,HCS 必填校验会失败 | +| `invoice.invoice-type-mapping` | 空映射 | 开票 C111-2 | 未映射时只告警、字段不传,可能被 HCS 必填校验拒绝 | +| `invoice.currency-id-mapping` | 样例 `RMB` 为空 | 开票 C111-2 | `RMB` 映射为空时所有开票登记推送失败 | +| `invoice.fund-nature-default` | `progress` | 开票 C111-2 | 款项性质传错会被合规校验拒绝 | +| `receipt-bill.invoice-type-mapping` | 空映射 | 收票 C109 | 未映射时发票类型字段为空,可能被 HCS 必填校验拒绝 | +| `risk.check-on-payment` | `true` | 付款提交 | 关闭后高风险/黑名单收款方不会被标记特批 | +| `credit.check-enabled` | `false` | 付款提交 | 关闭后不调 C106-1,额度超额不会自动转特批 | +| `credit.require-contract-synced` | `true` | 付款提交 | 额度校验开启时,关联合同未推送合规会直接拦截提交 | + +> 不同环境样例中的币种名称映射可能不同(`CNY` 与 `人民币`),上线前必须与合规方确认 HCS 主数据实际取值。 + +### 4.9 运维注意 + +- **开关灰度**:五个扫描开关默认全关且互相独立;项目立项/合同的即时推送受 `push.immediate-enabled` 控制,付款审批即时特批不受该开关控制;手动接口始终可用,不受开关限制。 +- **先看存量再开扫描**:扫描不区分“从未推送”和“推送失败”,开关一开,所有审批通过且未同步的历史单据都会进入扫描范围(每轮最多 50 条)。上线前先确认待推送数据量。 +- **修复根因优先**:失败单据每 5 分钟自动重试,达到 `push.max-retry`(默认 5)后停止;人工重推会清零次数,但不修复配置/数据会反复失败。 +- **付款依赖合同**:C106-3 要求关联合同已有合规合同ID,C103-9 也以合同 `hcs_fd_id` 为前提;合同未推送合规时付款特批/履约规则相关步骤会失败或跳过。 +- **开票必配币种映射**:`invoice.currency-id-mapping` 的 `RMB` 必须配置为 HCS 币种 fdId,否则开票登记每轮都会失败。 +- **收票合同关联**:回溯不到合同且未人工补录 `hcs_contract_id` / `hcs_contract_no` 时不会失败,但合同字段为空;需要完整合同信息时先补录再重推。 +- **模板映射逐个类别确认**:样例只配了承运商合同;客户合同、补充协议未配置时推送必失败。Nacos YAML 中文键必须写成 `"[承运商合同]"`。 +- **附件下载基址必配**:合同有附件时,`attachment.file-download-base` 为空会导致附件转传失败。 +- **生产禁用 Mock**:`thirdParty.hcs.mock-enabled` 生产环境必须为 `false`。 +- **主数据刷新**:模板映射调整后,可调用 `POST /hcs/master-data/refresh` 刷新模板/分类/币种缓存并校验映射。 +- **多实例与手动接口**:定时任务按 bizType 抢 Redis 锁(TTL 240 秒),抢不到本轮跳过;手动接口不抢锁,避免与定时任务同一时刻执行。 +- **日志排查**:服务日志按单据编号、合规ID、`hcs` 关键字检索;本地日志目录为 `F:\home\tms\blade-transport`。 + +--- + +## 5. 常见问题与异常处理(FAQ) + +| 现象 | 原因 | 处理 | +|---|---|---| +| 审批通过后合规状态一直“待推送” | 未开启即时推送,合同扫描也未开启或未到执行时间 | 手动点“推送合规”;或让运维开启对应开关 | +| 审批通过时提示“审批通过,但合规推送失败:xxx” | 审批已生效,HCS 推送失败 | 按失败原因修复后手动重推,不要重复审批 | +| 列表没有“推送合规”按钮 | 合同不是审批通过/变更审批通过,或合规状态已是“已推送” | 先完成审批;已推送合同如需修改走“变更” | +| 批量推送提示“选中数据中有 N 条不符合推送条件” | 勾选了未审批通过或已同步的合同 | 取消勾选不符合条件的合同,或单独推送 | +| 推送失败原因在哪里看 | 列表悬停合规状态,或详情“合规信息” | 看 `hcs_sync_msg`/失败说明,按原因处理 | +| 失败超过 5 次后不再自动重试 | 达到 `push.max-retry` 上限 | 修复根因后手动“推送合规”,会清零重试次数 | +| 提示“合同类别 [xxx] 未配置合规模板ID” | Nacos 模板映射缺该类合同 | 运维补齐 `contract.template-mapping` 后重推 | +| 提示附件转传失败 | 下载基址为空、附件不可下载、文件超限或 HCS 附件接口异常 | 检查 `attachment.file-download-base` 和附件,修正后重推 | +| 提示 C101-1 客商风险查询失败 | HCS 风险接口异常/无响应 | 联系 HCS 侧确认接口,恢复后重推 | +| 高风险客商没有触发特批 | 客商名称与档案不匹配导致风险校验被跳过,或 HCS 返回的风险等级不是高风险/黑名单 | 核对甲乙方名称与客商档案;在 HCS 侧确认客商风险等级和黑名单状态 | +| 提示 C103-1/C103-11 合同发起/更新失败 | HCS 业务校验不通过 | 按返回 msg 修正模板、字段或主数据后重推 | +| 特批状态一直是“待特批” | 存量数据或推送未成功,未进入特批流程 | 联系管理员重新推送;推送成功后会自动转“特批中” | +| 特批中想重推 | 特批流程未结束 | 不允许重推,等待 HCS 回传或人工确认结果 | +| 如何人工确认特批结果 | HCS 结果未自动回传 | 详情 → 合规信息 → 确认特批结果,输入 `approved`/`rejected`;仅“特批中”可用 | +| 特批驳回后合同变成“已驳回” | 特批结果联动合同审批 | 修改合同、重新提交审批、重新推送 | +| 点击“发起特批”提示接口不存在 | 旧版独立入口,后端接口已取消,特批随推送自动发起 | 忽略即可;正常流程不需要点击 | +| 已推送合同不能编辑 | 防止 HCS 产生重复合同 | 走“变更”流程,变更审批通过后更新 HCS | +| 金蝶合同ID为空 | HCS/金蝶同步有延迟 | 稍后刷新;长时间为空联系管理员 | +| 上传双章归档文件后归档状态没变 | 附件位置或类型不对,或合同未审批通过 | 附件位置选用印材料、类型选双章归档文件,审批通过后刷新 | +| “流程”提示流程实例为空 | 合同未关联外部审批流程实例 | 以列表/详情的审核状态和当前节点为准;需要流转记录联系管理员 | +| 变更页面“终止合同”不能提交 | 当前版本未开放终止入口 | 选择“合同信息变更”;确需终止联系管理员 | +| 临时合同审批通过后也变成待推送/已推送 | 当前实现按审批状态扫描,未按合同阶段过滤 | 属当前版本行为,按合规状态正常处理即可 | +| 项目审批通过后合规状态一直“待推送” | 未开启即时推送或项目扫描未开启/未到执行时间 | 列表点“推送合规”;或让运维开启 `push.project.sync-enabled` | +| 项目推送成功但“金蝶项目ID”为空 | 金蝶同步结果尚未回传 | 稍后刷新;长时间为空联系管理员 | +| 项目列表没有“推送合规”按钮 | 不是审批通过/变更审批通过,或已是“已推送” | 先完成审批;已推送项目需修改走“变更” | +| 项目提示“立项表单模板编码未配置” | `project.template-code` 为空 | 运维补齐配置后重推 | +| 付款审批通过后当前节点显示“特批审核” | 高风险/黑名单或额度超额,系统已自动发起付款特批 | 正常等待;特批通过后自动推送履约数据并同步金蝶 | +| 付款页面看不到“特批状态” | 付款页面没有独立特批状态列 | 看列表“当前节点”;特批中为“特批审核”,审核状态仍是“审批通过” | +| 高风险收款方提交付款被拦截 | 未上传“特批附件” | 在附件区新增一行,类型选“特批附件”后重新提交 | +| 付款特批被驳回,审核状态变“已驳回” | HCS 特批未通过 | 编辑修改后重新提交、重新审批,系统会重新发起特批 | +| 付款审批提示“已发起付款特批”但迟迟没有金蝶单据 | 特批仍在审批,特批通过后才同步金蝶 | 等待特批结果;长时间无结果联系实施人工确认 | +| 付款特批发起失败(模板未配/合同未推送) | `spec-pay.template-id` 为空或关联合同无合规ID | 运维修复后重推;不要重复审批 | +| 付款特批中能否重复审批 | 特批中审核状态已是“审批通过” | 不需要也不能重复审批;等待 HCS 结果 | + +--- + +## 6. 附:操作入口速查表 + +| 页面 | 按钮/操作 | 作用 | +|---|---|---| +| 合同管理列表 | 工具栏“新增” | 新增合同 | +| 合同管理列表 | 行内“编辑” | 编辑草稿/已撤回/已驳回/变更驳回合同 | +| 合同管理列表 | 行内“复制” | 以当前合同为模板新建草稿 | +| 合同管理列表 | 行内“删除” | 删除草稿合同 | +| 合同管理列表 | 行内“转正式合同” | 临时合同审批通过后转正式 | +| 合同管理列表 | 行内“重新提交转正” | 临时合同被驳回后重新提交 | +| 合同管理列表 | 行内“变更”/“重新提交变更” | 正式合同发起变更/变更驳回后重提 | +| 合同管理列表 | 行内“附件上传” | 正式合同审批通过后补传附件 | +| 合同管理列表 | 行内“推送合规” | 单条推送/重推 HCS | +| 合同管理列表 | 工具栏“批量推送合规” | 勾选多条合同批量推送 | +| 合同管理列表 | 审批中行内“通过/驳回/撤回” | 审批合同;通过时尝试推送 HCS | +| 合同详情 | 合规信息“确认特批结果” | 特批中人工确认 `approved`/`rejected` | +| 项目管理列表 | 行内“通过” | 审批项目;通过时尝试推送 HCS | +| 项目管理列表 | 行内“推送合规” | 单条推送/重推项目立项 | +| 项目管理列表 | 工具栏“批量推送合规” | 勾选多条项目批量推送 | +| 项目管理列表 | 行内“变更” | 项目变更;审批通过后更新 HCS | +| 付款申请表单 | 附件类型“特批附件” | 高风险/黑名单收款方提交前必传 | +| 付款申请列表 | 行内“通过” | 审批通过后自动发起付款特批 | +| 付款申请列表 | “当前节点”列 | 查看“特批审核/审批通过/已驳回”判断特批进度 | +| 运维专用 | `POST /hcs/sync/project/{id}` | 单条重推项目立项 | +| 运维专用 | `POST /hcs/sync/project/batch` | 按勾选ID批量重推项目立项 | +| 运维专用 | `POST /hcs/spec-pay/confirm` | 人工确认付款特批结果(参数 `id`、`result`;页面无按钮) | +| 运维专用 | `POST /hcs/sync/contract/{id}` | 单条重推合同 | +| 运维专用 | `POST /hcs/sync/contract` | 按扫描逻辑推送一轮待同步/失败合同 | +| 运维专用 | `POST /hcs/sync/contract/batch` | 按勾选ID批量重推(请求体为ID数组) | +| 运维专用 | `POST /hcs/contract/spec/confirm` | 人工确认合同特批结果(参数 `id`、`result`) | +| 运维专用 | `POST /hcs/master-data/refresh` | 刷新 HCS 模板/分类/币种缓存并校验模板映射 | + +> 五类单据通用手动入口:`POST /hcs/sync/{bizType}/{id}`(单条重推)、`POST /hcs/sync/{bizType}`(扫一轮);`bizType` 取 `project`、`contract`、`payment`、`invoice-application`、`invoice-receipt`,批量 `/batch` 仅支持 `project`、`contract`。各任务链路与接口详见 4.3–4.7。 + +--- + +> 本文基于 2026-10-23 代码与页面现状整理;按钮、提示和环境开关以实际页面为准。临时合同推送、旧版“发起特批”入口、变更页“终止合同”未开放属于当前版本现状,后续版本调整时请同步核对。 diff --git a/doc/docs/hcs合同/images/01-合同列表.png b/doc/docs/hcs合同/images/01-合同列表.png new file mode 100644 index 00000000..7553fe86 Binary files /dev/null and b/doc/docs/hcs合同/images/01-合同列表.png differ diff --git a/doc/docs/hcs合同/images/02-合同列表状态与操作列.png b/doc/docs/hcs合同/images/02-合同列表状态与操作列.png new file mode 100644 index 00000000..1c4ec31c Binary files /dev/null and b/doc/docs/hcs合同/images/02-合同列表状态与操作列.png differ diff --git a/doc/docs/hcs合同/images/03-合同列表筛选.png b/doc/docs/hcs合同/images/03-合同列表筛选.png new file mode 100644 index 00000000..d59436f0 Binary files /dev/null and b/doc/docs/hcs合同/images/03-合同列表筛选.png differ diff --git a/doc/docs/hcs合同/images/04-新增合同-基本信息.png b/doc/docs/hcs合同/images/04-新增合同-基本信息.png new file mode 100644 index 00000000..7d412bee Binary files /dev/null and b/doc/docs/hcs合同/images/04-新增合同-基本信息.png differ diff --git a/doc/docs/hcs合同/images/05-新增合同-计费结算与附件.png b/doc/docs/hcs合同/images/05-新增合同-计费结算与附件.png new file mode 100644 index 00000000..8b7c13ff Binary files /dev/null and b/doc/docs/hcs合同/images/05-新增合同-计费结算与附件.png differ diff --git a/doc/docs/hcs合同/images/06-合同详情-基本信息与合规信息.png b/doc/docs/hcs合同/images/06-合同详情-基本信息与合规信息.png new file mode 100644 index 00000000..f1e41822 Binary files /dev/null and b/doc/docs/hcs合同/images/06-合同详情-基本信息与合规信息.png differ diff --git a/doc/docs/hcs合同/images/07-合同详情-附件佐证材料.png b/doc/docs/hcs合同/images/07-合同详情-附件佐证材料.png new file mode 100644 index 00000000..2ac9e6a0 Binary files /dev/null and b/doc/docs/hcs合同/images/07-合同详情-附件佐证材料.png differ diff --git a/doc/docs/hcs合同/images/08-合同详情-变更记录.png b/doc/docs/hcs合同/images/08-合同详情-变更记录.png new file mode 100644 index 00000000..5039d0bd Binary files /dev/null and b/doc/docs/hcs合同/images/08-合同详情-变更记录.png differ diff --git a/doc/docs/hcs合同/images/09-合同变更-基本信息.png b/doc/docs/hcs合同/images/09-合同变更-基本信息.png new file mode 100644 index 00000000..d678cc82 Binary files /dev/null and b/doc/docs/hcs合同/images/09-合同变更-基本信息.png differ diff --git a/doc/docs/hcs合同/images/10-合同变更-原因与材料.png b/doc/docs/hcs合同/images/10-合同变更-原因与材料.png new file mode 100644 index 00000000..b9548596 Binary files /dev/null and b/doc/docs/hcs合同/images/10-合同变更-原因与材料.png differ diff --git a/doc/docs/hcs合同/images/11-附件上传弹窗.png b/doc/docs/hcs合同/images/11-附件上传弹窗.png new file mode 100644 index 00000000..ca69250e Binary files /dev/null and b/doc/docs/hcs合同/images/11-附件上传弹窗.png differ diff --git a/doc/docs/hcs合同/images/12-特批待发起.png b/doc/docs/hcs合同/images/12-特批待发起.png new file mode 100644 index 00000000..d2b444f0 Binary files /dev/null and b/doc/docs/hcs合同/images/12-特批待发起.png differ diff --git a/doc/docs/hcs合同/images/13-确认特批结果.png b/doc/docs/hcs合同/images/13-确认特批结果.png new file mode 100644 index 00000000..ef3baffb Binary files /dev/null and b/doc/docs/hcs合同/images/13-确认特批结果.png differ diff --git a/doc/docs/hcs合同/images/14-待推送与推送失败列表.png b/doc/docs/hcs合同/images/14-待推送与推送失败列表.png new file mode 100644 index 00000000..8a698514 Binary files /dev/null and b/doc/docs/hcs合同/images/14-待推送与推送失败列表.png differ diff --git a/doc/docs/hcs合同/images/15-手动推送合规确认.png b/doc/docs/hcs合同/images/15-手动推送合规确认.png new file mode 100644 index 00000000..e990dc4e Binary files /dev/null and b/doc/docs/hcs合同/images/15-手动推送合规确认.png differ diff --git a/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md b/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md new file mode 100644 index 00000000..64022897 --- /dev/null +++ b/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md @@ -0,0 +1,200 @@ +# 付款申请全链路业务流程(前端提交 → 客商风险与特批 → 同步金蝶 → 结果回写) + +> 梳理日期:2026-10-13 +> 涉及模块:`tms-erp-web/src/views/payment/payment-application-form.vue`(前端表单)、`blade-service/blade-transport`(业务编排)、`blade-third-party-api/blade-hcs-api`(合规 Feign)、`blade-third-party-api/blade-bfi-api`(金蝶 BFI Feign) +> 关联文档:`doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md`(特批机制专项)、`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(图03)、`业务流程/2026-09-16-付款管理正式付款同步金蝶….md` 与 `业务流程/2026-09-17-付款管理金蝶付款结果定时回写….md`(金蝶链路专项) +> 2026-10-13 变更:付款链路黑名单不再直接阻断,与高风险统一走特批(提示 + 特批附件必传 + C106-3 特批 + 回调联动审批状态 + 金蝶推送门禁);前端付款表单新增风险客商提示条与特批附件提交校验。 + +--- + +## 一、全链路总览 + +```mermaid +flowchart TD + subgraph FE["前端 · 付款申请表单"] + f1["新建付款申请
选付款类型(正式结算付款/预付款/项目预付)"] + f1 --> f2["选合同/结算单 → 联动收款账户
客商档案反查收款方"] + f2 --> f3{"C101-1 客商风险查询
(收款方外部客商才查)"} + f3 -->|"高风险/黑名单"| f4["风险客商提示条(常驻)
要求上传「特批附件」"] + f3 -->|"低风险/中风险/内部客商"| f5["正常填写"] + f4 --> f6 + f5 --> f6["保存草稿 / 提交"] + end + + subgraph TMS["TMS 后端 · 提交与审批"] + f6 --> t1{"提交校验
(submit 事务内)"} + t1 -->|"特批附件缺失"| t2["拦截:必须上传「特批附件」"] + t1 -->|"通过"| t3["reviewing 财务审核
(高风险/黑名单置 hcs_spec_status=need)"] + t3 --> t4{"本地审批"} + t4 -->|"驳回"| t5["returned 已驳回 → 重新编辑"] + t4 -->|"通过"| t6["approved 审批通过
hcs_sync_status=unsynced"] + end + + subgraph HCS["合同合规系统(HCS)"] + t6 -->|"定时扫描发起 C106-3"| h1["结算付款特批单生成"] + h1 --> h2["TMS 挂起 currentNode=特批审核
(need/pushing/processing 禁止推送金蝶)"] + h2 --> h3{"集团特批审批"} + h3 -->|"paymentLegal/result:APPROVED"| h4["恢复 审批通过 → 放行金蝶推送"] + h3 -->|"paymentLegal/result:REJECTED"| h5["联动付款审批驳回 returned
重新编辑 → 提交 → 重新特批"] + end + + subgraph JD["金蝶星瀚(经 iPaaS /gwzh/BFI)"] + h4 -->|"手动/批量同步"| j1{"付款类型路由"} + j1 -->|"正式结算付款"| j2["A102-4 预检财务应付单已生成
→ A103-2 幂等预检 → A103-1 应付下推"] + j1 -->|"预付款"| j3["A103-3 预付款申请保存"] + j2 --> j4["kingdeeStatus=synced(锁单)"] + j3 --> j4 + j4 --> j5["财务共享审批 → 付款处理 → 付款完成"] + j5 -->|"A103-2/A103-4 定时回写"| j6["更新付款结果/履约数据"] + end +``` + +三条主线:**合规管控线**(C101-1 风险 → 特批附件 → C106-3 特批 → paymentLegal/result 联动)、**本地审批线**(draft → reviewing → approved/returned)、**金蝶付款线**(A103-1/A103-3 → 锁单 → A103-2/A103-4 回写)。硬约束:**高风险/黑名单收款方特批不通过,付款申请不得推送金蝶**。 + +--- + +## 二、阶段一:前端表单与提交(`payment-application-form.vue`) + +| 步骤 | 动作 | 实现要点 | +|---|---|---| +| 1 | 新建/编辑付款申请 | 付款类型路由引用单据:正式结算付款→正式结算单(可多张)、预付款→预结算单、项目预付→项目+合同 | +| 2 | 选合同联动 | `fillBillTypeFromContract`:带出票据类型/付款方(合同甲方)/收款账户候选(合同乙方客商) | +| 3 | **收款方风险查询** | `loadReceiptAccountOptions`:按合同乙方名称反查客商档案 → `GET /hcs/risk?customerCode=`(C101-1 实时查,后端对内部组织客商默认返回无风险)→ `payeeRisky/payeeRiskLevel/payeeBlackStatus/payeeRiskTip` 留档 | +| 4 | **风险客商提示条** | 高风险/黑名单时基本信息"备注"下方常驻展示(橙黄底红字,同合同新增页):`风险等级:高风险|黑名单:是。【名称】:…风险项…,按集团规定不可直接付款,提交前须上传「特批附件」类型附件,审批通过后将自动发起付款特批`;附件清单同步显示"未上传:特批附件" | +| 5 | 附件材料 | 材料清单:磅单/结算单(必传)、合同签章文件(首次合同付款/项目预付)、**特批附件**(收款方高风险/黑名单或客商等级 D/E/F 时必传,`attachmentType=special_approval`)、其他 | +| 6 | 保存草稿 | `POST /payment-application`(save)→ draft,可反复编辑 | +| 7 | 提交 | `submitForm`:静默保存 → **特批附件缺失校验**(高风险/黑名单阻断,提示与后端同口径)→ `POST /payment-application/submit` → 发起 MK 审批流 | + +> 风险查询失败不阻断前端填写,提交时后端强校验兜底。 + +--- + +## 三、阶段二:后端提交校验(`PaymentApplicationServiceImpl#submit`,事务内) + +``` +前置校验:状态(draft/returned)→ 申请金额>0 → 尾款/项目预付合同/选票/额度校验 + ↓ +HcsCreditCheckSupport#checkOnSubmit(risk.check-on-payment 默认 true) + ↓ +① C101-1 收款方风险校验(checkRelativeRisk): + - 反查不到客商档案 → skip 留痕不阻断 + - 内部组织客商(customerKind=internal)→ 默认无风险,不调 C101-1 + - 外部客商 → C101-1 实时查询,risky = 风险等级2(高风险)或 fdBlackStatus=yes(黑名单) + - risky → 「特批附件必传」校验(attachmentsJson 含 attachmentType=special_approval,缺失抛异常) + → hcs_spec_status=need + 清空旧 hcs_spec_fd_id(特批单次有效,重新提交重新发起) +② C106-1 额度校验(credit.check-enabled 默认 false,一期未对接): + - 合同已推送(有 hcs_fd_id)→ 额度内 pass / 超额不拦截 → hcs_credit_status=over + need + ↓ +全部通过:approvalStatus=reviewing,currentNode="财务审核",留痕 blade_hcs_check_log(biz_type=payment,check_type=risk/credit) +``` + +--- + +## 四、阶段三:本地审批 + +| 动作 | 状态流转 | 附加动作 | +|---|---|---| +| 审批通过 `approve` | reviewing → **approved**,currentNode="审批通过" | `hcs_sync_status=unsynced`(零埋点 ADR-0001,扫描任务据此接管后续推送) | +| 审批驳回 `returnBill` | reviewing → **returned**,currentNode="已驳回" | — | +| 作废 `voidBill` | approved → **voided**,currentNode="已作废" | 票据付款释放票据占用 | + +--- + +## 五、阶段四:HCS 付款特批推送(`HcsPushTask#pushHcsPayment`) + +- **定时扫描**:cron 默认 `0 2/5 * * * ?`,开关 `push.payment.sync-enabled` **默认 false**(灰度开启);条件 `approvalStatus=approved` 且 `hcs_sync_status ∈ {unsynced, failed}` 且重试 < `push.max-retry`(默认 5);Redis 任务锁 TTL 240s 防多实例重扫。 +- **推送编排**(`HcsPaymentSyncSupport#pushPayment`): + 1. **① 结算付款特批**(`hcs_spec_status=need` 时,`HcsSpecPaySyncSupport#pushSpecPay`): + - `hcs_spec_fd_id` 为空 → **C106-3 发起**:报文主题 `结算付款特批-{paymentNo}`,合同传 `blade_contract_manage.hcs_fd_id`,金额/收付款方/账户随单,附件经 C103-2 转传挂 `fd_spc_approval_attach`(发起带 `fdBindType=ADD`)→ 返回双层 `data.data.fdId` 回填 `hcs_spec_fd_id` → 状态 **processing** + **挂起 `currentNode="特批审核"`**; + - 非空 → **C106-4 更新**(单据变更场景,附件不带 fdBindType)。 + 2. ② C108 履约记录(默认关)。 + 3. `markSynced`/`markFailed` 回写整体同步状态。 +- **手动重推**:`POST /hcs/sync/payment/{id}`(重置 unsynced 立即推)、`POST /hcs/sync/payment`(批量扫一轮)。 +- **金蝶推送门禁**(`doSyncKingdee`):`hcs_spec_status ∈ {need, pushing, processing}` 抛"收款方客商为高风险/黑名单,付款特批审核中(特批审核),特批完成后方可推送金蝶"。 + +--- + +## 六、阶段五:特批结果联动(`HcsPaymentSyncSupport`) + +| 路径 | 机制 | 结果联动 | +|---|---|---| +| **合规回调(主)** | 合规审批完成后回调 `POST /hcs/paymentLegal/result`(网关 `/gwzh/HTHG/paymentLegal/result`,blade.secure 放行)→ `handleLegalResult`:`sourceSystem` 须为 `third-party.hcs.src-system`(默认 WLTMS,不符抛"来源系统标识错误"),`requestId`=付款申请ID 定位,`status=APPROVED/REJECTED` | 见下 | +| **人工确认(兜底)** | `POST /hcs/spec-pay/confirm`(仅 processing 可确认,与回调等效) | 见下 | + +| 特批结果 | `hcs_spec_status` | 付款审批联动 | 后续 | +|---|---|---|---| +| APPROVED | approved | currentNode="审批通过"(审批状态保持 approved) | 金蝶推送门禁解除,可同步金蝶 | +| REJECTED | rejected | **approvalStatus=returned + currentNode="已驳回"** | 走"重新编辑 → 提交(重新校验风险与特批附件、重新置 need)→ 审批 → 重新发起 C106-3"闭环 | + +> 付款特批无 C106 结果查询接口,不做定时查询兜底(合同链路才有 C103-4 兜底);回调未达时靠页面人工确认。 + +--- + +## 七、阶段六:同步金蝶(`syncKingdee` → `doSyncKingdee`,单据级事务经自代理) + +**前置**:`approvalStatus=approved` + 特批门禁通过 + `kingdeeStatus` 非终态(synced/paying/paid/closed 直接幂等返回)。 + +| 付款类型 | 链路 | +|---|---| +| **正式结算付款**(settlement_payment,应付类) | ① `requirePayableSettlement` 校验结算类型 → ② 逐张正式结算单 **A102-4** 预检财务应付单已在金蝶生成(缺则拦截"请先在正式付款单页面同步金蝶")→ ③ **A103-2** 幂等预检(上次下推成功但本地回滚的场景直接回填)→ ④ **A103-1 付款申请保存(应付下推)** | +| **预付款 / 项目预付** | 上次失败先 **A103-2** 勾稽恢复 → **A103-3 付款申请保存(预付款)**,不绑应付单 | + +- **成功**:`kingdeeBillId` 回填 + `kingdeeStatus=synced`(业务单据锁定,ERP 不可改);付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 回填。 +- **失败**:事务回滚后独立事务 `markKingdeeSyncFailed` → `kingdeeStatus=failed`,可重试同步。 +- 批量:`syncKingdeeBatch`(全批共享档案缓存 + A102-4 批量预检,逐单单据级事务)。 + +--- + +## 八、阶段七:付款结果回写与履约联动 + +- **定时回写**(`PaymentKingdeeResultTask`,专项文档见 `业务流程/2026-09-17-….md`):**A103-2** 付款申请单查询 + **A103-4** 付款处理查询 → 回写 `kingdeeBillNo`/付款状态(paying/paid/closed)→ 付款完成联动正式结算付款记录(`blade_formal_settlement_payment`)与履约数据。 +- **HCS 履约/附件**(默认关/独立周期):C108 履约记录推送(`perf-record.sync-enabled`)、C106-2 付款附件每日定时推送——不在本文展开,见特批专项文档 §四。 + +--- + +## 九、状态机速查 + +**付款审批状态 `approvalStatus`**:`draft` →(提交)`reviewing` →(通过)`approved` /(驳回)`returned` /(作废,仅 approved)`voided`;特批驳回联动 `approved → returned`。 + +**HCS 特批状态 `hcs_spec_status`**:`none` →(提交时高风险/黑名单或额度超额)`need` →(C106-3 发起成功)`processing`(挂起"特批审核")→(paymentLegal/result 回调/人工确认)`approved` / `rejected`。 + +**金蝶状态 `kingdeeStatus`**:空 →(同步成功)`synced` →(回写)`paying` → `paid` / `closed`;同步失败 `failed`(可重推)。 + +**当前节点 `currentNode`**:财务审核 → 审批通过 →(特批中)**特批审核** → 审批通过(放行)/ 已驳回(returned 闭环)。 + +--- + +## 十、配置项与接口速查 + +| 配置(`thirdParty.hcs.*`) | 默认 | 说明 | +|---|---|---| +| `risk.check-on-payment` | true | 提交时收款方 C101-1 校验开关 | +| `credit.check-enabled` | false | C106-1 额度校验(一期未对接,超额为特批第二触发源) | +| `push.payment.sync-enabled` / `push.payment.cron` | **false** / `0 2/5 * * * ?` | 付款特批+履约定时扫描(**未开启时高风险单审批通过后停在 need,且金蝶推送被门禁拦截**) | +| `push.max-retry` / `push.batch-size` | 5 / 50 | 重试上限 / 单轮批量 | +| `spec-pay.payment-purpose/nature/type`、`doc-status` | — | C106 报文字典值 | +| `src-system` | WLTMS | paymentLegal/result 回调来源校验值 | + +| 接口 | 提供方 | 用途 | +|---|---|---| +| `GET /hcs/risk` | TMS 代理 C101-1 | 前端收款方风险查询(提示条) | +| C106-3 / C106-4 | 合同合规 | 结算付款特批发起 / 更新 | +| `POST /hcs/paymentLegal/result` | 合同合规 → TMS | 特批结果回调(APPROVED 放行 / REJECTED 联动审批驳回) | +| `POST /hcs/spec-pay/confirm` | TMS | 特批结果人工确认(兜底) | +| A102-4 / A103-2 | 金蝶 | 财务应付单预检 / 付款申请单幂等预检与结果回写 | +| A103-1 / A103-3 | 金蝶 | 应付下推付款申请 / 预付款申请保存 | +| B901 | ERP → 金蝶 | 锁单/解锁(已推金蝶单据 ERP 锁定) | + +--- + +## 十一、异常与逆向 + +| 场景 | 处理 | +|---|---| +| C101-1 查询失败 | 提交抛"C101-1 客商风险查询失败"(不留风险缺口);前端查询失败仅跳过提示 | +| 收款方反查不到客商档案 | skip 留痕不阻断(§9-U6 口径) | +| 特批附件缺失 | 前端提交阻断 + 后端提交抛异常(双重校验) | +| C106-3 发起失败 | `hcs_sync_status=failed` + 重试计数,下轮扫描自动补偿,超限人工重推 | +| 特批驳回 | 付款审批联动驳回(returned),重新编辑提交后重新置 need、清空旧 `hcs_spec_fd_id`,重新发起特批 | +| 金蝶推送失败 | `kingdeeStatus=failed`,A103-2 幂等预检防重复推送;批量同步单条失败不影响整批 | +| 金蝶侧驳回/需修改 | 金蝶反审核+删除 → 解锁 → TMS 重推(已推单据 ERP 不可改,财务共享审批对 TMS 黑盒) | diff --git a/doc/docs/桂物合同集成相关接口说明-v4.docx b/doc/docs/桂物合同集成相关接口说明-v4.docx new file mode 100644 index 00000000..89280f37 Binary files /dev/null and b/doc/docs/桂物合同集成相关接口说明-v4.docx differ diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index 063e4cc1..7607c247 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -32,7 +32,7 @@ spring: blade: #分布式锁配置 lock: - enabled: false + enabled: true address: redis://127.0.0.1:6379 #多团队协作服务配置 loadbalancer: @@ -47,10 +47,9 @@ blade: datasource: dev: # MySql - # rewriteBatchedStatements=true:MyBatis-Plus saveBatch/updateBatchById 依赖该参数才会合并为多值语句,否则仍是逐条往返(仅 MySQL 支持,切换其他库需移除) url: jdbc:mysql://localhost:3306/transport?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true username: root - password: root + password: 123456 # PostgreSQL #url: jdbc:postgresql://127.0.0.1:5432/bladex #username: postgres @@ -76,6 +75,18 @@ blade: #username: kingbase #password: 123456 +#日志级别配置 +logging: + level: + org.springblade: debug + # 临时 + org.springframework.amqp: ERROR + org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer: OFF + # feign: debug +# logging: +# file: +# path: ./logs # 日志文件目录 +# name: ./logs/app.log # 或直接指定文件名 #第三方接口配置 thirdParty: ocr: @@ -93,12 +104,11 @@ thirdParty: queryDepartmentPageUrl: /gwzh/OA/OA_GET_DEPARTMENT_LIST queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} - lbs: - # LBS 网关地址(车辆实时定位) - baseUrl: http://172.16.204.83:38000 - locateUrl: /gwzh/LBS/LBS_LOCATE - trackUrl: /gwzh/LBS/LBS_TRACK - authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} + wechat: + mini: + # 微信小程序(手机号一键登录 / openid) + appId: ${WECHAT_MINI_APP_ID:} + appSecret: ${WECHAT_MINI_APP_SECRET:} track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 @@ -120,7 +130,6 @@ thirdParty: processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute} processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete} getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo} - getProcessInfoUrl: ${MK_PROCESS_INFO_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getProcessInfo} getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos} getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode} pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept} @@ -133,11 +142,7 @@ thirdParty: downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download} queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list} queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list} - wechat: - mini: - # 微信小程序(手机号一键登录 / openid) - appId: ${WECHAT_MINI_APP_ID:} - appSecret: ${WECHAT_MINI_APP_SECRET:} + # 业财融合(BFI)应用及接口配置,敏感值通过环境变量注入 bfi: # 业财融合(BFI)应用及接口配置,敏感值通过环境变量注入 baseUrl: ${BFI_BASE_URL:http://172.16.203.134:8000/gwzh/BFI} @@ -149,6 +154,22 @@ thirdParty: language: ${BFI_LANGUAGE:zh_CN} # 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS) srcSys: ${BFI_SRC_SYS:WLTMS} + + # 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值): + # 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务; + # 装卸大类及手动添加的非运费杂费映射装卸物料 + material: + # 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种) + domestic: + "[公路运输]": ${BFI_MATERIAL_DOMESTIC_ROAD:XDWL11010001} + "[铁路运输]": ${BFI_MATERIAL_DOMESTIC_RAIL:XDWL11010002} + "[水路运输]": ${BFI_MATERIAL_DOMESTIC_WATER:XDWL11010003} + "[航空运输]": ${BFI_MATERIAL_DOMESTIC_AIR:Item-00001082} + # 国际运输业务模式物料编码(国际货运代理服务) + international: ${BFI_MATERIAL_INTERNATIONAL:Item-00001081} + # 装卸大类、手动添加非运费杂费物料编码(装卸) + loading: ${BFI_MATERIAL_LOADING:XDWL11002004} + # 付款申请推送配置(A103-3 付款申请保存) payApply: # 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请 @@ -164,6 +185,28 @@ thirdParty: syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true} # 调度 cron,默认每10分钟 cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?} + # 收款认领公告查询配置(A301-2) + claimAnnounce: + # 查询时间窗口(月数) + months: ${BFI_CLAIM_ANNOUNCE_MONTHS:6} + # 认领人:配置后公告查询固定使用该值,不再按当前登录用户姓名查询 + claimusers: ${BFI_CLAIM_ANNOUNCE_CLAIMUSERS:徐媛} + + # 收票管理金蝶全票池发票定时绑定(A801-1 查询全票池发票数据) + invoiceReceipt: + # 绑定任务开关,默认关闭(联调稳定后开启) + syncEnabled: ${BFI_INVOICE_RECEIPT_SYNC_ENABLED:false} + # 调度 cron,默认每30分钟 + cron: ${BFI_INVOICE_RECEIPT_CRON:0 */30 * * * ?} + # 增量窗口天数(按票池更新时间),首轮/兜底全量比对窗口 + incrementDays: ${BFI_INVOICE_RECEIPT_INCREMENT_DAYS:281} + # 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制 + invoiceTypes: ${BFI_INVOICE_RECEIPT_INVOICE_TYPES:26,27} + # 补偿绑定单轮限量(未绑定登记按发票号码精确补查条数上限) + compensateLimit: ${BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT:200} + # 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在客商发票信息中的发票,前端票池展示查询同用此开关过滤 + customerUnitsOnly: ${BFI_INVOICE_RECEIPT_CUSTOMER_UNITS_ONLY:false} + # 收款认领推送金蝶定时任务(A301-3 收款认领结果推送) receiptClaimPush: # 推送任务开关,默认关闭(联调稳定后开启) @@ -174,6 +217,138 @@ thirdParty: # 进程异常退出时锁在租约到期后自动释放 lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800} + # 审批通过自动同步金蝶(事件触发:结算单A102-2/A302-2、付款申请A103-1/A103-3、开票申请A303-1、收票A303-2+A302-6) + autoSync: + # 结算单审批后自动推送开关 + settlement: ${BFI_AUTO_SYNC_SETTLEMENT:true} + # 付款申请审批后自动推送开关 + payment: ${BFI_AUTO_SYNC_PAYMENT:true} + # 开票申请审批后自动推送开关 + invoiceApply: ${BFI_AUTO_SYNC_INVOICE_APPLY:true} + # 收票登记审批后自动推送开关 + receipt: ${BFI_AUTO_SYNC_RECEIPT:true} + # failed 单据定时重推开关(仅重推生成失败,不扫 unsynced 存量) + retryEnabled: ${BFI_AUTO_SYNC_RETRY_ENABLED:false} + # 重推调度 cron,默认每10分钟 + retryCron: ${BFI_AUTO_SYNC_RETRY_CRON:0 */10 * * * ?} + + # 合同合规系统 + hcs: + # Mock 开关(true 时 IHcsClient 走本地 JSON 桩 F:/home/tms/mock/hcs/{方法名}.json,联调前自测用,生产必须 false;改配置需重启,改桩文件即时生效) + mock-enabled: ${HCS_MOCK_ENABLED:true} + # 合同合规系统(HCS)开放接口地址(v4 接口说明:带网关前缀,直连可配,代码不感知) + baseUrl: ${HCS_BASE_URL:http://172.16.203.134:8000/gwzh/HTHG} + # Authorization 请求头整值(含 Basic 前缀,敏感值通过环境变量注入) + authorization: ${HCS_AUTHORIZATION:Basic d2Njazp0RzQjd044JWpYNmM=} + # Key-Auth 请求头值(v4 接口说明:所有接口固定携带 Authorization + Key-Auth 两个头) + key-auth: ${HCS_KEY_AUTH:3F7e8yI6RV8S53mJ9tYa0672r3W4n279} + # 来源系统标识(fd_sources,约定值 WLTMS) + src-system: ${HCS_SRC_SYSTEM:WLTMS} + # 操作人登录名(fdLoginName / curUser.fdLoginName,全部合同接口共用,默认 srvadmin) + operator-login-name: ${HCS_OPERATOR_LOGIN_NAME:13737128880} + # 创建单位 fdId(fd_creator_unit 等组织引用,HCS 组织主数据固定值,项目立项、合同等接口共用;与 name 任一为空回退单据开题部门名称仅传名称) + creator-unit-id: ${HCS_CREATOR_UNIT_ID:1k4vq0lhsw98w1gkegw1d2g9nep42p9j1cw0} + # 创建单位名称(与 creator-unit-id 配套) + creator-unit-name: ${HCS_CREATOR_UNIT_NAME:百色物产新源供应链有限公司} + + # 推送任务(审批通过即时推送 + 定时扫描兜底,HcsPushTask 五类任务独立开关) + push: + # 审批通过即时推送总开关(事务提交后异步推送,关闭时仅走扫描兜底) + immediate-enabled: ${HCS_PUSH_IMMEDIATE_ENABLED:true} + # 每类单据单轮扫描批量 + batch-size: ${HCS_PUSH_BATCH_SIZE:50} + # 推送失败自动重试上限,超限仅人工重推 + max-retry: ${HCS_PUSH_MAX_RETRY:5} + # 五类扫描任务独立开关(灰度逐类打开;扫描只认审批通过且未同步/失败未超限的单据,失败由下轮自动补偿,超限人工重推) + # 各任务 cron 有代码默认值每5分钟、分钟位 0/1/2/3/4 错峰,无需配置;多实例按任务 Redis 分布式锁防重扫(TTL 240s) + project: + # 项目立项(C102)定时推送 + sync-enabled: ${HCS_PUSH_PROJECT_ENABLED:false} + contract: + # 合同(C103)定时推送(含风险校验/附件转传/履约规则 C103-9,及特批 C103-4 结果查询兜底) + sync-enabled: ${HCS_PUSH_CONTRACT_ENABLED:false} + payment: + # 付款链定时推送(特批 C106-3 + 履约记录 C108) + sync-enabled: ${HCS_PUSH_PAYMENT_ENABLED:false} + invoice-application: + # 开票登记(C111)定时推送(含金蝶开票完成后回填) + sync-enabled: ${HCS_PUSH_INVOICE_APP_ENABLED:false} + invoice-receipt: + # 收票单(C109)定时推送 + sync-enabled: ${HCS_PUSH_INVOICE_RCV_ENABLED:false} + # 项目立项(C102) + project: + # 立项 bpmOperation.templateCode(环境相关值,§9-U9 待合规方提供) + template-code: ${HCS_PROJECT_TEMPLATE_CODE:template_15x0og} + # 合同(C103-1/11) + contract: + # TMS 合同类别 -> HCS 模板ID(§9-U7 待合规方提供模板清单) + template-mapping: + "[承运商合同]": "1j5tddhugw3w2kiwumvajq27ediq26fp5aw0" + # 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: + # 履约规则行级同步开关(默认关,对接后打开) + sync-enabled: ${HCS_EXE_STAGE_SYNC_ENABLED:false} + # 金额类型默认值(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: + # 额度校验开关(默认关,关闭后特批无自动触发,对接后打开) + check-enabled: ${HCS_CREDIT_CHECK_ENABLED:false} + # 付款提交校验是否要求合同已推送合规 + 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:2001} + payment-nature: ${HCS_SPEC_PAY_PAYMENT_NATURE:1} + payment-type: ${HCS_SPEC_PAY_PAYMENT_TYPE:1} + # 付款特批流程模板ID(fdTemplateId,每个环境/公司模板不同,WLTMS 示例值,按环境维护) + template-id: ${HCS_SPEC_PAY_TEMPLATE_ID:1k5hp8mu5w9dwajnhw1g78dm3pd4a1r11aw0} + # 开票登记(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: {} + # 履约记录(C108-1/2,一期暂不对接) + perf-record: + # 履约记录同步开关(默认关,对接后打开) + sync-enabled: ${HCS_PERF_RECORD_SYNC_ENABLED:false} + # 表单模板ID(fd_xform_id,环境相关值,§9-U9 待合规方提供) + xform-id: ${HCS_PERF_RECORD_XFORM_ID:} + # 附件转传(C103-2) + attachment: + # 附件转传总开关 + 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} + + #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: ocr: @@ -185,6 +360,14 @@ baidu: request-timeout: 30s token-refresh-advance: 1m +#MinIO文件存储配置 +file: + storage: + minio: + endpoint: http://8.148.239.157:9000 + access-key-id: transport + access-key-secret: transport + bucket-name: transport powerjob: worker: diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index 56207ecb..8515a4fb 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -237,6 +237,15 @@ blade: - /feign/client/businessProcess/getProcessInfo - /feign/client/customerArchive/syncProcessNode - /feign/client/mkProcess/** + # HCS 合同/付款特批结果回传 + 项目/合同金蝶同步结果回传:合规系统经 iPaaS 网关无令牌调用,免登免授权 + - /blade-transport/hcs/contractLegal/result + - /hcs/contractLegal/result + - /blade-transport/hcs/paymentLegal/result + - /hcs/paymentLegal/result + - /blade-transport/hcs/projectLegal/result + - /hcs/projectLegal/result + - /blade-transport/hcs/contractKingdee/result + - /hcs/contractKingdee/result #授权认证配置 auth: - method: ALL @@ -264,6 +273,31 @@ blade: - method: POST pattern: /kingdee-bfi/** expression: "permitAll()" + # HCS 合同/付款特批结果回传 + 项目/合同金蝶同步结果回传:合规系统经 iPaaS 网关无令牌调用,免登免授权 + - method: POST + pattern: /blade-transport/hcs/contractLegal/result + expression: "permitAll()" + - method: POST + pattern: /hcs/contractLegal/result + expression: "permitAll()" + - method: POST + pattern: /blade-transport/hcs/paymentLegal/result + expression: "permitAll()" + - method: POST + pattern: /hcs/paymentLegal/result + expression: "permitAll()" + - method: POST + pattern: /blade-transport/hcs/projectLegal/result + expression: "permitAll()" + - method: POST + pattern: /hcs/projectLegal/result + expression: "permitAll()" + - method: POST + pattern: /blade-transport/hcs/contractKingdee/result + expression: "permitAll()" + - method: POST + pattern: /hcs/contractKingdee/result + expression: "permitAll()" #基础认证配置 basic: - method: ALL diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index 41dd3399..cda8991b 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -32,6 +32,7 @@ thirdParty: # 微信小程序 AppId / AppSecret(Nacos 配置,勿提交真实 secret) appId: ${WECHAT_MINI_APP_ID:} appSecret: ${WECHAT_MINI_APP_SECRET:} + # 业财融合(BFI)应用及接口配置,敏感值通过环境变量注入 bfi: # 业财融合(BFI)开放接口地址 baseUrl: ${BFI_BASE_URL:http://172.16.203.134:8000/gwzh/BFI} @@ -47,11 +48,13 @@ thirdParty: accountId: ${BFI_ACCOUNT_ID:} # 语言字串,默认 zh_CN language: ${BFI_LANGUAGE:zh_CN} + # BFI 客户端本地 Mock 开关(联调自测用,生产必须 false): # true 时 IBfiClient 有桩用桩({mockDir}/{接口方法名}.json)、无桩透传真实 Feign 接口,调用侧零改动 mock-enabled: ${BFI_MOCK_ENABLED:false} # Mock 桩文件目录:文件名为接口方法名,如 getToken.json、payApplyQuery.json;文件每次调用现读,改文件即生效 mockDir: ${BFI_MOCK_DIR:./mock/bfi} + # 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS) srcSys: ${BFI_SRC_SYS:WLTMS} # 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值): @@ -82,9 +85,10 @@ thirdParty: # 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询) payResult: # 回写任务开关 - syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true} - # 调度 cron,默认每10分钟(:01 起步,错峰避让其他金蝶任务) + syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:false} + # 调度 cron,默认每10分钟 cron: ${BFI_PAY_RESULT_CRON:0 1/10 * * * ?} + # 金蝶收票单结果定时回写(A801-3 异常回收/指定补偿/单号补登) receivedInvoiceResult: # 回写任务开关,默认关闭(联调稳定后开启) @@ -95,15 +99,16 @@ thirdParty: receiptClaimPush: # 推送任务开关,默认关闭(联调稳定后开启) syncEnabled: ${BFI_RECEIPT_CLAIM_PUSH_SYNC_ENABLED:false} - # 调度 cron,默认每10分钟(:04 起步,错峰避让其他金蝶任务) + # 调度 cron,默认每10分钟 cron: ${BFI_RECEIPT_CLAIM_PUSH_CRON:0 4/10 * * * ?} # single-flight 分布式锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告+逐条推送), # 进程异常退出时锁在租约到期后自动释放 lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800} + # 金蝶单据锁定定时补偿 billLock: # 锁定/解锁总开关(推送后锁定+解锁回调+补偿重锁共用),默认开启保持既有行为 - enabled: ${BFI_BILL_LOCK_ENABLED:true} + enabled: ${BFI_BILL_LOCK_ENABLED:false} # 锁定补偿开关,默认关闭(安全兜底) retryEnabled: ${BFI_BILL_LOCK_RETRY_ENABLED:false} # 补偿调度 cron,默认每5分钟(:02 起步,错峰避让其他金蝶任务) @@ -113,10 +118,10 @@ thirdParty: # 查询时间窗口(月数) months: ${BFI_CLAIM_ANNOUNCE_MONTHS:6} # 认领人:配置后公告查询固定使用该值,不再按当前登录用户姓名查询 - claimusers: ${BFI_CLAIM_ANNOUNCE_CLAIMUSERS:} - # 金蝶全票池发票定时同步(A801-1 窗口拉取+镜像 upsert,选票统一查本地) + claimusers: ${BFI_CLAIM_ANNOUNCE_CLAIMUSERS:徐媛} + # 收票管理金蝶全票池发票定时绑定(A801-1 查询全票池发票数据) invoiceReceipt: - # 同步任务开关,默认关闭(上线先手动 POST /invoice-receipt/sync-invoice-pool 跑一次全窗口铺底) + # 绑定任务开关,默认关闭(联调稳定后开启) syncEnabled: ${BFI_INVOICE_RECEIPT_SYNC_ENABLED:false} # 调度 cron,默认每30分钟(:16 起步,错峰避让其他金蝶任务) cron: ${BFI_INVOICE_RECEIPT_CRON:0 16/30 * * * ?} @@ -130,20 +135,22 @@ thirdParty: compensateLimit: ${BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT:200} # single-flight 分布式锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放 lockLeaseSeconds: ${BFI_INVOICE_RECEIPT_LOCK_LEASE_SECONDS:1800} + # 审批通过自动同步金蝶(事件触发:结算单A102-2/A302-2、付款申请A103-1/A103-3、开票申请A303-1、收票A303-2+A302-6) autoSync: # 结算单审批后自动推送开关 - settlement: ${BFI_AUTO_SYNC_SETTLEMENT:false} + settlement: ${BFI_AUTO_SYNC_SETTLEMENT:true} # 付款申请审批后自动推送开关 - payment: ${BFI_AUTO_SYNC_PAYMENT:false} + payment: ${BFI_AUTO_SYNC_PAYMENT:true} # 开票申请审批后自动推送开关 - invoiceApply: ${BFI_AUTO_SYNC_INVOICE_APPLY:false} + invoiceApply: ${BFI_AUTO_SYNC_INVOICE_APPLY:true} # 收票登记审批后自动推送开关 - receipt: ${BFI_AUTO_SYNC_RECEIPT:false} + receipt: ${BFI_AUTO_SYNC_RECEIPT:true} # failed 单据定时重推开关(仅重推生成失败,不扫 unsynced 存量) retryEnabled: ${BFI_AUTO_SYNC_RETRY_ENABLED:false} - # 重推调度 cron,默认每10分钟(:05 起步,错峰避让其他金蝶任务) + # 重推调度 cron,默认每10分钟 retryCron: ${BFI_AUTO_SYNC_RETRY_CRON:0 5/10 * * * ?} + # 客商金蝶数据定时同步(A902-1 复查 / A902-2 推送) customer: # 同步任务开关,默认关闭 @@ -152,3 +159,133 @@ thirdParty: cron: ${BFI_CUSTOMER_SYNC_CRON:0 0 2 * * ?} # 同步 single-flight 锁租约秒数,须大于最坏一轮耗时(复查 + 批量推送 + 停用联动) lockLeaseSeconds: ${BFI_CUSTOMER_SYNC_LOCK_LEASE_SECONDS:1800} + + # 合同合规系统 + hcs: + # Mock 开关(true 时 IHcsClient 走本地 JSON 桩 F:/home/tms/mock/hcs/{方法名}.json,联调前自测用,生产必须 false;改配置需重启,改桩文件即时生效) + mock-enabled: ${HCS_MOCK_ENABLED:false} + # 合同合规系统(HCS)开放接口地址(v4 接口说明:带网关前缀,直连可配,代码不感知) + # baseUrl: ${HCS_BASE_URL:http://172.16.203.134:8000/gwzh/HTHG} # uat + baseUrl: ${HCS_BASE_URL:https://contract-sit.gxwuzi.com} + # Authorization 请求头整值(含 Basic 前缀,敏感值通过环境变量注入) + authorization: ${HCS_AUTHORIZATION:Basic d2Njazp0RzQjd044JWpYNmM=} + # Key-Auth 请求头值(v4 接口说明:所有接口固定携带 Authorization + Key-Auth 两个头) + key-auth: ${HCS_KEY_AUTH:3F7e8yI6RV8S53mJ9tYa0672r3W4n279} + # 来源系统标识(fd_sources,约定值 WLTMS) + src-system: ${HCS_SRC_SYSTEM:WLTMS} + # 操作人登录名(fdLoginName / curUser.fdLoginName,全部合同接口共用,默认 srvadmin) + operator-login-name: ${HCS_OPERATOR_LOGIN_NAME:13737128880} + # 创建单位 fdId(fd_creator_unit 等组织引用,HCS 组织主数据固定值,项目立项、合同等接口共用;与 name 任一为空回退单据开题部门名称仅传名称) + # creator-unit-id: ${HCS_CREATOR_UNIT_ID:1j56etj23w358wne0ow2k5214nk4f24213w0} # uat + # creator-unit-id: ${HCS_CREATOR_UNIT_ID:1k4vq0lhsw98w1gkegw1d2g9nep42p9j1cw0} # 百色物产新源供应链有限公司 + creator-unit-id: ${HCS_CREATOR_UNIT_ID:1jj0tkeofw1pwkjstw3jqn806b7rpftb6dw0} # 广西桂物智慧科技有限公司 + + # 创建单位名称(与 creator-unit-id 配套) + # creator-unit-name: ${HCS_CREATOR_UNIT_NAME:百色物产新源供应链有限公司} + creator-unit-name: ${HCS_CREATOR_UNIT_NAME:广西桂物智慧科技有限公司} + + # 推送任务(审批通过即时推送 + 定时扫描兜底,HcsPushTask 五类任务独立开关) + push: + # 审批通过即时推送总开关(事务提交后异步推送,关闭时仅走扫描兜底) + immediate-enabled: ${HCS_PUSH_IMMEDIATE_ENABLED:true} + # 每类单据单轮扫描批量 + batch-size: ${HCS_PUSH_BATCH_SIZE:50} + # 推送失败自动重试上限,超限仅人工重推 + max-retry: ${HCS_PUSH_MAX_RETRY:5} + # 五类扫描任务独立开关(灰度逐类打开;扫描只认审批通过且未同步/失败未超限的单据,失败由下轮自动补偿,超限人工重推) + # 各任务 cron 有代码默认值每5分钟、分钟位 0/1/2/3/4 错峰,无需配置;多实例按任务 Redis 分布式锁防重扫(TTL 240s) + project: + # 项目立项(C102)定时推送 + sync-enabled: ${HCS_PUSH_PROJECT_ENABLED:false} + contract: + # 合同(C103)定时推送(含风险校验/附件转传/履约规则 C103-9,及特批 C103-4 结果查询兜底) + sync-enabled: ${HCS_PUSH_CONTRACT_ENABLED:false} + payment: + # 付款链定时推送(特批 C106-3 + 履约记录 C108) + sync-enabled: ${HCS_PUSH_PAYMENT_ENABLED:false} + invoice-application: + # 开票登记(C111)定时推送(含金蝶开票完成后回填) + sync-enabled: ${HCS_PUSH_INVOICE_APP_ENABLED:false} + invoice-receipt: + # 收票单(C109)定时推送 + sync-enabled: ${HCS_PUSH_INVOICE_RCV_ENABLED:false} + # 项目立项(C102) + project: + # 立项 bpmOperation.templateCode(环境相关值,§9-U9 待合规方提供) + template-code: ${HCS_PROJECT_TEMPLATE_CODE:template_15x0og} + # 合同(C103-1/11) + contract: + # TMS 合同类别 -> HCS 模板ID(§9-U7 待合规方提供模板清单) + template-mapping: + # "[承运商合同]": "1jvnp5s7mw13gw5q4w4g9qin1n1a2rn14gw0" # uat临时 + + "[承运商合同]": "1j5tddhugw3w2kiwumvajq27ediq26fp5aw0" + "[客户合同]": "1j5tddhugw3w2kiwumvajq27ediq26fp5aw0" + # TMS 币种码 -> HCS 币种名称(照金蝶 RMB->CNY 映射惯例) + currency-name-mapping: { + "RMB": "人民币", + "CNY": "人民币", + "USD": "美元", + } + # 合同 fdSepcailContract 固定 false(D15,配置项预留) + special-contract-enabled: ${HCS_CONTRACT_SPECIAL_ENABLED:true} + # 合同推送前是否强制客商风险校验 + require-risk-check: ${HCS_CONTRACT_REQUIRE_RISK_CHECK:true} + # 履约规则(C103-9,一期暂不对接) + exe-stage: + # 履约规则行级同步开关(默认关,对接后打开) + sync-enabled: ${HCS_EXE_STAGE_SYNC_ENABLED:true} + # 金额类型默认值(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: + # 额度校验开关(默认关,关闭后特批无自动触发,对接后打开) + check-enabled: ${HCS_CREDIT_CHECK_ENABLED:false} + # 付款提交校验是否要求合同已推送合规 + 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:2001} + payment-nature: ${HCS_SPEC_PAY_PAYMENT_NATURE:1} + payment-type: ${HCS_SPEC_PAY_PAYMENT_TYPE:1} + # 付款特批流程模板ID(fdTemplateId,每个环境/公司模板不同,WLTMS 示例值,按环境维护) + template-id: ${HCS_SPEC_PAY_TEMPLATE_ID:1k5hp8mu5w9dwajnhw1g78dm3pd4a1r11aw0} + # 开票登记(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: {} + # 履约记录(C108-1/2,一期暂不对接) + perf-record: + # 履约记录同步开关(默认关,对接后打开) + sync-enabled: ${HCS_PERF_RECORD_SYNC_ENABLED:false} + # 表单模板ID(fd_xform_id,环境相关值,§9-U9 待合规方提供) + xform-id: ${HCS_PERF_RECORD_XFORM_ID:} + + # 附件转传(C103-2) + attachment: + # 附件转传总开关 + 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} + diff --git a/doc/sql/transport/lqj/20261024/01_blade_hcs_contract_fields_20260927.sql b/doc/sql/transport/lqj/20261024/01_blade_hcs_contract_fields_20260927.sql new file mode 100644 index 00000000..ec6854b5 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/01_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/doc/sql/transport/lqj/20261024/02_blade_hcs_exe_stage_20260927.sql b/doc/sql/transport/lqj/20261024/02_blade_hcs_exe_stage_20260927.sql new file mode 100644 index 00000000..97b3b811 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/02_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; diff --git a/doc/sql/transport/lqj/20261024/03_blade_hcs_payment_fields_20260927.sql b/doc/sql/transport/lqj/20261024/03_blade_hcs_payment_fields_20260927.sql new file mode 100644 index 00000000..5de893b4 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/03_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; diff --git a/doc/sql/transport/lqj/20261024/04_blade_hcs_receipt_fields_20260927.sql b/doc/sql/transport/lqj/20261024/04_blade_hcs_receipt_fields_20260927.sql new file mode 100644 index 00000000..77e81263 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/04_blade_hcs_receipt_fields_20260927.sql @@ -0,0 +1,18 @@ +-- 桂物合同合规系统(HCS)接入:收票单合同补录字段(设计文档 §4,P4,D13)。 +-- blade_invoice_receipt 追加合同补录字段:应付结算链路回溯不到时页面人工补录。 +-- 通过 information_schema 判断字段是否存在,可重复执行。 +SET @db_name = DATABASE(); + +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_contract_id'), + 'SELECT 1', + 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_contract_id bigint DEFAULT NULL COMMENT ''合规推送合同ID(应付结算回溯不到时人工补录)''' +); +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_contract_no'), + 'SELECT 1', + 'ALTER TABLE blade_invoice_receipt ADD COLUMN hcs_contract_no varchar(64) DEFAULT NULL COMMENT ''合规推送合同编号(应付结算回溯不到时人工补录)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/doc/sql/transport/lqj/20261024/05_blade_hcs_sync_fields_20260927.sql b/doc/sql/transport/lqj/20261024/05_blade_hcs_sync_fields_20260927.sql new file mode 100644 index 00000000..02d53bd9 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/05_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; diff --git a/doc/sql/transport/lqj/20261024/06_blade_hcs_contract_spec_fields_20261009.sql b/doc/sql/transport/lqj/20261024/06_blade_hcs_contract_spec_fields_20261009.sql new file mode 100644 index 00000000..a7d0fc9b --- /dev/null +++ b/doc/sql/transport/lqj/20261024/06_blade_hcs_contract_spec_fields_20261009.sql @@ -0,0 +1,41 @@ +-- 桂物合同合规系统(HCS)接入:合同特批字段组(C103-8 发起特批 / B103 结果回传,前端接入设计 §3.4)。 +-- blade_contract_manage 追加特批流程ID/状态/时间/原因/材料字段;可重复执行。 +SET @db_name = DATABASE(); + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_fd_id'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_fd_id varchar(64) DEFAULT NULL COMMENT ''合规特批流程ID(C103-8 返回)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_status'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_status varchar(16) DEFAULT ''none'' COMMENT ''合规特批状态 none/need/processing/approved/rejected''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_time'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_time datetime DEFAULT NULL COMMENT ''合规最近特批动作时间''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_msg'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_msg varchar(512) DEFAULT NULL COMMENT ''合规特批结果/发起失败原因''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'hcs_spec_files_json'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN hcs_spec_files_json text COMMENT ''特批申请材料+内部审批附件JSON(含C103-2 合规附件ID)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 存量合同的特批状态初始化为 none(新增列带 DEFAULT 'none' 已覆盖;此处兜底历史执行场景) +UPDATE blade_contract_manage SET hcs_spec_status = 'none' WHERE hcs_spec_status IS NULL AND is_deleted = 0; diff --git a/doc/sql/transport/lqj/20261024/07_blade_hcs_menu_20261009.sql b/doc/sql/transport/lqj/20261024/07_blade_hcs_menu_20261009.sql new file mode 100644 index 00000000..bebc8238 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/07_blade_hcs_menu_20261009.sql @@ -0,0 +1,11 @@ +-- 桂物合同合规系统(HCS)前端接入:合规主数据菜单(前端接入设计 §4.3,照 AGENTS §15 菜单 SQL 规范)。 +-- 合规主数据页(模板列表 + 刷新缓存);字符集 utf8mb4_general_ci。ID 为雪花段占位,如与现有数据冲突请调整。 +-- 注意:/hcs/** 端点当前登录即可访问(照 HcsSyncController 现状),菜单落地后可按需补 @PreAuth(menu) 注解。 + +DELETE FROM blade_menu WHERE code IN ('hcs_master_data', 'hcs_master_data_refresh'); + +INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted) +VALUES (2075432100100000001, 1164733399668962201, 'hcs_master_data', '合规主数据', 'hcs_master_data', '/business/hcs-master-data', 'iconfont icon-caidan', 40, 1, 0, 1, '', '', 0); + +INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted) +VALUES (2075432100100000002, 2075432100100000001, 'hcs_master_data_refresh', '刷新缓存', 'hcs_master_data_refresh', '', '', 1, 2, 0, 1, '', '', 0); diff --git a/doc/sql/transport/lqj/20261024/08_blade_hcs_attachment_sync_20261012.sql b/doc/sql/transport/lqj/20261024/08_blade_hcs_attachment_sync_20261012.sql new file mode 100644 index 00000000..4db61dd7 --- /dev/null +++ b/doc/sql/transport/lqj/20261024/08_blade_hcs_attachment_sync_20261012.sql @@ -0,0 +1,32 @@ +-- blade_hcs_attachment_sync 建表(HCS 附件转传映射,照金蝶附件同步表 blade_kingdee_attachment_sync 设计: +-- biz_type+biz_id+attachment_key 唯一,C103-2 转传前查命中即复用合规附件ID,仅落成功记录) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'CREATE TABLE `blade_hcs_attachment_sync` ( + `id` bigint NOT NULL COMMENT ''主键'', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''租户ID'', + `biz_type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT ''业务单据类型(contract/payment/invoice-application/invoice-receipt)'', + `biz_id` bigint NOT NULL COMMENT ''业务单据id'', + `attachment_key` varchar(768) COLLATE utf8mb4_general_ci NOT NULL COMMENT ''附件幂等键(url|link|fileUrl|downloadUrl|uid|originalName|name 依次取首个非空)'', + `attachment_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''附件名称(展示/排查用)'', + `fd_id` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规附件ID(C103-2 返回 fdId)'', + `sync_time` datetime DEFAULT NULL COMMENT ''最近转传时间'', + `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 NULL COMMENT ''业务状态'', + `is_deleted` int DEFAULT 0 COMMENT ''是否已删除'', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_biz_attachment` (`biz_type`,`biz_id`,`attachment_key`(256)), + KEY `idx_biz_type_biz_id` (`biz_type`,`biz_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=''HCS附件转传映射表(C103-2)''', + 'SELECT 1') + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_hcs_attachment_sync' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; diff --git a/doc/sql/transport/lqj/20261024/09_blade_contract_attachment_type_dict_20261013.sql b/doc/sql/transport/lqj/20261024/09_blade_contract_attachment_type_dict_20261013.sql new file mode 100644 index 00000000..b3d8e9ba --- /dev/null +++ b/doc/sql/transport/lqj/20261024/09_blade_contract_attachment_type_dict_20261013.sql @@ -0,0 +1,39 @@ +-- 合同附件类型业务字典(contract_attachment_type):合同文件/双章归档文件/特批附件。 +-- 后端高风险/黑名单客商提交校验按 fileType=特批附件 精确匹配(HcsSyncConstant#ATTACHMENT_TYPE_SPEC_EXPLANATION),缺字典会导致无法上传特批附件。 +-- 本地环境曾走界面新增,未沉淀脚本;本脚本幂等可重复执行,已存在的项跳过。 + +-- ① 字典父项:合同附件类型 +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099691747182096386, '000000', 0, 'contract_attachment_type', '-1', '合同附件类型', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 +); + +-- ② 子项:parent_id 动态取父项ID,兼容各环境父项已单独创建的情况 +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099691808217608194, '000000', + (SELECT id FROM blade_dict_biz WHERE code = 'contract_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1), + 'contract_attachment_type', '合同文件', '合同文件', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_attachment_type' AND dict_key = '合同文件' AND tenant_id = '000000' AND is_deleted = 0 +); + +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099691889775849473, '000000', + (SELECT id FROM blade_dict_biz WHERE code = 'contract_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1), + 'contract_attachment_type', '双章归档文件', '双章归档文件', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_attachment_type' AND dict_key = '双章归档文件' AND tenant_id = '000000' AND is_deleted = 0 +); + +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099691980955824130, '000000', + (SELECT id FROM blade_dict_biz WHERE code = 'contract_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1), + 'contract_attachment_type', '特批附件', '特批附件', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_attachment_type' AND dict_key = '特批附件' AND tenant_id = '000000' AND is_deleted = 0 +); diff --git a/doc/sql/transport/lqj/20261024/10_blade_contract_spec_attachment_dict_20261017.sql b/doc/sql/transport/lqj/20261024/10_blade_contract_spec_attachment_dict_20261017.sql new file mode 100644 index 00000000..bdb0a1ca --- /dev/null +++ b/doc/sql/transport/lqj/20261024/10_blade_contract_spec_attachment_dict_20261017.sql @@ -0,0 +1,37 @@ +-- 特批附件归口「附件(佐证材料等)」区上传(产品口径 2026-10-17)。 +-- ① 下架用印材料字典 contract_attachment_type 中的特批附件(隐藏,逻辑删除,幂等); +-- ② 新建佐证材料附件类型字典 contract_supporting_attachment_type:特批附件/其他文件。 +-- 佐证材料区此前取不到字典时前端走兜底选项,落库后各环境与前端兜底保持一致。 + +-- ① 用印材料字典下架特批附件 +UPDATE blade_dict_biz +SET is_deleted = 1 +WHERE code = 'contract_attachment_type' AND dict_key = '特批附件' AND tenant_id = '000000' AND is_deleted = 0; + +-- ② 佐证材料附件类型字典父项 +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099700000000000001, '000000', 0, 'contract_supporting_attachment_type', '-1', '佐证材料附件类型', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_supporting_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 +); + +-- ③ 子项:特批附件(高风险/黑名单客商合同必传,fileType=特批附件) +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099700000000000002, '000000', + (SELECT id FROM blade_dict_biz WHERE code = 'contract_supporting_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1), + 'contract_supporting_attachment_type', '特批附件', '特批附件', 1, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_supporting_attachment_type' AND dict_key = '特批附件' AND tenant_id = '000000' AND is_deleted = 0 +); + +-- ④ 子项:其他文件(佐证材料历史唯一类型) +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2099700000000000003, '000000', + (SELECT id FROM blade_dict_biz WHERE code = 'contract_supporting_attachment_type' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1), + 'contract_supporting_attachment_type', '其他文件', '其他文件', 2, NULL, 0, 1, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM blade_dict_biz + WHERE code = 'contract_supporting_attachment_type' AND dict_key = '其他文件' AND tenant_id = '000000' AND is_deleted = 0 +); diff --git a/doc/sql/transport/lqj/20261024/11_blade_hcs_kingdee_id_20261020.sql b/doc/sql/transport/lqj/20261024/11_blade_hcs_kingdee_id_20261020.sql new file mode 100644 index 00000000..92a8fd4e --- /dev/null +++ b/doc/sql/transport/lqj/20261024/11_blade_hcs_kingdee_id_20261020.sql @@ -0,0 +1,30 @@ +-- HCS 金蝶同步结果回调回填字段: +-- blade_project_apply 追加金蝶项目ID/编号(D902 项目金蝶同步结果回调 projectLegal/result 回填); +-- blade_contract_manage 追加金蝶合同ID(合同特批金蝶同步结果回调 contractLegal/result 回填)。 +-- 通过 information_schema 判断字段是否存在,可重复执行。 +SET @db_name = DATABASE(); + +-- ==================== blade_project_apply(D902 项目金蝶同步结果回填) ==================== + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_project_apply' AND column_name = 'kingdee_id'), + 'SELECT 1', + 'ALTER TABLE blade_project_apply ADD COLUMN kingdee_id varchar(64) DEFAULT NULL COMMENT ''金蝶项目ID(D902 回调回填)''' +); +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 = 'kingdee_number'), + 'SELECT 1', + 'ALTER TABLE blade_project_apply ADD COLUMN kingdee_number varchar(64) DEFAULT NULL COMMENT ''金蝶项目编号(D902 回调回填)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- ==================== blade_contract_manage(contractLegal/result 金蝶同步结果回填) ==================== + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_contract_manage' AND column_name = 'kingdee_id'), + 'SELECT 1', + 'ALTER TABLE blade_contract_manage ADD COLUMN kingdee_id varchar(64) DEFAULT NULL COMMENT ''金蝶合同ID(contractLegal/result 回调回填)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/pom.xml b/pom.xml index 9a7485b3..f9fc2e3d 100644 --- a/pom.xml +++ b/pom.xml @@ -162,6 +162,11 @@ blade-bfi-api ${revision} + + org.springblade + blade-hcs-api + ${revision} + org.springblade blade-wechat-api