!6 fix: 项目+合同对接合同系统

Merge pull request !6 from mindless/feature/hcs-api-p0
This commit is contained in:
2026-10-27 06:24:01 +00:00
committed by Gitee
187 changed files with 18843 additions and 169 deletions
@@ -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;
}
@@ -0,0 +1,78 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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,单附件行级,照金蝶附件同步表设计)
* <p>
* 仅落成功记录: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;
}
@@ -0,0 +1,79 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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;
}
@@ -0,0 +1,101 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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)
* <p>
* 合同维度的资金计划阶段,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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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)
* <p>
* 内部组织类型客商默认无风险(不调 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;
}
@@ -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;
+4
View File
@@ -51,6 +51,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-bfi-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-hcs-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
@@ -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 {
@@ -0,0 +1,223 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.constant;
/**
* HCS(合同合规系统)C103-1/C103-11 合同发起 xformFields 字段名常量
* <p>
* 字段名取自 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() {
}
}
@@ -0,0 +1,116 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.constant;
/**
* HCS(合同合规系统)列表查询 / 回调字段名常量(TMS 业务侧)
* <p>
* 与 {@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() {
}
}
@@ -0,0 +1,342 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.constant;
/**
* HCS(合同合规系统)同步常量(TMS 业务侧)
* <p>
* 命名照 {@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() {
}
}
@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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)
* <p>
* 行级差异(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<HcsContractExeStage>> 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<Boolean> submit(@Parameter(description = "合同ID", required = true) @RequestParam Long contractId,
@RequestBody List<HcsContractExeStage> rows) {
return R.status(contractExeStageService.submitRows(contractId, rows));
}
/**
* 删除履约规则行(软删+标记,已同步行待 C103-9 上报删除)
*/
@PostMapping("/remove")
@ApiOperationSupport(order = 3)
@Operation(summary = "删除履约规则行", description = "传入规则行ID,软删并标记 delete 待同步")
public R<Boolean> remove(@Parameter(description = "规则行ID", required = true) @RequestParam Long id) {
return R.status(contractExeStageService.removeRow(id));
}
}
@@ -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")
@@ -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("审批通过");
}
@@ -1,123 +0,0 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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 端点
* <p>
* 合同法务审批结果回调(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<String> contractLegalResult(@RequestBody(required = false) Map<String, Object> param) {
this.checkSourceSystem("合同法务审批结果", param);
return R.data("mock特批结果已处理:approved");
}
/**
* 付款法务审批结果回调(D104)
*/
@PostMapping("/paymentLegal/result")
@ApiOperationSupport(order = 16)
@Operation(summary = "付款法务审批结果回调Mock", description = "D104 合规推送付款特批结果,仅校验来源系统并返回 mock 数据")
public R<String> paymentLegalResult(@RequestBody(required = false) Map<String, Object> param) {
this.checkSourceSystem("付款法务审批结果", param);
return R.data("mock付款特批结果接收已处理:approved");
}
/**
* 项目金蝶同步结果回调(D902)
*/
@PostMapping("/projectLegal/result")
@ApiOperationSupport(order = 18)
@Operation(summary = "项目金蝶同步结果回调Mock", description = "D902 合规推送项目金蝶同步结果,仅校验来源系统并返回 mock 数据")
public R<String> projectLegalResult(@RequestBody(required = false) Map<String, Object> param) {
this.checkSourceSystem("项目金蝶同步结果", param);
return R.data("mock项目金蝶同步结果接收已处理:approved");
}
/**
* 合同金蝶同步结果回传接收(D903)
*/
@PostMapping("/contractKingdee/result")
@ApiOperationSupport(order = 19)
@Operation(summary = "合同金蝶同步结果回传接收Mock", description = "D903 合规推送合同金蝶最终同步结果,仅校验来源系统并返回 mock 数据")
public R<String> contractKingdeeResult(@RequestBody(required = false) Map<String, Object> param) {
this.checkSourceSystem("合同金蝶同步结果", param);
return R.data("mock合同金蝶同步结果接收处理:approved");
}
/**
* 来源系统校验:仅接受合规方约定的来源标识,防伪造回传
*
* @param bizName 业务名称(日志用)
* @param param 回调报文
*/
private void checkSourceSystem(String bizName, Map<String, Object> 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("来源系统标识错误");
}
}
}
@@ -0,0 +1,349 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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 手动入口)
* <p>
* 前端列表页"推送合规/重推"按钮(照"同步金蝶"按钮模式):
* 单条重推重置 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<String> 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<String> 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<String> repushBatch(@Parameter(description = "业务类型 project/contract", required = true) @PathVariable String bizType,
@Parameter(description = "单据ID列表", required = true) @RequestBody List<Long> 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<String> 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<String> 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<String> 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<List<HcsTemplateVO>> templates() {
return R.data(hcsMasterDataSupport.getTemplates());
}
/**
* 合规币种列表(C103-12,Redis 缓存优先,前端接入设计 §3.1)
*/
@GetMapping("/master-data/currency")
@ApiOperationSupport(order = 16)
@Operation(summary = "合规币种列表查询", description = "C103-12 币种列表,走服务端 Redis 缓存,未命中自动全量拉取")
public R<List<HcsCurrencyVO>> currencies() {
return R.data(hcsMasterDataSupport.getCurrencies());
}
/**
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路):
* 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案
*/
@GetMapping("/risk")
@ApiOperationSupport(order = 10)
@Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕")
public R<CustomerRiskVO> 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<List<CustomerRiskVO>> 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<CustomerRiskVO> 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<String> uploadAttachment(@Parameter(description = "附件文件", required = true) @RequestParam("fdFile") MultipartFile fdFile) {
return R.data(hcsAttachmentSupport.uploadOne(fdFile, AuthUtil.getUserAccount()));
}
/**
* 合同附件下载(C103-5 代理,前端接入设计 §3.2):文件流透传浏览器
*/
@GetMapping("/attachment/download")
@ApiOperationSupport(order = 12)
@Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名")
public void downloadAttachment(@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
@Parameter(description = "下载文件名(可选)") @RequestParam(required = false) String name,
HttpServletResponse response) throws Exception {
String fileName = StringUtil.isBlank(name) ? "hcs-attachment-" + fdId : name;
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"));
try (Response hcsResponse = hcsAttachmentSupport.downloadRaw(fdId);
InputStream inputStream = hcsResponse.body().asInputStream();
OutputStream outputStream = response.getOutputStream()) {
inputStream.transferTo(outputStream);
outputStream.flush();
}
}
/**
* 合同特批结果人工确认(§6-U1 过渡方案:与合规推送等效)
*/
@PostMapping("/contract/spec/confirm")
@ApiOperationSupport(order = 14)
@Operation(summary = "合同特批结果人工确认", description = "传入合同ID 与结果(approved/rejected),仅特批中(processing)允许确认")
public R<String> confirmContractSpec(@Parameter(description = "合同ID", required = true) @RequestParam Long id,
@Parameter(description = "确认结果 approved/rejected", required = true) @RequestParam String result) {
return R.data(hcsContractSpecSyncSupport.confirmSpecResult(id, result));
}
/**
* 合同特批结果回传接收(合规系统 contractLegal/result 推送,替代未建设的 D103,原文留痕)
* <p>
* 免登免授权: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<String> contractLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) {
return R.data(hcsContractSpecSyncSupport.handleLegalResult(param));
}
/**
* 付款特批结果回传接收(合规系统 paymentLegal/result 推送)
* <p>
* 免登免授权同合同接口(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<String> paymentLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) {
return R.data(hcsPaymentSyncSupport.handleLegalResult(param));
}
/**
* 项目金蝶同步结果回传接收(合规系统 projectLegal/result 推送,D902,原文留痕)
* <p>
* 免登免授权: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<String> projectLegalResult(@RequestBody(required = false) HcsProjectLegalResultDTO param) {
return R.data(hcsProjectSyncSupport.handleLegalResult(param));
}
/**
* 合同金蝶同步结果回传接收(合规系统 contractKingdee/result 推送,D903,投送人/钢厂/结算中心合同共用,原文留痕)
* <p>
* 免登免授权: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<String> contractKingdeeResult(@RequestBody(required = false) HcsContractLegalResultDTO param) {
return R.data(hcsContractSyncSupport.handleKingdeeResult(param));
}
}
@@ -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("审批通过");
}
@@ -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("审批通过");
}
@@ -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")
@@ -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")
@@ -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("已驳回"); }
@@ -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<String> SYNC_FAILURE = new ThreadLocal<>();
/** 当前审批请求线程的自动推送成功结果(金蝶单据号/推送摘要),供 Controller 拼进提示文案 */
private static final ThreadLocal<String> 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);
}
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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<HcsAttachmentSync> {
}
@@ -0,0 +1,40 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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<HcsContractCheckLog> {
}
@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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<HcsContractExeStage> {
/**
* 查询合同的待同步行(含软删行: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<HcsContractExeStage> selectSyncRows(@Param("contractId") Long contractId);
}
@@ -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<PaymentApplicationSettlement> {
/**
* 按付款申请主键物理删除关联记录(表存在不含逻辑删除字段的关系唯一索引,重存前必须物理清理)
*
* @param paymentApplicationId 付款申请主键
* @return 删除行数
*/
@Delete("DELETE FROM blade_payment_application_settlement WHERE payment_application_id = #{paymentApplicationId}")
int deleteByPaymentApplicationId(@Param("paymentApplicationId") Long paymentApplicationId);
}
@@ -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;
}
@@ -0,0 +1,112 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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<HcsContractExeStage> {
/**
* 按合同查询履约规则行(表单维护用,不含软删行)
*
* @param contractId 合同ID
* @return 规则行列表
*/
List<HcsContractExeStage> 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<HcsContractExeStage> rows);
/**
* 删除履约规则行(软删+标记:已同步行置 row_op=delete 待 C103-9 上报;未同步行直接软删)
*
* @param id 规则行ID
* @return 删除结果
*/
boolean removeRow(Long id);
}
@@ -0,0 +1,302 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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)
* <p>
* 行级差异标记:新行/未同步行 add;已同步行编辑 update;已同步行删除 delete(软删+标记,不可物理删除)。
* 行级同步由扫描任务随合同链路调 C103-9 完成(HcsExeStageSyncSupport)。
*
* @author Chill
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeStageMapper, HcsContractExeStage> implements IContractExeStageService {
private final ContractManageMapper contractManageMapper;
private final HcsProperties hcsProperties;
@Override
public List<HcsContractExeStage> listByContractId(Long contractId) {
return list(Wrappers.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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.<HcsContractExeStage>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<HcsContractExeStage> 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.<HcsContractExeStage>lambdaQuery().eq(HcsContractExeStage::getContractId, contractId)) > 0) {
return;
}
HcsContractExeStage row = new HcsContractExeStage();
row.setContractId(contractId);
if ("承运商合同".equals(contractManage.getContractCategory())) {
row.setPerformType(HcsSyncConstant.PERFORM_TYPE_PAYMENT);
row.setInvoiceRule(mapInvoiceRule(contractManage.getSettlementMode()));
} else if ("客户合同".equals(contractManage.getContractCategory())) {
row.setPerformType(HcsSyncConstant.PERFORM_TYPE_COLLECTION);
} else {
log.info("合同类别非承运商/客户,不自动创建履约计划行:contractNo={},contractCategory={}", contractManage.getContractNo(), contractManage.getContractCategory());
return;
}
// 默认进度款计划行:金额/资金日期留空,随合同链路推 C103-9
row.setFundNature(HcsSyncConstant.FUND_NATURE_PROGRESS);
row.setFundType(hcsProperties.getExeStage().getFundType());
row.setIsFinish(0);
row.setRowOp(HcsSyncConstant.ROW_OP_ADD);
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
row.setHcsSyncMsg("审批通过自动创建,待随合同链路同步");
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("计划金额不能为负数");
}
}
}
@@ -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<ContractManageMapper, ContractManage> 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 BaseServiceImpl<ContractManageMap
prepare(contractManage);
validateDraft(contractManage);
validateTemporary(contractManage);
// 高风险/黑名单客商管控:必须上传特批附件(外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(contractManage);
if (Func.isNotEmpty(contractManage.getId())) {
validateContractPartyCombinationUnique(contractManage);
}
@@ -172,6 +187,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
throw new ServiceException("仅草稿或已撤回/已驳回的临时合同允许提交临时合同审批");
}
validateTemporary(contractManage);
// 高风险/黑名单客商管控:必须上传特批附件(外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(contractManage);
// 临时放开新增合同从草稿转临时合同时的组合重复校验。
// validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_TEMPORARY);
@@ -190,6 +207,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
throw new ServiceException("仅临时合同或已撤回/已驳回的正式合同允许提交正式合同审批");
}
validateFormal(contractManage);
// 高风险/黑名单客商管控:必须上传特批附件(外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(contractManage);
// 临时放开新增合同从临时转正式合同时的组合重复校验。
// validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_FORMAL);
@@ -222,6 +241,15 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setContractFileJson(markContractFilesApproved(contractManage.getContractFileJson()));
refreshArchiveStatusOnApprove(contractManage);
updateLatestReviewingChangeRecord(contractManage, contractManage.getApprovalStatus(), "审核通过");
// HCS 推送:审批通过(含变更通过)置待同步状态,事务提交后由 Controller 同步推送并回传失败原因,扫描任务保留兜底
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
// 合同审批通过:履约规则未同步行统一置待同步标记,随合同链路调 C103-9(ADR-0002)
// 审批通过自动创建默认履约计划行(承运商=付款进度款/客户=收款进度款),已有规则行时不重复创建
contractExeStageService.createDefaultRowOnApprove(contractManage);
hcsContractExeStageMapper.update(null, Wrappers.<HcsContractExeStage>lambdaUpdate()
.eq(HcsContractExeStage::getContractId, contractManage.getId())
.isNull(HcsContractExeStage::getHcsStageFdId)
.set(HcsContractExeStage::getRowOp, "add"));
return updateById(contractManage);
}
@@ -371,6 +399,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
normalizeOptionalIntegerFields(source);
validateContractAmountRequired(source);
validateAdditionalFields(source);
source.setBillingPlanJson(request.getBillingPlanJson()); source.setSettlementRuleJson(request.getSettlementRuleJson()); source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); source.setPaymentRatioJson(request.getPaymentRatioJson());
source.setContractFileJson(request.getContractFileJson()); source.setAttachmentsJson(request.getAttachmentsJson());
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
// 高风险/黑名单客商管控:变更提交同样必须携带特批附件(与新增/暂存/转正式一致,外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(source);
source.setBillingPlanJson(request.getBillingPlanJson());
source.setSettlementRuleJson(request.getSettlementRuleJson());
source.setPreSettlementConfigJson(request.getPreSettlementConfigJson());
@@ -508,9 +541,29 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
.orderByDesc(ContractManage::getCreateTime);
applyEffectiveType(queryWrapper, contractManage.getEffectiveType());
applyExpireScope(queryWrapper, contractManage.getExpireScope());
// 是否同步金蝶(kingdee_id 是否已回填)/ 是否同步合规(hcs_sync_status 是否 synced)
applySyncFlagFilter(queryWrapper, contractManage.getKingdeeSynced(), contractManage.getHcsSynced());
return queryWrapper;
}
/**
* 金蝶/合规同步与否筛选(列表搜索:kingdeeSynced 按 kingdee_id 空值判断,hcsSynced 按 hcs_sync_status=synced 判断)
*/
private void applySyncFlagFilter(LambdaQueryWrapper<ContractManage> 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<ContractManage> queryWrapper, String effectiveType) {
if (Func.isEmpty(effectiveType)) {
return;
@@ -1205,6 +1258,49 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
}
/**
* 高风险/黑名单客商提交校验:甲/乙方为外部客商且高风险(C101-1 风险等级=2)或黑名单时,
* 必须已上传「特批附件」类型附件(fileType=特批附件,业务字典 contract_supporting_attachment_type),
* 归口「附件(佐证材料等)」(attachmentsJson)区上传;用印材料(contractFileJson)历史已传行兼容放行;内部组织类型客商默认无风险
*
* @param contractManage 合同单
*/
private void validateSpecExplanationOnSubmit(ContractManage contractManage) {
boolean risky = isRiskyCustomer(contractManage.getPartyA()) || isRiskyCustomer(contractManage.getPartyB());
if (!risky) {
return;
}
boolean hasSpecExplanation = this.hasSpecAttachment(contractManage.getAttachmentsJson())
|| this.hasSpecAttachment(contractManage.getContractFileJson());
if (!hasSpecExplanation) {
throw new ServiceException("合同相对方存在高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批");
}
}
/**
* 附件 JSON 中是否已存在特批附件(fileType=特批附件)
*
* @param attachmentsJson 附件元数据 JSON 数组
* @return 是否存在特批附件
*/
private boolean hasSpecAttachment(String attachmentsJson) {
return parseJsonArray(attachmentsJson).stream()
.filter(Map.class::isInstance)
.map(Map.class::cast)
.anyMatch(attachment -> 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<Object> merged = parseJsonArray(existingJson);
List<Object> pending = parseJsonArray(pendingJson);
@@ -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<InvoiceApplic
invoiceWritten += inserted;
// 查到发票:开票申请单推进开票完成(终态)
application.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_INVOICED);
// HCS 开票登记待更新:扫描任务凭 hcs_fd_id 走 C111-3 回填票据日期(D12)
application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
toUpdateApplications.add(application);
invoicedCount++;
log.info("金蝶开票结果回写:开票申请[{}]状态推进为invoiced,本次写入发票{}张", application.getApplicationNo(), inserted);
@@ -1671,6 +1674,10 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
InvoiceApplication entity = existing(id);
if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作");
entity.setApprovalStatus(to);
if (TransportBusiConstant.APPROVED.equals(to)) {
// HCS 推送零埋点:审批通过仅置待同步状态,扫描任务据此推开票登记(ADR-0001/D12)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
}
entity.setCurrentNode(node);
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
@@ -50,6 +50,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.KingdeeBfiConstant;
@@ -87,6 +88,8 @@ import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
import org.springblade.transport.support.KingdeeReceivedInvoiceSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springblade.transport.wrapper.InvoiceReceiptWrapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -121,6 +124,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Service
@RefreshScope
@RequiredArgsConstructor
public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMapper, InvoiceReceipt>
implements IInvoiceReceiptService {
@@ -2073,6 +2077,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
String actionName, String reason) {
String fromStatus = entity.getApprovalStatus();
entity.setApprovalStatus(toStatus);
if (TransportBusiConstant.APPROVED.equals(toStatus)) {
// HCS 推送零埋点:审批通过仅置待同步状态,扫描任务据此推收票单(ADR-0001)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
}
entity.setCurrentNode(actionName);
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
@@ -50,6 +50,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
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.constant.TransportBusiConstant;
@@ -85,6 +86,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.KingdeeBillLock;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PaymentApplicationInvoice;
@@ -97,6 +99,7 @@ import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
@@ -105,6 +108,7 @@ import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@@ -174,6 +178,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
private final BladeRedis bladeRedis;
private final ApplicationEventPublisher eventPublisher;
private final HcsCreditCheckSupport hcsCreditCheckSupport;
private final IContractExeStageService contractExeStageService;
@Override
public IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query) {
@@ -460,6 +466,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
syncBillOccupation(entity);
}
// HCS 提交同步校验(事务内查询类同步,D15/R2-Q3):C101-1 风险(收款方外部客商高风险/黑名单须特批附件并标记待特批)+ C106-1 额度(超额转特批)
hcsCreditCheckSupport.checkOnSubmit(entity);
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
@@ -516,7 +524,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
useBillBalance(entity);
}
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
// HCS 推送零埋点:审批通过仅置待同步状态;特批发起由 /approve 与 MK 审批回调经 HcsImmediatePushSupport 即时触发,扫描任务兜底(ADR-0001)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
updateById(entity);
// 履约计划款项金额累加(款项金额=申请付款金额,多次付款累加),已同步计划行置 update 待随付款链重推 C103-9
this.contractExeStageService.accumulatePaidAmountOnApprove(entity);
refreshFormalSettlement(entity);
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION, entity.getId()));
@@ -601,6 +613,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 单据号入上下文:批量同步失败提示用单号定位单据(雪花id用户无法对应),不再额外查库
context.paymentNoCache().put(id, entity.getPaymentNo());
if (!TransportBusiConstant.APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据");
// 高风险/黑名单客商付款特批门禁:待特批/特批审核中(挂起"特批审核")禁止推送金蝶,
// 特批通过(合规 paymentLegal/result 回调恢复"审批通过")后放行
if (entity.getHcsSpecStatus() != null && List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PUSHING,
HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(entity.getHcsSpecStatus())) {
throw new ServiceException("收款方客商为高风险/黑名单,付款特批审核中(" + entity.getCurrentNode() + "),特批完成后方可推送金蝶");
}
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(entity.getKingdeeStatus())
@@ -954,6 +972,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
} else if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
}
// HCS 履约记录推进:实际付款变化后置待同步,扫描任务凭 hcs_perf_fd_id 走 C108-2 更新(D11)
if (entity.getHcsPerfFdId() != null) {
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
}
updateById(entity);
if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
// 付款完成:联动履约付款数据(预结算预付记录、正式结算付款记录)
@@ -1288,14 +1310,38 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
// 【必填】对方系统唯一行id(不能重复):付款申请号-1
entry.setAwrpEntryCode(bfiEntryCode(entity.getPaymentNo(), 1));
// 备注(非必传)
entry.setERemark(entity.getRemark());
// 备注(非必传):携带关联履约计划id(进度款计划行合规阶段ID),用于合同系统关联履约付款数据与履约计划
entry.setERemark(this.bfiExeStageRemark(entity));
param.setEntry(List.of(entry));
BfiBillSaveResultVO saveResult = bfiService.savePrePayApply(param);
return requireBfiBillIdAndFillNo(entity, saveResult, "推送金蝶付款申请");
}
/**
* 金蝶付款申请备注:携带关联履约计划id(进度款计划行合规阶段ID hcs_stage_fd_id),
* 用于合同系统关联履约付款数据与履约计划;无关联计划行时不携带(与 C108 履约记录"无关联则不传"口径一致),
* 计划行查询异常不阻断金蝶推送
*
* @param entity 付款申请实体
* @return 备注文本(履约计划id + 原备注)
*/
private String bfiExeStageRemark(PaymentApplication entity) {
String stageFdId = null;
try {
HcsContractExeStage stageRow = this.contractExeStageService.findPaymentStageRow(entity.getContractId(),
HcsSyncConstant.fundNatureOfPayment(entity.getPaymentType()));
stageFdId = stageRow == null ? null : stageRow.getHcsStageFdId();
} catch (Exception exception) {
log.warn("付款申请[{}]查询关联履约计划行异常,金蝶备注不携带履约计划id", entity.getPaymentNo(), exception);
}
String planPart = Func.isBlank(stageFdId) ? "" : "履约计划id:" + stageFdId;
if (Func.isBlank(entity.getRemark())) {
return planPart;
}
return planPart.isEmpty() ? entity.getRemark() : planPart + ";" + entity.getRemark();
}
/**
* 校验正式结算付款对应的结算单结算类型:应付(财务应付单 A102-2)直接放行,
* 应收(对应财务应收单 A302-2)推送规则待业务确认,暂拦截报错。
@@ -1372,8 +1418,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
// 期望付款日(非必传,不传时根据应付单自动带出)
// entry.setEExpaydate(applyDateText);
// 备注(非必传)
// entry.setERemark(entity.getRemark());
// 备注(非必传):携带关联履约计划id(进度款计划行合规阶段ID),用于合同系统关联履约付款数据与履约计划
entry.setERemark(this.bfiExeStageRemark(entity));
// 【必填】外部单据类型 todo
entry.setAwrpSrcType(bfiProperties.getPayApply().getSrcType());
param.setEntry(List.of(entry));
@@ -2129,9 +2175,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void saveSettlementRelations(PaymentApplication entity, List<FormalSettlement> settlements,
List<PaymentApplicationSettlementLineRequest> lines) {
if (settlements.isEmpty()) return;
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>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);
@@ -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<ProjectApplyMapper,
projectApply.setCurrentNode("审批通过");
projectApply.setCurrentProcessor(AuthUtil.getUserName());
projectApply.setApprovedTime(LocalDateTime.now());
// HCS 推送:审批通过置待同步状态,事务提交后由 Controller 同步推送并回传失败原因,扫描任务保留兜底
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return updateById(projectApply);
}
@@ -404,9 +407,29 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
.toList();
queryWrapper.in(ProjectApply::getId, matchedIds.isEmpty() ? List.of(-1L) : matchedIds);
}
// 是否同步金蝶(kingdee_id 是否已回填)/ 是否同步合规(hcs_sync_status 是否 synced)
applySyncFlagFilter(queryWrapper, projectApply.getKingdeeSynced(), projectApply.getHcsSynced());
return queryWrapper;
}
/**
* 金蝶/合规同步与否筛选(列表搜索:kingdeeSynced 按 kingdee_id 空值判断,hcsSynced 按 hcs_sync_status=synced 判断)
*/
private void applySyncFlagFilter(LambdaQueryWrapper<ProjectApply> 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<ProjectApplyVO> records) {
if (records == null || records.isEmpty()) {
return;
@@ -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<KingdeeReceiptFlowMa
private final BfiProperties bfiProperties;
private final TransactionTemplate transactionTemplate;
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
private final HcsClaimSyncSupport hcsClaimSyncSupport;
@Override
public IPage<ReceiptFlowVO> selectPage(IPage<KingdeeReceiptFlow> page, ReceiptFlowVO query) {
@@ -904,10 +906,15 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
/**
* 事务内推送:流水行锁覆盖闸门校验、对账、推送与状态落库全过程,
* 防定时任务与手动按钮并发对同一流水双推
* 防定时任务与手动按钮并发对同一流水双推;
* 推送成功后触发合同合规认领链推送(C103-9/C107-1/C108-1,事务提交后执行,失败不影响金蝶推送结果)
*/
private ReceiptClaimPushResult pushClaimLocked(Long flowId, BfiClaimAnnounceVO announce) {
return transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce));
ReceiptClaimPushResult result = transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce));
if (result != null && result.isSuccess()) {
hcsClaimSyncSupport.pushClaimToHcs(flowId);
}
return result;
}
/**
@@ -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);
// 【必填】款项性质.编码
@@ -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,与应付单/付款申请同口径)
@@ -0,0 +1,367 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.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)
* <p>
* 流程:解析单据附件 JSON 元素(去重键照 ContractManage#attachmentKey 惯例)→
* 按元数据 URL 从文件服务拉取流(基址可配 attachment.file-download-base)→
* C103-2 上传取得合规附件ID → 落映射同步表(blade_hcs_attachment_sync,照金蝶附件同步表设计:
* biz_type+biz_id+attachment_key 唯一,转传前查命中即复用,仅落成功记录)。
* 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试(失败原因落单据 hcs_sync_msg)。
* <p>
* 合同链路挂载字段 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<String> 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<HcsAttachmentRefDTO> transferAttachments(String bizType, Long bizId, String attachmentsJson, String operatorLoginName) {
if (StringUtil.isBlank(attachmentsJson)) {
return List.of();
}
List<Map<?, ?>> attachments = parseJsonArray(attachmentsJson);
List<HcsAttachmentRefDTO> 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<HcsAttachmentRefDTO> transferContractAttachments(String bizType, Long bizId, String contractFileJson, String attachmentsJson, String operatorLoginName) {
List<HcsAttachmentRefDTO> 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<HcsAttachmentVO> result = hcsClient.uploadAttachment(file, "false", loginName);
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
log.error("HCS附件上传失败响应:{}", result == null ? "无响应" : String.format("code=%s, msg=%s, traceId=%s",
result.getCode(), result.getMsg(), result.getTraceId()));
throw new HcsException("HCS附件上传失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getFdId();
}
/**
* 合规附件下载(C103-5 代理,前端接入设计 §3.2):返回原始文件流响应,由控制器透传浏览器
*
* @param fdId 合规附件ID
* @return Feign 文件流响应(调用方负责关闭)
*/
public feign.Response downloadRaw(String fdId) {
HcsAttachmentDownloadDTO param = new HcsAttachmentDownloadDTO();
// 【必填】附件ID:上传时返回的 fdId
param.setFdId(fdId);
feign.Response response = hcsClient.downloadAttachment(param);
if (response == null || response.body() == null) {
throw new HcsException("HCS附件下载失败:响应为空(fdId=" + fdId + ")");
}
return response;
}
/**
* 转传单个附件:映射同步表命中直接复用合规附件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.<HcsAttachmentSync>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<HcsAttachmentVO> 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<Map<?, ?>> 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);
}
}
}
}
@@ -0,0 +1,379 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for their own commercial purposes.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 履约流水)
* <p>
* 触发点:金蝶认领推送(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)
* <p>
* 复用既有开关: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<ReceiptClaim> claims = claimMapper.selectList(Wrappers.<ReceiptClaim>lambdaQuery()
.eq(ReceiptClaim::getReceiptFlowId, flowId)
.eq(ReceiptClaim::getClaimStatus, CLAIMED));
if (claims.isEmpty()) {
return;
}
// 有效分摊行(作废时已置 status=0)
List<ReceiptClaimSettlement> allocations = claimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0));
if (allocations.isEmpty()) {
return;
}
// 结算单批量定位合同(一次 IN 查询,避免逐行 N+1)
Map<Long, FormalSettlement> 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<Long, Map<Long, BigDecimal>> 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<Long, Map<Long, BigDecimal>> 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<ReceiptClaim> claims, Long contractId,
Map<Long, BigDecimal> 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<Long, ReceiptClaim> claimsById = claims.stream()
.collect(Collectors.toMap(ReceiptClaim::getId, item -> item, (first, second) -> first));
for (Map.Entry<Long, BigDecimal> 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<HcsIdVO> 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<HcsFormSaveVO> 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<HcsPageVO<HcsCollectionFlowVO>> 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<ReceiptClaim> 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();
}
}
@@ -0,0 +1,284 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.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 定时查询兜底 + 人工确认)。
* <p>
* 业务闭环(2026-10-13 起):高风险/黑名单客商合同随 C103-1/C103-11 报文 fdSepcailContract=true 在合规侧走特批流程
* (C103-8 独立发起接口合规未提供,已删除),TMS 挂起"特批审核"(processing)→
* 集团审批结果经合规 contractLegal/result 接口推送 TMS(替代未建设的 D103),或定时 C103-4 查询兜底联动 →
* approved 恢复流程,rejected 联动合同审批驳回。
* <p>
* 结果推送报文 {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<ContractManage> contracts = contractManageMapper.selectList(Wrappers.<ContractManage>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<HcsContractDetailVO> 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.<ContractManage>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);
}
}
@@ -0,0 +1,787 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.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)
* <p>
* 扫描任务按序执行:⓪ 特批挂起门禁 → ① 客商风险校验(可配开关,仅留痕,高风险不再阻断)→
* ② 附件转传(挂载点为与 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.<ContractManage>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<Object> records = StringUtil.isBlank(contractManage.getChangeRecordJson())
? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(contractManage.getChangeRecordJson(), Object.class));
Map<String, Object> 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<HcsAttachmentRefDTO> 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<Map<?, ?>> parseJsonArray(String json) {
if (StringUtil.isBlank(json)) {
return List.of();
}
try {
List<Map<?, ?>> 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<HcsRiskVO> 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<HcsPageVO<HcsContractVO>> 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<HcsContractVO> 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<HcsAttachmentRefDTO> attachmentRefs, boolean risky) {
HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentRefs, risky);
log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个,特批流程={})", contractManage.getContractNo(), attachmentRefs.size(), risky);
HcsResult<HcsContractDraftVO> 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<Void> 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<HcsAttachmentRefDTO> 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<HcsXformFieldDTO> buildXformFields(ContractManage contractManage, boolean risky) {
List<HcsXformFieldDTO> 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<String, String> 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<HcsSignerDTO> buildSigners(ContractManage contractManage) {
List<HcsSignerDTO> 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<HcsAttachmentRefDTO> transferAttachmentsIfEnabled(ContractManage contractManage) {
if (!hcsProperties.getAttachment().isTransferEnabled()) {
return List.of();
}
List<HcsAttachmentRefDTO> 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<HcsContractDraftVO> 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<String, String> 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.<CustomerArchive>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<String, String> 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) {
}
}
@@ -0,0 +1,391 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import 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)
* <p>
* 付款申请提交(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<HcsRiskVO> 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<HcsCreditCheckVO> 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.<CustomerArchive>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<HcsRiskVO> result = hcsClient.getRelativeRisk(param);
if (result == null || !result.isSuccess()) {
log.error("HCS客商风险查询失败响应:{},fdNumber={}",
result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), customerCode);
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsRiskVO risk = result.getData();
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
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<CustomerRiskVO> queryCustomerRiskBatch(List<String> customerCodes) {
List<CustomerRiskVO> 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.<CustomerArchive>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);
}
}
}
@@ -0,0 +1,255 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsExeplanStageDTO;
import org.springblade.hcs.dto.HcsExeplanStageVO;
import org.springblade.hcs.dto.HcsExeplanSyncDTO;
import org.springblade.hcs.dto.HcsExeplanVO;
import org.springblade.hcs.dto.HcsResult;
import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.HcsContractExeStageMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* HCS 履约规则同步支撑组件(C103-9 新版行级同步,设计文档 §5.3,ADR-0002)
* <p>
* 随合同推送链路执行(合同须已同步、hcs_fd_id 存在):聚合 row_op 非 none 的规则行
* (add 行不带 fdId,update/delete 行必须带合规阶段ID),整体 fdEopExeplanOperateType
* 首次为 add、后续为 update;返回的 fdExeplanId 回填 blade_contract_manage.hcs_exeplan_id,
* 变更阶段 fdId 按行回填 hcs_stage_fd_id(合规返回的 fdId 必须持久化,后续行更新/删除全靠它)。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HcsExeStageSyncSupport {
/**
* 行级扩展内容固定空 JSON
*/
private static final String EXTEND_CONTENT_EMPTY = "{}";
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ContractManageMapper contractManageMapper;
private final HcsContractExeStageMapper hcsContractExeStageMapper;
/**
* 同步合同履约规则到 HCS(C103-9),成功后回填 fdExeplanId 与各阶段 fdId 并清除行级操作标记
*
* @param contractManage 合同(须已同步,hcs_fd_id 非空)
* @return 同步结果描述(无待同步行时返回提示)
*/
public String syncExeStages(ContractManage contractManage) {
if (StringUtil.isBlank(contractManage.getHcsFdId())) {
throw new HcsException("C103-9 履约规则同步失败,合同尚未推送合规(contractNo=" + contractManage.getContractNo() + ")");
}
// 含软删行:delete 行已逻辑删除但须凭合规阶段ID 上报(原生 SQL 绕过逻辑删除过滤)
List<HcsContractExeStage> pendingRows = hcsContractExeStageMapper.selectSyncRows(contractManage.getId());
if (pendingRows.isEmpty()) {
return "履约规则无待同步行";
}
HcsExeplanSyncDTO param = buildSyncParam(contractManage, pendingRows);
log.info("HCS履约规则同步开始:contractNo={},整体操作 {},{} 行", contractManage.getContractNo(),
param.getFdEopExeplanOperateType(), pendingRows.size());
HcsResult<HcsExeplanVO> result = hcsClient.exeplanAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null) {
log.error("HCS履约规则同步失败响应:{},contractNo={}", result == null ? "无响应" : 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<HcsContractExeStage> pendingRows) {
HcsExeplanSyncDTO param = new HcsExeplanSyncDTO();
// 【必填】合同ID:HCS 合同ID(blade_contract_manage.hcs_fd_id,非 TMS 主键)
param.setFdContractId(contractManage.getHcsFdId());
// 【必填】外部系统标识:fd_sources,配置 thirdParty.hcs.src-system(D14)
param.setFdOuterSystem(hcsProperties.getSrcSystem());
// 【必填】操作人登录名:配置 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<HcsExeplanStageDTO> stages = new ArrayList<>();
for (HcsContractExeStage row : pendingRows) {
HcsExeplanStageDTO stage = new HcsExeplanStageDTO();
// 阶段项ID:行级 update/delete 必传(取 hcs_stage_fd_id),add 行不带
if (!"add".equals(row.getRowOp())) {
stage.setFdId(row.getHcsStageFdId());
}
// 【必填】行级操作:add / update / delete
stage.setFdEopExeplanOperateType(row.getRowOp());
// 【必填】履约类型:payment=付款 / collection=收款
stage.setFdPerformType(row.getPerformType());
// 【必填】款项性质:prepayment/progress/acceptance/retention
stage.setFdFundNature(row.getFundNature());
// 【必填】款项金额(元,付款审批通过按申请付款金额累加更新)
stage.setFdFundAmount(row.getFundAmount());
// 【必填】资金日期(毫秒时间戳)
stage.setFdFundDate(toMillis(row.getFundDate()));
// 【必填】开票规则:payFirst=先款后票 / 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 并清除行级操作标记
* <p>
* TODO 待联调确认:fdChangeStages 与请求行的对应关系 v4 未明确,优先按请求行序对应(首次全 add 场景),
* 行数不一致时按业务属性组合键匹配,仍匹配不到的 add 行保持待同步(下轮重试)。
*/
private void backfill(ContractManage contractManage, List<HcsContractExeStage> pendingRows,
List<HcsExeplanStageDTO> requestStages, HcsExeplanVO exeplan) {
// 履约计划ID 回填(后续行更新/删除依赖,必须持久化)
if (StringUtil.isNotBlank(exeplan.getFdExeplanId())
&& !exeplan.getFdExeplanId().equals(contractManage.getHcsExeplanId())) {
contractManage.setHcsExeplanId(exeplan.getFdExeplanId());
}
backfillExeplanId(contractManage);
List<HcsExeplanStageVO> changeStages = exeplan.getFdChangeStages() == null ? List.of() : exeplan.getFdChangeStages();
// 行数一致时按请求行序一一对应;不一致时按业务属性组合键匹配
boolean positional = changeStages.size() == requestStages.size();
Map<String, HcsExeplanStageVO> byAttribute = positional ? Map.of() : mapByAttribute(changeStages);
int addRowCount = 0;
int unmatchedCount = 0;
for (int index = 0; index < pendingRows.size(); index++) {
HcsContractExeStage row = pendingRows.get(index);
String stageFdId = row.getHcsStageFdId();
if ("add".equals(row.getRowOp())) {
addRowCount++;
HcsExeplanStageVO matched = positional ? changeStages.get(index) : byAttribute.get(attributeKey(requestStages.get(index)));
stageFdId = matched == null ? null : matched.getFdId();
if (StringUtil.isBlank(stageFdId)) {
unmatchedCount++;
}
}
markRowSynced(row, stageFdId);
}
log.info("HCS履约规则阶段ID回填完成:contractNo={},add {} 行,未匹配 {} 行(模式:{})",
contractManage.getContractNo(), addRowCount, unmatchedCount, positional ? "行序对应" : "属性匹配");
}
/**
* 行同步成功回写:回填合规阶段ID、清除行级操作标记并置 synced
*/
private void markRowSynced(HcsContractExeStage row, String stageFdId) {
if (StringUtil.isNotBlank(stageFdId)) {
row.setHcsStageFdId(stageFdId);
} else if ("add".equals(row.getRowOp())) {
// add 行未取得阶段ID:保持待同步,下轮重试(ADR-0002:阶段ID 必须持久化)
row.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
row.setHcsSyncMsg("返回阶段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<String, HcsExeplanStageVO> mapByAttribute(List<HcsExeplanStageVO> changeStages) {
Map<String, HcsExeplanStageVO> byAttribute = new HashMap<>();
for (HcsExeplanStageVO stage : changeStages) {
byAttribute.putIfAbsent(String.join("|", Objects.toString(stage.getFdPerformType(), ""),
Objects.toString(stage.getFdFundNature(), ""),
Objects.toString(stage.getFdFundAmount(), ""),
Objects.toString(stage.getFdFundDate(), "")), stage);
}
return byAttribute;
}
/**
* 请求行的业务属性组合键
*/
private String attributeKey(HcsExeplanStageDTO stage) {
return String.join("|", Objects.toString(stage.getFdPerformType(), ""),
Objects.toString(stage.getFdFundNature(), ""),
Objects.toString(stage.getFdFundAmount(), ""),
Objects.toString(stage.getFdFundDate(), ""));
}
/**
* 合同履约计划ID 回写(fdExeplanId 必须持久化;失败仅告警不阻断行回填,下轮同步会再次携带)
*/
private void backfillExeplanId(ContractManage contractManage) {
try {
contractManageMapper.updateById(contractManage);
log.info("HCS履约计划ID已回填:contractNo={},fdExeplanId={}", contractManage.getContractNo(), contractManage.getHcsExeplanId());
} catch (Exception exception) {
log.error("HCS履约计划ID回写失败:contractNo={}", contractManage.getContractNo(), exception);
}
}
/**
* LocalDateTime → 毫秒时间戳
*/
private Long toMillis(LocalDateTime localDateTime) {
return localDateTime == null ? null : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
}
@@ -0,0 +1,300 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import 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 增补)
* <p>
* 项目立项(C102)/合同(C103)/付款申请(C106 付款特批)审批通过(含变更通过)事务提交后立即触发单条推送,不再等待扫描周期;
* 执行体复用 {@link HcsProjectSyncSupport#repushProject(Long)} / {@link HcsContractSyncSupport#repushContract(Long)} /
* {@link HcsPaymentSyncSupport#repushPayment(Long)}
* (已含审批状态校验、失败 markFailed、高风险特批挂起判断),失败仅留痕,由 HcsPushTask 扫描兜底补偿
* (ADR-0001 修订:审批代码埋点为一行触发调用,推送逻辑仍收敛在 hcs support 包)。
* <p>
* 触发模型:审批事务内调用则注册 afterCommit 回调(事务回滚不推送),无活动事务(MK 回调单条更新自动提交)直接提交;
* 自建 2 线程 daemon 池(工程未启用 @EnableAsync),HCS 推送可达 20s+ 不阻塞审批响应;
* Redis 单条锁防与扫描任务并发双推(TTL 120s,持有值比对释放,照 HcsPushTask 惯例),
* 极端双推由 HCS 侧判重/更新幂等吸收。与扫描任务同线程模型(无租户上下文),行为一致。
* <p>
* 例外:TMS 合同/项目立项的"通过"按钮路径由 Controller 在审批事务返回后分别调用
* {@link #pushContractAndWait(Long)}、{@link #pushProjectAndWait(Long)} 同步推送
* (审批响应会等待附件转传 + HCS 调用完成,与正式结算单审批一致),
* 失败原因经 {@link #pollSyncFailure()} 交给 Controller 拼进审批响应(照 KingdeeApprovalSyncListener);
* MK 回调路径仍异步,避免阻塞第三方回调。
* <p>
* 付款申请按"审批即发起特批"要求:即时推送不受 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<String> 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<String> 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();
}
}
@@ -0,0 +1,361 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import 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)
* <p>
* 审批通过即推 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<HcsAttachmentRefDTO> 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<HcsIdVO> 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<HcsIdVO> 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<HcsPageVO<HcsInvoiceRegVO>> 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<HcsInvoiceRegVO> 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.<InvoiceApplicationSettlement>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.<InvoiceApplicationInvoice>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();
}
}
@@ -0,0 +1,207 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.json.JSONUtil;
import 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)
* <p>
* 启动/首次使用时拉取并缓存(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<HcsTemplateVO> getTemplates() {
List<HcsTemplateVO> cached = readCache(HcsSyncConstant.MASTER_CACHE_TEMPLATES, HcsTemplateVO.class);
if (cached != null) {
return cached;
}
return loadTemplates();
}
/**
* 获取合同分类列表(缓存优先,未命中全量拉取)
*/
public List<HcsCategoryVO> getCategories() {
List<HcsCategoryVO> cached = readCache(HcsSyncConstant.MASTER_CACHE_CATEGORIES, HcsCategoryVO.class);
if (cached != null) {
return cached;
}
return loadCategories();
}
/**
* 获取币种列表(缓存优先,未命中全量拉取)
*/
public List<HcsCurrencyVO> getCurrencies() {
List<HcsCurrencyVO> 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<HcsTemplateVO> loadTemplates() {
HcsPageQueryDTO query = baseQuery();
List<HcsTemplateVO> 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<HcsCategoryVO> loadCategories() {
HcsPageQueryDTO query = baseQuery();
List<HcsCategoryVO> 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<HcsCurrencyVO> loadCurrencies() {
HcsPageQueryDTO query = baseQuery();
List<HcsCurrencyVO> 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<String, String> mapping = hcsProperties.getContract().getTemplateMapping();
if (mapping.isEmpty()) {
log.warn("HCS合同模板映射(thirdParty.hcs.contract.template-mapping)未配置,合同推送将失败(§9-U7 待合规方提供模板ID清单)");
return;
}
List<String> 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 <T> List<T> readCache(String cacheKey, Class<T> 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()));
}
}
@@ -0,0 +1,327 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.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)
* <p>
* 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 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<String> 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);
}
}
@@ -0,0 +1,225 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.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)
* <p>
* 付款申请为主线:审批通过 → 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<HcsFormSaveVO> 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<Void> 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<HcsAttachmentRefDTO> 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();
}
}
@@ -0,0 +1,467 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import cn.hutool.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)
* <p>
* 触发与链路:审批通过(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<HcsFormSaveVO> 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<Void> 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<HcsPageVO<HcsProjectVO>> 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<HcsProjectVO> 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<Object> records = StringUtil.isBlank(projectApply.getChangeRecordJson())
? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(projectApply.getChangeRecordJson(), Object.class));
Map<String, Object> 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.<ProjectApply>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);
}
}
@@ -0,0 +1,301 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import 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)
* <p>
* 审批通过 → 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<HcsResult<HcsFormSaveVO>> 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<HcsResult<Void>> 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<HcsAttachmentRefDTO> 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<HcsResult<HcsFormSaveVO>> result) {
if (result == null || !result.isSuccess() || result.getData() == null || !result.getData().isSuccess()
|| result.getData().getData() == null || StringUtil.isBlank(result.getData().getData().getFdId())) {
log.error("HCS收票单新建失败响应:{},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.<InvoiceReceiptSettlement>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) {
}
}
@@ -0,0 +1,146 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.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,运维收尾)
* <p>
* 按来源单据编号回查 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 二选一)
* <p>
* 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;
}
}
@@ -0,0 +1,234 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.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)
* <p>
* 触发:付款申请提交时收款方外部客商高风险/黑名单(须特批附件)或额度超额标记 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<HcsResult<HcsFormSaveVO>> 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<HcsResult<Void>> 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<HcsAttachmentRefDTO> 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<HcsResult<HcsFormSaveVO>> result) {
if (result == null || !result.isSuccess() || result.getData() == null || !result.getData().isSuccess()
|| result.getData().getData() == null || StringUtil.isBlank(result.getData().getData().getFdId())) {
log.error("HCS结算付款特批发起失败响应:{},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();
}
}
@@ -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 {
@@ -0,0 +1,352 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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)
* <p>
* 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
* 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
* <p>
* 五类任务各自独立调度、独立开关(Nacos thirdParty.hcs.push.{bizType}.*):
* <pre>
* 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.*
* </pre>
* 每个任务配置项: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<String> 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<String> 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<ProjectApply> projectList = projectApplyMapper.selectList(Wrappers.<ProjectApply>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<ContractManage> contractList = contractManageMapper.selectList(Wrappers.<ContractManage>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<PaymentApplication> paymentList = paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>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<InvoiceApplication> applicationList = invoiceApplicationMapper.selectList(Wrappers.<InvoiceApplication>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<InvoiceReceipt> receiptList = invoiceReceiptMapper.selectList(Wrappers.<InvoiceReceipt>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;
}
}
@@ -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 {
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springblade</groupId>
<artifactId>blade-third-party-api</artifactId>
<version>${revision}</version>
</parent>
<artifactId>blade-hcs-api</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<description>BladeX 合同合规系统(HCS)API 契约模块</description>
<dependencies>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-tool</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,81 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.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 客户端配置
* <p>
* 仅通过 {@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);
}
}
@@ -0,0 +1,348 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.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)配置类
* <p>
* 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<String, String> templateMapping = new HashMap<>();
/**
* TMS 审核状态 -> HCS 合同状态(xformFields CONTRACT_STATUS 必填,draft/approving/approved 等,§9-U3 待联调后配置)
*/
private Map<String, String> statusMapping = new HashMap<>();
/**
* TMS 币种码 -> HCS 币种名称(照金蝶 RMB->CNY 映射惯例)
*/
private Map<String, String> 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<String, String> invoiceTypeMapping = new HashMap<>();
/**
* TMS 币种码 -> HCS 币种 fdId(经 C103-12 币种表缓存取值,§9-U3 待联调后配置)
*/
private Map<String, String> currencyIdMapping = new HashMap<>();
/**
* 开票登记款项性质默认值(fdFundNature)
*/
private String fundNatureDefault = "progress";
}
@Data
public static class ReceiptBill {
/**
* TMS 发票类型 -> HCS 收票发票类型字典值(§9-U3 待联调后配置)
*/
private Map<String, String> 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;
}
}
@@ -0,0 +1,41 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.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 {
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.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";
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,70 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)附件引用项(附件先经 C103-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;
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)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;
}
@@ -0,0 +1,78 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C107-1 新增认领记录入参(将收款流水认领到合同/履约项)
* <p>
* 校验: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;
}
@@ -0,0 +1,74 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for their own commercial purposes.
* <p>
* 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.
* <p>
* 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.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C104-2 收款流水分页行(认领记录推送前按明细流水号定位合规侧收款流水ID)
* <p>
* 合规侧收款流水由合规自 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;
}
@@ -0,0 +1,56 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-4 合同详情入参(fdId 与 fdNumber 二选一)
* <p>
* 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;
}
@@ -0,0 +1,519 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 合同合规系统(HCS)C103-4 合同详情出参(v4 字段表)
* <p>
* 级联/集合类字段(对象/数组)内部结构 v4 标注以实际返回为准,暂以 Object/List&lt;Object&gt; 承接,
* 联调确认后细化。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<Object> fdReaders;
/**
* 所有可阅读者(结构以实际返回为准)
*/
private List<Object> fdAllReaders;
/**
* 所有可阅读者ID(结构以实际返回为准)
*/
private List<Object> fdAllReaderIds;
/**
* 可维护者(结构以实际返回为准)
*/
private List<Object> fdEditors;
/**
* 所有可编辑者(结构以实际返回为准)
*/
private List<Object> fdAllEditors;
/**
* 协办人(结构以实际返回为准)
*/
private List<Object> fdCoTakers;
/**
* 合同时间线(结构以实际返回为准)
*/
private List<Object> fdTimeLines;
/**
* 合同关联(结构以实际返回为准)
*/
private List<Object> fdRelations;
/**
* 合同签约方及地位(结构以实际返回为准)
*/
private List<Object> fdStandingSigners;
/**
* 签印文件(结构以实际返回为准)
*/
private List<Object> fdSignFiles;
/**
* 业务明细(结构以实际返回为准)
*/
private List<Object> fdBusinessDetails;
/**
* 付款记录(结构以实际返回为准)
*/
private List<Object> fdPaymentRecords;
/**
* 收款记录(结构以实际返回为准)
*/
private List<Object> fdCollectionRecords;
/**
* 收票记录(结构以实际返回为准)
*/
private List<Object> fdInInvoices;
/**
* 开票记录(结构以实际返回为准)
*/
private List<Object> fdOutInvoices;
/**
* 预填写物理签印信息(结构以实际返回为准)
*/
private List<Object> fdPsignContents;
/**
* 审批人(结构以实际返回为准)
*/
private List<Object> fdApprovers;
/**
* 履约规则(结构以实际返回为准)
*/
private List<Object> fdExePlanRules;
}
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
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<HcsAttachmentRefDTO> attachment;
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 合同合规系统(HCS)C103-1 合同发起 fdFieldJson 节点(C103-1 新建 / C103-11 更新共用)
* <p>
* 附件挂载在顶层报文 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<HcsXformFieldDTO> xformFields;
/**
* 签署方(相对方)列表
*/
private List<HcsSignerDTO> signer;
}
@@ -0,0 +1,77 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同特批完成后合规系统回传 TMS 的结果报文(POST /hcs/contractLegal/result,iPaaS 网关 /gwzh/HTHG/contractLegal/result)
* <p>
* 报文为"合同同步结果"结构:合规特批完成后同步金蝶,回调携带金蝶同步结果;
* 定位合同用 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;
}
@@ -0,0 +1,62 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
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;
}
@@ -0,0 +1,69 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C106-1 项目/合同额度校验出参
* <p>
* 注意:超额时外层 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;
}
@@ -0,0 +1,86 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C103-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;
}
@@ -0,0 +1,93 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C103-9(新) 履约规则同步 行级操作项(数据源 blade_contract_exe_stage,ADR-0002)
* <p>
* 新增行可不带 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;
}
@@ -0,0 +1,97 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C103-9(新) 履约规则同步出参-变更阶段行
* <p>
* 合规返回的阶段 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;
}
@@ -0,0 +1,74 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 合同合规系统(HCS)C103-9(新) 合同履约规则同步入参(支持整批增删改)
* <p>
* 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<HcsExeplanStageDTO> fdStages;
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
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<HcsExeplanStageVO> fdChangeStages;
}
@@ -0,0 +1,58 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)流程单据新建返回结果(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;
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)单据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;
}
@@ -0,0 +1,107 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 合同合规系统(HCS)C111-2 新增开票登记入参
* <p>
* 校验: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<String> fdAttIds;
}
@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 合同合规系统(HCS)C111-3 更新开票登记入参(金蝶开票完成后回填票据日期/状态)
* <p>
* 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;
}
@@ -0,0 +1,127 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
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;
}
@@ -0,0 +1,69 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)特批结果回传报文
* <p>
* 合规系统审批完成后经 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;
}
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
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<HcsAttachmentRefDTO> attachment;
}
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 合同合规系统(HCS)通用分页查询入参(§1.5 分页与查询约定,各查询接口通用)
* <p>
* 注意: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<String> columns;
/**
* 查询条件,空对象表示无条件;支持操作符,如 {"fdName": {"$contains": "xx"}}(包含匹配)
*/
private Map<String, Object> conditions;
/**
* 分页偏移量(必填,从 0 开始)
*/
private Integer offset;
/**
* 每页条数(必填)
*/
private Integer pageSize;
/**
* 排序条件,如 {"fdCreateTime": "desc"}
*/
private Map<String, Object> sorts;
/**
* 当前操作人(C102-3 项目立项查询需携带,其余查询接口按需)
*/
private HcsUserRefVO curUser;
}
@@ -0,0 +1,62 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 合同合规系统(HCS)分页查询返回结果(位于统一出参 data 内)
*
* @param <T> 数据行类型
* @author Chill
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class HcsPageVO<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 数据列表
*/
private List<T> content;
/**
* 分页偏移量(从 0 开始)
*/
private Integer offset;
/**
* 每页条数
*/
private Integer pageSize;
/**
* 总条数
*/
private Integer totalSize;
}
@@ -0,0 +1,66 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)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;
}
@@ -0,0 +1,130 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C108-1/C108-2 履约记录 dynamicProps 业务字段(字段名按接口文档下划线原样传值)
* <p>
* 注意: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;
}
@@ -0,0 +1,61 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)C108-1 履约记录新建入参(C108-2 更新入参结构相同,复用本 DTO)
* <p>
* 更新时 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;
}
@@ -0,0 +1,66 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)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;
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.hcs.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 合同合规系统(HCS)项目立项 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;
}

Some files were not shown because too many files have changed in this diff Show More