feat: 付款特批结果附件落库并经C103-5下载联带上传金蝶
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 增量脚本
This commit is contained in:
+2
@@ -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;
|
||||
}
|
||||
|
||||
+5
@@ -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)
|
||||
*/
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ public class HcsSyncController {
|
||||
* <p>
|
||||
* 免登免授权同合同接口(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")
|
||||
|
||||
+40
-2
@@ -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 BaseServiceImpl<PaymentApplic
|
||||
public PaymentApplicationVO detail(Long id) {
|
||||
PaymentApplication entity = existing(id);
|
||||
PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(entity);
|
||||
// 合规特批结果附件动态追加展示(不落 attachmentsJson 原文,下载经 C103-5 代理透传)
|
||||
this.appendSpecAttachDisplay(vo);
|
||||
fillKingdeeLockStatus(List.of(vo));
|
||||
vo.setInvoices(invoiceMapper.selectList(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
|
||||
.eq(PaymentApplicationInvoice::getPaymentApplicationId, id).orderByAsc(PaymentApplicationInvoice::getLineNo)));
|
||||
@@ -585,6 +589,34 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情附件追加合规特批结果附件展示行(不落 attachmentsJson 原文):
|
||||
* 前端通用附件列表直接渲染,下载经 /hcs/attachment/download(C103-5 代理)透传;
|
||||
* 先剔除前端随单保存的历史展示行,避免重批后重复
|
||||
*
|
||||
* @param vo 付款申请详情 VO
|
||||
*/
|
||||
private void appendSpecAttachDisplay(PaymentApplicationVO vo) {
|
||||
if (Func.isBlank(vo.getHcsSpecAttachFdId())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Object> 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<String, Object> 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<PaymentApplic
|
||||
log.warn("付款申请[{}]金蝶附件联带上传跳过:星瀚单据内码未落库,待重推补传", id);
|
||||
return;
|
||||
}
|
||||
kingdeeAttachmentSyncSupport.syncUserAttachments(KingdeeAttachmentConstant.BIZ_PAY_APPLICATION,
|
||||
id, entity.getKingdeeBillId(), entity.getAttachmentsJson());
|
||||
List<KingdeeAttachmentSyncSupport.KingdeeAttachmentItem> 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);
|
||||
}
|
||||
|
||||
+38
-7
@@ -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,超限/下载失败记原因跳过。</p>
|
||||
* <p>附件来源两类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url)与
|
||||
* <p>附件来源三类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url,仅接受 http(s) 绝对地址)、
|
||||
* 票池镜像发票文件(attachmentsJson 对象 {source,path,name},经 A801-2 下载回传,幂等键=path,
|
||||
* path 变化(红冲重开)视为新文件自动触发重传)。</p>
|
||||
* path 变化(红冲重开)视为新文件自动触发重传)与
|
||||
* HCS 合规附件(付款特批结果附件等,按合规附件ID 经 C103-5 下载,幂等键=fdId)。</p>
|
||||
*
|
||||
* @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()) {
|
||||
|
||||
+12
-7
@@ -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.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getHcsSpecFdId, requestId.trim())
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
@@ -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)
|
||||
*/
|
||||
|
||||
+5
-1
@@ -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;
|
||||
}
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user