From 80e56d01c9dd39f3f7d498d5a10044fb1efba1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Tue, 27 Oct 2026 15:12:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E7=89=B9=E6=89=B9?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E9=99=84=E4=BB=B6=E8=90=BD=E5=BA=93=E5=B9=B6?= =?UTF-8?q?=E7=BB=8FC103-5=E4=B8=8B=E8=BD=BD=E8=81=94=E5=B8=A6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E9=87=91=E8=9D=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit paymentLegal/result 回调 fileUrl(合规附件ID)落库 hcs_spec_attach_fd_id;金蝶附件联带上传新增合规附件来源(C103-5 按 fdId 下载,幂等键=fdId,20MB 封顶);付款申请详情动态追加特批附件展示行走 C103-5 代理下载;同步提交同链路配套修正:paymentLegal/result 回调改按合规特批流程ID(hcs_spec_fd_id)定位付款申请、C106-3 特批发起报文补充 fd_contract_plan 款项日期(履约计划行合规阶段ID);含 blade_payment_application 增量脚本 --- .../pojo/entity/PaymentApplication.java | 2 + .../transport/constant/HcsSyncConstant.java | 5 +++ .../controller/HcsSyncController.java | 2 +- .../impl/PaymentApplicationServiceImpl.java | 42 ++++++++++++++++- .../support/KingdeeAttachmentSyncSupport.java | 45 ++++++++++++++++--- .../support/hcs/HcsPaymentSyncSupport.java | 19 +++++--- .../support/hcs/HcsSpecPaySyncSupport.java | 17 +++++++ .../hcs/dto/HcsLegalResultDTO.java | 6 ++- .../hcs/dto/HcsSpecPayPropsDTO.java | 5 +++ ...ade_payment_spec_attach_fd_id_20261027.sql | 11 +++++ 10 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java index e4021397..b5b00417 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java @@ -141,6 +141,8 @@ public class PaymentApplication extends TenantEntity { private String hcsSpecFdId; /** 特批状态:none/need/pushing/processing/approved/rejected */ private String hcsSpecStatus; + /** 合规付款特批结果附件ID(paymentLegal/result 回调 fileUrl,C103-5 下载入参、金蝶附件联带用) */ + private String hcsSpecAttachFdId; /** 履约记录ID(C108-1 返回,C108-2 更新必传) */ private String hcsPerfFdId; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java index 42eaaf09..5585ea46 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 @@ -190,6 +190,11 @@ public final class HcsSyncConstant { */ public static final String PAYMENT_ATTACH_TYPE_SPECIAL = "special_approval"; + /** + * 付款申请附件类型值:合规特批结果附件(paymentLegal/result 回传展示行标记,详情展示用,不参与特批附件提交校验) + */ + public static final String PAYMENT_ATTACH_TYPE_SPEC_RESULT = "spec_result"; + /** * 附件类型:用印附件(双章归档文件,取值沿用 ContractManageServiceImpl#FILE_TYPE_SEAL_ARCHIVE) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java index e192d772..2526f7dc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java @@ -307,7 +307,7 @@ public class HcsSyncController { *

* 免登免授权同合同接口(blade.secure 放行); * iPaaS 网关注册映射:/gwzh/HTHG/paymentLegal/result → /hcs/paymentLegal/result; - * 报文 {sourceSystem, requestId=TMS付款申请ID, status=APPROVED/REJECTED, reason}, + * 报文 {sourceSystem, requestId=合规特批流程ID(hcs_spec_fd_id), status=APPROVED/REJECTED, reason}, * approved 恢复"审批通过"放行,rejected 联动付款审批驳回走重新提交闭环。 */ @PostMapping("/paymentLegal/result") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 98777d55..4f957049 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -116,6 +116,8 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -261,6 +263,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationInvoice::getPaymentApplicationId, id).orderByAsc(PaymentApplicationInvoice::getLineNo))); @@ -585,6 +589,34 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl rows = Func.isBlank(vo.getAttachmentsJson()) + ? new ArrayList<>() : new ArrayList<>(JsonUtil.parse(vo.getAttachmentsJson(), List.class)); + rows.removeIf(row -> row instanceof Map map + && HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType"))); + Map specRow = new LinkedHashMap<>(); + specRow.put("name", "付款特批附件"); + specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT); + specRow.put("url", "/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId() + + "&name=" + URLEncoder.encode("付款特批附件", StandardCharsets.UTF_8)); + rows.add(specRow); + vo.setAttachmentsJson(JsonUtil.toJson(rows)); + } catch (Exception exception) { + log.warn("付款申请[{}]特批结果附件展示行追加失败", vo.getId(), exception); + } + } + /** * 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用, * 以本地 kingdeeBillId(星瀚单据内码)定位金蝶单据;本地缺失(存量数据)跳过待重推补传; @@ -599,8 +631,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl items = new ArrayList<>( + kingdeeAttachmentSyncSupport.parseUserAttachments(entity.getAttachmentsJson())); + // 合规付款特批结果附件(paymentLegal/result 回传):C103-5 按合规附件ID 下载后联带上传金蝶 + if (Func.isNotBlank(entity.getHcsSpecAttachFdId())) { + items.add(KingdeeAttachmentSyncSupport.KingdeeAttachmentItem.hcsAttachment("付款特批附件", entity.getHcsSpecAttachFdId())); + } + kingdeeAttachmentSyncSupport.syncAttachments(KingdeeAttachmentConstant.BIZ_PAY_APPLICATION, + id, entity.getKingdeeBillId(), items); } catch (Exception exception) { log.error("付款申请[{}]金蝶附件联带上传异常(不影响主单同步状态,重推自动补传)", id, exception); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java index dd20ddde..ed9a38f4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java @@ -37,6 +37,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.transport.constant.KingdeeAttachmentConstant; import org.springblade.transport.mapper.KingdeeAttachmentSyncMapper; import org.springblade.transport.pojo.entity.KingdeeAttachmentSync; +import org.springblade.transport.support.hcs.HcsAttachmentSupport; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; @@ -64,9 +65,10 @@ import java.util.stream.Collectors; * 2. 附件失败永不回滚、不污染主单同步状态(逐附件独立 try/catch); * 3. 同 (biz_type, biz_id, attachment_key) 最多一条成功记录(唯一键 + 成功集合幂等跳过); * 4. 不自动重试,重推主单时自动补传;5. 单文件软限制 20MB,超限/下载失败记原因跳过。

- *

附件来源两类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url)与 + *

附件来源三类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url,仅接受 http(s) 绝对地址)、 * 票池镜像发票文件(attachmentsJson 对象 {source,path,name},经 A801-2 下载回传,幂等键=path, - * path 变化(红冲重开)视为新文件自动触发重传)。

+ * path 变化(红冲重开)视为新文件自动触发重传)与 + * HCS 合规附件(付款特批结果附件等,按合规附件ID 经 C103-5 下载,幂等键=fdId)。

* * @author Chill */ @@ -80,6 +82,7 @@ public class KingdeeAttachmentSyncSupport { private final IBfiService bfiService; private final KingdeeAttachmentSyncMapper attachmentSyncMapper; + private final HcsAttachmentSupport hcsAttachmentSupport; /** 附件下载客户端(无状态,进程内复用) */ private final HttpClient downloadClient = HttpClient.newBuilder() @@ -91,11 +94,30 @@ public class KingdeeAttachmentSyncSupport { * 待同步附件项 * * @param name 附件名称(含扩展名,展示/金蝶侧文件名) - * @param key 幂等键:用户附件=附件 url;发票文件=A801-2 的 path + * @param key 幂等键:用户附件=附件 url;发票文件=A801-2 的 path;合规附件=合规附件ID fdId * @param size 附件大小(字节,可空:空时下载后按实际字节校验) * @param kingdeePathSource true 表示 key 为星瀚侧文件 path(经 A801-2 下载);false 表示 http(s) 附件 url + * @param hcsFdIdSource true 表示 key 为合规附件ID fdId(经 C103-5 下载) */ - public record KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource) { + public record KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource, boolean hcsFdIdSource) { + + /** + * 兼容既有四参构造(默认非合规附件来源) + */ + public KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource) { + this(name, key, size, kingdeePathSource, false); + } + + /** + * HCS 合规附件项(C103-5 按合规附件ID 下载) + * + * @param name 附件名称 + * @param fdId 合规附件ID + * @return 附件项 + */ + public static KingdeeAttachmentItem hcsAttachment(String name, String fdId) { + return new KingdeeAttachmentItem(name, fdId, null, false, true); + } } /** @@ -225,7 +247,8 @@ public class KingdeeAttachmentSyncSupport { continue; } String url = firstText(map, "url", "link", "fileUrl", "downloadUrl", "domain", "src"); - if (Func.isBlank(url)) { + // 仅接受 http(s) 绝对地址:相对/代理路径(如合规特批附件展示行)无法直下,跳过防下载必败 + if (Func.isBlank(url) || !(url.startsWith("http://") || url.startsWith("https://"))) { continue; } String name = firstText(map, "originalName", "name", "fileName"); @@ -362,10 +385,18 @@ public class KingdeeAttachmentSyncSupport { } /** - * 拉取附件内容:星瀚侧文件经 A801-2 下载(流式读取,防大文件全量进内存失控); - * 用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验 + * 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控); + * 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验 */ private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception { + if (item.hcsFdIdSource()) { + Response response = hcsAttachmentSupport.downloadRaw(item.key()); + try (InputStream inputStream = response.body().asInputStream()) { + return readCapped(inputStream); + } finally { + response.close(); + } + } if (item.kingdeePathSource()) { Response response = bfiService.downloadInvoice(item.key()); try (InputStream inputStream = response.body().asInputStream()) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java index 46059777..3bb56b68 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java @@ -26,6 +26,7 @@ package org.springblade.transport.support.hcs; import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; @@ -219,7 +220,7 @@ public class HcsPaymentSyncSupport { /** * 付款特批结果接收处理(合规 paymentLegal/result 回调通知特批审批结果): * sourceSystem 须与 third-party.hcs.src-system 配置一致(不符抛出"来源系统标识错误"异常), - * requestId=TMS 付款申请ID、status=APPROVED/REJECTED; + * requestId=合规特批流程ID(hcs_spec_fd_id,C106-3 回存值)、status=APPROVED/REJECTED; * 特批结果联动付款审批状态:approved 恢复"审批通过"放行(后续金蝶推送放行), * rejected 联动付款审批驳回(returned),走"重新编辑→提交→审批→重新特批"闭环 * @@ -250,6 +251,11 @@ public class HcsPaymentSyncSupport { } syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)); payment.setHcsSpecStatus(status); + // 特批通过回传特批结果附件(fileUrl 值=合规附件ID):落库供 C103-5 下载与金蝶附件联带; + // 回调无附件时保留旧值(shortcut: 驳回重批期间旧附件仍展示,重批回调会覆盖,金蝶侧特批中门禁不会误传) + if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status) && StringUtil.isNotBlank(param.getFileUrl())) { + payment.setHcsSpecAttachFdId(param.getFileUrl().trim()); + } paymentApplicationMapper.updateById(payment); // 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推 if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)) { @@ -288,17 +294,16 @@ public class HcsPaymentSyncSupport { } /** - * 按 TMS 付款申请ID 定位付款申请(合规推送 requestId) + * 按合规特批流程ID定位付款申请(合规推送 requestId=hcs_spec_fd_id,即 C106-3 返回并回存的流程 fdId; + * 与合同链路 locateContractByFdId 同口径,C106-3 发起报文未传 TMS 付款申请ID,合规无法回传数字ID) */ private PaymentApplication locatePayment(String requestId) { if (StringUtil.isBlank(requestId)) { return null; } - try { - return paymentApplicationMapper.selectById(Long.valueOf(requestId.trim())); - } catch (NumberFormatException exception) { - return null; - } + return paymentApplicationMapper.selectOne(Wrappers.lambdaQuery() + .eq(PaymentApplication::getHcsSpecFdId, requestId.trim()) + .last("limit 1")); } /** 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 6f4de144..eb258a17 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 @@ -42,7 +42,9 @@ 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.math.BigDecimal; @@ -74,6 +76,7 @@ public class HcsSpecPaySyncSupport { private final HcsProperties hcsProperties; private final ContractManageMapper contractManageMapper; private final PaymentApplicationMapper paymentApplicationMapper; + private final IContractExeStageService contractExeStageService; private final HcsAttachmentSupport hcsAttachmentSupport; /** @@ -156,6 +159,8 @@ public class HcsSpecPaySyncSupport { props.setFdDocStatus(hcsProperties.getSpecPay().getDocStatus()); // 特批附件字段标识:固定 fd_spc_approval_attach props.setFdSpcApprovalAttach(SPEC_ATTACH_ENTITY_KEY); + // 【必填】款项日期:传履约计划id(匹配付款计划行合规阶段ID,无关联不传,口径同 C108 履约记录) + props.setFdContractPlan(this.resolveStageFdId(payment)); formData.setDynamicProps(props); // 【必填】表单层单据主题 formData.setFdDocSubject("结算付款特批-" + payment.getPaymentNo()); @@ -168,6 +173,18 @@ public class HcsSpecPaySyncSupport { return param; } + /** + * 匹配合同的付款履约计划行合规阶段ID(无匹配返回 null,DTO NON_NULL 序列化时不传,与 C108 履约记录口径一致) + */ + private String resolveStageFdId(PaymentApplication payment) { + if (payment.getContractId() == null) { + return null; + } + HcsContractExeStage stageRow = this.contractExeStageService.findPaymentStageRow(payment.getContractId(), + HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType())); + return stageRow == null ? null : stageRow.getHcsStageFdId(); + } + /** * 特批附件挂载(附件转传开关关闭或无附件时返回 null) */ diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java index b74880d9..1cb8bb4f 100644 --- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java @@ -51,7 +51,7 @@ public class HcsLegalResultDTO implements Serializable { */ private String sourceSystem; /** - * 定位单据ID(付款特批=TMS 付款申请ID;合同特批=合规合同ID,与 id 同值,按 blade_contract_manage.hcs_fd_id 匹配) + * 定位单据ID(付款特批=合规特批流程ID(hcs_spec_fd_id,C106-3 回存值);合同特批=合规合同ID,与 id 同值,按 blade_contract_manage.hcs_fd_id 匹配) */ private String requestId; /** @@ -66,4 +66,8 @@ public class HcsLegalResultDTO implements Serializable { * 结果说明(驳回原因等,非必传) */ private String reason; + /** + * 特批结果附件(付款特批回传携带,值为合规附件ID,C103-5 下载入参,非必传) + */ + private String fileUrl; } diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java index d2ddbce8..eb9d3fc1 100644 --- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java @@ -145,4 +145,9 @@ public class HcsSpecPayPropsDTO implements Serializable { */ @JsonProperty("fd_spc_approval_attach") private String fdSpcApprovalAttach; + /** + * 款项日期(传履约计划id,接口必填;无关联计划行不传,与 C108 履约记录口径一致) + */ + @JsonProperty("fd_contract_plan") + private String fdContractPlan; } diff --git a/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql b/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql new file mode 100644 index 00000000..4cb562eb --- /dev/null +++ b/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql @@ -0,0 +1,11 @@ +-- 付款特批结果回传附件(paymentLegal/result 回调 fileUrl=合规附件ID): +-- blade_payment_application 追加合规特批结果附件ID 字段,供 C103-5 下载与金蝶附件联带上传。 +-- 通过 information_schema 判断字段是否存在,可重复执行。 +SET @db_name = DATABASE(); + +SET @sql = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_payment_application' AND column_name = 'hcs_spec_attach_fd_id'), + 'SELECT 1', + 'ALTER TABLE blade_payment_application ADD COLUMN hcs_spec_attach_fd_id varchar(128) DEFAULT NULL COMMENT ''合规付款特批结果附件ID(paymentLegal/result 回调 fileUrl,C103-5 下载入参)''' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;