✨ 新增金蝶付款结果定时回写任务(A103-2/A103-4)并抽取BFI分页通用翻页工具

This commit is contained in:
刘泉佳
2026-09-17 16:40:01 +08:00
parent e18c7dcfda
commit bdd1d7571f
15 changed files with 860 additions and 21 deletions
@@ -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 {
@@ -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";
/**
* 按中文名称从映射字典转换单据类型编码。
*
@@ -100,4 +100,8 @@ public class PaymentApplicationController extends BladeController {
public R<List<String>> syncKingdeeBatch(@RequestBody List<Long> ids) {
return R.data(paymentApplicationService.syncKingdeeBatch(ids));
}
@PostMapping("/sync-kingdee-result")
@ApiOperationSupport(order = 12)
@Operation(summary = "手动回写金蝶付款结果(A103-2/A103-4)")
public R<String> syncKingdeeResult() { return R.data(paymentApplicationService.syncKingdeePaymentResult()); }
}
@@ -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<PaymentApplicati
void voidBill(PaymentApplicationStatusRequest request);
String syncKingdee(Long id);
List<String> syncKingdeeBatch(List<Long> 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);
}
@@ -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<PaymentApplic
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
/** A103-2 按 awrp_src_code 批量查询单次条数上限 */
private static final int PAY_RESULT_QUERY_BATCH = 50;
/** A103-2 / A103-4 分页查询页大小 */
private static final int PAY_RESULT_PAGE_SIZE = 200;
/** 历史模拟付款记录单号前缀 */
private static final String MOCK_PAYMENT_RECORD_PREFIX = "MOCK-PAY-";
private final PaymentApplicationInvoiceMapper invoiceMapper;
private final PaymentApplicationRecordMapper recordMapper;
private final PaymentApplicationSettlementMapper settlementRelationMapper;
@@ -212,7 +228,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setPaymentNo(nextNo());
entity.setApprovalStatus(DRAFT);
entity.setCurrentNode("草稿");
entity.setKingdeeStatus("unsynced");
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED);
entity.setPaidAmount(BigDecimal.ZERO);
entity.setInvoiceStatus("unmatched");
}
@@ -330,7 +346,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
public String syncKingdee(Long id) {
PaymentApplication entity = lockedPayment(id);
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据");
if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo();
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_CLOSED.equals(entity.getKingdeeStatus())) {
// 存量数据金蝶单据id可能仍存于 kingdeeBillNo,兜底返回
return Func.isNotEmpty(entity.getKingdeeBillId()) ? entity.getKingdeeBillId() : entity.getKingdeeBillNo();
}
String kingdeeBillId;
try {
if ("settlement_payment".equals(entity.getPaymentType())) {
@@ -352,13 +374,17 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
markSyncFailed(entity);
throw new ServiceException("推送金蝶付款申请失败:" + exception.getMessage());
}
entity.setKingdeeBillNo(kingdeeBillId);
entity.setKingdeeStatus("synced");
entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
mockPaymentRecords(entity);
// 星瀚单据内部主键落 kingdeeBillId(附件上传等使用);
// 金蝶单据编号(billno)与付款状态由定时回写任务经 A103-2/A103-4 查询回填
entity.setKingdeeBillId(kingdeeBillId);
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
// 付款完成模拟逻辑已停用(mockPaymentRecords / 履约付款数据直接置 paid),
// 真实付款结果改由定时回写任务驱动;联调环境需要模拟时恢复以下注释即可
// entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
// mockPaymentRecords(entity);
// syncPreSettlementAdvance(entity);
// syncFormalSettlementPayments(entity);
updateById(entity);
syncPreSettlementAdvance(entity);
syncFormalSettlementPayments(entity);
return kingdeeBillId;
}
@@ -379,6 +405,282 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return result;
}
@Override
public String syncKingdeePaymentResult() {
// ① 待回写付款申请:审批通过且金蝶状态为 已生成/付款中(终态已付款/已关闭不再轮询)
List<PaymentApplication> applications = list(Wrappers.<PaymentApplication>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<String, BfiPayApplyVO> payApplyMap = new HashMap<>();
if (!applications.isEmpty()) {
// ② A103-2 付款申请单查询:按 paymentNo(awrp_src_code)分批查询,同一来源单号取最近修改的一条
List<String> paymentNos = applications.stream()
.map(PaymentApplication::getPaymentNo).filter(Func::isNotEmpty).distinct().toList();
for (int from = 0; from < paymentNos.size(); from += PAY_RESULT_QUERY_BATCH) {
List<String> 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<Long, PaymentApplication> 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.<PaymentApplication>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<String, List<PaymentApplication>> 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<String, BfiPayBillVO> payBillBySourceId = new HashMap<>();
Map<String, BfiPayBillVO> 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.<PaymentApplicationRecord>lambdaQuery()
.eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId())
.likeRight(PaymentApplicationRecord::getPaymentNo, MOCK_PAYMENT_RECORD_PREFIX));
String billNo = payBill.getBillno();
PaymentApplicationRecord record = recordMapper.selectList(Wrappers.<PaymentApplicationRecord>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<BfiPayApplyVO> queryPayApplyPages(List<String> 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<BfiPayBillVO> 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<String, BfiPayBillVO> payBillBySourceId,
Map<String, BfiPayBillVO> 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.<PaymentApplicationRecord>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 BaseServiceImpl<PaymentApplic
String billDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
BigDecimal applyAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
BfiApBillSaveDTO param = new BfiApBillSaveDTO();
// 【必填】单据类型.编码
param.setBilltypeidNumber(billtypeNumber);
// 【必填】结算组织.编码
param.setOrgNumber(orgNumber);
// 【必填】单据日期
param.setBizdate(billDateText);
// 【必填】往来类型(bd_supplier:供应商)
param.setAsstacttype(KingdeeBfiConstant.BFI_SUPPLIER_ASSTACT_TYPE);
// 【必填】往来户.编码
param.setAsstactNumber(bfiSupplierCode(entity));
// 【必填】付款方式(CASH:现购, CREDIT:赊购)
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE);
// 【必填】结算币别.货币代码
param.setCurrencyNumber(currency);
// 【必填】付款组织.编码
param.setPayorgNumber(orgNumber);
param.setPurorgNumber(orgNumber);
// 采购组织.编码(非必传)
// param.setPurorgNumber(orgNumber);
// 【必填】款项性质.编码
param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AP_BILL_PAY_PROPERTY);
// TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)需金蝶侧确认后补充,当前不传
param.setIsincludetax(true);
param.setIspricetotal(true);
// 录入含税价(非必传)
// param.setIsincludetax(true);
// 录入总价,为 true 后可不录入单价(非必传)
// param.setIspricetotal(true);
// 汇率表.编码(非必传)
param.setExratetableNumber(bfiPayApplyExratetable);
// 汇率日期(非必传)
param.setExratedate(billDateText);
param.setAsstactname(entity.getPayeeName());
// 往来单位名称(非必传)
// param.setAsstactname(entity.getPayeeName());
// 备注(非必传)
param.setRemark(entity.getPaymentNo() + (Func.isEmpty(entity.getSettlementNo()) ? "" : " " + entity.getSettlementNo()));
// 【必填】外部系统编号(候选键)
param.setAwrpSrcSys(bfiPayApplySrcSys);
// 【必填】外部系统来源单据号(候选键)
param.setAwrpSrcCode(entity.getPaymentNo());
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
// 上游外部系统单据号/上游合同号(非必传)
if (Func.isNotEmpty(entity.getContractNo())) {
param.setAwrpSrcConNo(entity.getContractNo());
}
// 【必填】外部单据类型
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE);
BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO();
// 【必填】物料编码.编码
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL);
// 【必填】计量单位.编码
entry.setMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASURE_UNIT);
// 无吨位数据:数量固定 1,配合 ispricetotal=true 录入总价(含税应付金额)
// 【必填】数量(无吨位数据:数量固定 1,配合 ispricetotal=true 录入总价)
entry.setQuantity(BigDecimal.ONE);
// 应付金额(含税)(非必传)
entry.setEPricetaxtotal(applyAmount);
// 【必填】项目.项目编码
entry.setProjectNumber(bfiProjectNumber(entity));
// 应付单分录唯一标识,A103-1 下推时以 awrp_finapbill_entryid 关联此行
// 【必填】外部系统来源分录标识(候选键),A103-1 下推时以 awrp_finapbill_entryid 关联此行
entry.setAwrpEntryCode(entity.getPaymentNo() + "-1");
// 分录备注(非必传)
entry.setERemark(entity.getRemark());
param.setDetailentry(List.of(entry));
@@ -599,7 +927,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
private void markSyncFailed(PaymentApplication entity) {
entity.setKingdeeStatus("failed");
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_FAILED);
updateById(entity);
}
@@ -652,6 +980,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return project == null || Func.isEmpty(project.getProjectCode()) ? KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE : project.getProjectCode();
}
/**
* 付款完成模拟(已停用):原在同步成功后模拟付款完成数据(paidAmount = 申请金额、MOCK-PAY- 前缀付款记录)。
* 真实付款结果现由定时回写任务 syncKingdeePaymentResult 经 A103-2/A103-4 查询回填驱动,
* 暂保留本方法以备联调环境无金蝶付款数据时模拟使用。
*
* @param entity 付款申请实体
*/
private void mockPaymentRecords(PaymentApplication entity) {
recordMapper.delete(Wrappers.<PaymentApplicationRecord>lambdaQuery()
.eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId()));
@@ -0,0 +1,64 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* 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;
/**
* 金蝶付款结果定时回写任务
* <p>
* 定时查询金蝶(星瀚)付款结果并更新付款信息与履约付款数据:
* A103-2 付款申请单查询回填金蝶单据编号与单据状态,A103-4 付款处理查询回填付款记录。
* 业务逻辑收敛在 {@link IPaymentApplicationService#syncKingdeePaymentResult()},便于手动触发联调。
* <p>
* 调度配置(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);
}
}
}
@@ -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<PaymentApplicat
default -> 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;