✨ 新增金蝶客商数据定时同步任务(自动推送+使用状态停用联动)

This commit is contained in:
刘泉佳
2026-09-18 21:34:37 +08:00
parent d9f94bb161
commit 0531177ed7
4 changed files with 209 additions and 1 deletions
@@ -132,6 +132,21 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/
CustomerArchive syncKingdee(Long id);
/**
* 定时同步金蝶客商数据:待推送客商自动推送 + 已同步客商使用状态回写
*
* @return 同步汇总说明
*/
String syncKingdeeCustomerData();
/**
* 金蝶侧停用联动:停用客商档案并记录变更(仅当前启用的客商生效)
*
* @param id 主键
* @return 是否执行停用
*/
boolean disableCustomerFromKingdee(Long id);
/**
* 基于启用评分量化表生成评分记录
*
@@ -36,6 +36,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.core.tool.utils.StringUtil;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
@@ -128,6 +129,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private static final String KINGDEE_STATUS_SYNCED = "synced";
private static final String KINGDEE_STATUS_FAILED = "failed";
private static final String CUSTOMER_CODE_PREFIX = "KS";
/**
* A902-1 批量查询分批大小
*/
private static final int KINGDEE_CUSTOMER_QUERY_BATCH = 50;
/**
* 变更快照忽略字段:明细每次保存会重建主键/审计字段,不能作为“是否有修改”的依据
*/
@@ -391,6 +396,120 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
return text.length() > 500 ? text.substring(0, 500) : text;
}
/**
* 自代理:跨方法调用确保事务注解生效(单条推送、停用各自独立事务)
*/
private ICustomerArchiveService self() {
return SpringUtil.getBean(ICustomerArchiveService.class);
}
@Override
public String syncKingdeeCustomerData() {
// ① 待推送客商:审批通过且未推送/推送失败,复用单条推送(A902-1 复查 + A902-2 保存),单条失败不中断批次
List<CustomerArchive> pendingList = list(Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED)
.in(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_NONE, KINGDEE_STATUS_FAILED)
.orderByAsc(CustomerArchive::getId));
ICustomerArchiveService proxy = self();
int pushed = 0;
int existed = 0;
int pushFailed = 0;
for (CustomerArchive customer : pendingList) {
try {
CustomerArchive after = proxy.syncKingdee(customer.getId());
if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_SYNCED)) {
pushed++;
} else if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_EXIST)) {
existed++;
} else {
pushFailed++;
}
} catch (Exception exception) {
pushFailed++;
log.error("定时同步金蝶客商推送异常,id={},unifiedCreditCode={}",
customer.getId(), customer.getUnifiedCreditCode(), exception);
}
}
// ② 已同步客商使用状态回写:金蝶禁用(enable=0)→ TMS 自动停用;金蝶恢复可用不自动启用(TMS 人工控制)
List<CustomerArchive> enabledList = list(Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED)
.in(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_SYNCED, KINGDEE_STATUS_EXIST)
.eq(CustomerArchive::getStatus, STATUS_ENABLED)
.orderByAsc(CustomerArchive::getId));
int disabled = 0;
int disableFailed = 0;
if (!enabledList.isEmpty()) {
List<String> creditCodes = enabledList.stream()
.map(CustomerArchive::getUnifiedCreditCode)
.filter(Func::isNotEmpty)
.map(code -> code.trim().toUpperCase())
.distinct()
.toList();
Map<String, BfiCustomerVO> kingdeeMap = queryKingdeeCustomerMap(creditCodes);
for (CustomerArchive customer : enabledList) {
BfiCustomerVO kingdeeCustomer = kingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase());
if (kingdeeCustomer == null || !"0".equals(kingdeeCustomer.getEnable())) {
continue;
}
try {
if (proxy.disableCustomerFromKingdee(customer.getId())) {
disabled++;
}
} catch (Exception exception) {
disableFailed++;
log.error("定时同步金蝶客商停用异常,id={},unifiedCreditCode={}",
customer.getId(), customer.getUnifiedCreditCode(), exception);
}
}
}
return "待推送 " + pendingList.size() + " 条(推送成功 " + pushed + ",金蝶已有 " + existed
+ ",失败 " + pushFailed + "),停用联动 " + disabled + " 条(失败 " + disableFailed + ")";
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean disableCustomerFromKingdee(Long id) {
CustomerArchive customer = ensureCustomerAccessible(id);
if (!Objects.equals(customer.getStatus(), STATUS_ENABLED)) {
return false;
}
CustomerArchive after = copyCustomer(customer);
after.setStatus(STATUS_DISABLED);
addChangeRecord(id, "金蝶侧客商已停用,同步停用客商档案", customer, after);
return updateById(after);
}
/**
* 按统一社会信用代码分批查询金蝶客商,键为大写信用代码
*/
private Map<String, BfiCustomerVO> queryKingdeeCustomerMap(List<String> creditCodes) {
Map<String, BfiCustomerVO> result = new HashMap<>();
for (int start = 0; start < creditCodes.size(); start += KINGDEE_CUSTOMER_QUERY_BATCH) {
List<String> batch = creditCodes.subList(start, Math.min(start + KINGDEE_CUSTOMER_QUERY_BATCH, creditCodes.size()));
if (batch.isEmpty()) {
continue;
}
try {
BfiCustomerQueryDTO param = new BfiCustomerQueryDTO();
param.setPageNo(1);
param.setPageSize(batch.size());
param.setSocietycreditcode(batch);
BfiPageVO<BfiCustomerVO> page = bfiService.queryCustomer(param);
List<BfiCustomerVO> rows = page == null ? null : page.getRows();
if (rows != null) {
for (BfiCustomerVO vo : rows) {
if (Func.isNotEmpty(vo.getSocietycreditcode())) {
result.put(vo.getSocietycreditcode().trim().toUpperCase(), vo);
}
}
}
} catch (Exception exception) {
log.error("定时同步金蝶客商批量查询异常,batchSize={}", batch.size(), exception);
}
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean changeStatus(Long id, Integer status) {
@@ -0,0 +1,73 @@
/**
* 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.ICustomerArchiveService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 金蝶客商数据定时同步任务
* <p>
* 对应业财融合「客商数据定时同步子流程」(图01,ERP 泳道):
* ① 待推送客商(审批通过且未推送/推送失败)自动执行 A902-1 复查 + A902-2 推送;
* ② 已同步客商回写金蝶使用状态,金蝶禁用(enable=0)联动停用客商档案。
* A901-1 供应商查询待金蝶 SDK(blade-bfi-api)提供能力后扩展。
* 业务逻辑收敛在 {@link ICustomerArchiveService#syncKingdeeCustomerData()},便于手动触发联调。
* <p>
* 调度配置(Nacos thirdParty.bfi.customer.*):syncEnabled 开关、cron 表达式。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerKingdeeSyncTask {
private final ICustomerArchiveService customerArchiveService;
/** 客商金蝶数据同步开关,默认关闭 */
@Value("${thirdParty.bfi.customer.syncEnabled:false}")
private boolean syncEnabled;
/** 客商金蝶数据同步调度,默认每天 02:00 执行 */
@Scheduled(cron = "${thirdParty.bfi.customer.cron:0 0 2 * * ?}")
public void syncKingdeeCustomerData() {
if (!syncEnabled) {
return;
}
try {
String summary = customerArchiveService.syncKingdeeCustomerData();
log.info("定时同步金蝶客商数据:{}", summary);
} catch (Exception exception) {
log.error("定时同步金蝶客商数据异常", exception);
}
}
}
@@ -287,7 +287,7 @@ VALUES (2075431800000000001, {客商档案菜单id}, 'customer_archive_sync', '
| 推送时复查到金蝶暂存/已提交 | 置 `failed`,`kingdee_fail_reason='金蝶侧该客商审核中,请稍后重推'` |
| A902-2 返回 `billStatus=false` / `failCount>0` | 置 `failed`,`errors` 拼接写入 `kingdee_fail_reason`(截断 500 字),日志 `@Slf4j` 占位符输出含客商 id 与信用代码 |
| A902-2 网关异常(超时/5xx/token 失效) | `BfiException` 统一上抛,同上置 `failed`;token 由 `BfiServiceImpl` 自动续期 |
| 重推仍失败 | 保持 `failed`,可继续人工重推;连续失败转人工核对金蝶侧该信用代码客商状态 |
| 重推仍失败 | 保持 `failed`;定时任务(`CustomerKingdeeSyncTask`,开关 `thirdParty.bfi.customer.syncEnabled`,默认关闭)每轮自动重推,也可列表「同步金蝶」人工重推;连续失败转人工核对金蝶侧该信用代码客商状态 |
---
@@ -309,3 +309,4 @@ VALUES (2075431800000000001, {客商档案菜单id}, 'customer_archive_sync', '
- **取消 `kingdee_number` 字段**:A902-2 `number` 直接传 TMS 客商编号,金蝶客户编码与客商编号保持一致;DDL 缩减为 3 字段,前端移除「金蝶客户编码」列及相关引用。
- **客商数据定时同步子流程本期不做**:经确认 ERP/TMS 无此既有逻辑,且金蝶 SDK(blade-bfi-api)暂未提供 A901-1 供应商查询;该子流程不阻塞核心链路,待 A901-1 能力补齐后按 §2.3 步骤9(ERP 定时调 A902-1/A901-1 更新客商数据与使用状态)另行立项。推送失败(`failed`)由列表「同步金蝶」按钮人工重推。
- 待人工执行:`doc/sql/transport/blade_customer_archive_kingdee_20260917.sql`(含「同步金蝶」按钮菜单 INSERT);与财务核对客商性质映射键与 `createorg_number` 取值。
- 2026-09-17(第三轮新增定时任务):新增 `task/CustomerKingdeeSyncTask`(仿 `PaymentKingdeeResultTask`,开关 `thirdParty.bfi.customer.syncEnabled` 默认关闭、cron `thirdParty.bfi.customer.cron` 默认每天 02:00)与 `ICustomerArchiveService.syncKingdeeCustomerData()`:① 待推送客商(approved 且 none/failed)复用 `syncKingdee` 自动推送(自代理逐条独立事务,单条失败不中断);② 已同步客商(synced/exist)按信用代码分批(50/批)A902-1 查询,金蝶禁用(enable=0)联动停用客商档案并记变更,金蝶恢复可用不自动启用。A901-1 供应商查询仍待 SDK 提供能力后扩展。`mvn clean compile` 通过。