From 63c2f7ed1ee1c04221acfcd1fc8b4d7f50f64758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Mon, 12 Oct 2026 16:52:39 +0800 Subject: [PATCH] =?UTF-8?q?:feat:=20=E5=90=88=E5=90=8C=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E6=8C=82=E8=BD=BD=E6=8C=89=20eopCdraft=20=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E5=88=86=E7=B1=BB=EF=BC=88fdEntityKey=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HcsSyncConstant 新增 CONTRACT_ATTACH_KEY_SPECIAL/SEAL/OTHER(eopCdraftSpecial/eopCdraftSign/eopCdraftOther) - 转传接口返回 HcsAttachmentRefDTO(fdId+fdEntityKey+fdFileName),替代纯 fdId 列表 - 合同链路:主文件 approved=true 挂 eopCdraftSign、其它附件挂 eopCdraftOther、特批说明挂 eopCdraftSpecial - buildDraftParam 直接消费引用列表统一补 fdBindType=ADD;特批付款/履约记录/收票单/开票登记四个挂载点改为在返回引用上补各自字段,行为不变 --- .../transport/constant/HcsSyncConstant.java | 47 ++++++-- .../support/hcs/HcsAttachmentSupport.java | 106 ++++++++++++------ .../support/hcs/HcsContractSyncSupport.java | 103 ++++++++--------- .../support/hcs/HcsInvoiceRegSyncSupport.java | 10 +- .../support/hcs/HcsPerfRecordSyncSupport.java | 28 ++--- .../hcs/HcsReceiptBillSyncSupport.java | 34 ++---- .../support/hcs/HcsSpecPaySyncSupport.java | 29 ++--- ...026-09-27-桂物合同合规系统HCS接入技术设计.md | 4 +- 8 files changed, 192 insertions(+), 169 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java index 7eed4fed..6e4da84b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java @@ -180,6 +180,28 @@ public final class HcsSyncConstant { */ public static final String ATTACHMENT_TYPE_SPEC_EXPLANATION = "特批说明"; + /** + * 附件类型:用印附件(双章归档文件,取值沿用 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,特批发起后挂起展示,结果回传后恢复) */ @@ -202,16 +224,6 @@ public final class HcsSyncConstant { */ public static final String MASTER_CACHE_CURRENCIES = "blade:third:hcs:master:currencies"; - /** - * 附件映射缓存 key 前缀(hcs:att:{attachmentKey},TMS 附件元数据键 → 合规附件ID) - */ - public static final String ATTACHMENT_CACHE_KEY_PREFIX = "blade:third:hcs:att:"; - - /** - * 附件映射缓存 TTL(天),重复上传仅浪费存储无业务风险,故不做持久化映射表(设计文档 §6) - */ - public static final long ATTACHMENT_CACHE_TTL_DAYS = 7L; - /** * 推送任务分布式锁 key 前缀(多实例部署防重扫,完整 key = 前缀 + 业务类型,任务间互不阻塞) */ @@ -227,6 +239,21 @@ public final class HcsSyncConstant { */ 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; + } + private HcsSyncConstant() { } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java index 4e1351ca..8e63c988 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java @@ -26,19 +26,21 @@ 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.redis.cache.BladeRedis; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.StringUtil; import org.springblade.hcs.config.HcsProperties; -import org.springblade.hcs.constant.HcsConstant; import org.springblade.hcs.dto.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; @@ -48,7 +50,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -59,12 +61,12 @@ import java.util.Objects; *

* 流程:解析单据附件 JSON 元素(去重键照 ContractManage#attachmentKey 惯例)→ * 按元数据 URL 从文件服务拉取流(基址可配 attachment.file-download-base)→ - * C103-2 上传取得合规附件ID → Redis 映射缓存(blade:third:hcs:att:{attachmentKey},TTL 7 天), - * 未命中重传(重复上传仅浪费存储,无业务风险,故不做持久化映射表)。 - * 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试。 + * C103-2 上传取得合规附件ID → 落映射同步表(blade_hcs_attachment_sync,照金蝶附件同步表设计: + * biz_type+biz_id+attachment_key 唯一,转传前查命中即复用,仅落成功记录)。 + * 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试(失败原因落单据 hcs_sync_msg)。 *

- * TODO 待联调确认(§9-U8):v4 的 C103-1 入参未定义附件字段,合同附件向 HCS 的挂载方式待合规方确认, - * 本组件先建设通用转传能力并将合规附件ID 缓存备用,确认后按其要求挂载。 + * 合同链路挂载字段 fdEntityKey(v4 枚举):主文件 approved=true → eopCdraftSign(用印)、 + * 其它附件 → eopCdraftOther、特批说明 → eopCdraftSpecial(C103-8 特批链路)。 * * @author Chill */ @@ -80,48 +82,52 @@ public class HcsAttachmentSupport { private final IHcsClient hcsClient; private final HcsProperties hcsProperties; - private final BladeRedis bladeRedis; + private final HcsAttachmentSyncMapper hcsAttachmentSyncMapper; /** - * 解析单据附件 JSON 并转传,返回合规附件ID 列表(保持行序) + * 解析单据附件 JSON 并转传,返回合规附件引用列表(保持行序) * + * @param bizType 业务单据类型(HcsSyncConstant.BIZ_TYPE_*) + * @param bizId 业务单据id * @param attachmentsJson 附件元数据 JSON 数组 * @param operatorLoginName 操作人登录名(C103-2 fdLoginName) - * @return 合规附件ID 列表 + * @return 合规附件引用列表(fdId + fdEntityKey) */ - public List transferAttachments(String attachmentsJson, String operatorLoginName) { + public List transferAttachments(String bizType, Long bizId, String attachmentsJson, String operatorLoginName) { if (StringUtil.isBlank(attachmentsJson)) { return List.of(); } List> attachments = parseJsonArray(attachmentsJson); - List attachmentFdIds = new ArrayList<>(); + List attachmentRefs = new ArrayList<>(); for (Map attachment : attachments) { - attachmentFdIds.add(transferOne(attachment, operatorLoginName)); + attachmentRefs.add(this.transferOne(bizType, bizId, attachment, operatorLoginName)); } - return attachmentFdIds; + return attachmentRefs; } /** - * 解析合同主文件 JSON(仅 approved=true 的文件)与其它附件 JSON 并转传 + * 解析合同主文件 JSON(仅 approved=true 的文件,挂用印字段)与其它附件 JSON(挂其他附件字段)并转传 * + * @param bizType 业务单据类型(HcsSyncConstant.BIZ_TYPE_*) + * @param bizId 业务单据id * @param contractFileJson 合同主文件 JSON * @param attachmentsJson 其它附件 JSON * @param operatorLoginName 操作人登录名 - * @return 合规附件ID 列表 + * @return 合规附件引用列表(fdId + fdEntityKey) */ - public List transferContractAttachments(String contractFileJson, String attachmentsJson, String operatorLoginName) { - List> attachments = new ArrayList<>(); + public List transferContractAttachments(String bizType, Long bizId, String contractFileJson, String attachmentsJson, String operatorLoginName) { + List attachmentRefs = new ArrayList<>(); for (Map file : parseJsonArray(contractFileJson)) { if (isApproved(file)) { - attachments.add(file); + attachmentRefs.add(this.transferOne(bizType, bizId, file, operatorLoginName, + HcsSyncConstant.CONTRACT_ATTACH_KEY_SEAL)); } } - attachments.addAll(parseJsonArray(attachmentsJson)); - List attachmentFdIds = new ArrayList<>(); - for (Map attachment : attachments) { - attachmentFdIds.add(transferOne(attachment, operatorLoginName)); + for (Map attachment : parseJsonArray(attachmentsJson)) { + attachmentRefs.add(this.transferOne(bizType, bizId, attachment, operatorLoginName, + HcsSyncConstant.CONTRACT_ATTACH_KEY_OTHER)); } - return attachmentFdIds; + return attachmentRefs; } /** @@ -163,14 +169,26 @@ public class HcsAttachmentSupport { } /** - * 转传单个附件:映射缓存命中直接返回,未命中下载上传后写缓存 + * 转传单个附件:映射同步表命中直接复用合规附件ID,未命中下载上传后落成功记录; + * 返回引用默认不带 fdEntityKey,由调用方(或合同主文件/其他附件重载)指定挂载字段 */ - private String transferOne(Map attachment, String operatorLoginName) { + 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); - String cacheKey = HcsSyncConstant.ATTACHMENT_CACHE_KEY_PREFIX + attachmentKey; - String cachedFdId = bladeRedis.get(cacheKey); - if (StringUtil.isNotBlank(cachedFdId)) { - return cachedFdId; + HcsAttachmentSync existing = hcsAttachmentSyncMapper.selectOne(Wrappers.lambdaQuery() + .eq(HcsAttachmentSync::getBizType, bizType) + .eq(HcsAttachmentSync::getBizId, bizId) + .eq(HcsAttachmentSync::getAttachmentKey, attachmentKey) + .last("limit 1")); + if (existing != null && StringUtil.isNotBlank(existing.getFdId())) { + String url = resolveUrl(attachment); + return buildRef(existing.getFdId(), resolveFileName(attachment, url == null ? "" : url), fdEntityKey); } String url = resolveUrl(attachment); if (StringUtil.isBlank(url)) { @@ -184,12 +202,30 @@ public class HcsAttachmentSupport { 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())); + throw new HcsException("HCS附件转传失败[" + fileName + "]:" + (result == null ? "响应为空" : result.getMsg())); } String attachmentFdId = result.getData().getFdId(); - bladeRedis.getValueOps().setIfAbsent(cacheKey, attachmentFdId, Duration.ofDays(HcsSyncConstant.ATTACHMENT_CACHE_TTL_DAYS)); - log.info("HCS附件转传完成:{} → 合规附件ID {}", fileName, attachmentFdId); - return attachmentFdId; + 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; } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java index 375ecb7b..876b6f6a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java @@ -43,12 +43,9 @@ import org.springblade.hcs.dto.HcsResult; import org.springblade.hcs.dto.HcsRiskQueryDTO; import org.springblade.hcs.dto.HcsRiskVO; import org.springblade.hcs.dto.HcsSignerDTO; -import org.springblade.hcs.dto.HcsUserRefVO; import org.springblade.hcs.dto.HcsXformFieldDTO; import org.springblade.hcs.exception.HcsException; import org.springblade.hcs.feign.IHcsClient; -import org.springblade.system.cache.UserCache; -import org.springblade.system.pojo.entity.User; import org.springblade.transport.constant.HcsContractFieldConstant; import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.mapper.ContractManageMapper; @@ -124,8 +121,8 @@ public class HcsContractSyncSupport { if (hcsProperties.getContract().isRequireRiskCheck()) { risky = checkRelativeRisk(contractManage); } - // ② 附件转传(v4 已明确挂载点:C103-1 fdFieldJson.attachment,转传取得合规附件ID 后随发起报文挂载) - List attachmentFdIds = transferAttachmentsIfEnabled(contractManage); + // ② 附件转传(v4 已明确挂载点:C103-1 fdFieldJson.attachment,转传取得合规附件引用后随发起报文挂载) + List attachmentRefs = transferAttachmentsIfEnabled(contractManage); // ③④ 首推判重后起草(C103-1);已有合规ID(变更单)走更新(C103-11) String resultMessage; if (StringUtil.isBlank(contractManage.getHcsFdId())) { @@ -134,7 +131,7 @@ public class HcsContractSyncSupport { markSynced(contractManage, existFdId, "判重命中,回填已有合规合同ID"); resultMessage = "判重命中,回填已有合规合同ID"; } else { - String fdContractId = draftContract(contractManage, attachmentFdIds); + String fdContractId = draftContract(contractManage, attachmentRefs); // 先持久化合规合同ID(调用方必须保存),履约规则同步失败时下轮凭其走更新补偿 markSynced(contractManage, fdContractId, "C103-1 合同发起成功"); resultMessage = "C103-1 合同发起成功"; @@ -204,7 +201,7 @@ public class HcsContractSyncSupport { contractManage.getCurrentNode()); try { List attachments = transferSpecExplanationAttachments(contractManage); - hcsContractSpecSyncSupport.applySpec(contractManage.getId(), attachments, requireLoginName(contractManage)); + hcsContractSpecSyncSupport.applySpec(contractManage.getId(), attachments); } catch (Exception exception) { log.warn("HCS合同自动特批发起失败(保持待特批,可人工重发):contractNo={},{}", contractManage.getContractNo(), exception.getMessage(), exception); @@ -213,7 +210,7 @@ public class HcsContractSyncSupport { /** * 筛选合同「特批说明」类型附件(fileType=特批说明,业务字典 contract_attachment_type)并转传合规(C103-2), - * 返回特批附件引用;无特批说明附件返回空列表(提交时已强制校验,此处由 C103-8 防御性拦截) + * 返回特批附件引用(fdEntityKey=eopCdraftSpecial);无特批说明附件返回空列表(提交时已强制校验,此处由 C103-8 防御性拦截) */ private List transferSpecExplanationAttachments(ContractManage contractManage) { List> specAttachments = new ArrayList<>(); @@ -227,17 +224,15 @@ public class HcsContractSyncSupport { return List.of(); } String specJson = JsonUtil.toJson(specAttachments); - String loginName = requireLoginName(contractManage); - List attachmentFdIds = hcsAttachmentSupport.transferAttachments(specJson, loginName); - List attachments = new ArrayList<>(); - for (int index = 0; index < specAttachments.size(); index++) { - HcsAttachmentRefDTO ref = new HcsAttachmentRefDTO(); - // 附件ID:C103-2 转传返回的合规附件ID(与元数据行序一致) - ref.setFdId(attachmentFdIds.get(index)); - ref.setFdFileName(resolveAttachmentName(specAttachments.get(index))); - attachments.add(ref); + String loginName = hcsProperties.getOperatorLoginName(); + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_CONTRACT, contractManage.getId(), specJson, loginName); + for (int index = 0; index < attachmentRefs.size(); index++) { + // 特批附件字段 key:与元数据行序一致 + attachmentRefs.get(index).setFdEntityKey(HcsSyncConstant.CONTRACT_ATTACH_KEY_SPECIAL); + attachmentRefs.get(index).setFdFileName(resolveAttachmentName(specAttachments.get(index))); } - return attachments; + return attachmentRefs; } /** @@ -327,9 +322,9 @@ public class HcsContractSyncSupport { /** * C103-1 合同发起,返回合规合同 fdContractId */ - private String draftContract(ContractManage contractManage, List attachmentFdIds) { - HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentFdIds); - log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个)", contractManage.getContractNo(), attachmentFdIds.size()); + private String draftContract(ContractManage contractManage, List attachmentRefs) { + HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentRefs); + log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个)", contractManage.getContractNo(), attachmentRefs.size()); HcsResult result = hcsClient.contractDraft(param); return validateDraftResult(contractManage, result); } @@ -349,9 +344,9 @@ public class HcsContractSyncSupport { /** * 组装 C103-1/C103-11 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.2); - * attachmentFdIds 新建传合规附件ID 列表(必填,无附件传空列表),更新传 null 不挂载 + * attachmentRefs 新建传合规附件引用列表(fdId+fdEntityKey,必填,无附件传空列表),更新传 null 不挂载 */ - private HcsContractDraftDTO buildDraftParam(ContractManage contractManage, List attachmentFdIds) { + private HcsContractDraftDTO buildDraftParam(ContractManage contractManage, List attachmentRefs) { HcsContractDraftDTO param = new HcsContractDraftDTO(); // 【必填】合同ID:C103-11 更新顶层必填(传 C103-1 返回的 fdContractId),C103-1 新建不传(v4 新增) param.setFdId(contractManage.getHcsFdId()); @@ -359,24 +354,22 @@ public class HcsContractSyncSupport { param.setFdTemplateId(requireTemplateId(contractManage)); // 【必填】外部系统标识:fd_sources,配置 thirdParty.hcs.src-system(D14) param.setFdOuterSystem(hcsProperties.getSrcSystem()); - // 【必填】操作人登录名:合同承办人登录名(§9-U2) - param.setFdLoginName(requireLoginName(contractManage)); + // 【必填】操作人登录名:配置 thirdParty.hcs.operator-login-name(默认 srvadmin,§9-U2) + param.setFdLoginName(hcsProperties.getOperatorLoginName()); // 【必填】保存类型:submit(提交) param.setSaveType("submit"); HcsContractFieldJsonDTO fieldJson = new HcsContractFieldJsonDTO(); fieldJson.setXformFields(buildXformFields(contractManage)); // 相对方(签署方):甲方/乙方,编号按名称反查 CustomerArchive(§9-U6) fieldJson.setSigner(buildSigners(contractManage)); - // 【必填】附件数据:C103-1 必填(无附件传空列表),fdBindType=ADD;更新不挂载(传 null) - if (attachmentFdIds != null) { - fieldJson.setAttachment(attachmentFdIds.stream().map(fdId -> { - HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO(); - // 【必填】附件ID(C103-2 转传返回) - attachment.setFdId(fdId); + // 【必填】附件数据:C103-1 必填(无附件传空列表),fdEntityKey=特批 eopCdraftSpecial/用印 eopCdraftSign/其他 eopCdraftOther, + // fdBindType=ADD;更新不挂载(传 null) + if (attachmentRefs != null) { + fieldJson.setAttachment(new ArrayList<>(attachmentRefs)); + for (HcsAttachmentRefDTO attachment : fieldJson.getAttachment()) { // 【必填】操作类型:ADD 新增 attachment.setFdBindType("ADD"); - return attachment; - }).toList()); + } } param.setFdFieldJson(fieldJson); return param; @@ -401,12 +394,17 @@ public class HcsContractSyncSupport { // 币种: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()))); - // 合同结算类型:settlement-type-mapping[settlementMode](必填:payment/payment_collection/collection/none,映射到"付款/收款"待联调,§9-U3,未配置不传) - String settlementType = hcsProperties.getContract().getSettlementTypeMapping().get(contractManage.getSettlementMode()); + // 合同结算类型:按合同类别固定规则——承运商合同=付款,客户合同=收款(2026-10-12 确认) + 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合同结算类型映射缺失:settlementMode=[{}] 不在 contract.settlement-type-mapping,fdSettlementType 不传(§9-U3)", contractManage.getSettlementMode()); + log.warn("HCS合同结算类型无法确定:contractCategory=[{}] 非承运商合同/客户合同,fdSettlementType 不传", contractManage.getContractCategory()); } // 特批流程:fdSepcailContract 固定 false(原文拼写笔误必须沿用,D15,配置项预留) fields.add(field(HcsContractFieldConstant.FIELD_FD_SEPCAIL_CONTRACT, "特批流程", hcsProperties.getContract().isSpecialContractEnabled() ? "true" : "false")); @@ -475,16 +473,18 @@ public class HcsContractSyncSupport { } /** - * 附件转传(可配开关):合同主文件 approved=true + 其它附件,返回合规附件ID 列表(挂载 C103-1 fdFieldJson.attachment) + * 附件转传(可配开关):合同主文件 approved=true(挂 eopCdraftSign)+ 其它附件(挂 eopCdraftOther), + * 返回合规附件引用列表(挂载 C103-1 fdFieldJson.attachment) */ - private List transferAttachmentsIfEnabled(ContractManage contractManage) { + private List transferAttachmentsIfEnabled(ContractManage contractManage) { if (!hcsProperties.getAttachment().isTransferEnabled()) { return List.of(); } - List attachmentFdIds = hcsAttachmentSupport.transferContractAttachments( - contractManage.getContractFileJson(), contractManage.getAttachmentsJson(), requireLoginName(contractManage)); - log.info("HCS合同附件转传完成:contractNo={},共 {} 个附件(随 C103-1 挂载)", contractManage.getContractNo(), attachmentFdIds.size()); - return attachmentFdIds; + List attachmentRefs = hcsAttachmentSupport.transferContractAttachments( + HcsSyncConstant.BIZ_TYPE_CONTRACT, contractManage.getId(), + contractManage.getContractFileJson(), contractManage.getAttachmentsJson(), hcsProperties.getOperatorLoginName()); + log.info("HCS合同附件转传完成:contractNo={},共 {} 个附件(随 C103-1 挂载)", contractManage.getContractNo(), attachmentRefs.size()); + return attachmentRefs; } /** @@ -523,6 +523,7 @@ public class HcsContractSyncSupport { } catch (Exception exception) { String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage(); markFailed(contractManage, message); + log.error("HCS合同推送失败:{}", message, exception); throw new ServiceException("HCS合同推送失败:" + message); } } @@ -535,7 +536,7 @@ public class HcsContractSyncSupport { contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED); contractManage.setHcsRetryCount(0); contractManage.setHcsSyncTime(LocalDateTime.now()); - contractManage.setHcsSyncMsg(message); + contractManage.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); contractManageMapper.updateById(contractManage); log.info("HCS合同推送成功:contractNo={},fdContractId={},{}", contractManage.getContractNo(), fdId, message); } @@ -547,7 +548,7 @@ public class HcsContractSyncSupport { contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED); contractManage.setHcsRetryCount(contractManage.getHcsRetryCount() == null ? 1 : contractManage.getHcsRetryCount() + 1); contractManage.setHcsSyncTime(LocalDateTime.now()); - contractManage.setHcsSyncMsg(message); + contractManage.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); contractManageMapper.updateById(contractManage); log.error("HCS合同推送失败:contractNo={},retryCount={},{}", contractManage.getContractNo(), contractManage.getHcsRetryCount(), message); } @@ -565,20 +566,6 @@ public class HcsContractSyncSupport { return templateId; } - /** - * 合同承办人登录名(fdLoginName 必填) - */ - private String requireLoginName(ContractManage contractManage) { - if (contractManage.getHandlerUserId() == null) { - throw new HcsException("HCS合同推送失败,合同承办人未维护(contractNo=" + contractManage.getContractNo() + ")"); - } - User user = UserCache.getUser(contractManage.getHandlerUserId()); - if (user == null || StringUtil.isBlank(user.getAccount())) { - throw new HcsException("HCS合同推送失败,承办人登录名解析不到(handlerUserId=" + contractManage.getHandlerUserId() + ",§9-U2)"); - } - return user.getAccount(); - } - /** * 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java index 59466cf8..9a7fef20 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java @@ -32,6 +32,7 @@ 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; @@ -181,7 +182,10 @@ public class HcsInvoiceRegSyncSupport { param.setFdThirdCreatorDeptCode(resolveDeptCode(application)); // 附件ID列表:attachmentsJson 经 C103-2 转传 if (hcsProperties.getAttachment().isTransferEnabled() && StringUtil.isNotBlank(application.getAttachmentsJson())) { - param.setFdAttIds(hcsAttachmentSupport.transferAttachments(application.getAttachmentsJson(), resolveCreatorCode(application))); + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, application.getId(), + application.getAttachmentsJson(), resolveCreatorCode(application)); + param.setFdAttIds(attachmentRefs.stream().map(HcsAttachmentRefDTO::getFdId).toList()); } log.info("HCS开票登记新增开始:applicationNo={}", application.getApplicationNo()); HcsResult result = hcsClient.invoiceRegistrationAdd(param); @@ -298,7 +302,7 @@ public class HcsInvoiceRegSyncSupport { application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED); application.setHcsRetryCount(0); application.setHcsSyncTime(LocalDateTime.now()); - application.setHcsSyncMsg(message); + application.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); invoiceApplicationMapper.updateById(application); log.info("HCS开票登记推送成功:applicationNo={},fdId={},{}", application.getApplicationNo(), fdId, message); } @@ -310,7 +314,7 @@ public class HcsInvoiceRegSyncSupport { application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED); application.setHcsRetryCount(application.getHcsRetryCount() == null ? 1 : application.getHcsRetryCount() + 1); application.setHcsSyncTime(LocalDateTime.now()); - application.setHcsSyncMsg(message); + application.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); invoiceApplicationMapper.updateById(application); log.error("HCS开票登记推送失败:applicationNo={},retryCount={},{}", application.getApplicationNo(), application.getHcsRetryCount(), message); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java index 740cfbc7..149c8b0f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java @@ -40,8 +40,7 @@ import org.springblade.hcs.dto.HcsFormSaveVO; import org.springblade.hcs.dto.HcsUserRefVO; import org.springblade.hcs.exception.HcsException; import org.springblade.hcs.feign.IHcsClient; -import org.springblade.system.cache.UserCache; -import org.springblade.system.pojo.entity.User; +import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.HcsContractExeStageMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; @@ -189,18 +188,16 @@ public class HcsPerfRecordSyncSupport { if (!hcsProperties.getAttachment().isTransferEnabled() || StringUtil.isBlank(payment.getAttachmentsJson())) { return null; } - List attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName); - if (attachmentFdIds.isEmpty()) { + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_PAYMENT, payment.getId(), payment.getAttachmentsJson(), operatorLoginName); + if (attachmentRefs.isEmpty()) { return null; } HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); - mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> { - HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO(); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { // 附件ID(C103-2 转传返回) - attachment.setFdId(fdId); - // 履约记录附件字段标识 - attachment.setFdEntityKey(PERF_ATTACH_ENTITY_KEY); - return attachment; + ref.setFdEntityKey(PERF_ATTACH_ENTITY_KEY); + return ref; }).toList()); return mechanisms; } @@ -220,17 +217,10 @@ public class HcsPerfRecordSyncSupport { } /** - * 操作人登录名(付款申请创建人,§9-U2) + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) */ private String resolveLoginName(PaymentApplication payment) { - if (payment.getCreateUser() == null) { - throw new HcsException("C108 履约记录推送失败,付款申请创建人未维护(paymentNo=" + payment.getPaymentNo() + ")"); - } - User user = UserCache.getUser(payment.getCreateUser()); - if (user == null || StringUtil.isBlank(user.getAccount())) { - throw new HcsException("C108 履约记录推送失败,创建人登录名解析不到(createUser=" + payment.getCreateUser() + ")"); - } - return user.getAccount(); + return hcsProperties.getOperatorLoginName(); } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java index af7cdf54..525cc3cd 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptBillSyncSupport.java @@ -40,8 +40,6 @@ import org.springblade.hcs.dto.HcsReceiptBillSaveDTO; import org.springblade.hcs.dto.HcsResult; import org.springblade.hcs.exception.HcsException; import org.springblade.hcs.feign.IHcsClient; -import org.springblade.system.cache.UserCache; -import org.springblade.system.pojo.entity.User; import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.FormalSettlementMapper; @@ -186,18 +184,17 @@ public class HcsReceiptBillSyncSupport { param.setFormData(formData); // 附件挂载(发票扫描件):fdEntityKey=fd_attach,发起带 fdBindType=ADD,更新不带 if (hcsProperties.getAttachment().isTransferEnabled() && StringUtil.isNotBlank(invoiceReceipt.getAttachmentsJson())) { - List attachmentFdIds = hcsAttachmentSupport.transferAttachments(invoiceReceipt.getAttachmentsJson(), operatorLoginName); - if (!attachmentFdIds.isEmpty()) { + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, invoiceReceipt.getId(), + invoiceReceipt.getAttachmentsJson(), operatorLoginName); + if (!attachmentRefs.isEmpty()) { HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); - mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> { - HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO(); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { // 附件ID(C103-2 转传返回) - attachment.setFdId(fdId); - // 收票单附件字段标识 - attachment.setFdEntityKey(RECEIPT_ATTACH_ENTITY_KEY); + ref.setFdEntityKey(RECEIPT_ATTACH_ENTITY_KEY); // 绑定类型:仅发起传 ADD - attachment.setFdBindType(add ? "ADD" : null); - return attachment; + ref.setFdBindType(add ? "ADD" : null); + return ref; }).toList()); param.setMechanisms(mechanisms); } @@ -258,7 +255,7 @@ public class HcsReceiptBillSyncSupport { invoiceReceipt.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED); invoiceReceipt.setHcsRetryCount(0); invoiceReceipt.setHcsSyncTime(LocalDateTime.now()); - invoiceReceipt.setHcsSyncMsg(message); + invoiceReceipt.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); invoiceReceiptMapper.updateById(invoiceReceipt); log.info("HCS收票单推送成功:invoiceNo={},{}", invoiceReceipt.getInvoiceNo(), message); } @@ -270,23 +267,16 @@ public class HcsReceiptBillSyncSupport { invoiceReceipt.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED); invoiceReceipt.setHcsRetryCount(invoiceReceipt.getHcsRetryCount() == null ? 1 : invoiceReceipt.getHcsRetryCount() + 1); invoiceReceipt.setHcsSyncTime(LocalDateTime.now()); - invoiceReceipt.setHcsSyncMsg(message); + invoiceReceipt.setHcsSyncMsg(HcsSyncConstant.truncateMsg(message)); invoiceReceiptMapper.updateById(invoiceReceipt); log.error("HCS收票单推送失败:invoiceNo={},retryCount={},{}", invoiceReceipt.getInvoiceNo(), invoiceReceipt.getHcsRetryCount(), message); } /** - * 操作人登录名(收票单创建人,§9-U2) + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) */ private String resolveLoginName(InvoiceReceipt invoiceReceipt) { - if (invoiceReceipt.getCreateUser() == null) { - throw new HcsException("C109 收票单推送失败,收票单创建人未维护(invoiceNo=" + invoiceReceipt.getInvoiceNo() + ")"); - } - User user = UserCache.getUser(invoiceReceipt.getCreateUser()); - if (user == null || StringUtil.isBlank(user.getAccount())) { - throw new HcsException("C109 收票单推送失败,创建人登录名解析不到(createUser=" + invoiceReceipt.getCreateUser() + ")"); - } - return user.getAccount(); + return hcsProperties.getOperatorLoginName(); } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java index 40ea1383..09e8d863 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java @@ -38,9 +38,7 @@ import org.springblade.hcs.dto.HcsSpecPayPropsDTO; import org.springblade.hcs.dto.HcsSpecPaySaveDTO; import org.springblade.hcs.exception.HcsException; import org.springblade.hcs.feign.IHcsClient; -import org.springblade.system.cache.UserCache; import org.springblade.transport.constant.HcsSyncConstant; -import org.springblade.system.pojo.entity.User; import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PaymentApplication; @@ -172,21 +170,19 @@ public class HcsSpecPaySyncSupport { if (!hcsProperties.getAttachment().isTransferEnabled() || StringUtil.isBlank(payment.getAttachmentsJson())) { return null; } - List attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName); - if (attachmentFdIds.isEmpty()) { + List attachmentRefs = hcsAttachmentSupport.transferAttachments( + HcsSyncConstant.BIZ_TYPE_PAYMENT, payment.getId(), payment.getAttachmentsJson(), operatorLoginName); + if (attachmentRefs.isEmpty()) { return null; } HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); boolean add = StringUtil.isBlank(payment.getHcsSpecFdId()); - mechanisms.setAttachment(attachmentFdIds.stream().map(fdId -> { - HcsAttachmentRefDTO attachment = new HcsAttachmentRefDTO(); + mechanisms.setAttachment(attachmentRefs.stream().map(ref -> { // 附件ID(C103-2 转传返回) - attachment.setFdId(fdId); - // 特批附件字段标识 - attachment.setFdEntityKey(SPEC_ATTACH_ENTITY_KEY); + ref.setFdEntityKey(SPEC_ATTACH_ENTITY_KEY); // 绑定类型:仅发起传 ADD - attachment.setFdBindType(add ? "ADD" : null); - return attachment; + ref.setFdBindType(add ? "ADD" : null); + return ref; }).toList()); return mechanisms; } @@ -214,16 +210,9 @@ public class HcsSpecPaySyncSupport { } /** - * 操作人登录名(付款申请创建人,§9-U2) + * 操作人登录名(thirdParty.hcs.operator-login-name,默认 srvadmin,§9-U2) */ private String resolveLoginName(PaymentApplication payment) { - if (payment.getCreateUser() == null) { - throw new HcsException("C106 特批推送失败,付款申请创建人未维护(paymentNo=" + payment.getPaymentNo() + ")"); - } - User user = UserCache.getUser(payment.getCreateUser()); - if (user == null || StringUtil.isBlank(user.getAccount())) { - throw new HcsException("C106 特批推送失败,创建人登录名解析不到(createUser=" + payment.getCreateUser() + ")"); - } - return user.getAccount(); + return hcsProperties.getOperatorLoginName(); } } diff --git a/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md b/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md index a37ddc36..60225e94 100644 --- a/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md +++ b/doc/docs/hcs合同/2026-09-27-桂物合同合规系统HCS接入技术设计.md @@ -82,7 +82,7 @@ org.springblade.transport │ ├── HcsSpecPaySyncSupport.java # 特批 C106-3/4 │ ├── HcsInvoiceRegSyncSupport.java # 开票登记 C111 │ ├── HcsReceiptBillSyncSupport.java # 收票单 C109 -│ └── HcsAttachmentSupport.java # 附件转传 + TMS→HCS 附件ID映射缓存 +│ └── HcsAttachmentSupport.java # 附件转传 + TMS→HCS 附件ID映射同步表(blade_hcs_attachment_sync) ├── task/HcsPushTask.java # @Scheduled 扫描首推/补偿(照 PaymentKingdeeResultTask 模式) ├── controller/HcsSyncController.java # 手动推送/重推、校验重放、对账查询端点 └── entity/HcsContractExeStage.java # 履约规则实体(blade-transport-api) @@ -338,7 +338,7 @@ flowchart LR 1. 输入:单据 `attachmentsJson`/`contractFileJson`(JSON 元素含 url/fileType/approved 等,去重键照 `attachmentKey` 惯例 url|link|fileUrl|downloadUrl|uid|originalName)。 2. 下载:按元数据 URL 从文件服务拉取流(文件服务基址可配 `attachment.file-download-base`)。 3. 上传:C103-2 multipart(fdFile、fdAnonymous=false、fdLoginName=操作人)→ 附件 `fdId`。 -4. 映射缓存:Redis `hcs:att:{attachmentKey}` → 合规 fdId(TTL 7d),未命中重传(重复上传仅浪费存储,无业务风险,故不做持久化映射表)。 +4. 映射同步表:`blade_hcs_attachment_sync`(biz_type+biz_id+attachment_key 唯一,照金蝶附件同步表设计)落业务单据 → 合规 fdId 映射,转传前查命中即复用;仅落成功记录(失败整单重试,原因落单据 hcs_sync_msg)。 5. 任一附件转传失败 → 整单推送失败进补偿重试。 ---