:feat: 合同附件挂载按 eopCdraft 枚举分类(fdEntityKey)

- HcsSyncConstant 新增 CONTRACT_ATTACH_KEY_SPECIAL/SEAL/OTHER(eopCdraftSpecial/eopCdraftSign/eopCdraftOther)
- 转传接口返回 HcsAttachmentRefDTO(fdId+fdEntityKey+fdFileName),替代纯 fdId 列表
- 合同链路:主文件 approved=true 挂 eopCdraftSign、其它附件挂 eopCdraftOther、特批说明挂 eopCdraftSpecial
- buildDraftParam 直接消费引用列表统一补 fdBindType=ADD;特批付款/履约记录/收票单/开票登记四个挂载点改为在返回引用上补各自字段,行为不变
This commit is contained in:
刘泉佳
2026-10-12 16:52:39 +08:00
parent 9e4b8a6d58
commit 63c2f7ed1e
8 changed files with 192 additions and 169 deletions
@@ -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() {
}
}
@@ -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;
* <p>
* 流程:解析单据附件 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)。
* <p>
* 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<String> transferAttachments(String attachmentsJson, String operatorLoginName) {
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<String> attachmentFdIds = new ArrayList<>();
List<HcsAttachmentRefDTO> 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<String> transferContractAttachments(String contractFileJson, String attachmentsJson, String operatorLoginName) {
List<Map<?, ?>> attachments = new ArrayList<>();
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)) {
attachments.add(file);
attachmentRefs.add(this.transferOne(bizType, bizId, file, operatorLoginName,
HcsSyncConstant.CONTRACT_ATTACH_KEY_SEAL));
}
}
attachments.addAll(parseJsonArray(attachmentsJson));
List<String> 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.<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)) {
@@ -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;
}
/**
@@ -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<String> attachmentFdIds = transferAttachmentsIfEnabled(contractManage);
// ② 附件转传(v4 已明确挂载点:C103-1 fdFieldJson.attachment,转传取得合规附件引用后随发起报文挂载)
List<HcsAttachmentRefDTO> 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<HcsAttachmentRefDTO> 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<HcsAttachmentRefDTO> transferSpecExplanationAttachments(ContractManage contractManage) {
List<Map<?, ?>> specAttachments = new ArrayList<>();
@@ -227,17 +224,15 @@ public class HcsContractSyncSupport {
return List.of();
}
String specJson = JsonUtil.toJson(specAttachments);
String loginName = requireLoginName(contractManage);
List<String> attachmentFdIds = hcsAttachmentSupport.transferAttachments(specJson, loginName);
List<HcsAttachmentRefDTO> 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<HcsAttachmentRefDTO> 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<String> attachmentFdIds) {
HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentFdIds);
log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个)", contractManage.getContractNo(), attachmentFdIds.size());
private String draftContract(ContractManage contractManage, List<HcsAttachmentRefDTO> attachmentRefs) {
HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentRefs);
log.info("HCS合同推送开始:{}(C103-1 发起,挂载附件 {} 个)", contractManage.getContractNo(), attachmentRefs.size());
HcsResult<HcsContractDraftVO> 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<String> attachmentFdIds) {
private HcsContractDraftDTO buildDraftParam(ContractManage contractManage, List<HcsAttachmentRefDTO> 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<String> transferAttachmentsIfEnabled(ContractManage contractManage) {
private List<HcsAttachmentRefDTO> transferAttachmentsIfEnabled(ContractManage contractManage) {
if (!hcsProperties.getAttachment().isTransferEnabled()) {
return List.of();
}
List<String> attachmentFdIds = hcsAttachmentSupport.transferContractAttachments(
contractManage.getContractFileJson(), contractManage.getAttachmentsJson(), requireLoginName(contractManage));
log.info("HCS合同附件转传完成:contractNo={},共 {} 个附件(随 C103-1 挂载)", contractManage.getContractNo(), attachmentFdIds.size());
return attachmentFdIds;
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;
}
/**
@@ -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)
*/
@@ -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<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);
@@ -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);
}
@@ -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<String> attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName);
if (attachmentFdIds.isEmpty()) {
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(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();
}
/**
@@ -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<String> attachmentFdIds = hcsAttachmentSupport.transferAttachments(invoiceReceipt.getAttachmentsJson(), operatorLoginName);
if (!attachmentFdIds.isEmpty()) {
List<HcsAttachmentRefDTO> 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();
}
/**
@@ -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<String> attachmentFdIds = hcsAttachmentSupport.transferAttachments(payment.getAttachmentsJson(), operatorLoginName);
if (attachmentFdIds.isEmpty()) {
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(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();
}
}
@@ -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. 任一附件转传失败 → 整单推送失败进补偿重试。
---