diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index 9b2c7bde..f69890b5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -28,6 +28,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.toolkit.Db; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; @@ -36,7 +37,6 @@ 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.system.pojo.entity.UserCustomerScope; import org.springblade.thirdparty.bfi.constant.BfiConstant; import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO; @@ -46,6 +46,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO; import org.springblade.thirdparty.bfi.service.IBfiService; +import org.springblade.thirdparty.bfi.util.BfiUtil; import org.springblade.transport.excel.CustomerArchiveExcel; import org.springblade.transport.mapper.CreditRatingStandardMapper; import org.springblade.transport.mapper.CreditScoreCategoryMapper; @@ -104,6 +105,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * 客商档案 服务实现类 @@ -370,17 +372,18 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl receiptAccounts) { + CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class)); + detail.setReceiptAccounts(receiptAccounts); + return detail; + } + + /** + * 定时同步变更集中落库:档案批量更新 + 变更记录批量插入,失败仅记录日志,不影响汇总返回 + */ + private void flushSyncChanges(List updateList, List changeRecords) { + if (!updateList.isEmpty()) { + try { + updateBatchById(updateList); + } catch (Exception exception) { + log.error("定时同步金蝶客商批量更新档案异常,size={}", updateList.size(), exception); + } + } + if (!changeRecords.isEmpty()) { + try { + Db.saveBatch(changeRecords); + } catch (Exception exception) { + log.error("定时同步金蝶客商批量写入变更记录异常,size={}", changeRecords.size(), exception); + } + } } @Override public String syncKingdeeCustomerData() { log.info("金蝶客商数据定时同步开始"); - // ① 待推送客商:审批通过且未推送/推送失败,复用单条推送(A902-1 复查 + A902-2 保存),单条失败不中断批次 - List pendingList = list(Wrappers.lambdaQuery() + // 客商数据量不大,一次查询审批通过客商,内存按金蝶协同状态分组 + List approvedList = list(Wrappers.lambdaQuery() .eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED) - .in(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_NONE, KINGDEE_STATUS_FAILED) + .eq(CustomerArchive::getCustomerCode, "KS000067") .orderByAsc(CustomerArchive::getId)); - log.info("金蝶客商数据定时同步查询到待推送客商 {} 条", pendingList.size()); - ICustomerArchiveService proxy = self(); + // ① 待推送客商:未推送/推送失败,单条失败不中断批次 + List pendingList = approvedList.stream() + .filter(customer -> KINGDEE_STATUS_NONE.equals(customer.getKingdeeStatus()) + || KINGDEE_STATUS_FAILED.equals(customer.getKingdeeStatus())) + .toList(); + // ② 停用联动候选:已同步/金蝶已有且启用的客商 + List enabledList = approvedList.stream() + .filter(customer -> (KINGDEE_STATUS_SYNCED.equals(customer.getKingdeeStatus()) + || KINGDEE_STATUS_EXIST.equals(customer.getKingdeeStatus())) + && Objects.equals(customer.getStatus(), STATUS_ENABLED)) + .toList(); + // ③ 已推送客商:A902-2 成功仅代表金蝶受理(status=B),复查 status=C 已审核才是最终成功 + List syncedList = approvedList.stream() + .filter(customer -> KINGDEE_STATUS_SYNCED.equals(customer.getKingdeeStatus())) + .toList(); + log.info("金蝶客商数据定时同步查询到审批通过客商 {} 条(待推送 {},已推送 {},可停用联动 {})", + approvedList.size(), pendingList.size(), syncedList.size(), enabledList.size()); int pushed = 0; int existed = 0; int pushFailed = 0; + // 变更集中落库:档案批量更新、变更记录批量插入,避免逐条 update/insert 的 N+1 写入 + List updateList = new ArrayList<>(); + List changeRecords = new ArrayList<>(); + // 批量 A902-1 复查待推送客商:金蝶已审核直接建档(exist)、审核中置 failed,仅不存在金蝶档案的才逐条推送,避免逐条复查的 N+1 调用 + Map pendingKingdeeMap = queryKingdeeCustomerMap(pendingList.stream() + .map(CustomerArchive::getUnifiedCreditCode) + .filter(Func::isNotEmpty) + .map(code -> code.trim().toUpperCase()) + .distinct() + .toList()); + List pushList = new ArrayList<>(); for (CustomerArchive customer : pendingList) { + BfiCustomerVO kingdeeCustomer = pendingKingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase()); + if (kingdeeCustomer == null) { + pushList.add(customer); + continue; + } + CustomerArchive after = copyCustomer(customer); + after.setKingdeeSyncTime(LocalDateTime.now()); + if (BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) { + // 金蝶已存在已审核档案:跳过推送,直接建档 + after.setKingdeeStatus(KINGDEE_STATUS_EXIST); + after.setKingdeeFailReason(null); + existed++; + collectChangeRecord(changeRecords, customer.getId(), "金蝶已存在该客商档案,跳过推送", customer, after); + } else { + // 金蝶侧暂存/已提交:审核中,置失败待重推 + after.setKingdeeStatus(KINGDEE_STATUS_FAILED); + after.setKingdeeFailReason("金蝶侧该客商审核中,请稍后重推"); + pushFailed++; + collectChangeRecord(changeRecords, customer.getId(), "推送金蝶失败:金蝶侧客商审核中", customer, after); + } + log.info("金蝶客商数据定时同步复查处理完成,id={},unifiedCreditCode={},kingdeeStatus={}", + customer.getId(), customer.getUnifiedCreditCode(), after.getKingdeeStatus()); + updateList.add(after); + } + // 金蝶不存在档案的逐条推送(A902-2 保存): + // 报文仅需主表字段 + 收款账户,按客商ID批量一次查询收款账户组装报文,避免逐条 detail 的 N+1 查询 + Map> receiptAccountsMap = loadReceiptAccountsMap(pushList.stream() + .map(CustomerArchive::getId) + .toList()); + for (CustomerArchive customer : pushList) { try { - CustomerArchive after = proxy.syncKingdee(customer.getId()); + CustomerArchiveVO detail = buildSyncArchiveVO(customer, + receiptAccountsMap.getOrDefault(customer.getId(), new ArrayList<>())); + CustomerArchive after = copyCustomer(customer); + after.setKingdeeSyncTime(LocalDateTime.now()); + pushCustomerToKingdee(customer.getId(), customer, after, detail); + updateList.add(after); if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_SYNCED)) { pushed++; log.info("金蝶客商数据定时同步推送成功,id={},unifiedCreditCode={}", customer.getId(), customer.getUnifiedCreditCode()); - } else if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_EXIST)) { - existed++; - log.info("金蝶客商数据定时同步金蝶已有档案跳过推送,id={},unifiedCreditCode={}", - customer.getId(), customer.getUnifiedCreditCode()); } else { pushFailed++; log.warn("金蝶客商数据定时同步推送失败,id={},unifiedCreditCode={},kingdeeStatus={},failReason={}", @@ -488,34 +571,62 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl enabledList = list(Wrappers.lambdaQuery() - .eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED) - .in(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_SYNCED, KINGDEE_STATUS_EXIST) - .eq(CustomerArchive::getStatus, STATUS_ENABLED) - .orderByAsc(CustomerArchive::getId)); + // ②③ 共用:对已推送(synced)/金蝶已有(exist)客商按信用代码批量复查金蝶档案(审核状态 + 使用状态),仅调用一次 A902-1 int disabled = 0; int disableFailed = 0; - if (!enabledList.isEmpty()) { - log.info("金蝶客商数据定时同步查询到已同步可用客商 {} 条,开始联动金蝶使用状态", enabledList.size()); - List creditCodes = enabledList.stream() + int confirmed = 0; + int auditing = 0; + int unknown = 0; + if (!enabledList.isEmpty() || !syncedList.isEmpty()) { + List reviewCreditCodes = Stream.concat(syncedList.stream(), enabledList.stream()) .map(CustomerArchive::getUnifiedCreditCode) .filter(Func::isNotEmpty) .map(code -> code.trim().toUpperCase()) .distinct() .toList(); - Map kingdeeMap = queryKingdeeCustomerMap(creditCodes); + Map kingdeeMap = reviewCreditCodes.isEmpty() + ? Map.of() + : queryKingdeeCustomerMap(reviewCreditCodes); + // ③ 审核确认:A902-1 复查 status=C 已审核才是最终成功,synced 升级为 exist(终态); + // A/B 审核中保持 synced 等待下轮复查(不重推,避免金蝶侧重复建档) + for (CustomerArchive customer : syncedList) { + BfiCustomerVO kingdeeCustomer = kingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase()); + if (kingdeeCustomer == null) { + unknown++; + log.warn("金蝶客商审核状态复查未查询到档案,id={},unifiedCreditCode={}", + customer.getId(), customer.getUnifiedCreditCode()); + continue; + } + if (!BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) { + auditing++; + log.info("金蝶客商审核中,等待下轮复查,id={},unifiedCreditCode={},kingdeeStatus={}", + customer.getId(), customer.getUnifiedCreditCode(), kingdeeCustomer.getStatus()); + continue; + } + confirmed++; + CustomerArchive after = copyCustomer(customer); + after.setKingdeeStatus(KINGDEE_STATUS_EXIST); + after.setKingdeeFailReason(null); + updateList.add(after); + collectChangeRecord(changeRecords, customer.getId(), "金蝶客商已审核,推送最终成功", customer, after); + log.info("金蝶客商已审核,推送最终成功并升级为金蝶已有档案,id={},unifiedCreditCode={}", + customer.getId(), customer.getUnifiedCreditCode()); + } + // ② 停用联动:金蝶禁用(enable=0)→ TMS 自动停用;金蝶恢复可用不自动启用(TMS 人工控制); + // 直接基于已查询档案内存处理,避免逐条自代理调用重复查询档案与权限 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++; - log.info("金蝶侧客商已停用,联动停用客商档案成功,id={},unifiedCreditCode={}", - customer.getId(), customer.getUnifiedCreditCode()); - } + CustomerArchive after = copyCustomer(customer); + after.setStatus(STATUS_DISABLED); + collectChangeRecord(changeRecords, customer.getId(), "金蝶侧客商已停用,同步停用客商档案", customer, after); + updateList.add(after); + disabled++; + log.info("金蝶侧客商已停用,联动停用客商档案成功,id={},unifiedCreditCode={}", + customer.getId(), customer.getUnifiedCreditCode()); } catch (Exception exception) { disableFailed++; log.error("定时同步金蝶客商停用异常,id={},unifiedCreditCode={}", @@ -523,8 +634,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl 金蝶客商 */ private Map queryKingdeeCustomerMap(List creditCodes) { Map result = new HashMap<>(); @@ -554,16 +673,15 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl page = bfiService.queryCustomer(param); - List 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); - } + List rows = BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(KINGDEE_CUSTOMER_QUERY_BATCH); + return bfiService.queryCustomer(param); + }); + for (BfiCustomerVO vo : rows) { + if (Func.isNotEmpty(vo.getSocietycreditcode())) { + result.put(vo.getSocietycreditcode().trim().toUpperCase(), vo); } } } catch (Exception exception) { @@ -983,12 +1101,26 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl loadReceiptAccounts(Long customerId) { + return loadReceiptAccountsMap(List.of(customerId)).getOrDefault(customerId, new ArrayList<>()); + } + + /** + * 按客商ID集合批量查询收款账户(键为客商ID),供定时同步批量组装金蝶报文等场景使用,避免逐条查询 + * + * @param customerIds 客商ID集合 + * @return 客商ID -> 收款账户列表(默认一条扣款排序) + */ + private Map> loadReceiptAccountsMap(List customerIds) { + if (Func.isEmpty(customerIds)) { + return Map.of(); + } return receiptAccountMapper.selectList(Wrappers.lambdaQuery() - .eq(CustomerReceiptAccount::getCustomerId, customerId) + .in(CustomerReceiptAccount::getCustomerId, customerIds) .eq(CustomerReceiptAccount::getIsDeleted, 0) .orderByDesc(CustomerReceiptAccount::getIsDefault) .orderByAsc(CustomerReceiptAccount::getCreateTime)) - .stream().map(account -> Objects.requireNonNull(BeanUtil.copyProperties(account, CustomerReceiptAccountVO.class))).toList(); + .stream().map(account -> Objects.requireNonNull(BeanUtil.copyProperties(account, CustomerReceiptAccountVO.class))) + .collect(Collectors.groupingBy(CustomerReceiptAccountVO::getCustomerId)); } private List loadInvoices(Long customerId) { @@ -1309,11 +1441,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl beforeSnapshot = customerSnapshot(before); - Map afterSnapshot = customerSnapshot(after); - addDetailSnapshot(beforeSnapshot, null); - addDetailSnapshot(afterSnapshot, null); - writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + CustomerChangeRecord record = buildChangeRecord(customerId, content, before, after); + if (record != null) { + changeRecordMapper.insert(record); + } } private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after, @@ -1322,10 +1453,35 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl afterSnapshot = customerSnapshot(after); addDetailSnapshot(beforeSnapshot, beforeDetail); addDetailSnapshot(afterSnapshot, afterDetail); - writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + CustomerChangeRecord record = buildChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + if (record != null) { + changeRecordMapper.insert(record); + } } - private void writeChangeRecord(Long customerId, String content, Map beforeSnapshot, + /** + * 收集变更记录供定时同步集中批量插入(无实质变更不收集),避免逐条 insert + */ + private void collectChangeRecord(List records, Long customerId, String content, + CustomerArchive before, CustomerArchive after) { + CustomerChangeRecord record = buildChangeRecord(customerId, content, before, after); + if (record != null) { + records.add(record); + } + } + + /** + * 构建变更记录(无实质变更返回 null),供单条写入与定时同步批量收集共用 + */ + private CustomerChangeRecord buildChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) { + Map beforeSnapshot = customerSnapshot(before); + Map afterSnapshot = customerSnapshot(after); + addDetailSnapshot(beforeSnapshot, null); + addDetailSnapshot(afterSnapshot, null); + return buildChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + } + + private CustomerChangeRecord buildChangeRecord(Long customerId, String content, Map beforeSnapshot, Map afterSnapshot) { for (String field : afterSnapshot.keySet()) { beforeSnapshot.putIfAbsent(field, null); @@ -1346,7 +1502,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl