|
@@ -0,0 +1,325 @@
|
|
|
|
|
+package cn.iocoder.yudao.module.pms.job.sap;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
+import cn.hutool.core.collection.CollectionUtil;
|
|
|
|
|
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
|
|
|
|
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
|
|
|
|
+import cn.iocoder.yudao.module.pms.dal.mysql.iotsapstock.IotSapStockMapper;
|
|
|
|
|
+import cn.iocoder.yudao.module.pms.sap.SapConnector;
|
|
|
|
|
+import cn.iocoder.yudao.module.pms.sap.service.IotSapService;
|
|
|
|
|
+import cn.iocoder.yudao.module.pms.sap.vo.IotSapInventoryAgeVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.api.saporg.SapOrgApi;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.api.saporg.dto.SapOrgRespDTO;
|
|
|
|
|
+import com.sap.conn.jco.JCoFunction;
|
|
|
|
|
+import com.sap.conn.jco.JCoParameterList;
|
|
|
|
|
+import com.sap.conn.jco.JCoTable;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+import static cn.iocoder.yudao.module.pms.framework.config.MultiThreadConfiguration.PMS_THREAD_POOL_TASK_EXECUTOR;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 同步 SAP 库龄数据 定时任务
|
|
|
|
|
+ */
|
|
|
|
|
+@Component
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class SyncSapInventoryAgeJob implements JobHandler {
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private SapConnector sapConnector;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private SapOrgApi sapOrgApi;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource(name = PMS_THREAD_POOL_TASK_EXECUTOR)
|
|
|
|
|
+ private ThreadPoolTaskExecutor pmsThreadPoolTaskExecutor;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IotSapService iotSapService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IotSapStockMapper iotSapStockMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private DeptApi deptApi;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @TenantIgnore
|
|
|
|
|
+ public String execute(String param) throws Exception {
|
|
|
|
|
+ // 查询所有工厂 根据工厂 多线程处理 SAP 库存
|
|
|
|
|
+ List<String> factoryCodes = factoryCodes();
|
|
|
|
|
+ if (CollUtil.isEmpty(factoryCodes)) {
|
|
|
|
|
+ return "No SAP Factory";
|
|
|
|
|
+ }
|
|
|
|
|
+ log.error("共找到 {} 个工厂需要同步: {}", factoryCodes.size(), factoryCodes);
|
|
|
|
|
+
|
|
|
|
|
+ // 提前获取并缓存基础数据,避免每个线程重复查询
|
|
|
|
|
+ SyncSapStockAgeContext syncContext = preloadSyncContext();
|
|
|
|
|
+ if (!syncContext.isValid()) {
|
|
|
|
|
+ return "基础数据加载失败,无法继续同步";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
|
|
+
|
|
|
|
|
+ // 使用单线程循环处理每个工厂(移除了多线程)
|
|
|
|
|
+ List<FactorySyncResult> results = new ArrayList<>();
|
|
|
|
|
+ for (String factory : factoryCodes) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ FactorySyncResult result = syncFactoryStockAgeData(factory, syncContext);
|
|
|
|
|
+ results.add(result);
|
|
|
|
|
+ log.info("工厂 {} 数据处理完成", factory);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("工厂 {} 处理过程中发生异常", factory, e);
|
|
|
|
|
+ results.add(FactorySyncResult.failure(factory, e));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 汇总所有工厂的数据
|
|
|
|
|
+ List<IotSapInventoryAgeVO> allStockAgeData = new ArrayList<>();
|
|
|
|
|
+ List<String> failedFactories = new ArrayList<>();
|
|
|
|
|
+ List<String> successFactories = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (FactorySyncResult result : results) {
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ if (CollUtil.isNotEmpty(result.stockAgeData)) {
|
|
|
|
|
+ allStockAgeData.addAll(result.stockAgeData);
|
|
|
|
|
+ }
|
|
|
|
|
+ successFactories.add(result.factoryCode);
|
|
|
|
|
+ log.error("工厂 {} SAP数据获取成功,获取 {} 条记录", result.factoryCode,
|
|
|
|
|
+ CollUtil.size(result.stockAgeData));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ failedFactories.add(result.factoryCode);
|
|
|
|
|
+ log.error("工厂 {} SAP数据获取失败", result.factoryCode, result.error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 统一处理所有库存数据
|
|
|
|
|
+ if (CollUtil.isNotEmpty(allStockAgeData)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.error("开始统一处理所有工厂的库龄数据,总计 {} 条记录", allStockAgeData.size());
|
|
|
|
|
+ iotSapService.processSapStockAgeBatch(allStockAgeData, syncContext);
|
|
|
|
|
+ log.error("库存数据处理完成");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("统一处理库龄数据时发生错误", e);
|
|
|
|
|
+ // 如果数据处理失败,认为所有工厂都失败
|
|
|
|
|
+ failedFactories.addAll(successFactories);
|
|
|
|
|
+ successFactories.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long endTime = System.currentTimeMillis();
|
|
|
|
|
+ log.error("所有工厂同步完成,总耗时: {}ms", (endTime - startTime));
|
|
|
|
|
+
|
|
|
|
|
+ // 汇总执行结果
|
|
|
|
|
+ String result = buildResultMessage(factoryCodes.size(), successFactories.size(), failedFactories);
|
|
|
|
|
+ log.error("SAP库存同步任务完成: {}", result);
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 同步单个工厂的库龄(仅获取数据,不处理)
|
|
|
|
|
+ */
|
|
|
|
|
+ private FactorySyncResult syncFactoryStockAgeData(String factory, SyncSapStockAgeContext context) {
|
|
|
|
|
+ JCoFunction function = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 每个线程使用独立的JCoFunction实例
|
|
|
|
|
+ function = context.destination.getRepository().getFunction("ZMM_GET_ZMM020");
|
|
|
|
|
+ if (function == null) {
|
|
|
|
|
+ throw new RuntimeException("未找到SAP函数 ZMM_GET_ZMM020");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置输入参数
|
|
|
|
|
+ JCoParameterList input = function.getImportParameterList();
|
|
|
|
|
+ input.setValue("WERKS", factory);
|
|
|
|
|
+ input.setValue("LFMON", "20260901");
|
|
|
|
|
+ input.setValue("AGE01", 365);
|
|
|
|
|
+ input.setValue("AGE02", 365);
|
|
|
|
|
+ input.setValue("AGE03", 365);
|
|
|
|
|
+ input.setValue("AGE04", 365);
|
|
|
|
|
+ input.setValue("INC01", "X");
|
|
|
|
|
+ input.setValue("INC02", "X");
|
|
|
|
|
+ input.setValue("INC03", "X");
|
|
|
|
|
+
|
|
|
|
|
+ // 执行 RFC 调用
|
|
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
|
|
+ function.execute(context.destination);
|
|
|
|
|
+ long endTime = System.currentTimeMillis();
|
|
|
|
|
+
|
|
|
|
|
+ log.error("工厂 {} SAP调用耗时: {}ms", factory, (endTime - startTime));
|
|
|
|
|
+
|
|
|
|
|
+ // 处理返回数据
|
|
|
|
|
+ JCoTable etStockTable = function.getTableParameterList().getTable("OUTTAB");
|
|
|
|
|
+ if (etStockTable == null) {
|
|
|
|
|
+ log.error("工厂 {} 未返回 OUTTAB 表数据", factory);
|
|
|
|
|
+ return FactorySyncResult.success(factory, Collections.emptyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int rowCount = etStockTable.getNumRows();
|
|
|
|
|
+ log.error("工厂 {} 获取到 {} 条库龄数据", factory, rowCount);
|
|
|
|
|
+
|
|
|
|
|
+ List<IotSapInventoryAgeVO> sapStockAges = Collections.emptyList();
|
|
|
|
|
+ if (rowCount > 0) {
|
|
|
|
|
+ sapStockAges = parseSapInventoryAgeData(etStockTable, factory);
|
|
|
|
|
+ log.error("工厂 {} 解析出 {} 条库龄记录", factory, sapStockAges.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return FactorySyncResult.success(factory, sapStockAges);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("工厂 {} SAP数据获取失败", factory, e);
|
|
|
|
|
+ return FactorySyncResult.failure(factory, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 清理JCo资源
|
|
|
|
|
+ if (function != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // JCoFunction没有close方法,但可以显式清理
|
|
|
|
|
+ function = null;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("清理JCoFunction资源时发生警告", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析SAP库存数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<IotSapInventoryAgeVO> parseSapInventoryAgeData(JCoTable etStockTable, String factory) {
|
|
|
|
|
+ List<IotSapInventoryAgeVO> sapStockAges = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < etStockTable.getNumRows(); i++) {
|
|
|
|
|
+ etStockTable.setRow(i);
|
|
|
|
|
+ try {
|
|
|
|
|
+ IotSapInventoryAgeVO sapStockAge = new IotSapInventoryAgeVO();
|
|
|
|
|
+ sapStockAge.setGJAHR(etStockTable.getString("GJAHR"));
|
|
|
|
|
+ sapStockAge.setRPMAX(etStockTable.getString("RPMAX"));
|
|
|
|
|
+ sapStockAge.setWERKS(etStockTable.getString("WERKS"));
|
|
|
|
|
+ sapStockAge.setMATNR(etStockTable.getString("MATNR"));
|
|
|
|
|
+ sapStockAge.setLGORT(etStockTable.getString("LGORT"));
|
|
|
|
|
+ sapStockAge.setBKLAS(etStockTable.getString("BKLAS"));
|
|
|
|
|
+ sapStockAge.setMATKL(etStockTable.getString("MATKL"));
|
|
|
|
|
+ sapStockAge.setMTART(etStockTable.getString("MTART"));
|
|
|
|
|
+ sapStockAge.setMAKTX(etStockTable.getString("MAKTX"));
|
|
|
|
|
+ sapStockAge.setVBELN(etStockTable.getString("VBELN"));
|
|
|
|
|
+ sapStockAge.setPOSNR(etStockTable.getString("POSNR"));
|
|
|
|
|
+ sapStockAge.setBEZEI(etStockTable.getString("BEZEI"));
|
|
|
|
|
+ sapStockAge.setTOTAL(etStockTable.getBigDecimal("TOTAL"));
|
|
|
|
|
+ sapStockAge.setSTPRS(etStockTable.getBigDecimal("STPRS"));
|
|
|
|
|
+
|
|
|
|
|
+ sapStockAge.setAMTTOTAL(etStockTable.getBigDecimal("AMTTOTAL"));
|
|
|
|
|
+ sapStockAge.setPEINH(etStockTable.getBigDecimal("PEINH"));
|
|
|
|
|
+ sapStockAge.setQTY1(etStockTable.getBigDecimal("QTY1"));
|
|
|
|
|
+ sapStockAge.setQTY2(etStockTable.getBigDecimal("QTY2"));
|
|
|
|
|
+ sapStockAge.setQTY3(etStockTable.getBigDecimal("QTY3"));
|
|
|
|
|
+ sapStockAge.setQTY4(etStockTable.getBigDecimal("QTY4"));
|
|
|
|
|
+ sapStockAge.setAMT1(etStockTable.getBigDecimal("AMT1"));
|
|
|
|
|
+ sapStockAge.setAMT2(etStockTable.getBigDecimal("AMT2"));
|
|
|
|
|
+ sapStockAge.setAMT3(etStockTable.getBigDecimal("AMT3"));
|
|
|
|
|
+ sapStockAge.setAMT4(etStockTable.getBigDecimal("AMT4"));
|
|
|
|
|
+ sapStockAge.setAMTN(etStockTable.getBigDecimal("AMTN"));
|
|
|
|
|
+ sapStockAges.add(sapStockAge);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析工厂 {} 第 {} 行库存数据时发生错误", factory, i, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return sapStockAges;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建结果消息
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildResultMessage(int total, int success, List<String> failedFactories) {
|
|
|
|
|
+ StringBuilder result = new StringBuilder();
|
|
|
|
|
+ result.append("SAP库龄同步完成。总计: ").append(total)
|
|
|
|
|
+ .append(", 成功: ").append(success)
|
|
|
|
|
+ .append(", 失败: ").append(total - success);
|
|
|
|
|
+
|
|
|
|
|
+ if (CollUtil.isNotEmpty(failedFactories)) {
|
|
|
|
|
+ result.append("。失败工厂: ").append(failedFactories);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询所有SAP工厂
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<String> factoryCodes(){
|
|
|
|
|
+ List<SapOrgRespDTO> sapOrgs = sapOrgApi.getSapOrgByType(1);
|
|
|
|
|
+ if (CollUtil.isEmpty(sapOrgs)) {
|
|
|
|
|
+ return CollectionUtil.newArrayList();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> factoryCodes = sapOrgs.stream()
|
|
|
|
|
+ .filter(dto -> "age".equals(dto.getCreator()))
|
|
|
|
|
+ .map(SapOrgRespDTO::getFactoryCode)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ return CollUtil.isEmpty(factoryCodes) ? CollUtil.empty(String.class) : factoryCodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 预加载同步上下文数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private SyncSapStockAgeContext preloadSyncContext() {
|
|
|
|
|
+ SyncSapStockAgeContext context = new SyncSapStockAgeContext();
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ // 提前获取SAP连接
|
|
|
|
|
+ context.destination = sapConnector.getDestination();
|
|
|
|
|
+ context.destination.ping(); // 测试连接
|
|
|
|
|
+
|
|
|
|
|
+ context.existStockAges = new ArrayList<>();
|
|
|
|
|
+ context.depts = deptApi.ignoredTenantDepts();
|
|
|
|
|
+ context.storageLocations = sapOrgApi.getSapOrgByType(3);
|
|
|
|
|
+ context.factories = sapOrgApi.getSapOrgByType(1);
|
|
|
|
|
+
|
|
|
|
|
+ // 构建映射关系
|
|
|
|
|
+ context.buildMappings();
|
|
|
|
|
+
|
|
|
|
|
+ log.error("同步上下文预加载完成: 工厂{}个, 库存地点{}个, 部门{}个, 现有库龄{}条",
|
|
|
|
|
+ CollUtil.size(context.factories),
|
|
|
|
|
+ CollUtil.size(context.storageLocations),
|
|
|
|
|
+ CollUtil.size(context.depts),
|
|
|
|
|
+ CollUtil.size(context.existStockAges));
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println(String.format("工厂=%s, 库存地点=%s, 部门=%s, 现有库存=%s",
|
|
|
|
|
+ CollUtil.size(context.factories), CollUtil.size(context.storageLocations), CollUtil.size(context.depts), CollUtil.size(context.existStockAges)));
|
|
|
|
|
+
|
|
|
|
|
+ return context;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("预加载同步上下文失败", e);
|
|
|
|
|
+ return context.invalid();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 内部类用于封装工厂同步结果
|
|
|
|
|
+ private static class FactorySyncResult {
|
|
|
|
|
+ String factoryCode;
|
|
|
|
|
+ List<IotSapInventoryAgeVO> stockAgeData;
|
|
|
|
|
+ Exception error;
|
|
|
|
|
+ boolean success;
|
|
|
|
|
+
|
|
|
|
|
+ FactorySyncResult(String factoryCode) {
|
|
|
|
|
+ this.factoryCode = factoryCode;
|
|
|
|
|
+ this.success = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static FactorySyncResult success(String factoryCode, List<IotSapInventoryAgeVO> stockAgeData) {
|
|
|
|
|
+ FactorySyncResult result = new FactorySyncResult(factoryCode);
|
|
|
|
|
+ result.stockAgeData = stockAgeData;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static FactorySyncResult failure(String factoryCode, Exception error) {
|
|
|
|
|
+ FactorySyncResult result = new FactorySyncResult(factoryCode);
|
|
|
|
|
+ result.error = error;
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|