From 2e91e112ffdb490ed0c8e9c900d817a60bf96e75 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, 29 Sep 2026 01:02:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B6=E6=AC=BE=E8=AE=A4=E9=A2=86?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=EF=BC=8C=E8=AE=A4=E9=A2=86=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E6=89=8D=E6=8E=A8=E9=87=91=E8=9D=B6=E2=80=94=E2=80=94?= =?UTF-8?q?A301-3=E5=AE=9A=E6=97=B6+=E6=89=8B=E5=8A=A8=E5=8F=8C=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E6=8C=89=E6=B5=81=E6=B0=B4=E5=BD=92=E9=9B=86=E6=8E=A8?= =?UTF-8?q?=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 认领只落本地:claim() 删除事务内同步推送,认领上限/认领完成口径改为公告未认领金额 - 推送闸门:认领完成且未推送过才可推;推送前重查A301-2最新公告对账,不一致拦截并提示差额 - 按流水归集:一条流水一张金蝶收款单,分录取全部有效分摊行;awrp_src_code 上移至流水、重试复用防重试双单 - 已推送终态闸门禁止二次推送;推送失败落 push_status/push_fail_reason 可重试,本地认领不回滚 - 定时任务 ReceiptClaimPushTask(thirdParty.bfi.receiptClaimPush.*,按认领人分组重查公告)+ 手动按钮 POST /receipt-flow/push-kingdee - A301-1 回写轮询对象改流水并级联刷新认领主单冗余字段;作废仍不触发推送 - 认领链路批量化:结算单锁行/认领额聚合/汇总刷新改 IN 批量,候选单列表消除每页200次N+1,同步留痕 Db.saveBatch --- .../pojo/dto/ReceiptClaimPushResult.java | 70 +++ .../pojo/entity/KingdeeReceiptFlow.java | 26 +- .../transport/pojo/entity/ReceiptClaim.java | 6 +- .../transport/pojo/vo/ReceiptFlowVO.java | 4 + .../constant/KingdeeBfiConstant.java | 12 + .../controller/ReceiptFlowController.java | 9 + .../transport/mapper/ReceiptClaimMapper.xml | 1 - .../service/IReceiptFlowService.java | 17 + .../impl/ReceiptClaimRecordServiceImpl.java | 90 ++-- .../service/impl/ReceiptFlowServiceImpl.java | 474 +++++++++++++----- .../transport/task/ReceiptClaimPushTask.java | 70 +++ .../transport/wrapper/ReceiptFlowWrapper.java | 17 +- 12 files changed, 645 insertions(+), 151 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimPushResult.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/ReceiptClaimPushTask.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimPushResult.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimPushResult.java new file mode 100644 index 00000000..dd27a8d9 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimPushResult.java @@ -0,0 +1,70 @@ +/** + * 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 + * is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for their own commercial purposes. + *

+ * Using this software signifies agreement to this license agreement and terms of use, + * and you expressly agree to be bound by the terms of use of this software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 收款认领推送金蝶结果(A301-3) + *

+ * 推送失败不抛异常跨事务边界(保证失败状态落库不被回滚),以本结果对象向调用方报告成败与原因。 + * + * @author Chill + */ +@Data +@Schema(description = "收款认领推送金蝶结果") +public class ReceiptClaimPushResult implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "是否推送成功") + private boolean success; + + @Schema(description = "金蝶收款单号(成功时返回,可能为空:金蝶响应未返回单号)") + private String kingdeeBillNo; + + @Schema(description = "失败原因(成功时为空)") + private String message; + + public static ReceiptClaimPushResult success(String kingdeeBillNo) { + ReceiptClaimPushResult result = new ReceiptClaimPushResult(); + result.setSuccess(true); + result.setKingdeeBillNo(kingdeeBillNo); + return result; + } + + public static ReceiptClaimPushResult fail(String message) { + ReceiptClaimPushResult result = new ReceiptClaimPushResult(); + result.setSuccess(false); + result.setMessage(message); + return result; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java index bc9f3bcf..0499d56b 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java @@ -121,9 +121,33 @@ public class KingdeeReceiptFlow extends TenantEntity { */ private BigDecimal claimedAmount; /** - * 认领状态(0未认领/1部分认领/2已认领,本地认领驱动) + * 认领状态(unclaimed未认领/partial部分认领/claimed认领完成,累计认领与公告未认领金额对表) */ private String claimStatus; + /** + * 推送状态(none未推送/pushed已推送/failed推送失败;已推送为终态闸门,不允许二次推送) + */ + private String pushStatus; + /** + * 最近一次推送失败原因(成功后清空) + */ + private String pushFailReason; + /** + * 首次推送金蝶成功时间 + */ + private LocalDateTime pushedTime; + /** + * 金蝶推送来源单据编码(SKCL+时间戳+流水id尾号6位,按流水生成、重试复用,A301-1 回写匹配键) + */ + private String awrpSrcCode; + /** + * 金蝶收款单号(推送响应回填 / A301-1 回写推进) + */ + private String kingdeeBillNo; + /** + * 金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败) + */ + private String kingdeeBillStatus; /** * 来源更新时间(取交易日期,同步时刷新) */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java index dda3cbca..2294d0a9 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java @@ -66,10 +66,12 @@ public class ReceiptClaim extends TenantEntity { private String remark; private String claimStatus; /** - * 金蝶认领结果推送来源单据编码(A301-3 awrp_src_code,A301-1 回写按此匹配) + * 金蝶收款单号(冗余展示:推送成功/A301-1 回写时由流水级联刷新) */ - private String awrpSrcCode; private String kingdeeBillNo; + /** + * 金蝶回写状态(冗余展示:与流水 kingdee_bill_status 同步刷新) + */ private String kingdeeBillStatus; @JsonSerialize(using = ToStringSerializer.class) private Long voidedBy; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java index b9365180..585ba107 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java @@ -58,6 +58,10 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow { @TableField(exist = false) private String kingdeeClaimStatusName; @TableField(exist = false) + private String pushStatusName; + @TableField(exist = false) + private String kingdeeBillStatusName; + @TableField(exist = false) private BigDecimal remainingAmount; @TableField(exist = false) private String createUserName; 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 3285c404..b296c051 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 @@ -215,6 +215,18 @@ public final class KingdeeBfiConstant { /** A301-3 认领推送:付款人类型(默认客户 bd_customer,待金蝶确认后如有调整直接改此常量)。 */ public static final String BFI_CLAIM_PAYMENT_TYPE = "bd_customer"; + /** 收款流水推送状态:未推送(未满足推送条件或尚未轮到推送)。 */ + public static final String RECEIPT_CLAIM_PUSH_NONE = "none"; + + /** 收款流水推送状态:已推送(终态闸门,不允许二次推送)。 */ + public static final String RECEIPT_CLAIM_PUSH_PUSHED = "pushed"; + + /** 收款流水推送状态:推送失败(本地认领保留,可重新推送,重试复用同一来源编码)。 */ + public static final String RECEIPT_CLAIM_PUSH_FAILED = "failed"; + + /** 收款流水金蝶回写状态:未推送(认领完成推送前的初始状态)。 */ + public static final String RECEIPT_CLAIM_BILL_NONE = "none"; + /** 金蝶同步状态:认领结果已推送(A301-3 成功,等待金蝶收款单审核/收款,可继续回写)。 */ public static final String KINGDEE_STATUS_PUSHED = "pushed"; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java index 8fe86d81..1bbee361 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java @@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.ReceiptClaimPushResult; import org.springblade.transport.pojo.dto.ReceiptClaimRequest; import org.springblade.transport.pojo.vo.ReceiptFlowVO; import org.springblade.transport.service.IReceiptFlowService; @@ -98,4 +99,12 @@ public class ReceiptFlowController extends BladeController { public R sync() { return R.data(receiptFlowService.sync()); } + + @PostMapping("/push-kingdee") + @ApiOperationSupport(order = 6) + @Operation(summary = "推送金蝶收款认领(A301-3 收款认领结果推送,仅认领完成流水可推送)") + public R pushKingdee(@RequestParam Long flowId) { + ReceiptClaimPushResult result = receiptFlowService.pushClaimToKingdee(flowId); + return result.isSuccess() ? R.data(result) : R.fail(result.getMessage()); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml index fcbafc57..07a25531 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml @@ -22,7 +22,6 @@ c.attachments_json, c.remark, c.claim_status, - c.awrp_src_code, c.kingdee_bill_no, c.kingdee_bill_status, c.voided_by, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java index 0f7c5686..89586f79 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java @@ -27,6 +27,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.ReceiptClaimPushResult; import org.springblade.transport.pojo.dto.ReceiptClaimRequest; import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; import org.springblade.transport.pojo.vo.ReceiptFlowVO; @@ -57,4 +58,20 @@ public interface IReceiptFlowService extends BaseService { * @return 本次同步条数 */ int sync(); + + /** + * 手动推送金蝶(A301-3 收款认领结果推送):认领完成且未推送过的流水才可推送, + * 推送前以当前用户姓名重查 A301-2 最新公告对账。与定时任务共用同一推送实现。 + * + * @param flowId 收款流水id + * @return 推送结果(失败不抛异常,原因随结果返回) + */ + ReceiptClaimPushResult pushClaimToKingdee(Long flowId); + + /** + * 定时推送金蝶:扫描认领完成且未推送过的流水,按认领人姓名分组重查 A301-2 最新公告后逐条推送。 + * + * @return 执行摘要 + */ + String pushClaimByTask(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java index 270a5417..e1b2623e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java @@ -215,23 +215,41 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl orderedRelations = relations.stream() .sorted(Comparator.comparing(ReceiptClaimSettlement::getFormalSettlementId)) .toList(); - for (ReceiptClaimSettlement relation : orderedRelations) { - FormalSettlement settlement = formalSettlementMapper.selectOne( + // 批量锁行关联结算单(一次 IN 查询替代逐单 selectOne,id 升序加锁防死锁) + List settlementIds = orderedRelations.stream() + .map(ReceiptClaimSettlement::getFormalSettlementId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (settlementIds.isEmpty()) { + throw new ServiceException("关联结算单不存在"); + } + Map settlements = formalSettlementMapper.selectList( Wrappers.lambdaQuery() - .eq(FormalSettlement::getId, relation.getFormalSettlementId()) - .last("FOR UPDATE")); + .in(FormalSettlement::getId, settlementIds) + .orderByAsc(FormalSettlement::getId) + .last("FOR UPDATE")).stream() + .collect(Collectors.toMap(FormalSettlement::getId, Function.identity())); + for (ReceiptClaimSettlement relation : orderedRelations) { + FormalSettlement settlement = settlements.get(relation.getFormalSettlementId()); if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { throw new ServiceException("关联结算单不存在"); } - relation.setStatus(0); - claimSettlementMapper.updateById(relation); - formalSettlementService.refreshPaymentSummary(settlement.getId()); } + // 分摊行批量置无效(单条 UPDATE 替代逐行 updateById,实体携带 status 保留审计字段填充) + ReceiptClaimSettlement voidTemplate = new ReceiptClaimSettlement(); + voidTemplate.setStatus(0); + claimSettlementMapper.update(voidTemplate, Wrappers.lambdaUpdate() + .in(ReceiptClaimSettlement::getId, relations.stream() + .map(ReceiptClaimSettlement::getId).toList())); + // 批量刷新结算单收款汇总(固定数次查询,避免逐单 N+1) + formalSettlementService.refreshPaymentSummaries(settlementIds); BigDecimal flowClaimedAfter = money(flow.getClaimedAmount()) .subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO); flow.setClaimedAmount(flowClaimedAfter); - flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getReceiptAmount())); + // 认领状态对照公告未认领金额重算(作废分摊不计入累计) + flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getUnclaimedAmount())); receiptFlowMapper.updateById(flow); String kingdeeBillNo = buildKingdeeBillNo(claim.getId()); @@ -260,34 +278,35 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl * 回写口径:A301-1 契约本身不支持按来源单号过滤,故按近 30 天业务日期窗口全量翻页后, - * 以金蝶收款单 sourcebillnumber == 认领记录 awrp_src_code(A301-3 推送时生成)在本地匹配; - * 仅推进金蝶单据状态与补登收款单号,不改本地认领金额/分摊。 + * 以金蝶收款单 sourcebillnumber == 流水 awrp_src_code(A301-3 推送时按流水生成、重试复用)在本地匹配; + * 仅推进金蝶回写状态与补登收款单号,不改本地认领金额/分摊;名下未作废认领主单冗余展示字段级联刷新。 * * @return 回写条数 */ @Override public int refreshKingdeeResult() { - List pending = baseMapper.selectList(Wrappers.lambdaQuery() - .eq(ReceiptClaim::getClaimStatus, CLAIMED) - .isNotNull(ReceiptClaim::getAwrpSrcCode) - .in(ReceiptClaim::getKingdeeBillStatus, - List.of(KingdeeBfiConstant.KINGDEE_STATUS_PUSHED, APPROVED)) - .orderByAsc(ReceiptClaim::getId)); + List pending = receiptFlowMapper.selectList( + Wrappers.lambdaQuery() + .eq(KingdeeReceiptFlow::getPushStatus, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED) + .in(KingdeeReceiptFlow::getKingdeeBillStatus, + List.of(KingdeeBfiConstant.KINGDEE_STATUS_PUSHED, APPROVED)) + .eq(KingdeeReceiptFlow::getStatus, 1) + .orderByAsc(KingdeeReceiptFlow::getId)); if (pending.isEmpty()) { - log.info("金蝶认领结果回写:无待回写认领记录(已推送/已审核且未终态)"); + log.info("金蝶认领结果回写:无待回写流水(已推送/已审核且未终态)"); return 0; } Set srcCodes = pending.stream() - .map(ReceiptClaim::getAwrpSrcCode) + .map(KingdeeReceiptFlow::getAwrpSrcCode) .filter(Func::isNotEmpty) .collect(Collectors.toSet()); if (srcCodes.isEmpty()) { return 0; } - log.info("金蝶认领结果回写:待回写认领记录 {} 条", pending.size()); + log.info("金蝶认领结果回写:待回写流水 {} 条", pending.size()); LocalDate end = LocalDate.now(); LocalDate begin = end.minusDays(RESULT_WINDOW_DAYS); @@ -310,21 +329,27 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl first, HashMap::new)); int updated = 0; - for (ReceiptClaim claim : pending) { - BfiRecBillVO bill = matched.get(claim.getAwrpSrcCode()); + for (KingdeeReceiptFlow flow : pending) { + BfiRecBillVO bill = matched.get(flow.getAwrpSrcCode()); if (bill == null) { // 窗口内未查到:金蝶回单延迟/状态未同步,不改状态,由下次回写或人工处置兜底 continue; } - if (Func.isEmpty(claim.getKingdeeBillNo())) { - claim.setKingdeeBillNo(bill.getBillno()); + if (Func.isEmpty(flow.getKingdeeBillNo())) { + flow.setKingdeeBillNo(bill.getBillno()); } - claim.setKingdeeBillStatus(billStatusOf(bill.getBillstatus())); - updateById(claim); + flow.setKingdeeBillStatus(billStatusOf(bill.getBillstatus())); + receiptFlowMapper.updateById(flow); + // 级联刷新名下未作废认领主单的冗余展示字段 + baseMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceiptClaim::getReceiptFlowId, flow.getId()) + .eq(ReceiptClaim::getClaimStatus, CLAIMED) + .set(ReceiptClaim::getKingdeeBillNo, flow.getKingdeeBillNo()) + .set(ReceiptClaim::getKingdeeBillStatus, flow.getKingdeeBillStatus())); updated++; - log.info("金蝶认领结果回写 claimId:{},awrpSrcCode:{},金蝶收款单号:{},状态:{} → {}", - claim.getId(), claim.getAwrpSrcCode(), bill.getBillno(), bill.getBillstatus(), - claim.getKingdeeBillStatus()); + log.info("金蝶认领结果回写 flowId:{},awrpSrcCode:{},金蝶收款单号:{},状态:{} → {}", + flow.getId(), flow.getAwrpSrcCode(), bill.getBillno(), bill.getBillstatus(), + flow.getKingdeeBillStatus()); } log.info("金蝶认领结果回写完成:匹配 {} 条,更新 {} 条", matched.size(), updated); return updated; @@ -375,11 +400,14 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl= 0 ? "claimed" : "partial"; + return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? "claimed" : "partial"; } private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index 3ae967f3..3a5c8ddc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -28,6 +28,7 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.Db; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; @@ -52,6 +53,7 @@ import org.springblade.transport.mapper.KingdeeReceiptFlowMapper; import org.springblade.transport.mapper.ReceiptClaimMapper; import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; import org.springblade.transport.mapper.ReceiptFlowRecordMapper; +import org.springblade.transport.pojo.dto.ReceiptClaimPushResult; import org.springblade.transport.pojo.dto.ReceiptClaimRequest; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; @@ -65,6 +67,7 @@ import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.wrapper.ReceiptFlowWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import java.math.BigDecimal; import java.math.RoundingMode; @@ -72,10 +75,12 @@ 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.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -117,6 +122,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl selectPage(IPage page, ReceiptFlowVO query) { @@ -152,7 +158,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpllambdaQuery() .eq(ReceiptFlowRecord::getReceiptFlowId, id) - .eq(ReceiptFlowRecord::getActionType, "claim") + .in(ReceiptFlowRecord::getActionType, List.of("claim", "push")) .orderByDesc(ReceiptFlowRecord::getCreateTime))); return vo; } @@ -171,8 +177,12 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl claimedMap = receiptClaimedAmounts(settlements.stream() + .map(FormalSettlement::getId).toList()); return settlements.stream() - .map(this::candidateRow) + .map(settlement -> candidateRow(settlement, + claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO))) .filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0) .toList(); } @@ -240,6 +250,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl allocationMap = allocationMap(request.getSettlements()); // 结算单id列表(升序) @@ -250,31 +263,37 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl settlements = settlementIds.stream().map(settlementMap::get).toList(); assertCompatible(settlements); - // 结算单id → 认领前已认领金额 - Map previousClaimedMap = new LinkedHashMap<>(); + // 结算单id → 认领前已认领金额(一次聚合查询,避免逐单 N+1) + Map previousClaimedMap = receiptClaimedAmounts(settlementIds); // 本次分摊总金额 BigDecimal allocatedTotal = BigDecimal.ZERO; for (FormalSettlement settlement : settlements) { // 本次分摊金额 BigDecimal allocated = allocationMap.get(settlement.getId()); // 认领前已认领金额 - BigDecimal previousClaimed = settlementClaimedAmount(settlement.getId()); + BigDecimal previousClaimed = previousClaimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO); // 结算总金额 BigDecimal settlementAmount = positiveMoney(settlement.getSettlementAmount(), "结算总金额"); if (previousClaimed.add(allocated).compareTo(settlementAmount) > 0) { throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的累计认领金额不能超过结算总应收含税金额"); } - previousClaimedMap.put(settlement.getId(), previousClaimed); allocatedTotal = allocatedTotal.add(allocated); } - // 收款金额 - BigDecimal receiptAmount = positiveMoney(flow.getReceiptAmount(), "收款金额"); + // 公告未认领金额(认领上限与认领完成判定基准,A301-2 口径) + if (flow.getUnclaimedAmount() == null) { + throw new ServiceException("公告未认领金额缺失,请先手动同步金蝶收款流水"); + } + BigDecimal unclaimedAmount = money(flow.getUnclaimedAmount()); + if (unclaimedAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("该流水公告未认领金额为0,无需认领"); + } // 流水认领前已认领金额 BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId()); - if (previousFlowClaimed.add(allocatedTotal).compareTo(receiptAmount) > 0) { - throw new ServiceException("本次分摊金额不能超过流水剩余可认领金额"); + if (previousFlowClaimed.add(allocatedTotal).compareTo(unclaimedAmount) > 0) { + throw new ServiceException("累计认领金额不能超过公告未认领金额,剩余可认领 " + + unclaimedAmount.subtract(previousFlowClaimed).max(BigDecimal.ZERO)); } // 当前用户部门 @@ -301,17 +320,15 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl announces = BfiUtil.queryAllPages(pageNo -> { - BfiClaimAnnounceQueryDTO queryDTO = new BfiClaimAnnounceQueryDTO(); - queryDTO.setClaimusers(claimusers); - queryDTO.setModifytimeS(modifytimeS); - queryDTO.setModifytimeE(modifytimeE); - queryDTO.setPageNo(pageNo); - queryDTO.setPageSize(SYNC_PAGE_SIZE); - return bfiService.queryClaimAnnounce(queryDTO); - }); - log.info("调用BFI接口 收款认领公告查询结束 耗时:{}ms,拉取 {} 条", - System.currentTimeMillis() - start, announces.size()); + List announces = queryClaimAnnounces(claimusers); if (announces.isEmpty()) { record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO, @@ -468,8 +465,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl records = new ArrayList<>(toInsert.size() + toUpdate.size()); for (KingdeeReceiptFlow entity : toInsert) { - record(entity.getId(), null, "sync", "新增金蝶收款流水", - entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo()); + records.add(buildRecord(entity.getId(), null, "sync", "新增金蝶收款流水", + entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo())); } for (KingdeeReceiptFlow entity : toUpdate) { - record(entity.getId(), null, "sync", "更新金蝶收款流水", - entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo()); + records.add(buildRecord(entity.getId(), null, "sync", "更新金蝶收款流水", + entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo())); + } + if (!records.isEmpty()) { + Db.saveBatch(records); } log.info("收款流水操作日志记录完成"); @@ -510,8 +512,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl candidateRow(FormalSettlement settlement) { - BigDecimal claimedAmount = settlementClaimedAmount(settlement.getId()); + private Map candidateRow(FormalSettlement settlement, BigDecimal claimedAmount) { BigDecimal settlementAmount = money(settlement.getSettlementAmount()); Map row = new LinkedHashMap<>(); row.put("id", settlement.getId()); @@ -547,16 +548,20 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl lockSettlements(List settlementIds) { - List settlements = new ArrayList<>(); + // 一次锁行查询替代逐单 selectOne(id 升序加锁,与逐单版加锁顺序一致防死锁) + List settlements = formalSettlementMapper.selectList( + Wrappers.lambdaQuery() + .in(FormalSettlement::getId, settlementIds) + .orderByAsc(FormalSettlement::getId) + .last("FOR UPDATE")); + Map settlementMap = new LinkedHashMap<>(); for (Long settlementId : settlementIds) { - FormalSettlement settlement = formalSettlementMapper.selectOne( - Wrappers.lambdaQuery() - .eq(FormalSettlement::getId, settlementId) - .last("FOR UPDATE")); - settlements.add(availableSettlement(settlement)); + FormalSettlement settlement = settlements.stream() + .filter(item -> settlementId.equals(item.getId())) + .findFirst().orElse(null); + settlementMap.put(settlementId, availableSettlement(settlement)); } - return settlements.stream().collect(Collectors.toMap(FormalSettlement::getId, - Function.identity(), (first, second) -> first, LinkedHashMap::new)); + return settlementMap; } private FormalSettlement availableSettlement(FormalSettlement settlement) { @@ -582,13 +587,20 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl receiptClaimedAmounts(List settlementIds) { + List ids = settlementIds.stream().filter(Objects::nonNull).distinct().toList(); + if (ids.isEmpty()) { + return Map.of(); + } return claimSettlementMapper.selectList(Wrappers.lambdaQuery() - .eq(ReceiptClaimSettlement::getFormalSettlementId, settlementId) - .eq(ReceiptClaimSettlement::getStatus, 1)).stream() - .map(ReceiptClaimSettlement::getAllocatedReceiptAmount) - .map(this::money) - .reduce(BigDecimal.ZERO, BigDecimal::add); + .in(ReceiptClaimSettlement::getFormalSettlementId, ids) + .eq(ReceiptClaimSettlement::getStatus, 1)).stream() + .collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId, + Collectors.reducing(BigDecimal.ZERO, + item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add))); } private BigDecimal flowClaimedAmount(Long flowId) { @@ -622,34 +634,183 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl - * 认领在本地落库后立即推送,推送失败抛异常由事务整体回滚,保证金蝶与本地认领口径一致; - * awrpSrcCode 同时作为 A301-1 收款单查询回写的匹配键(金蝶收款单 sourcebillnumber)。 - * - * @param flow 收款流水(须含 billId) - * @param claim 本地认领主单(已落库) - * @param settlements 分摊的应收正式结算单(保持 id 升序) - * @param allocationMap 结算单 id → 分摊金额 + * 仅认领完成且未推送过的流水可推送;推送前以当前用户姓名重查 A301-2 最新公告对账。 + * 推送失败不抛异常跨事务边界(保证失败状态落库),原因随结果对象返回。 */ - private void pushClaimResultToKingdee(KingdeeReceiptFlow flow, ReceiptClaim claim, - List settlements, Map allocationMap) { - if (flow.getBillId() == null) { - throw new ServiceException("认领公告id缺失,请先手动同步金蝶收款流水"); + @Override + public ReceiptClaimPushResult pushClaimToKingdee(Long flowId) { + if (flowId == null) { + throw new ServiceException("收款流水ID不能为空"); } - // 来源单据编码:多个结算单号用_拼接 - String awrpSrcCode = settlements.stream() - .map(FormalSettlement::getFormalSettlementNo) - .collect(Collectors.joining("_")); - claim.setAwrpSrcCode(awrpSrcCode); + KingdeeReceiptFlow flow = existing(flowId); + if (flow.getBillId() == null) { + return ReceiptClaimPushResult.fail("认领公告id缺失,请先手动同步金蝶收款流水"); + } + String claimusers = UserCache.getUserRealName(AuthUtil.getUserId()); + if (Func.isEmpty(claimusers)) { + throw new ServiceException("当前用户无姓名,无法推送金蝶收款认领"); + } + // 推送前对账:以当前用户身份重查 A301-2 拿最新公告(手动推送者须在公告认领范围内) + BfiClaimAnnounceVO announce = queryClaimAnnounces(claimusers).stream() + .filter(item -> flow.getBillId().equals(item.getId())) + .findFirst().orElse(null); + if (announce == null) { + return ReceiptClaimPushResult.fail("金蝶侧未查询到该认领公告,当前用户可能不在公告认领范围内,请重新同步后重试"); + } + return pushClaimLocked(flow.getId(), announce); + } + /** + * 定时推送金蝶:扫描认领完成且未推送过的流水,按认领人姓名分组重查 A301-2 最新公告后逐条推送 + *

+ * A301-2 的 claimusers 必填且为单个用户姓名,定时任务无登录人上下文, + * 故取候选流水的有效认领人姓名去重后逐人查询公告,按公告id(bill_id)匹配流水。 + * 单条流水推送失败独立消化,不影响同批其他流水。 + */ + @Override + public String pushClaimByTask() { + List candidates = list(Wrappers.lambdaQuery() + .isNotNull(KingdeeReceiptFlow::getBillId) + .in(KingdeeReceiptFlow::getPushStatus, List.of( + KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_NONE, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_FAILED)) + .eq(KingdeeReceiptFlow::getClaimStatus, CLAIMED) + .eq(KingdeeReceiptFlow::getStatus, 1) + .orderByAsc(KingdeeReceiptFlow::getId)); + if (candidates.isEmpty()) { + return "无待推送流水"; + } + List flowIds = candidates.stream().map(KingdeeReceiptFlow::getId).toList(); + Set claimerNames = claimMapper.selectList(Wrappers.lambdaQuery() + .in(ReceiptClaim::getReceiptFlowId, flowIds) + .eq(ReceiptClaim::getClaimStatus, CLAIMED)).stream() + .map(ReceiptClaim::getClaimerName) + .filter(Func::isNotEmpty) + .collect(Collectors.toSet()); + if (claimerNames.isEmpty()) { + return "待推送流水 " + candidates.size() + " 条,但无有效认领人,跳过"; + } + // 公告按公告id归集(多个认领人查询到同一公告时取首条) + Map announceByBillId = new HashMap<>(); + for (String claimerName : claimerNames) { + try { + for (BfiClaimAnnounceVO announce : queryClaimAnnounces(claimerName)) { + if (announce.getId() != null) { + announceByBillId.putIfAbsent(announce.getId(), announce); + } + } + } catch (Exception exception) { + log.error("定时推送收款认领:认领人[{}]公告查询失败", claimerName, exception); + } + } + int successCount = 0; + int failedCount = 0; + int skippedCount = 0; + for (KingdeeReceiptFlow flow : candidates) { + BfiClaimAnnounceVO announce = announceByBillId.get(flow.getBillId()); + if (announce == null) { + skippedCount++; + log.info("定时推送收款认领:流水[{}]未在认领人公告查询结果中命中,跳过", flow.getId()); + continue; + } + try { + ReceiptClaimPushResult result = pushClaimLocked(flow.getId(), announce); + if (result.isSuccess()) { + successCount++; + } else { + failedCount++; + } + } catch (Exception exception) { + failedCount++; + log.error("定时推送收款认领:流水[{}]推送异常", flow.getId(), exception); + } + } + String summary = "扫描 " + candidates.size() + " 条,推送成功 " + successCount + + " 条,推送失败 " + failedCount + " 条,公告未命中跳过 " + skippedCount + " 条"; + log.info("定时推送收款认领完成:{}", summary); + return summary; + } + + /** + * 事务内推送:流水行锁覆盖闸门校验、对账、推送与状态落库全过程, + * 防定时任务与手动按钮并发对同一流水双推 + */ + private ReceiptClaimPushResult pushClaimLocked(Long flowId, BfiClaimAnnounceVO announce) { + return transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce)); + } + + /** + * 推送单条流水:静态闸门 → 最新公告对账 → 按流水归集分摊行推送 → 成败状态落库 + */ + private ReceiptClaimPushResult doPushClaimToKingdee(Long flowId, BfiClaimAnnounceVO announce) { + KingdeeReceiptFlow flow = lockedFlow(flowId); + if (flow.getBillId() == null) { + return ReceiptClaimPushResult.fail("认领公告id缺失,请先手动同步金蝶收款流水"); + } + // 终态闸门:已推送流水不允许二次推送(作废重认领使等式再满足也不推,金蝶侧人工处置) + if (KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED.equals(flow.getPushStatus())) { + return ReceiptClaimPushResult.fail("该流水已推送金蝶,不允许重复推送"); + } + // 推送前对账:以最新公告刷新公告侧未认领金额并重算认领状态(快照落库,页面口径同步修正) + flow.setUnclaimedAmount(money(announce.getUnclaimamount())); + flow.setKingdeeClaimStatus(trimToNull(announce.getClaimstatus())); + flow.setClaimMe(announce.getClaimme()); + BigDecimal claimedTotal = flowClaimedAmount(flow.getId()); + flow.setClaimedAmount(claimedTotal); + String claimStatus = amountClaimStatus(claimedTotal, flow.getUnclaimedAmount()); + flow.setClaimStatus(claimStatus); + updateById(flow); + if (!CLAIMED.equals(claimStatus)) { + return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与公告未认领金额(" + + money(flow.getUnclaimedAmount()) + ")不一致,不满足推送条件"); + } + // 推送来源编码:首次生成落库,重试复用(金蝶按 awrp_src_sys + awrp_src_code 幂等,防重试双单) + if (Func.isEmpty(flow.getAwrpSrcCode())) { + flow.setAwrpSrcCode(buildAwrpSrcCode(flow.getId())); + } + // 分录取该流水全部有效分摊行(结算单id升序,跨认领主单归集) + List allocations = claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptFlowId, flow.getId()) + .eq(ReceiptClaimSettlement::getStatus, 1) + .orderByAsc(ReceiptClaimSettlement::getFormalSettlementId)); + if (allocations.isEmpty()) { + return ReceiptClaimPushResult.fail("该流水无有效认领分摊,无法推送"); + } + List settlementIds = allocations.stream() + .map(ReceiptClaimSettlement::getFormalSettlementId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map settlements = settlementIds.isEmpty() ? Map.of() + : formalSettlementMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlement::getId, settlementIds)).stream() + .collect(Collectors.toMap(FormalSettlement::getId, Function.identity(), (first, second) -> first)); + + BfiClaimPushDTO param = buildClaimPushDTO(flow, allocations, settlements); + log.info("调用BFI接口 收款认领结果推送开始 flowId:{},billId:{},awrpSrcCode:{},分摊明细 {} 行,认领金额 {}", + flow.getId(), flow.getBillId(), flow.getAwrpSrcCode(), allocations.size(), claimedTotal); + try { + BfiBillSaveResultVO result = bfiService.pushClaimResult(param); + return applyClaimPushSuccess(flow, extractClaimPushBillNo(result)); + } catch (Exception exception) { + return applyClaimPushFailure(flow, exception); + } + } + + /** + * A301-3 推送报文组装:billid=公告id,分录取各分摊行(一笔流水一张金蝶收款单) + */ + private BfiClaimPushDTO buildClaimPushDTO(KingdeeReceiptFlow flow, List allocations, + Map settlements) { BfiClaimPushDTO param = new BfiClaimPushDTO(); // 【必填】认领公告id param.setBillid(flow.getBillId()); // 【必填】来源系统编码 param.setAwrpSrcSys(bfiProperties.getSrcSys()); - // 【必填】来源系统单据编码 - param.setAwrpSrcCode(awrpSrcCode); + // 【必填】来源系统单据编码(按流水生成,重试复用) + param.setAwrpSrcCode(flow.getAwrpSrcCode()); // 【必填】收款类型.编码 param.setRecpaytypeNumber(KingdeeBfiConstant.BFI_CLAIM_REC_PAY_TYPE); // 【必填】付款人类型 @@ -659,42 +820,32 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl entries = new ArrayList<>(); - BigDecimal allocatedTotal = BigDecimal.ZERO; - for (FormalSettlement settlement : settlements) { + int seq = 1; + for (ReceiptClaimSettlement allocation : allocations) { BfiClaimPushEntryDTO entry = new BfiClaimPushEntryDTO(); - // 【必填】应收金额 - entry.setEReceivableamt(allocationMap.get(settlement.getId())); - // 结算单id - entry.setSeq(settlement.getId()); - if (Func.isNotEmpty(settlement.getKingdeeBillNo())) { + // 【必填】应收金额(分摊金额) + entry.setEReceivableamt(money(allocation.getAllocatedReceiptAmount())); + // 分录行序号 + entry.setSeq((long) seq++); + FormalSettlement settlement = settlements.get(allocation.getFormalSettlementId()); + if (settlement != null && Func.isNotEmpty(settlement.getKingdeeBillNo())) { // 结算单已同步金蝶应收单时携带核心单据引用,金蝶按应收单号核销认领金额 // 核心单据类型 // entry.setECorebilltype(KingdeeBfiConstant.BFI_CLAIM_CORE_BILL_TYPE); // 核心单据编号 - entry.setECorebillno(String.valueOf(settlement.getId())); + entry.setECorebillno(settlement.getKingdeeBillNo()); } - allocatedTotal = allocatedTotal.add(money(allocationMap.get(settlement.getId()))); entries.add(entry); } // 【必填】单据体分录 param.setEntryentity(entries); - - log.info("调用BFI接口 收款认领结果推送开始 claimId:{},billId:{},awrpSrcCode:{},分摊结算单 {} 张,分摊金额 {}", - claim.getId(), flow.getBillId(), awrpSrcCode, settlements.size(), allocatedTotal); - BfiBillSaveResultVO result = bfiService.pushClaimResult(param); - applyClaimPushResult(claim, result); - claimMapper.updateById(claim); - log.info("调用BFI接口 收款认领结果推送成功 claimId:{},已回写金蝶状态:{}", - claim.getId(), claim.getKingdeeBillStatus()); + return param; } /** - * 解析 A301-3 推送结果并回写认领记录 - * - * @param claim 认领记录 - * @param result 推送结果 + * 解析 A301-3 推送结果:全项成功返回金蝶收款单号,否则抛出汇总错误文本(由失败路径落库) */ - private void applyClaimPushResult(ReceiptClaim claim, BfiBillSaveResultVO result) { + private String extractClaimPushBillNo(BfiBillSaveResultVO result) { if (result == null || result.getResult() == null || result.getResult().isEmpty()) { throw new ServiceException("金蝶认领结果推送失败,返回结果为空"); } @@ -708,12 +859,91 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpllambdaUpdate() + .eq(KingdeeReceiptFlow::getId, flow.getId()) + .set(KingdeeReceiptFlow::getPushStatus, flow.getPushStatus()) + .set(KingdeeReceiptFlow::getPushFailReason, null) + .set(KingdeeReceiptFlow::getPushedTime, flow.getPushedTime()) + .set(KingdeeReceiptFlow::getAwrpSrcCode, flow.getAwrpSrcCode()) + .set(KingdeeReceiptFlow::getKingdeeBillNo, kingdeeBillNo) + .set(KingdeeReceiptFlow::getKingdeeBillStatus, flow.getKingdeeBillStatus())); + // 级联刷新名下未作废认领主单的冗余展示字段 + claimMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceiptClaim::getReceiptFlowId, flow.getId()) + .eq(ReceiptClaim::getClaimStatus, CLAIMED) + .set(ReceiptClaim::getKingdeeBillNo, kingdeeBillNo) + .set(ReceiptClaim::getKingdeeBillStatus, KingdeeBfiConstant.KINGDEE_STATUS_PUSHED)); + record(flow.getId(), null, "push", "推送金蝶收款认领", CLAIMED, CLAIMED, flow.getClaimedAmount(), + "金蝶收款单号:" + (Func.isEmpty(kingdeeBillNo) ? "响应未返回" : kingdeeBillNo)); + return ReceiptClaimPushResult.success(kingdeeBillNo); + } + + /** + * 推送失败:流水置推送失败并记录原因,本地认领数据保留,可重新推送(重试复用同一来源编码) + */ + private ReceiptClaimPushResult applyClaimPushFailure(KingdeeReceiptFlow flow, Exception exception) { + String reason = Func.isEmpty(exception.getMessage()) + ? exception.getClass().getSimpleName() : exception.getMessage(); + reason = truncateText(reason, 1000); + log.error("调用BFI接口 收款认领结果推送失败 flowId:{},billId:{},awrpSrcCode:{}", + flow.getId(), flow.getBillId(), flow.getAwrpSrcCode(), exception); + this.update(Wrappers.lambdaUpdate() + .eq(KingdeeReceiptFlow::getId, flow.getId()) + .set(KingdeeReceiptFlow::getPushStatus, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_FAILED) + .set(KingdeeReceiptFlow::getPushFailReason, reason) + .set(KingdeeReceiptFlow::getAwrpSrcCode, flow.getAwrpSrcCode())); + record(flow.getId(), null, "push", "推送金蝶收款认领", CLAIMED, CLAIMED, flow.getClaimedAmount(), + "推送失败:" + truncateText(reason, 200)); + return ReceiptClaimPushResult.fail("金蝶认领结果推送失败:" + reason); + } + + /** + * 推送来源编码:SKCL + 时间戳 + 流水id尾号6位(按流水生成,重试复用) + */ + private String buildAwrpSrcCode(Long flowId) { + String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + String suffix = String.valueOf(flowId); + return "SKCL" + time + suffix.substring(Math.max(0, suffix.length() - 6)); + } + + /** + * A301-2 收款认领公告查询:按认领人姓名全量翻页,时间窗口取 Nacos 配置(默认近半年)。 + * 认领公告同步、手动推送对账与定时任务对账共用。 + */ + private List queryClaimAnnounces(String claimusers) { + int months = bfiProperties.getClaimAnnounce().getMonths(); + LocalDateTime endDate = LocalDateTime.now(); + LocalDateTime startDate = endDate.minusMonths(months); + long start = System.currentTimeMillis(); + log.info("调用BFI接口 收款认领公告查询开始 claimusers:{},时间窗口:{} ~ {}(近{}个月)", + claimusers, startDate.format(BFI_DATE_TIME_FORMATTER), endDate.format(BFI_DATE_TIME_FORMATTER), months); + BfiClaimAnnounceQueryDTO queryDTO = new BfiClaimAnnounceQueryDTO(); + queryDTO.setClaimusers(claimusers); + queryDTO.setModifytimeS(startDate.format(BFI_DATE_TIME_FORMATTER)); + queryDTO.setModifytimeE(endDate.format(BFI_DATE_TIME_FORMATTER)); + queryDTO.setPageSize(SYNC_PAGE_SIZE); + List announces = BfiUtil.queryAllPages(pageNo -> { + queryDTO.setPageNo(pageNo); + return bfiService.queryClaimAnnounce(queryDTO); + }); + log.info("调用BFI接口 收款认领公告查询结束 claimusers:{},耗时:{}ms,拉取 {} 条", + claimusers, System.currentTimeMillis() - start, announces.size()); + return announces; } /** @@ -779,11 +1009,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl= 0 ? CLAIMED : PARTIAL; + return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? CLAIMED : PARTIAL; } private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) { @@ -826,7 +1059,20 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl + * 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 + * is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for their own commercial purposes. + *

+ * Using this software signifies agreement to this license agreement and terms of use, + * and you expressly agree to be bound by the terms of use of this software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.transport.service.IReceiptFlowService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 收款认领推送金蝶定时任务(A301-3 收款认领结果推送) + *

+ * 认领动作只在本地登记,本任务扫描「认领完成且未推送过」的收款流水自动推送金蝶, + * 与收款流水页「推送金蝶」手动按钮共用同一服务方法(推送前按认领人重查 A301-2 最新公告对账)。 + * 单条流水推送失败独立消化、可重试,不影响同批其他流水。 + *

+ * 调度配置(Nacos thirdParty.bfi.receiptClaimPush.*):syncEnabled 开关、cron 表达式。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ReceiptClaimPushTask { + + private final IReceiptFlowService receiptFlowService; + + /** 收款认领推送开关,默认关闭(联调稳定后开启) */ + @Value("${thirdParty.bfi.receiptClaimPush.syncEnabled:false}") + private boolean syncEnabled; + + /** 收款认领推送调度,默认每10分钟执行一次 */ + @Scheduled(cron = "${thirdParty.bfi.receiptClaimPush.cron:0 */10 * * * ?}") + public void pushClaimToKingdee() { + if (!syncEnabled) { + return; + } + try { + String summary = receiptFlowService.pushClaimByTask(); + log.info("定时推送收款认领:{}", summary); + } catch (Exception exception) { + log.error("定时推送收款认领异常", exception); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java index f0997553..b4212704 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java @@ -58,9 +58,22 @@ public class ReceiptFlowWrapper extends BaseEntityWrapper "未认领"; default -> Objects.toString(entity.getKingdeeClaimStatus(), ""); }); - BigDecimal receiptAmount = money(entity.getReceiptAmount()); + vo.setPushStatusName(switch (Objects.toString(entity.getPushStatus(), "")) { + case "pushed" -> "已推送"; + case "failed" -> "推送失败"; + default -> "未推送"; + }); + vo.setKingdeeBillStatusName(switch (Objects.toString(entity.getKingdeeBillStatus(), "")) { + case "pushed" -> "已推送"; + case "approved" -> "审核通过"; + case "received" -> "已收款"; + case "failed" -> "处理失败"; + default -> "未推送"; + }); + // 剩余可认领金额 = 公告未认领金额 − 累计认领金额(认领上限为公告口径) + BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount()); BigDecimal claimedAmount = money(entity.getClaimedAmount()); - vo.setRemainingAmount(receiptAmount.subtract(claimedAmount).max(BigDecimal.ZERO)); + vo.setRemainingAmount(unclaimedAmount.subtract(claimedAmount).max(BigDecimal.ZERO)); vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); return vo;