feat: 正式结算单/开票申请同步金蝶后联带锁单,支持解锁回调与金蝶删单还原
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录实体类
|
||||
* <p>
|
||||
* 一行 = 一张金蝶目标单:推送成功后 lockBusiData 锁定(FAILED→LOCKED),金蝶经 unLockNotifyBusiData
|
||||
* 回调解锁(→UNLOCKED,DELETE 时废除源单并还原关联数据);同时承担当前锁状态(编辑守卫/回调定位)
|
||||
* 与锁定补偿扫表(FAILED+try_count)、锁定/解锁留痕
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_kingdee_bill_lock")
|
||||
@Schema(description = "金蝶单据锁定记录")
|
||||
public class KingdeeBillLock extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY,预留 PAY_APPLY/RECEIVED_INVOICE)
|
||||
*/
|
||||
private String sourceBillType;
|
||||
/**
|
||||
* WLTMS 业务主键
|
||||
*/
|
||||
private Long sourceBillId;
|
||||
/**
|
||||
* WLTMS 单号(正式结算单号/开票申请单号)
|
||||
*/
|
||||
private String sourceBillNo;
|
||||
/**
|
||||
* 金蝶单据类型(ApFin_other_BT_S/arfin_other_BT_S/cdm_paybill_bank_elect_BT_S 等固定值)
|
||||
*/
|
||||
private String targetBillType;
|
||||
/**
|
||||
* 金蝶单据编号(即业务单 kingdeeBillNo,开票申请按发票张次逐张一行)
|
||||
*/
|
||||
private String targetBillNo;
|
||||
/**
|
||||
* 锁状态:failed-锁定失败(补偿重试对象)/locked-金蝶锁定中/unlocked-已解锁(终态)
|
||||
*/
|
||||
private String lockStatus;
|
||||
/**
|
||||
* 最近动作来源操作类型:SAVE-单据新增,DELETE-单据删除(DELETE 回调触发废除还原)
|
||||
*/
|
||||
private String operationType;
|
||||
/**
|
||||
* 锁定重试次数(补偿任务用,超上限停试)
|
||||
*/
|
||||
private Integer tryCount;
|
||||
/**
|
||||
* 锁定时间
|
||||
*/
|
||||
private LocalDateTime lockTime;
|
||||
/**
|
||||
* 解锁时间
|
||||
*/
|
||||
private LocalDateTime unlockTime;
|
||||
/**
|
||||
* 推单发起人(锁定操作人)
|
||||
*/
|
||||
private String lockOperator;
|
||||
/**
|
||||
* 回调操作人(金蝶侧操作人)
|
||||
*/
|
||||
private String notifyOperator;
|
||||
/**
|
||||
* 锁定失败原因
|
||||
*/
|
||||
private String failReason;
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定/解锁(lockBusiData / unLockNotifyBusiData)固定值。
|
||||
* <p>lockBusiData 由 WLTMS 调金蝶(源系统 WLTMS、目标系统 XH),
|
||||
* unLockNotifyBusiData 由金蝶调 WLTMS(源系统 XH、目标系统 WLTMS);
|
||||
* WLTMS 单据类型自定义,金蝶目标单据类型为固定值</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class KingdeeLockConstant {
|
||||
|
||||
/** 锁动作:锁定 */
|
||||
public static final String ACTION_LOCK = "LOCK";
|
||||
|
||||
/** 锁动作:解锁 */
|
||||
public static final String ACTION_UNLOCK = "UNLOCK";
|
||||
|
||||
/** 来源操作类型:单据新增 */
|
||||
public static final String OPERATION_SAVE = "SAVE";
|
||||
|
||||
/** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
|
||||
public static final String OPERATION_DELETE = "DELETE";
|
||||
|
||||
/** 系统标识:WLTMS(运输管理系统,即本系统) */
|
||||
public static final String SYSTEM_WLTMS = "WLTMS";
|
||||
|
||||
/** 系统标识:金蝶星瀚 XH */
|
||||
public static final String SYSTEM_XH = "XH";
|
||||
|
||||
/** 锁状态:锁定失败(补偿定时任务的重试对象) */
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
|
||||
/** 锁状态:金蝶锁定中(源单冻结,不可编辑/作废) */
|
||||
public static final String STATUS_LOCKED = "locked";
|
||||
|
||||
/** 锁状态:已解锁(终态;同单重推生成新锁记录) */
|
||||
public static final String STATUS_UNLOCKED = "unlocked";
|
||||
|
||||
/** WLTMS 单据类型:正式结算单(应付/应收共用,按结算单 settlementType 区分金蝶目标类型) */
|
||||
public static final String SOURCE_SETTLEMENT = "SETTLEMENT";
|
||||
|
||||
/** WLTMS 单据类型:开票申请单 */
|
||||
public static final String SOURCE_INVOICE_APPLY = "INVOICE_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_PAY_APPLY = "PAY_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_RECEIVED_INVOICE = "RECEIVED_INVOICE";
|
||||
|
||||
/** 金蝶目标单据类型:财务应付单(A102-2,应付结算单推送后锁定) */
|
||||
public static final String TARGET_AP_BILL = "ApFin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:财务应收单(A302-2,应收结算单推送后锁定) */
|
||||
public static final String TARGET_AR_BILL = "arfin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:开票单(A303-1 开票申请单,按发票张次逐张一行锁记录) */
|
||||
public static final String TARGET_INVOICE_BILL = "cdm_paybill_bank_elect_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String TARGET_PAY_APPLY = "ap_payapply_oth_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String TARGET_RECEIVED_INVOICE = "cdm_recbill_bank_BT_S";
|
||||
|
||||
/** 锁定重试上限:补偿任务超过该次数不再自动重试,人工排查(失败原因见 fail_reason) */
|
||||
public static final int RETRY_MAX_COUNT = 5;
|
||||
|
||||
/** 金蝶目标单据类型 -> WLTMS 单据类型反查映射(解锁回调按 targetBillType 分发废除还原用) */
|
||||
public static final Map<String, String> TARGET_TO_SOURCE_MAP = Map.ofEntries(
|
||||
Map.entry(TARGET_AP_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_AR_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_INVOICE_BILL, SOURCE_INVOICE_APPLY),
|
||||
Map.entry(TARGET_PAY_APPLY, SOURCE_PAY_APPLY),
|
||||
Map.entry(TARGET_RECEIVED_INVOICE, SOURCE_RECEIVED_INVOICE)
|
||||
);
|
||||
|
||||
private KingdeeLockConstant() {
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定回调 控制器
|
||||
* <p>
|
||||
* unLockNotifyBusiData:金蝶(星瀚)侧解锁/删单后回调 WLTMS 送结果,
|
||||
* 无登录态公开端点(模式同 CustomerArchivePublicController);
|
||||
* operationType=DELETE 时 WLTMS 废除源单并还原关联数据。
|
||||
* 应答与 lockBusiData 出参同构:{updateCount, updateResultList},逐单处理(1-成功,0-未匹配/失败)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@TenantIgnore
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@RequestMapping("kingdee-bfi")
|
||||
@Tag(name = "金蝶单据锁定回调", description = "金蝶星瀚调用:单据解锁/删单通知")
|
||||
public class KingdeeBfiPublicController {
|
||||
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/**
|
||||
* 单据解锁通知(unLockNotifyBusiData):多张单据编号逗号拼接批量回调,逐张处理
|
||||
*
|
||||
* @param param 金蝶回调参数
|
||||
* @return 处理结果,与 lockBusiData 出参同构
|
||||
*/
|
||||
@PostMapping("unLockNotifyBusiData")
|
||||
@Operation(summary = "单据解锁通知", description = "金蝶侧解锁/删单后回调;operationType=DELETE 时废除源单并还原关联数据")
|
||||
public BfiBillLockResultVO unLockNotifyBusiData(@RequestBody BfiBillLockDTO param) {
|
||||
log.info("金蝶单据解锁回调:{}", JSON.toJSONString(param));
|
||||
try {
|
||||
return BfiBillLockResultVO.of(kingdeeBillLockService.onUnlockNotify(param));
|
||||
} catch (Exception exception) {
|
||||
log.error("金蝶单据解锁回调处理失败:lockAction={},operationType={},targetBillType={},targetBillNo={}",
|
||||
param == null ? null : param.getLockAction(), param == null ? null : param.getOperationType(),
|
||||
param == null ? null : param.getTargetBillType(), param == null ? null : param.getTargetBillNo(), exception);
|
||||
return BfiBillLockResultVO.of(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
|
||||
/** 金蝶单据锁定记录 Mapper @author Chill */
|
||||
@Mapper
|
||||
public interface KingdeeBillLockMapper extends BaseMapper<KingdeeBillLock> {
|
||||
}
|
||||
+9
@@ -42,6 +42,15 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
|
||||
void voidBill(FormalSettlementStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 金蝶删单回调废除(unLockNotifyBusiData DELETE 分发):作废结算单、同步信息还原推送前状态
|
||||
* (kingdeeSyncStatus→unsynced,清单据编号/星瀚内码/同步时间),操作人取金蝶回调报文
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
void voidByKingdee(Long id, String operator);
|
||||
|
||||
/**
|
||||
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用):
|
||||
* A102-2/A302-2 保存并回写结算单单据编号与星瀚内码(kingdeeBillId)
|
||||
|
||||
+9
@@ -56,6 +56,15 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
|
||||
void voidBill(InvoiceApplicationStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 金蝶删单回调作废(unLockNotifyBusiData DELETE 分发):作废开票申请、同步信息还原
|
||||
* (kingdeeStatus→unsynced,清单据编号/同步时间)并刷新关联结算单开票状态,操作人取金蝶回调报文
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
void voidByKingdee(Long id, String operator);
|
||||
|
||||
/**
|
||||
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用):
|
||||
* A303-1 保存并回写开票申请单单据编号与星瀚内码(kingdeeBillId)
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IKingdeeBillLockService extends IService<KingdeeBillLock> {
|
||||
|
||||
/**
|
||||
* 推送金蝶成功后锁单(由各推送链路在主单事务提交后调用):
|
||||
* 每张金蝶目标单一行锁记录(先落 failed 再调 lockBusiData,成功翻 locked);
|
||||
* 同源单的目标单合并为一次调用(编号逗号拼接批量传参);已 locked 幂等跳过,failed 原地重锁;
|
||||
* 异常独立捕获,不影响主单同步状态
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY)
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
* @param sourceBillNo WLTMS 单号
|
||||
* @param targetBillType 金蝶目标单据类型
|
||||
* @param targetBillNos 金蝶单据编号集合(开票申请为多张)
|
||||
*/
|
||||
void lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos);
|
||||
|
||||
/**
|
||||
* 金蝶解锁回调(unLockNotifyBusiData):校验来源/目标系统与 lockAction,
|
||||
* targetBillNo 支持逗号拼接批量,逐张按 targetBillType+targetBillNo 定位最新 locked 锁记录;
|
||||
* operationType=DELETE 时按单据类型分发废除源单并还原关联数据
|
||||
*
|
||||
* @param param 金蝶回调参数
|
||||
* @return 处理单据数(应答 updateCount)
|
||||
*/
|
||||
int onUnlockNotify(BfiBillLockDTO param);
|
||||
|
||||
/**
|
||||
* 编辑守卫:源单存在 locked 锁记录时抛 ServiceException(金蝶锁定中不可修改)
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
*/
|
||||
void checkEditable(String sourceBillType, Long sourceBillId);
|
||||
|
||||
/**
|
||||
* 锁定补偿:扫 failed 且未超重试上限的锁记录重调 lockBusiData(由定时任务调用)
|
||||
*
|
||||
* @return 补偿摘要
|
||||
*/
|
||||
String retryFailedLocks();
|
||||
}
|
||||
+60
@@ -41,6 +41,7 @@ import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
@@ -82,6 +83,7 @@ import org.springblade.transport.pojo.vo.PreSettlementVO;
|
||||
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
@@ -131,6 +133,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
/** 金蝶票池认领闸门(收票服务承载 A801-1 重查与可用性口径) */
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
/** 金蝶单据锁定记录服务(lockBusiData 锁定/补偿/解锁守卫) */
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/** 金蝶应收单勾稽 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||
private static final String AR_RESULT_SYNC_LOCK_PREFIX = "transport:formal-settlement:kingdee-ar-result-sync:";
|
||||
|
||||
@@ -563,15 +568,70 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废正式结算单", request.getReason());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void voidByKingdee(Long id, String operator) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
log.info("正式结算单[{}]金蝶删单回调废除:金蝶操作人={}", settlement.getFormalSettlementNo(), operator);
|
||||
// 作废并还原同步信息为推送前状态:updateById 忽略 null 字段,清空字段须显式 set null
|
||||
this.update(Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.eq(FormalSettlement::getId, id)
|
||||
.set(FormalSettlement::getApprovalStatus, TransportBusiConstant.VOIDED)
|
||||
.set(FormalSettlement::getCurrentNode, "已作废")
|
||||
.set(FormalSettlement::getCurrentProcessor, operator)
|
||||
.set(FormalSettlement::getVoidReason, "金蝶侧删单回调自动废除")
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED)
|
||||
.set(FormalSettlement::getKingdeeBillNo, null)
|
||||
.set(FormalSettlement::getKingdeeBillId, null)
|
||||
.set(FormalSettlement::getSyncedTime, null));
|
||||
// 变更流水:回调无登录态,操作人取金蝶报文(兜底 system,与 saveChange 同款)
|
||||
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
|
||||
record.setFormalSettlementId(id);
|
||||
record.setChangeType("结算单基本信息");
|
||||
record.setOperationType("调整");
|
||||
record.setChangeContent("金蝶删单回调自动废除并还原同步信息(金蝶单据已删除,可还原重推)");
|
||||
record.setChangeReason("金蝶操作人:" + Func.toStr(operator));
|
||||
record.setOperatorName(Func.isEmpty(operator) ? "system" : operator);
|
||||
record.setChangeTime(LocalDateTime.now());
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
String billNo = SpringUtil.getBean(IFormalSettlementService.class).doSyncKingdee(id);
|
||||
// 主单事务提交后锁定金蝶单据(lockBusiData):失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态
|
||||
lockKingdeeBillAfterPush(id, billNo);
|
||||
// 主单事务提交后联带上传附件(A902):失败只记附件同步状态,不回滚不污染主单同步状态,重推自动补传
|
||||
syncAttachmentsAfterPush(id);
|
||||
return billNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定(lockBusiData):A102-2/A302-2 推送成功事务提交后调用,目标单据类型按结算类型路由;
|
||||
* 锁定异常独立捕获,不影响主单同步结果
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param billNo 金蝶单据编号
|
||||
*/
|
||||
private void lockKingdeeBillAfterPush(Long id, String billNo) {
|
||||
try {
|
||||
if (Func.isBlank(billNo)) {
|
||||
return;
|
||||
}
|
||||
FormalSettlement settlement = getById(id);
|
||||
if (settlement == null) {
|
||||
return;
|
||||
}
|
||||
kingdeeBillLockService.lockAfterPush(KingdeeLockConstant.SOURCE_SETTLEMENT, id, settlement.getFormalSettlementNo(),
|
||||
"receivable".equals(settlement.getSettlementType())
|
||||
? KingdeeLockConstant.TARGET_AR_BILL : KingdeeLockConstant.TARGET_AP_BILL,
|
||||
Func.toStrList(billNo));
|
||||
} catch (Exception exception) {
|
||||
log.error("正式结算单[{}]金蝶单据锁定联带动作异常(锁记录已落库待补偿)", id, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,以本地 kingdeeBillId 定位金蝶单据;
|
||||
* 本地缺失(存量数据/幂等跳过路径未带回内码)时经 A102-4/A302-4 按防重键反查回填;
|
||||
|
||||
+61
@@ -55,6 +55,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.CustomerArchiveMapper;
|
||||
@@ -93,6 +94,7 @@ import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO;
|
||||
import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO;
|
||||
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeArBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
@@ -157,6 +159,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
|
||||
/** 金蝶单据锁定记录服务(lockBusiData 锁定/补偿/解锁守卫) */
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/**
|
||||
* 金蝶开票结果回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写),
|
||||
* 否则租约到期后另一副本会并发起跑;进程异常退出时锁在租约到期后自动释放
|
||||
@@ -498,6 +503,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
public void voidBill(InvoiceApplicationStatusRequest request) {
|
||||
InvoiceApplication entity = existing(request.getId());
|
||||
if (!TransportBusiConstant.APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许作废");
|
||||
// 金蝶锁定中禁止作废(lockBusiData 已锁单,金蝶侧未解锁前单据不可改动)
|
||||
kingdeeBillLockService.checkEditable(KingdeeLockConstant.SOURCE_INVOICE_APPLY, request.getId());
|
||||
entity.setApprovalStatus(TransportBusiConstant.VOIDED);
|
||||
entity.setCurrentNode("已作废");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
@@ -507,15 +514,69 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
relationSettlementIds(entity.getId()).forEach(this::refreshSettlementInvoiceStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void voidByKingdee(Long id, String operator) {
|
||||
InvoiceApplication entity = existing(id);
|
||||
log.info("开票申请[{}]金蝶删单回调作废:金蝶操作人={}", entity.getApplicationNo(), operator);
|
||||
// 作废并还原同步信息:updateById 忽略 null 字段,清空字段须显式 set null(张次金蝶单号由重推覆盖,不清)
|
||||
this.update(Wrappers.<InvoiceApplication>lambdaUpdate()
|
||||
.eq(InvoiceApplication::getId, id)
|
||||
.set(InvoiceApplication::getApprovalStatus, TransportBusiConstant.VOIDED)
|
||||
.set(InvoiceApplication::getCurrentNode, "已作废")
|
||||
.set(InvoiceApplication::getCurrentProcessor, operator)
|
||||
.set(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED)
|
||||
.set(InvoiceApplication::getKingdeeBillNo, null)
|
||||
.set(InvoiceApplication::getSyncedTime, null));
|
||||
// 操作流水:回调无登录态,操作人取金蝶报文(兜底 system,与 record 同款)
|
||||
InvoiceApplicationRecord voidRecord = new InvoiceApplicationRecord();
|
||||
voidRecord.setInvoiceApplicationId(id);
|
||||
voidRecord.setActionType("kingdee_delete");
|
||||
voidRecord.setActionName("金蝶删单回调作废");
|
||||
voidRecord.setFromStatus(TransportBusiConstant.APPROVED);
|
||||
voidRecord.setToStatus(TransportBusiConstant.VOIDED);
|
||||
voidRecord.setOperatorName(Func.isEmpty(operator) ? "system" : operator);
|
||||
voidRecord.setReason("金蝶侧删单回调自动作废并还原同步信息(金蝶开票申请单已删除,可还原重推)");
|
||||
recordMapper.insert(voidRecord);
|
||||
// 关联数据还原:关联正式结算单开票状态刷新
|
||||
relationSettlementIds(id).forEach(this::refreshSettlementInvoiceStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
String billNo = SpringUtil.getBean(IInvoiceApplicationService.class).doSyncKingdee(id);
|
||||
// 主单事务提交后锁定金蝶开票单(lockBusiData):每张发票张次一张金蝶单,多张逗号拼接批量锁定;
|
||||
// 失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态
|
||||
lockKingdeeBillAfterPush(id, billNo);
|
||||
// 主单事务提交后联带上传附件(A902):失败只记附件同步状态,不回滚不污染主单同步状态,重推自动补传
|
||||
syncAttachmentsAfterPush(id);
|
||||
return billNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶开票单锁定(lockBusiData):A303-1 推送成功事务提交后调用,多张金蝶单批量锁定;
|
||||
* 锁定异常独立捕获,不影响主单同步结果
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @param billNo 金蝶开票单编号(多张逗号拼接)
|
||||
*/
|
||||
private void lockKingdeeBillAfterPush(Long id, String billNo) {
|
||||
try {
|
||||
if (Func.isBlank(billNo)) {
|
||||
return;
|
||||
}
|
||||
InvoiceApplication entity = getById(id);
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
kingdeeBillLockService.lockAfterPush(KingdeeLockConstant.SOURCE_INVOICE_APPLY, id, entity.getApplicationNo(),
|
||||
KingdeeLockConstant.TARGET_INVOICE_BILL, Func.toStrList(billNo));
|
||||
} catch (Exception exception) {
|
||||
log.error("开票申请[{}]金蝶单据锁定联带动作异常(锁记录已落库待补偿)", id, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):主单推送成功(含 A303-4 勾稽回填)后调用,以本地 kingdeeBillId 定位金蝶单据;
|
||||
* 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果
|
||||
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.KingdeeBillLockMapper;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录服务实现
|
||||
* <p>
|
||||
* 锁定:推送链路事务提交后按源单分组批量 LOCK(每组一行到多行锁记录,先落 failed 再调 lockBusiData,
|
||||
* 成功翻 locked),失败由补偿定时任务重试;解锁:金蝶 unLockNotifyBusiData 回调(支持逗号拼接批量),
|
||||
* DELETE 时分发废除源单并还原关联数据。
|
||||
* 与结算单/开票申请服务互相依赖,为避免构造器循环依赖,单据服务经 {@code SpringUtil.getBean} 运行时获取
|
||||
* (与本工程 doSyncKingdee 自代理同一惯用法)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMapper, KingdeeBillLock> implements IKingdeeBillLockService {
|
||||
|
||||
/** 金蝶各接口统一日期文本格式(与 A102-2/A102-4/A103-1 等一致) */
|
||||
private static final DateTimeFormatter BFI_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final BfiProperties bfiProperties;
|
||||
private final FormalSettlementMapper formalSettlementMapper;
|
||||
private final InvoiceApplicationMapper invoiceApplicationMapper;
|
||||
|
||||
@Override
|
||||
public void lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos) {
|
||||
if (Func.isEmpty(targetBillNos)) {
|
||||
return;
|
||||
}
|
||||
String operator = operatorName();
|
||||
// 待锁目标单:已 locked 幂等跳过(人工重推「同步金蝶」幂等命中路径不会重复锁);
|
||||
// 其余(新目标单/failed 重锁/同单重推)先落 failed 行占位,保证补偿扫表无遗漏
|
||||
List<KingdeeBillLock> pendingRecords = new ArrayList<>();
|
||||
for (String targetBillNo : targetBillNos.stream().filter(Func::isNotBlank).distinct().toList()) {
|
||||
KingdeeBillLock lockRecord = latestRecord(sourceBillType, sourceBillId, targetBillNo);
|
||||
if (lockRecord != null && KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
|
||||
continue;
|
||||
}
|
||||
if (lockRecord == null || KingdeeLockConstant.STATUS_UNLOCKED.equals(lockRecord.getLockStatus())) {
|
||||
lockRecord = new KingdeeBillLock();
|
||||
lockRecord.setSourceBillType(sourceBillType);
|
||||
lockRecord.setSourceBillId(sourceBillId);
|
||||
lockRecord.setSourceBillNo(sourceBillNo);
|
||||
lockRecord.setTargetBillType(targetBillType);
|
||||
lockRecord.setTargetBillNo(targetBillNo);
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_FAILED);
|
||||
lockRecord.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
|
||||
lockRecord.setTryCount(0);
|
||||
lockRecord.setLockOperator(operator);
|
||||
this.save(lockRecord);
|
||||
}
|
||||
pendingRecords.add(lockRecord);
|
||||
}
|
||||
if (pendingRecords.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 同源单目标单合并一次调用:编号逗号拼接批量传参,整批同成败(应答仅聚合 updateCount)
|
||||
try {
|
||||
bfiService.lockBusiData(this.buildLockParam(sourceBillType, sourceBillNo, targetBillType,
|
||||
pendingRecords.stream().map(KingdeeBillLock::getTargetBillNo).toList(), operator));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (KingdeeBillLock lockRecord : pendingRecords) {
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
|
||||
lockRecord.setLockTime(now);
|
||||
lockRecord.setFailReason(null);
|
||||
this.updateById(lockRecord);
|
||||
}
|
||||
log.info("金蝶单据批量锁定成功:源单[{}:{}],金蝶单{}张[{}]", sourceBillType, sourceBillNo,
|
||||
pendingRecords.size(), String.join(",", pendingRecords.stream().map(KingdeeBillLock::getTargetBillNo).toList()));
|
||||
} catch (Exception exception) {
|
||||
String failReason = limitText(exception.getMessage(), 500);
|
||||
for (KingdeeBillLock lockRecord : pendingRecords) {
|
||||
lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
|
||||
lockRecord.setFailReason(failReason);
|
||||
this.updateById(lockRecord);
|
||||
}
|
||||
log.error("金蝶单据批量锁定失败(待补偿重锁):源单[{}:{}],金蝶单{}张", sourceBillType, sourceBillNo, pendingRecords.size(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int onUnlockNotify(BfiBillLockDTO param) {
|
||||
if (param == null || !KingdeeLockConstant.SYSTEM_XH.equals(param.getSourceSystem())
|
||||
|| !KingdeeLockConstant.SYSTEM_WLTMS.equals(param.getTargetSystem())
|
||||
|| !KingdeeLockConstant.ACTION_UNLOCK.equals(param.getLockAction())) {
|
||||
throw new ServiceException("金蝶解锁回调参数非法:sourceSystem须为XH、targetSystem须为WLTMS、lockAction须为UNLOCK");
|
||||
}
|
||||
if (Func.isBlank(param.getTargetBillNo())) {
|
||||
throw new ServiceException("金蝶解锁回调参数非法:targetBillNo不能为空");
|
||||
}
|
||||
// 批量回调:targetBillNo 逗号拼接,逐张处理;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
|
||||
int processed = 0;
|
||||
for (String targetBillNo : Func.toStrList(param.getTargetBillNo())) {
|
||||
KingdeeBillLock lockRecord = this.getOne(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getTargetBillType, param.getTargetBillType())
|
||||
.eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
|
||||
.orderByDesc(KingdeeBillLock::getId)
|
||||
.last("limit 1"), false);
|
||||
if (lockRecord == null || !KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
|
||||
log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):targetBillType={},targetBillNo={}",
|
||||
param.getTargetBillType(), targetBillNo);
|
||||
continue;
|
||||
}
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
|
||||
lockRecord.setOperationType(param.getOperationType());
|
||||
lockRecord.setUnlockTime(LocalDateTime.now());
|
||||
lockRecord.setNotifyOperator(param.getOperator());
|
||||
this.updateById(lockRecord);
|
||||
processed++;
|
||||
log.info("金蝶单据[{}:{}]解锁回调落库:operationType={},操作人={}",
|
||||
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), param.getOperationType(), param.getOperator());
|
||||
if (KingdeeLockConstant.OPERATION_DELETE.equals(param.getOperationType())) {
|
||||
this.restoreOnKingdeeDelete(lockRecord, param.getOperator());
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶删单(DELETE 回调)分发:废除源单并还原关联数据(还原口径详见各 voidByKingdee 实现)
|
||||
*
|
||||
* @param lockRecord 锁记录(携带源单类型与源单主键)
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
private void restoreOnKingdeeDelete(KingdeeBillLock lockRecord, String operator) {
|
||||
String sourceType = KingdeeLockConstant.TARGET_TO_SOURCE_MAP.get(lockRecord.getTargetBillType());
|
||||
switch (sourceType == null ? "" : sourceType) {
|
||||
case KingdeeLockConstant.SOURCE_SETTLEMENT -> {
|
||||
FormalSettlement settlement = formalSettlementMapper.selectById(lockRecord.getSourceBillId());
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
log.warn("金蝶删单回调源结算单不存在:id={}", lockRecord.getSourceBillId());
|
||||
return;
|
||||
}
|
||||
if (TransportBusiConstant.VOIDED.equals(settlement.getApprovalStatus())) {
|
||||
log.info("金蝶删单回调源结算单[{}]已作废(幂等跳过)", settlement.getFormalSettlementNo());
|
||||
return;
|
||||
}
|
||||
SpringUtil.getBean(IFormalSettlementService.class).voidByKingdee(settlement.getId(), operator);
|
||||
}
|
||||
case KingdeeLockConstant.SOURCE_INVOICE_APPLY -> {
|
||||
InvoiceApplication application = invoiceApplicationMapper.selectById(lockRecord.getSourceBillId());
|
||||
if (application == null || Objects.equals(application.getIsDeleted(), 1)) {
|
||||
log.warn("金蝶删单回调源开票申请不存在:id={}", lockRecord.getSourceBillId());
|
||||
return;
|
||||
}
|
||||
if (TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())) {
|
||||
log.info("金蝶删单回调源开票申请[{}]已作废(幂等跳过)", application.getApplicationNo());
|
||||
return;
|
||||
}
|
||||
SpringUtil.getBean(IInvoiceApplicationService.class).voidByKingdee(application.getId(), operator);
|
||||
}
|
||||
default -> log.warn("金蝶删单回调暂不支持的单据类型:targetBillType={}(映射预留)", lockRecord.getTargetBillType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkEditable(String sourceBillType, Long sourceBillId) {
|
||||
long lockedCount = this.count(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.eq(KingdeeBillLock::getSourceBillId, sourceBillId)
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_LOCKED));
|
||||
if (lockedCount > 0) {
|
||||
throw new ServiceException("该单据已被金蝶锁定,不可修改");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryFailedLocks() {
|
||||
List<KingdeeBillLock> failedList = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_FAILED)
|
||||
.lt(KingdeeBillLock::getTryCount, KingdeeLockConstant.RETRY_MAX_COUNT));
|
||||
if (Func.isEmpty(failedList)) {
|
||||
return "无待补偿锁记录";
|
||||
}
|
||||
// 按源单分组,组内合并一次调用(编号逗号拼接批量传参,整批同成败)
|
||||
Map<String, List<KingdeeBillLock>> groupBySource = failedList.stream()
|
||||
.collect(Collectors.groupingBy(record -> record.getSourceBillType() + ":" + record.getSourceBillId(), LinkedHashMap::new, Collectors.toList()));
|
||||
int success = 0;
|
||||
for (List<KingdeeBillLock> group : groupBySource.values()) {
|
||||
KingdeeBillLock first = group.get(0);
|
||||
try {
|
||||
bfiService.lockBusiData(this.buildLockParam(first.getSourceBillType(), first.getSourceBillNo(), first.getTargetBillType(),
|
||||
group.stream().map(KingdeeBillLock::getTargetBillNo).toList(),
|
||||
Func.isBlank(first.getLockOperator()) ? "system" : first.getLockOperator()));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (KingdeeBillLock lockRecord : group) {
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
|
||||
lockRecord.setLockTime(now);
|
||||
lockRecord.setFailReason(null);
|
||||
this.updateById(lockRecord);
|
||||
}
|
||||
success += group.size();
|
||||
} catch (Exception exception) {
|
||||
String failReason = limitText(exception.getMessage(), 500);
|
||||
for (KingdeeBillLock lockRecord : group) {
|
||||
lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
|
||||
lockRecord.setFailReason(failReason);
|
||||
this.updateById(lockRecord);
|
||||
}
|
||||
log.error("金蝶单据锁定补偿失败:源单[{}:{}],金蝶单{}张", first.getSourceBillType(), first.getSourceBillNo(), group.size(), exception);
|
||||
}
|
||||
}
|
||||
return "待补偿" + failedList.size() + "张,成功" + success + "张";
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 lockBusiData 参数:WLTMS 调金蝶,源系统取配置(默认 WLTMS),目标系统 XH,
|
||||
* 操作类型固定 LOCK/SAVE,多张目标单编号逗号拼接批量传参
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillNo WLTMS 单号
|
||||
* @param targetBillType 金蝶目标单据类型
|
||||
* @param targetBillNos 金蝶单据编号集合
|
||||
* @param operator 操作人
|
||||
* @return 锁单参数
|
||||
*/
|
||||
private BfiBillLockDTO buildLockParam(String sourceBillType, String sourceBillNo, String targetBillType, List<String> targetBillNos, String operator) {
|
||||
BfiBillLockDTO param = new BfiBillLockDTO();
|
||||
param.setLockAction(KingdeeLockConstant.ACTION_LOCK);
|
||||
param.setSourceSystem(Func.isBlank(bfiProperties.getSrcSys()) ? KingdeeLockConstant.SYSTEM_WLTMS : bfiProperties.getSrcSys());
|
||||
param.setTargetSystem(KingdeeLockConstant.SYSTEM_XH);
|
||||
param.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
|
||||
param.setSourceBillType(sourceBillType);
|
||||
param.setSourceBillNo(sourceBillNo);
|
||||
param.setTargetBillType(targetBillType);
|
||||
param.setTargetBillNo(String.join(",", targetBillNos));
|
||||
param.setTime(LocalDateTime.now().format(BFI_DATE_TIME));
|
||||
param.setOperator(operator);
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查同源单同目标单最新一条锁记录
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
* @param targetBillNo 金蝶单据编号
|
||||
* @return 最新锁记录,无则 null
|
||||
*/
|
||||
private KingdeeBillLock latestRecord(String sourceBillType, Long sourceBillId, String targetBillNo) {
|
||||
return this.getOne(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.eq(KingdeeBillLock::getSourceBillId, sourceBillId)
|
||||
.eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
|
||||
.orderByDesc(KingdeeBillLock::getId)
|
||||
.last("limit 1"), false);
|
||||
}
|
||||
|
||||
/** 当前操作人:无登录态(定时任务/回调)时兜底 system(与 saveChange 同款兜底) */
|
||||
private String operatorName() {
|
||||
return Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName();
|
||||
}
|
||||
|
||||
/** 失败原因文本截断(fail_reason 列宽 500) */
|
||||
private String limitText(String text, int maxLength) {
|
||||
if (Func.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
return text.length() <= maxLength ? text : text.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿定时任务
|
||||
* <p>
|
||||
* 扫描锁定失败的锁记录(lock_status=failed 且未超重试上限)重调 lockBusiData,
|
||||
* 补偿推送成功但锁单失败的单据,不依赖人工重推「同步金蝶」。
|
||||
* 业务逻辑收敛在 {@link IKingdeeBillLockService#retryFailedLocks()},任务类只做开关与调度(模式同 PaymentKingdeeResultTask)。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.billLock.*):retryEnabled 开关、retryCron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeBillLockRetryTask {
|
||||
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/** 锁定补偿开关,代码默认关闭(安全兜底),由 Nacos thirdParty.bfi.billLock.retryEnabled 按环境显式开启 */
|
||||
@Value("${thirdParty.bfi.billLock.retryEnabled:false}")
|
||||
private boolean retryEnabled;
|
||||
|
||||
/** 锁定补偿调度,默认每5分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.billLock.retryCron:0 */5 * * * ?}")
|
||||
public void retryFailedBillLocks() {
|
||||
if (!retryEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = kingdeeBillLockService.retryFailedLocks();
|
||||
log.info("定时补偿金蝶单据锁定:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时补偿金蝶单据锁定异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 金蝶单据锁定记录表:lockBusiData 锁定 / unLockNotifyBusiData 解锁回调 / 锁定补偿
|
||||
-- 一行 = 一张金蝶目标单:锁定留痕(人/时间/失败原因)+ 当前锁状态(编辑守卫/回调定位/补偿扫表)
|
||||
-- lock_status:failed-锁定失败(补偿重试对象)/ locked-金蝶锁定中 / unlocked-已解锁(终态)
|
||||
|
||||
CREATE TABLE `blade_kingdee_bill_lock` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID',
|
||||
`source_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单据类型:SETTLEMENT正式结算单/INVOICE_APPLY开票申请单(预留PAY_APPLY付款申请单/RECEIVED_INVOICE收票单)',
|
||||
`source_bill_id` bigint NOT NULL COMMENT 'WLTMS业务主键',
|
||||
`source_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单号',
|
||||
`target_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据类型:ApFin_other_BT_S应付单/arfin_other_BT_S应收单/cdm_paybill_bank_elect_BT_S开票单等固定值',
|
||||
`target_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据编号(开票申请按发票张次逐张一行)',
|
||||
`lock_status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁状态:failed锁定失败/locked金蝶锁定中/unlocked已解锁',
|
||||
`operation_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '最近动作来源操作类型:SAVE单据新增/DELETE单据删除',
|
||||
`try_count` int DEFAULT '0' COMMENT '锁定重试次数(补偿任务用,超上限停试)',
|
||||
`lock_time` datetime DEFAULT NULL COMMENT '锁定时间',
|
||||
`unlock_time` datetime DEFAULT NULL COMMENT '解锁时间',
|
||||
`lock_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推单发起人(锁定操作人)',
|
||||
`notify_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回调操作人(金蝶侧操作人)',
|
||||
`fail_reason` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '锁定失败原因',
|
||||
`create_user` bigint DEFAULT NULL COMMENT '创建人',
|
||||
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_user` bigint DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`status` int DEFAULT NULL COMMENT '状态',
|
||||
`is_deleted` int DEFAULT '0' COMMENT '是否已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_kbl_source` (`source_bill_type`, `source_bill_id`),
|
||||
INDEX `idx_kbl_target` (`target_bill_type`, `target_bill_no`),
|
||||
INDEX `idx_kbl_status` (`lock_status`, `try_count`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '金蝶单据锁定记录' ROW_FORMAT = DYNAMIC;
|
||||
Reference in New Issue
Block a user