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 959df8bf..45ab1f4c 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 @@ -110,9 +110,11 @@ public class PaymentApplication extends TenantEntity { private String currentNode; /** 当前处理人 */ private String currentProcessor; - /** 金蝶付款申请单id(A103-3 预付款保存 / A103-1 应付下推返回) */ + /** 金蝶单据号(A103-2 查询回填的星瀚单据编号 billno,如 FKSQ-xxxx) */ private String kingdeeBillNo; - /** 金蝶同步状态:unsynced 未生成 / synced 已生成 / failed 生成失败 */ + /** 金蝶单据id(A103-1/A103-3 推送返回的星瀚内部主键,附件上传等使用) */ + private String kingdeeBillId; + /** 金蝶同步状态:unsynced 未生成 / synced 已生成 / paying 付款中 / paid 已付款 / closed 已关闭 / failed 生成失败 */ private String kingdeeStatus; /** 附件信息JSON */ private String attachmentsJson; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/TransportApplication.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/TransportApplication.java index 27a73b5b..f5ece848 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/TransportApplication.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/TransportApplication.java @@ -27,12 +27,14 @@ package org.springblade.transport; import org.springblade.core.cloud.client.BladeCloudApplication; import org.springblade.core.launch.BladeApplication; +import org.springframework.scheduling.annotation.EnableScheduling; /** * 运输服务启动器 * * @author Chill */ +@EnableScheduling @BladeCloudApplication public class TransportApplication { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java index a626e7dd..4c147ec1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java @@ -66,13 +66,13 @@ public final class KingdeeBfiConstant { * A102-2 明细必填:物料编码。TMS 无物料档案,当前为占位空值, * 推送前会校验拦截。待确认:需金蝶侧提供运输服务类物料编码后补充。 */ - public static final String BFI_AP_BILL_MATERIAL = ""; + public static final String BFI_AP_BILL_MATERIAL = "GWZB01.003"; /** * A102-2 明细必填:计量单位编码。TMS 无对应档案,当前为占位空值, * 推送前会校验拦截。待确认:需金蝶侧确认编码(如"吨")后补充。 */ - public static final String BFI_AP_BILL_MEASURE_UNIT = ""; + public static final String BFI_AP_BILL_MEASURE_UNIT = "PCS"; /** A102-2 必填:外部单据类型(应付单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */ public static final String BFI_AP_BILL_SRC_TYPE = "TMS财务应付单"; @@ -105,6 +105,36 @@ public final class KingdeeBfiConstant { "其他付款申请", "ap_payapply_oth_BT_S" ); + /** 金蝶同步状态:未生成(新建单据默认)。 */ + public static final String KINGDEE_STATUS_UNSYNCED = "unsynced"; + + /** 金蝶同步状态:已生成(推送成功,等待付款)。 */ + public static final String KINGDEE_STATUS_SYNCED = "synced"; + + /** 金蝶同步状态:付款中(A103-2 paystatus = Inpayment 部分付款)。 */ + public static final String KINGDEE_STATUS_PAYING = "paying"; + + /** 金蝶同步状态:已付款(A103-2 paystatus = Alreadypay,终态)。 */ + public static final String KINGDEE_STATUS_PAID = "paid"; + + /** 金蝶同步状态:已关闭(A103-2 billstatus = D,终态)。 */ + public static final String KINGDEE_STATUS_CLOSED = "closed"; + + /** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */ + public static final String KINGDEE_STATUS_FAILED = "failed"; + + /** A103-2 付款状态:已付款。 */ + public static final String BFI_PAY_STATUS_ALREADY_PAY = "Alreadypay"; + + /** A103-2 付款状态:部分付款。 */ + public static final String BFI_PAY_STATUS_IN_PAYMENT = "Inpayment"; + + /** A103-2 付款状态:未付款。 */ + public static final String BFI_PAY_STATUS_UNPAID = "Unpaid"; + + /** A103-2 单据状态:已关闭。 */ + public static final String BFI_BILL_STATUS_CLOSED = "D"; + /** * 按中文名称从映射字典转换单据类型编码。 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index e2c6aafe..b81ac131 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -100,4 +100,8 @@ public class PaymentApplicationController extends BladeController { public R> syncKingdeeBatch(@RequestBody List ids) { return R.data(paymentApplicationService.syncKingdeeBatch(ids)); } + @PostMapping("/sync-kingdee-result") + @ApiOperationSupport(order = 12) + @Operation(summary = "手动回写金蝶付款结果(A103-2/A103-4)") + public R syncKingdeeResult() { return R.data(paymentApplicationService.syncKingdeePaymentResult()); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java index 26779eda..b27d5c28 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -21,6 +21,8 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; import org.springblade.transport.pojo.entity.PaymentApplication; @@ -42,4 +44,28 @@ public interface IPaymentApplicationService extends BaseService syncKingdeeBatch(List ids); + + /** + * 定时回写金蝶付款结果(A103-2 付款申请单查询 / A103-4 付款处理查询) + * + * @return 执行摘要 + */ + String syncKingdeePaymentResult(); + + /** + * 回写单笔付款申请的金蝶付款申请单查询结果(A103-2),单据级事务 + * + * @param entity 付款申请实体 + * @param payApply A103-2 查询返回的付款申请单 + * @return 是否付款完成(paystatus = Alreadypay) + */ + boolean applyPayApplyResult(PaymentApplication entity, BfiPayApplyVO payApply); + + /** + * 回填单笔付款申请的金蝶付款处理记录(A103-4),单据级事务 + * + * @param entity 付款申请实体 + * @param payBill A103-4 查询返回的付款处理单 + */ + void applyPayBillRecord(PaymentApplication entity, BfiPayBillVO payBill); } 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 e5f1ea80..b365bf72 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 @@ -30,6 +30,7 @@ import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.SpringUtil; import org.springblade.transport.constant.KingdeeBfiConstant; import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.SysCache; @@ -39,11 +40,17 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushEntryDTO; +import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveEntryDTO; +import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO; import org.springblade.thirdparty.bfi.service.IBfiService; +import org.springblade.thirdparty.bfi.util.BfiUtil; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; import org.springblade.transport.mapper.BillLedgerMapper; @@ -93,7 +100,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -108,6 +118,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl applications = list(Wrappers.lambdaQuery() + .eq(PaymentApplication::getApprovalStatus, APPROVED) + .in(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED, KingdeeBfiConstant.KINGDEE_STATUS_PAYING) + .orderByAsc(PaymentApplication::getId)); + IPaymentApplicationService self = SpringUtil.getBean(IPaymentApplicationService.class); + int updated = 0; + int failed = 0; + int paidCount = 0; + Map payApplyMap = new HashMap<>(); + if (!applications.isEmpty()) { + // ② A103-2 付款申请单查询:按 paymentNo(awrp_src_code)分批查询,同一来源单号取最近修改的一条 + List paymentNos = applications.stream() + .map(PaymentApplication::getPaymentNo).filter(Func::isNotEmpty).distinct().toList(); + for (int from = 0; from < paymentNos.size(); from += PAY_RESULT_QUERY_BATCH) { + List batch = paymentNos.subList(from, Math.min(from + PAY_RESULT_QUERY_BATCH, paymentNos.size())); + for (BfiPayApplyVO payApply : queryPayApplyPages(batch)) { + if (Func.isEmpty(payApply.getAwrpSrcCode())) { + continue; + } + BfiPayApplyVO existing = payApplyMap.get(payApply.getAwrpSrcCode()); + if (existing == null || strCompare(payApply.getModifytime(), existing.getModifytime()) > 0) { + payApplyMap.put(payApply.getAwrpSrcCode(), payApply); + } + } + } + for (PaymentApplication application : applications) { + BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); + if (payApply == null) { + continue; + } + try { + if (self.applyPayApplyResult(application, payApply)) paidCount++; + updated++; + } catch (Exception exception) { + failed++; + log.error("付款申请[{}]回写金蝶付款申请单结果异常", application.getPaymentNo(), exception); + } + } + } + // ③ A103-4 付款处理查询:付款中/已付款单据 + 已付款但尚无付款记录的单据(补偿),回填付款记录 + Map recordCandidates = new LinkedHashMap<>(); + for (PaymentApplication application : applications) { + BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); + if (payApply != null && !KingdeeBfiConstant.BFI_PAY_STATUS_UNPAID.equals(payApply.getPaystatus())) { + recordCandidates.put(application.getId(), application); + } + } + list(Wrappers.lambdaQuery() + .eq(PaymentApplication::getApprovalStatus, APPROVED) + .eq(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_PAID)) + .stream().filter(application -> realRecordCount(application.getId()) == 0) + .forEach(application -> recordCandidates.putIfAbsent(application.getId(), application)); + int recordWritten = 0; + int recordFailed = 0; + if (!recordCandidates.isEmpty()) { + Map> orgGroups = new LinkedHashMap<>(); + for (PaymentApplication application : recordCandidates.values()) { + BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); + String orgNumber = payApply == null ? null : payApply.getPayorgNumber(); + if (Func.isBlank(orgNumber)) { + try { + // A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同所属组织解析 + orgNumber = bfiOrgNumber(requireContract(application)); + } catch (Exception exception) { + log.warn("付款申请[{}]付款处理查询跳过,付款组织编码获取失败:{}", application.getPaymentNo(), exception.getMessage()); + continue; + } + } + orgGroups.computeIfAbsent(orgNumber, key -> new ArrayList<>()).add(application); + } + // 以付款处理单源单id/源单编号建立索引,与本地金蝶单据id/单据编号勾稽 + Map payBillBySourceId = new HashMap<>(); + Map payBillBySourceNumber = new HashMap<>(); + for (String orgNumber : orgGroups.keySet()) { + for (BfiPayBillVO payBill : queryPayBillPages(orgNumber)) { + if (Func.isEmpty(payBill.getBillno())) continue; + if (payBill.getSourcebillid() != null) { + payBillBySourceId.put(String.valueOf(payBill.getSourcebillid()), payBill); + } + if (Func.isNotEmpty(payBill.getSourcebillnumber())) { + payBillBySourceNumber.put(payBill.getSourcebillnumber(), payBill); + } + } + } + for (PaymentApplication application : recordCandidates.values()) { + BfiPayBillVO payBill = matchPayBill(application, payBillBySourceId, payBillBySourceNumber); + if (payBill == null) { + continue; + } + try { + self.applyPayBillRecord(application, payBill); + recordWritten++; + } catch (Exception exception) { + recordFailed++; + log.error("付款申请[{}]回填金蝶付款处理记录异常", application.getPaymentNo(), exception); + } + } + } + if (applications.isEmpty() && recordCandidates.isEmpty()) { + return "金蝶付款结果回写:无待回写付款申请"; + } + return "金蝶付款结果回写完成:待查付款申请" + applications.size() + "笔,回写单据信息" + updated + + "笔(付款完成" + paidCount + "笔,失败" + failed + "笔),回填付款记录" + recordWritten + "条(失败" + recordFailed + "条)"; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean applyPayApplyResult(PaymentApplication entity, BfiPayApplyVO payApply) { + if (Func.isNotEmpty(payApply.getBillno())) { + entity.setKingdeeBillNo(payApply.getBillno()); + } + if (payApply.getId() != null) { + entity.setKingdeeBillId(String.valueOf(payApply.getId())); + } + String kingdeeStatus = kingdeeResultStatus(payApply); + entity.setKingdeeStatus(kingdeeStatus); + // 已付金额:A103-2 分录 e_paidamt 汇总;分录缺失且已付款时兜底取申请金额 + if (Func.isNotEmpty(payApply.getEntry())) { + BigDecimal paidAmount = payApply.getEntry().stream() + .map(BfiPayApplyEntryVO::getEPaidamt).filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + entity.setPaidAmount(paidAmount.setScale(2, RoundingMode.HALF_UP)); + } else if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) { + entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP)); + } + updateById(entity); + if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) { + // 付款完成:联动履约付款数据(预结算预付记录、正式结算付款记录) + syncPreSettlementAdvance(entity); + syncFormalSettlementPayments(entity); + return true; + } + return false; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyPayBillRecord(PaymentApplication entity, BfiPayBillVO payBill) { + // 清理历史模拟付款记录,真实付款记录以金蝶付款处理单为准 + recordMapper.delete(Wrappers.lambdaQuery() + .eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId()) + .likeRight(PaymentApplicationRecord::getPaymentNo, MOCK_PAYMENT_RECORD_PREFIX)); + String billNo = payBill.getBillno(); + PaymentApplicationRecord record = recordMapper.selectList(Wrappers.lambdaQuery() + .eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId()) + .eq(Func.isNotEmpty(billNo), PaymentApplicationRecord::getPaymentNo, billNo) + .last("limit 1")).stream().findFirst().orElse(null); + boolean isNew = record == null; + if (isNew) { + record = new PaymentApplicationRecord(); + record.setPaymentApplicationId(entity.getId()); + record.setPaymentNo(billNo); + } + if (payBill.getActpayamt() != null) { + record.setPaidAmount(payBill.getActpayamt().setScale(2, RoundingMode.HALF_UP)); + } + record.setPaidDate(parseKingdeeDate(payBill.getPaydate())); + record.setKingdeeBillNo(billNo); + // 付款处理单查询结果整体留档(凭证号、银行流水、电子回单等信息),便于追溯 + record.setVoucherJson(JsonUtil.toJson(payBill)); + if (isNew) { + recordMapper.insert(record); + } else { + recordMapper.updateById(record); + } + } + + /** + * A103-2 返回状态映射本地金蝶同步状态: + * paystatus=Alreadypay 已付款(终态)优先;billstatus=D 已关闭(终态)次之; + * paystatus=Inpayment 付款中;其余(A暂存/B已提交/C已审核 + Unpaid)保持已生成继续轮询 + * + * @param payApply A103-2 查询返回的付款申请单 + * @return 本地金蝶同步状态 + */ + private String kingdeeResultStatus(BfiPayApplyVO payApply) { + if (KingdeeBfiConstant.BFI_PAY_STATUS_ALREADY_PAY.equals(payApply.getPaystatus())) { + return KingdeeBfiConstant.KINGDEE_STATUS_PAID; + } + if (KingdeeBfiConstant.BFI_BILL_STATUS_CLOSED.equals(payApply.getBillstatus())) { + return KingdeeBfiConstant.KINGDEE_STATUS_CLOSED; + } + if (KingdeeBfiConstant.BFI_PAY_STATUS_IN_PAYMENT.equals(payApply.getPaystatus())) { + return KingdeeBfiConstant.KINGDEE_STATUS_PAYING; + } + return KingdeeBfiConstant.KINGDEE_STATUS_SYNCED; + } + + /** + * A103-2 分页查询:来源单据号批量 + 外部系统编号(防重键),自动翻页 + * + * @param awrpSrcCodes 来源单据号(付款申请单号)批次 + * @return 付款申请单列表 + */ + private List queryPayApplyPages(List awrpSrcCodes) { + BfiPayApplyQueryDTO param = new BfiPayApplyQueryDTO(); + param.setAwrpSrcCode(awrpSrcCodes); + param.setAwrpSrcSys(bfiPayApplySrcSys); + return BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryPayApply(param); + }); + } + + /** + * A103-4 分页查询:按付款人(公司)编码 + 最近1个月业务日期窗口(接口限制不超过1个月),自动翻页 + * + * @param orgNumber 付款人(公司)编码 + * @return 付款处理单列表 + */ + private List queryPayBillPages(String orgNumber) { + LocalDate endDate = LocalDate.now(); + // 业务日期窗口控制在不超过1个月 + LocalDate beginDate = endDate.minusMonths(1).plusDays(1); + BfiPayBillQueryDTO param = new BfiPayBillQueryDTO(); + param.setOrgNumber(List.of(orgNumber)); + param.setBeginBizdate(beginDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + param.setEndBizdate(endDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + return BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryPayBill(param); + }); + } + + /** + * 以金蝶付款处理单源单id(=付款申请单星瀚id)/源单编号(=付款申请单单据编号)勾稽匹配 + * + * @param application 付款申请实体 + * @param payBillBySourceId 源单id索引 + * @param payBillBySourceNumber 源单编号索引 + * @return 匹配的付款处理单,未匹配返回 null + */ + private BfiPayBillVO matchPayBill(PaymentApplication application, Map payBillBySourceId, + Map payBillBySourceNumber) { + if (Func.isNotEmpty(application.getKingdeeBillId()) + && payBillBySourceId.containsKey(application.getKingdeeBillId())) { + return payBillBySourceId.get(application.getKingdeeBillId()); + } + if (Func.isNotEmpty(application.getKingdeeBillNo())) { + return payBillBySourceNumber.get(application.getKingdeeBillNo()); + } + return null; + } + + /** 统计付款申请的真实付款记录数(剔除历史模拟记录,兼容手工记录付款单号为空的场景) */ + private long realRecordCount(Long applicationId) { + return recordMapper.selectCount(Wrappers.lambdaQuery() + .eq(PaymentApplicationRecord::getPaymentApplicationId, applicationId) + .and(wrapper -> wrapper.notLikeRight(PaymentApplicationRecord::getPaymentNo, MOCK_PAYMENT_RECORD_PREFIX) + .or().isNull(PaymentApplicationRecord::getPaymentNo))); + } + + /** 解析金蝶返回日期(yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss),解析失败返回 null */ + private LocalDate parseKingdeeDate(String dateText) { + if (Func.isBlank(dateText)) return null; + try { + return LocalDate.parse(dateText.trim().substring(0, 10)); + } catch (Exception exception) { + log.warn("金蝶日期解析失败:{}", dateText); + return null; + } + } + + /** 时间字符串比较(yyyy-MM-dd HH:mm:ss),空值视为最小 */ + private int strCompare(String left, String right) { + if (Objects.equals(left, right)) return 0; + if (left == null) return -1; + if (right == null) return 1; + return left.compareTo(right); + } + /** * 推送金蝶付款申请单(A103-3 付款申请保存,预付款路径),返回金蝶单据id * @@ -478,40 +780,66 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId())); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java new file mode 100644 index 00000000..e60548c9 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java @@ -0,0 +1,64 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes. + * Copyright of this software remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.transport.service.IPaymentApplicationService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 金蝶付款结果定时回写任务 + *

+ * 定时查询金蝶(星瀚)付款结果并更新付款信息与履约付款数据: + * A103-2 付款申请单查询回填金蝶单据编号与单据状态,A103-4 付款处理查询回填付款记录。 + * 业务逻辑收敛在 {@link IPaymentApplicationService#syncKingdeePaymentResult()},便于手动触发联调。 + *

+ * 调度配置(Nacos thirdParty.bfi.payResult.*):syncEnabled 开关、cron 表达式。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PaymentKingdeeResultTask { + + private final IPaymentApplicationService paymentApplicationService; + + /** 付款结果回写开关,默认开启 */ + @Value("${thirdParty.bfi.payResult.syncEnabled:false}") + private boolean syncEnabled; + + /** 金蝶付款结果回写调度,默认每10分钟执行一次 */ + @Scheduled(cron = "${thirdParty.bfi.payResult.cron:0 */10 * * * ?}") + public void syncKingdeePaymentResult() { + if (!syncEnabled) { + return; + } + try { + String summary = paymentApplicationService.syncKingdeePaymentResult(); + log.info("定时回写金蝶付款结果:{}", summary); + } catch (Exception exception) { + log.error("定时回写金蝶付款结果异常", exception); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java index 7d4f4826..d23438bc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java @@ -22,6 +22,7 @@ package org.springblade.transport.wrapper; import org.springblade.core.mp.support.BaseEntityWrapper; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.system.cache.UserCache; +import org.springblade.transport.constant.KingdeeBfiConstant; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.vo.PaymentApplicationVO; @@ -50,8 +51,11 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper entity.getApprovalStatus(); }); vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) { - case "synced" -> "已生成"; - case "failed" -> "生成失败"; + case KingdeeBfiConstant.KINGDEE_STATUS_SYNCED -> "已生成"; + case KingdeeBfiConstant.KINGDEE_STATUS_PAYING -> "付款中"; + case KingdeeBfiConstant.KINGDEE_STATUS_PAID -> "已付款"; + case KingdeeBfiConstant.KINGDEE_STATUS_CLOSED -> "已关闭"; + case KingdeeBfiConstant.KINGDEE_STATUS_FAILED -> "生成失败"; default -> "未生成"; }); return vo; diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java index a56d0fb9..ed603bb2 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java @@ -26,10 +26,15 @@ package org.springblade.thirdparty.bfi.util; import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; import org.springblade.thirdparty.bfi.pojo.dto.BfiBaseDTO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; +import java.util.function.IntFunction; /** * 业财融合(BFI)接口通用参数工具类 @@ -46,6 +51,11 @@ public class BfiUtil { */ public static final String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss"; + /** + * 分页查询默认最大页数保护,防止金蝶侧 lastPage 异常时死循环 + */ + public static final int DEFAULT_MAX_PAGES = 50; + private BfiUtil() { } @@ -79,4 +89,54 @@ public class BfiUtil { param.setTimestamp(timestamp()); return param; } + + /** + * BFI 分页查询通用翻页,自动翻页汇总全量数据行(默认最大页数保护) + *

+ * 适用所有返回 {@link BfiPageVO} 的查询接口 + * (A103-2 付款申请单查询、A103-4 付款处理查询、A102-4 财务应付单查询、A902-1 客户查询等): + *

{@code
+	 * List rows = BfiUtil.queryAllPages(pageNo -> {
+	 * 	param.setPageNo(pageNo);
+	 * 	param.setPageSize(pageSize);
+	 * 	return bfiService.queryPayApply(param);
+	 * });
+	 * }
+ * + * @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口 + * @param 数据行类型 + * @return 全量数据行 + */ + public static List queryAllPages(IntFunction> pageFetcher) { + return queryAllPages(pageFetcher, DEFAULT_MAX_PAGES); + } + + /** + * BFI 分页查询通用翻页,自动翻页汇总全量数据行 + *

+ * 终止条件:返回页为空、数据行为空、lastPage=true、数据行不满一页(按返回 pageSize 判断)、超出最大页数保护 + * + * @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口 + * @param maxPages 最大页数保护 + * @param 数据行类型 + * @return 全量数据行 + */ + public static List queryAllPages(IntFunction> pageFetcher, int maxPages) { + List rows = new ArrayList<>(); + for (int pageNo = 1; pageNo <= maxPages; pageNo++) { + BfiPageVO page = pageFetcher.apply(pageNo); + if (page == null || Func.isEmpty(page.getRows())) { + break; + } + rows.addAll(page.getRows()); + if (Boolean.TRUE.equals(page.getLastPage())) { + break; + } + // 末页数据不满一页时无需继续翻页(pageSize 取返回值,返回为空时依赖 lastPage 与最大页数保护) + if (page.getPageSize() != null && page.getRows().size() < page.getPageSize()) { + break; + } + } + return rows; + } } diff --git a/doc/docs/业务流程/2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md b/doc/docs/业务流程/2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md new file mode 100644 index 00000000..0a4a36e4 --- /dev/null +++ b/doc/docs/业务流程/2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md @@ -0,0 +1,101 @@ +# BFI 分页查询通用翻页工具(BfiUtil.queryAllPages)技术文档 + +> 适用系统:TMS(BladeX 微服务平台,模块:`blade-third-party-api/blade-bfi-api`) +> 组件类型:通用工具类(非业务流程),服务业财融合全部 `BfiPageVO` 分页查询接口 +> 关联文档:`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md` +> 最后更新:2026-09-17 + +--- + +## 1. 背景与范围 + +`IBfiService` 中共有 9 个查询接口返回 `BfiPageVO` 分页结构,各接口的分页翻页逻辑完全一致(页码递增、空页/末页终止、防死循环保护)。原实现在 `PaymentApplicationServiceImpl` 中以私有方法 `queryPage` 承载,无法被其他查询场景复用,现抽取为 `BfiUtil.queryAllPages` 通用工具。 + +设计目标: + +- **一处实现,全部查询复用**:付款结果回写(A103-2/A103-4)之外,后续 A102-4 应付勾稽、A302-4 应收核对、A303-4 开票结果回写、A801-3/A301-1/A301-2 收票收款认领等场景接入时零成本复用 +- **调用方只关心"取第 N 页"**:翻页推进、终止判断、全量汇总由工具类负责 +- **防死循环**:金蝶侧 `lastPage` 异常(恒为 false)时按最大页数保护终止 + +## 2. 适用接口清单 + +| 接口编号 | 名称 | `IBfiService` 方法 | 返回结构 | +| --- | --- | --- | --- | +| A103-2 | 付款申请单查询 | `queryPayApply` | `BfiPageVO` | +| A103-4 | 付款处理查询 | `queryPayBill` | `BfiPageVO` | +| A102-4 | 财务应付单查询 | `queryApBill` | `BfiPageVO` | +| A302-4 | 财务应收单查询 | `queryArBill` | `BfiPageVO` | +| A303-4 | 开票申请单查询 | `queryInvoiceApply` | `BfiPageVO` | +| A902-1 | 客户查询 | `queryCustomer` | `BfiPageVO` | +| A801-3 | 收票单查询 | `queryReceivedInvoice` | `BfiPageVO` | +| A301-1 | 收款单查询 | `queryRecBill` | `BfiPageVO` | +| A301-2 | 收款认领公告查询 | `queryClaimAnnounce` | `BfiPageVO` | + +不适用:`queryInvoicePool`(A801-1,直接返回 List)、`queryExchangeRate`(A801-4,单页调用由 `BfiServiceImpl` 内部处理)。 + +## 3. API 定义 + +```java +// 默认最大页数保护(50 页) +public static List queryAllPages(IntFunction> pageFetcher) + +// 自定义最大页数保护 +public static List queryAllPages(IntFunction> pageFetcher, int maxPages) +``` + +| 参数 | 说明 | +| --- | --- | +| `pageFetcher` | 页码 → 分页结果函数。lambda 内负责按传入页码设置请求参数(页码必设,页大小按各接口限制设置)并调用 `IBfiService` 对应查询方法 | +| `maxPages` | 最大页数保护,防止金蝶侧 `lastPage` 异常导致死循环;默认 `BfiUtil.DEFAULT_MAX_PAGES = 50` | +| 返回值 | 全量数据行汇总列表(按页序追加);无数据时返回空列表 | + +## 4. 翻页终止条件(按判断顺序) + +| 序 | 条件 | 说明 | +| --- | --- | --- | +| 1 | 返回页为 null 或数据行为空 | 本页无数据即止 | +| 2 | `lastPage = true` | 金蝶标识已最后一页 | +| 3 | `rows.size() < 响应体 pageSize` | 末页数据不满一页,无需继续;**pageSize 取响应体返回值**(`BfiPageVO.pageSize`),调用方无需额外传入,响应体未返回 pageSize 时自动退化为依赖条件 2 与最大页数保护 | +| 4 | 页码超过 `maxPages` | 防御性终止,异常场景兜底 | + +## 5. 用法示例 + +```java +// A103-2 付款申请单查询:来源单据号批量查询 +private List queryPayApplyPages(List awrpSrcCodes) { + BfiPayApplyQueryDTO param = new BfiPayApplyQueryDTO(); + param.setAwrpSrcCode(awrpSrcCodes); + param.setAwrpSrcSys(bfiPayApplySrcSys); + return BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAGE_SIZE); + return bfiService.queryPayApply(param); + }); +} +``` + +lambda 内通过闭包复用同一参数对象,仅需覆盖 `pageNo`;请求参数的各接口约束(如 A103-4 付款组织编码必填、时间范围不超过 1 个月)由调用方在构造参数时保证。 + +## 6. 设计说明 + +| 决策 | 说明 | +| --- | --- | +| 抽取为 `IntFunction>` 而非传入 DTO | 各查询接口的入参 DTO、Feign 方法签名各不相同,仅"按页码取结果"语义一致;函数式入参对调用方约束最小,DTO 字段设置留在调用方(编译期类型安全) | +| 不再需要 `rowsGetter` 参数 | 全部查询统一返回 `BfiPageVO`,`getRows` 内聚到工具类;早期版本传入 `BfiPageVO::getRows` 属冗余 | +| pageSize 判满取响应体 | 早期版本要求调用方传入请求 pageSize,两处易失配;响应体 `pageSize` 为金蝶实际执行值,更可靠 | +| 空列表而非 null | 无数据返回空列表,调用方无需判空 | + +## 7. 迁移记录 + +| 变更 | 内容 | +| --- | --- | +| 新增 | `BfiUtil.queryAllPages` 两个重载 + `DEFAULT_MAX_PAGES` 常量 | +| 删除 | `PaymentApplicationServiceImpl` 私有方法 `queryPage` 及 `PAY_RESULT_MAX_PAGES` 常量 | +| 替换 | 付款结果回写 `queryPayApplyPages` / `queryPayBillPages` 改调 `BfiUtil.queryAllPages` | + +## 8. 涉及代码位置 + +| 内容 | 位置 | +| --- | --- | +| 工具类 | `blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java` | +| 当前唯一使用方 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(queryPayApplyPages / queryPayBillPages) | diff --git a/doc/docs/业务流程/2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md b/doc/docs/业务流程/2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md new file mode 100644 index 00000000..46d7f4d1 --- /dev/null +++ b/doc/docs/业务流程/2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md @@ -0,0 +1,167 @@ +# 付款管理金蝶付款结果定时回写(A103-2 付款申请单查询 + A103-4 付款处理查询)技术文档 + +> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport) +> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关) +> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(4.3 节步骤 11)、`付款申请同步金蝶(星瀚)技术文档.md`、`2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md`、`2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md` +> 最后更新:2026-09-17 + +--- + +## 1. 背景与范围 + +付款申请推送金蝶(A103-1/A103-3)成功后,金蝶侧依次完成:财务共享审批(黑盒,机器人处理)→ 付款处理 → 付款完成。原实现将"付款完成"整段模拟(`paidAmount = 申请金额`、`MOCK-PAY-` 前缀付款记录、履约付款数据直接置 `paid`),本文档建设真实回写链路并停用模拟: + +- 触发入口: + - **定时任务**:`PaymentKingdeeResultTask`,`@Scheduled` 默认每 10 分钟(`thirdParty.bfi.payResult.cron`),开关 `thirdParty.bfi.payResult.syncEnabled` + - **手动触发**:付款管理页面"同步付款结果"按钮(权限码复用 `payment_application_sync`)→ `POST /payment-application/sync-kingdee-result` +- 对接接口: + - **A103-2 付款申请单查询** `POST /ierp/kapi/v2/awrp/ap/ap_payapply/query`:按 `awrp_src_code`(=paymentNo)批量查询,回填金蝶单据编号(billno)、单据状态、付款状态、已付金额 + - **A103-4 付款处理查询** `POST /ierp/kapi/v2/awrp/cas/cas_paybill/query`:按付款人(公司)编码 + 业务日期窗口查询,回填付款记录(付款单号、付款日期、付款金额、整体报文留档) +- 联动更新:付款完成(paystatus=Alreadypay)时复用既有逻辑联动履约付款数据(预结算预付记录 `blade_pre_settlement_advance`、正式结算付款记录 `blade_formal_settlement_payment`) + +## 2. 业务流程 + +### 2.1 集成链路(对应方案文档 4.3 节步骤 11) + +``` +付款申请已推送金蝶(kingdeeStatus = synced) + → 定时任务(每10分钟) + → A103-2 付款申请单查询(awrp_src_code 批量,50单/批) + 回填 kingdee_bill_id / kingdee_bill_no(billno)/ kingdeeStatus / paid_amount + → A103-4 付款处理查询(付款中/已付款单据,按付款组织分组 + 最近1个月窗口) + 回填付款记录 blade_payment_application_record(并清理历史 MOCK-PAY- 模拟记录) + → 付款完成(Alreadypay):联动履约付款数据(预结算预付、正式结算付款记录) + → (后续环节)付款附件推送 A902、合同系统履约同步 C106-2,待建设 +``` + +### 2.2 定时回写时序 + +``` +PaymentKingdeeResultTask PaymentApplicationServiceImpl 星瀚(BFI) + │ @Scheduled 每10分钟 │ │ + │ syncKingdeePaymentResult() ──────>│ │ + │ │ ①查本地 approved + synced/paying │ + │ │ ②awrp_src_code 分批(50) ─────────────────>│ A103-2 + │ │ <── billno/billstatus/paystatus/entry ────│ + │ │ 逐单独立事务:回填单号/状态/已付金额 │ + │ │ Alreadypay → 联动履约付款数据 │ + │ │ ③付款组织分组 + 最近1个月窗口 ──────────────>│ A103-4 + │ │ <── 付款处理单(sourcebillid/sourcebillnumber)│ + │ │ 勾稽匹配 → 回填付款记录(幂等) │ + │ <── 执行摘要日志 ──────────────────│ │ +``` + +### 2.3 查询范围与幂等约定 + +| 约定 | 说明 | +| --- | --- | +| A103-2 查询范围 | `approvalStatus = approved` 且 `kingdeeStatus IN (synced, paying)`;`paid/closed` 为终态不再轮询 | +| A103-2 防重键 | `awrp_src_code`(=paymentNo)+ `awrp_src_sys`(=WLTMS)批量查询,50 单/批,自动翻页(页大小 200,最大 50 页保护) | +| 同号多单 | 同一 `awrp_src_code` 返回多条时取 `modifytime` 最新的一条 | +| A103-4 查询范围 | 付款中/已付款单据(本轮 A103-2 查到 `paystatus ≠ Unpaid` 的)+ 已付款但尚无真实付款记录的单据(补偿轮询) | +| A103-4 入参约束 | 付款人(公司)编码必填(优先取 A103-2 返回 `payorg_number`,兜底按合同所属组织解析);业务日期窗口取最近 1 个月(接口限制不超过 1 个月) | +| A103-4 勾稽键 | `sourcebillid`(=付款申请单星瀚id `kingdee_bill_id`)优先,`sourcebillnumber`(=付款申请单编号 `kingdee_bill_no`)兜底 | +| 付款记录幂等 | 按 `payment_application_id + payment_no`(=付款处理单号)判重,存在则更新;回填时清理该申请单下 `MOCK-PAY-` 前缀的历史模拟记录 | +| 逐单隔离 | 每笔申请通过 `SpringUtil` 自代理走独立事务(`applyPayApplyResult` / `applyPayBillRecord`),单条失败不中断批次,异常记录日志后继续 | + +### 2.4 状态映射(kingdeeStatus 扩展) + +| A103-2 返回 | 本地 kingdeeStatus | 展示名 | 是否终态 | +| --- | --- | --- | --- | +| `paystatus = Alreadypay` | `paid` | 已付款 | 是(触发履约联动) | +| `billstatus = D`(已关闭)且未付款 | `closed` | 已关闭 | 是 | +| `paystatus = Inpayment` | `paying` | 付款中 | 否(继续轮询) | +| `billstatus = A暂存/B已提交/C已审核` + `paystatus = Unpaid` | `synced` | 已生成 | 否(继续轮询) | + +既有状态保持不变:`unsynced` 未生成、`failed` 生成失败(可重推)。 + +## 3. 字段映射关系 + +### 3.1 A103-2 回写(`BfiPayApplyVO` → `PaymentApplication`) + +| 星瀚字段 | 含义 | 落库字段 | 说明 | +| --- | --- | --- | --- | +| id | 星瀚单据内部主键 | `kingdee_bill_id` | A902 附件上传等后续环节使用 | +| billno | 单据编号(FKSQ-xxxx) | `kingdee_bill_no` | 展示用金蝶单据号;A103-4 勾稽键之一 | +| billstatus | 单据状态 A/B/C/D | `kingdee_status` | 见 2.4 状态映射 | +| paystatus | 付款状态 | `kingdee_status` | 见 2.4 状态映射 | +| entry[].e_paidamt | 分录已付金额(汇总) | `paid_amount` | 分录缺失且已付款时兜底取申请金额 | +| payorg_number | 付款组织编码 | —(内存) | A103-4 付款人(公司)编码入参 | + +### 3.2 A103-4 回填(`BfiPayBillVO` → `PaymentApplicationRecord`) + +| 星瀚字段 | 含义 | 落库字段 | 说明 | +| --- | --- | --- | --- | +| billno | 付款处理单号 | `payment_no` / `kingdee_bill_no` | 幂等判重键 | +| paydate | 付款日期 | `paid_date` | 取前 10 位解析 yyyy-MM-dd,解析失败置空并告警 | +| actpayamt | 付款金额 | `paid_amount` | 2 位小数 | +| 整体报文 | 付款处理单全量 JSON | `voucher_json` | 凭证号、银行流水、电子回单等信息留档追溯 | + +## 4. 付款完成联动(履约付款数据) + +付款状态变为 `paid` 时复用既有逻辑(与原模拟阶段一致,数据源改为真实回写值): + +| 联动项 | 逻辑 | 位置 | +| --- | --- | --- | +| 预结算预付记录 | `syncPreSettlementAdvance`:`paid_amount` = 申请金额、`bill_status = paid`、`kingdee_advance_no` = 金蝶单据号,刷新预结算单预付汇总与正式结算来源预付统计 | PaymentApplicationServiceImpl | +| 正式结算付款记录 | `syncFormalSettlementPayments`:按申请单-结算单关联分摊已付金额,创建/更新 `blade_formal_settlement_payment`(`bill_status = paid`、金蝶单据号),刷新结算单付款汇总 | 同上 | + +部分付款(`paying`)仅更新付款申请 `paid_amount` 与付款记录,**不**触发履约联动(待付款完成一次性联动)。 + +## 5. 模拟逻辑停用说明 + +- `mockPaymentRecords()`(`MOCK-PAY-` 付款记录模拟)与 `syncKingdee` 内的付款完成模拟代码段**保留方法体、注释调用处**并备注原因:联调环境无金蝶付款数据时可恢复注释模拟使用。 +- `syncKingdee` 现仅落 `kingdee_bill_id` + `kingdee_status = synced`;`kingdee_bill_no`、`paid_amount`、付款记录、履约付款数据全部由定时回写驱动。 +- 存量数据兼容: + - 已 `synced` 单据(`kingdee_bill_no` 存的是旧语义星瀚id)由任务首轮 A103-2 按 paymentNo 查询自动修正为 billno; + - 手动重推幂等返回兼容旧数据(`kingdee_bill_id` 为空时回退返回 `kingdee_bill_no`)。 + +## 6. 数据库变更 + +| 变更 | 内容 | 脚本 | +| --- | --- | --- | +| 新增列 | `blade_payment_application.kingdee_bill_id` varchar(32):金蝶单据id(星瀚内部主键) | `doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql`(幂等可重复执行) | +| 语义调整 | `kingdee_bill_no`:星瀚id → **金蝶单据编号 billno**(由回写任务回填) | 无 DDL | +| 建表模板 | `blade_payment_application_20260821.sql` 已同步包含新列 | 新装环境直接执行 | + +## 7. 配置项(Nacos:`thirdParty.bfi.payResult.*`) + +模板见 `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`: + +| 配置键 | 环境变量 | 默认值 | 说明 | +| --- | --- | --- | --- | +| thirdParty.bfi.payResult.syncEnabled | BFI_PAY_RESULT_SYNC_ENABLED | true | 回写任务开关 | +| thirdParty.bfi.payResult.cron | BFI_PAY_RESULT_CRON | 0 */10 * * * ? | 调度表达式(默认每10分钟) | + +调度启用:`TransportApplication` 增加 `@EnableScheduling`。 + +## 8. 异常处理 + +| 场景 | 行为 | +| --- | --- | +| A103-2/A103-4 接口级失败(status=false / errorCode≠0) | `BfiServiceImpl.commonCall` 抛 `BfiException`,当前批次终止,下轮任务自动重试 | +| 单笔回写异常 | 记录 error 日志(含 paymentNo 与堆栈),计数后继续下一单 | +| A103-4 付款组织编码获取失败(无 A103-2 数据且合同缺失/组织编码为空) | warn 日志跳过该单,下轮重试 | +| 付款日期解析失败 | warn 日志,`paid_date` 置空,其余字段正常回填 | +| 手动触发与定时任务并发 | 同单以 `updateById` 主键更新兜底,最终一致;付款记录按单号判重不重复插入 | + +## 9. 涉及代码位置 + +| 内容 | 位置 | +| --- | --- | +| 定时任务入口 | `blade-service/blade-transport/.../task/PaymentKingdeeResultTask.java` | +| 回写编排 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(syncKingdeePaymentResult :409 / applyPayApplyResult :512 / applyPayBillRecord :538 / queryPayApplyPages / queryPayBillPages / kingdeeResultStatus / matchPayBill) | +| 手动触发接口 | `blade-service/blade-transport/.../controller/PaymentApplicationController.java`(POST /sync-kingdee-result) | +| 状态常量 | `blade-service/blade-transport/.../constant/KingdeeBfiConstant.java`(KINGDEE_STATUS_* / BFI_PAY_STATUS_* / BFI_BILL_STATUS_CLOSED) | +| 实体/翻译 | `blade-transport-api/.../pojo/entity/PaymentApplication.java`(kingdeeBillId);`transport/wrapper/PaymentApplicationWrapper.java`(状态翻译) | +| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../service/IBfiService.java`(queryPayApply / queryPayBill,本次无改动);分页通用翻页 `util/BfiUtil.java`(queryAllPages,A102-4/A302-4/A303-4 等分页查询可复用) | +| 前端 | `tms-erp-web/src/views/payment/payment-application.vue`(同步付款结果按钮);`src/api/payment/paymentApplication.js`(syncKingdeeResult / kingdeeStatusOptions) | +| SQL | `doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql`;建表模板 `blade_payment_application_20260821.sql` | +| Nacos 配置模板 | `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`(thirdParty.bfi.payResult.*) | + +## 10. 遗留事项与联调确认 + +1. **A103-4 勾稽键语义待联调确认**:以 `sourcebillid = 付款申请单星瀚id`、`sourcebillnumber = 付款申请单编号(billno)` 假设实现,联调时需用真实付款处理单验证;若金蝶侧源单指向不同,仅需调整 `matchPayBill` 匹配逻辑。 +2. **付款日期格式**:`paydate` 按前 10 位 yyyy-MM-dd 兼容解析,金蝶若返回纯时间戳需调整 `parseKingdeeDate`。 +3. **A103-2 分页翻页**:`awrp_src_code` 批量查询理论单页返回(批次 50 < 页大小 200),翻页保护仅防御异常场景。 +4. **后续环节待建设**:付款附件推送 A902(凭 `kingdee_bill_id`)、合同系统履约同步 C106-2、付款申请反审核异常回收(B901 解锁)。 diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index fe387360..75193f53 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -140,6 +140,12 @@ thirdParty: exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01} # 外部单据类型 srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs} + # 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询) + payResult: + # 回写任务开关 + syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true} + # 调度 cron,默认每10分钟 + cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: @@ -153,6 +159,15 @@ baidu: token-refresh-advance: 1m +#MinIO文件存储配置 +file: + storage: + minio: + endpoint: http://8.148.239.157:9000 + access-key-id: transport + access-key-secret: transport + bucket-name: transport + powerjob: worker: enabled: false diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index e649e8c0..43dc86a3 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -112,6 +112,12 @@ thirdParty: exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01} # 外部单据类型 srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs} + # 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询) + payResult: + # 回写任务开关 + syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true} + # 调度 cron,默认每10分钟 + cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index 764056d9..fe35b961 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -43,3 +43,9 @@ thirdParty: exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01} # 外部单据类型 srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs} + # 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询) + payResult: + # 回写任务开关 + syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true} + # 调度 cron,默认每10分钟 + cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?} diff --git a/doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql b/doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql new file mode 100644 index 00000000..5b230785 --- /dev/null +++ b/doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql @@ -0,0 +1,17 @@ +-- 付款管理:金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询) +-- 新增 kingdee_bill_id:保存 A103-1/A103-3 推送返回的星瀚单据内部主键(A902 附件上传等使用); +-- kingdee_bill_no 字段语义调整为保存金蝶单据编号(billno,如 FKSQ-xxxx),由定时回写任务回填 + +-- 以下结构升级支持重复执行 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_payment_application` ADD COLUMN `kingdee_bill_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据id(星瀚内部主键)'' AFTER `kingdee_bill_no`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_payment_application' + AND COLUMN_NAME = 'kingdee_bill_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt;