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;