From eba2afffb0c66df59d3e2908c2609551c1be1d83 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Tue, 26 May 2026 13:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5Alpha=E7=89=A9=E6=96=99part?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../part/mapper/PartInventorySyncMapper.java | 12 ++ .../service/PartInventorySyncService.java | 11 + .../impl/PartInventorySyncServiceImpl.java | 76 +++++++ .../part/task/PartInventorySyncTask.java | 47 +++++ src/main/resources/application-dev.yml | 4 + .../mapper/part/PartInventorySyncMapper.xml | 194 ++++++++++++++++++ 6 files changed, 344 insertions(+) create mode 100644 src/main/java/com/xujie/sys/modules/part/mapper/PartInventorySyncMapper.java create mode 100644 src/main/java/com/xujie/sys/modules/part/service/PartInventorySyncService.java create mode 100644 src/main/java/com/xujie/sys/modules/part/service/impl/PartInventorySyncServiceImpl.java create mode 100644 src/main/java/com/xujie/sys/modules/part/task/PartInventorySyncTask.java create mode 100644 src/main/resources/mapper/part/PartInventorySyncMapper.xml diff --git a/src/main/java/com/xujie/sys/modules/part/mapper/PartInventorySyncMapper.java b/src/main/java/com/xujie/sys/modules/part/mapper/PartInventorySyncMapper.java new file mode 100644 index 00000000..d47fd1b4 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/part/mapper/PartInventorySyncMapper.java @@ -0,0 +1,12 @@ +package com.xujie.sys.modules.part.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface PartInventorySyncMapper { + + int updatePartFromMesInventoryView(@Param("batchSize") Integer batchSize); + + int insertPartFromMesInventoryView(@Param("batchSize") Integer batchSize); +} diff --git a/src/main/java/com/xujie/sys/modules/part/service/PartInventorySyncService.java b/src/main/java/com/xujie/sys/modules/part/service/PartInventorySyncService.java new file mode 100644 index 00000000..cb084804 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/part/service/PartInventorySyncService.java @@ -0,0 +1,11 @@ +package com.xujie.sys.modules.part.service; + +public interface PartInventorySyncService { + + /** + * 从 MES 物料视图同步数据到 part 表 + * + * @param batchSize 分批大小,防止长事务导致锁升级 + */ + void syncFromMesInventoryView(Integer batchSize); +} diff --git a/src/main/java/com/xujie/sys/modules/part/service/impl/PartInventorySyncServiceImpl.java b/src/main/java/com/xujie/sys/modules/part/service/impl/PartInventorySyncServiceImpl.java new file mode 100644 index 00000000..d7e4f751 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/part/service/impl/PartInventorySyncServiceImpl.java @@ -0,0 +1,76 @@ +package com.xujie.sys.modules.part.service.impl; + +import com.xujie.sys.modules.part.mapper.PartInventorySyncMapper; +import com.xujie.sys.modules.part.service.PartInventorySyncService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class PartInventorySyncServiceImpl implements PartInventorySyncService { + + private static final int DEFAULT_BATCH_SIZE = 500; + private static final int MAX_LOOP_COUNT = 2000; + + @Autowired + private PartInventorySyncMapper partInventorySyncMapper; + + @Override + public void syncFromMesInventoryView(Integer batchSize) { + int safeBatchSize = (batchSize == null || batchSize <= 0) ? DEFAULT_BATCH_SIZE : batchSize; + + int totalUpdateCount = executeUpdateInBatches(safeBatchSize); + int totalInsertCount = executeInsertInBatches(safeBatchSize); + + if (totalUpdateCount > 0 || totalInsertCount > 0) { + log.info("MES物料视图同步完成:更新{}条,新增{}条,批次大小{}", totalUpdateCount, totalInsertCount, safeBatchSize); + } else { + log.debug("MES物料视图同步无变化,批次大小{}", safeBatchSize); + } + } + + /** + * 分批更新,避免一次更新过多导致锁升级 + */ + private int executeUpdateInBatches(int batchSize) { + int totalCount = 0; + boolean reachLoopLimit = true; + + for (int i = 0; i < MAX_LOOP_COUNT; i++) { + int currentCount = partInventorySyncMapper.updatePartFromMesInventoryView(batchSize); + totalCount += currentCount; + if (currentCount < batchSize) { + reachLoopLimit = false; + break; + } + } + + if (reachLoopLimit) { + log.warn("MES物料视图更新达到最大批次限制{},请检查是否存在长时间大批量变化", MAX_LOOP_COUNT); + } + return totalCount; + } + + /** + * 分批新增,避免一次插入过多导致锁等待 + */ + private int executeInsertInBatches(int batchSize) { + int totalCount = 0; + boolean reachLoopLimit = true; + + for (int i = 0; i < MAX_LOOP_COUNT; i++) { + int currentCount = partInventorySyncMapper.insertPartFromMesInventoryView(batchSize); + totalCount += currentCount; + if (currentCount < batchSize) { + reachLoopLimit = false; + break; + } + } + + if (reachLoopLimit) { + log.warn("MES物料视图新增达到最大批次限制{},请检查是否存在长时间大批量新增", MAX_LOOP_COUNT); + } + return totalCount; + } +} diff --git a/src/main/java/com/xujie/sys/modules/part/task/PartInventorySyncTask.java b/src/main/java/com/xujie/sys/modules/part/task/PartInventorySyncTask.java new file mode 100644 index 00000000..ddb9c21d --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/part/task/PartInventorySyncTask.java @@ -0,0 +1,47 @@ +package com.xujie.sys.modules.part.task; + +import com.xujie.sys.modules.part.service.PartInventorySyncService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class PartInventorySyncTask { + + @Autowired + private PartInventorySyncService partInventorySyncService; + + + /** + * part 物料同步任务开关 + */ + @Value("${task.partInventorySync.enabled:false}") + private boolean partInventorySyncEnabled; + + /** + * 分批大小,避免锁升级 + */ + @Value("${task.partInventorySync.batchSize:500}") + private Integer batchSize; + + /** + * 每半小时同步一次(默认) + */ + @Scheduled(cron = "${task.partInventorySync.cron:0 0/30 * * * ?}") + public void syncPartFromMesInventoryView() { + if (!partInventorySyncEnabled) { + return; + } + + long startTime = System.currentTimeMillis(); + try { + partInventorySyncService.syncFromMesInventoryView(batchSize); + log.info("part物料视图同步任务执行完成,耗时{}ms", System.currentTimeMillis() - startTime); + } catch (Exception e) { + log.error("part物料视图同步任务执行失败", e); + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index d55ed9f9..e94bdaef 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -57,6 +57,10 @@ task: sendEmail: 0 0 12 ? * MON-FRI #发送邮件 createQCInspection: 0 01 17 * * ? # 创建质量任务 enabled: false + partInventorySync: + enabled: false # part物料视图同步开关 + cron: 0 0/30 * * * ? # 每30分钟执行一次 + batchSize: 1000 # 分批处理条数,避免锁升级 #--------------------------------------------ERF审批邮件提醒配置------------------------------------------------- erf: diff --git a/src/main/resources/mapper/part/PartInventorySyncMapper.xml b/src/main/resources/mapper/part/PartInventorySyncMapper.xml new file mode 100644 index 00000000..fde18bcf --- /dev/null +++ b/src/main/resources/mapper/part/PartInventorySyncMapper.xml @@ -0,0 +1,194 @@ + + + + + + + ;WITH changed_part AS ( + SELECT TOP (#{batchSize}) + p.id, + nv.part_description, + nv.spec, + nv.part_type, + nv.umid, + nv.active, + nv.key_part, + nv.qty_box_roll, + nv.qty_roll, + nv.cinvSourceCode, + nv.sourceBu, + nv.control_mes, + nv.standard_box_qty, + nv.sku, + nv.cinvname, + nv.part_desce_en, + nv.cwhcode, + nv.cwhname, + nv.invdefinetype, + nv.invrcost, + nv.box_length, + nv.box_width, + nv.box_height + FROM part p WITH (ROWLOCK, READPAST) + INNER JOIN view_custdev_mes_inventory v WITH (NOLOCK) + ON p.site = ISNULL(v.site, '5') + AND p.part_no = v.part_no + CROSS APPLY ( + SELECT + CASE WHEN v.part_description IS NULL THEN NULL ELSE LEFT(v.part_description, 500) END AS part_description, + CASE WHEN v.spec IS NULL THEN NULL ELSE LEFT(v.spec, 300) END AS spec, + CASE WHEN v.part_type IS NULL THEN NULL ELSE LEFT(v.part_type, 100) END AS part_type, + CASE WHEN v.umid IS NULL THEN NULL ELSE LEFT(v.umid, 8) END AS umid, + CASE WHEN v.active IS NULL THEN NULL ELSE LEFT(v.active, 1) END AS active, + CASE WHEN v.key_part IS NULL THEN NULL ELSE LEFT(CONVERT(varchar(10), v.key_part), 1) END AS key_part, + CASE WHEN v.qty_box_roll IS NULL THEN NULL ELSE CONVERT(varchar(50), v.qty_box_roll) END AS qty_box_roll, + CASE WHEN v.qty_roll IS NULL THEN NULL ELSE CONVERT(varchar(50), v.qty_roll) END AS qty_roll, + CASE WHEN v.cinvSourceCode IS NULL THEN NULL ELSE LEFT(v.cinvSourceCode, 50) END AS cinvSourceCode, + CASE WHEN v.sourceBu IS NULL THEN NULL ELSE LEFT(v.sourceBu, 50) END AS sourceBu, + CASE WHEN v.control_mes IS NULL THEN NULL ELSE LEFT(v.control_mes, 1) END AS control_mes, + v.standard_box_qty AS standard_box_qty, + CASE + WHEN v.sku IS NULL OR UPPER(LTRIM(RTRIM(v.sku))) = 'NULL' THEN LEFT(v.part_no, 50) + ELSE LEFT(v.sku, 50) + END AS sku, + CASE WHEN v.cinvname IS NULL THEN NULL ELSE LEFT(v.cinvname, 50) END AS cinvname, + CASE WHEN v.part_desce_en IS NULL THEN NULL ELSE LEFT(v.part_desce_en, 1000) END AS part_desce_en, + CASE WHEN v.cwhcode IS NULL THEN NULL ELSE LEFT(v.cwhcode, 20) END AS cwhcode, + CASE WHEN v.cwhname IS NULL THEN NULL ELSE LEFT(v.cwhname, 50) END AS cwhname, + CASE WHEN v.invdefinetype IS NULL THEN NULL ELSE LEFT(v.invdefinetype, 100) END AS invdefinetype, + v.invrcost AS invrcost, + v.box_length AS box_length, + v.box_width AS box_width, + v.box_height AS box_height + ) nv + WHERE ISNULL(p.part_desc, '') <> ISNULL(nv.part_description, '') + OR ISNULL(p.spec, '') <> ISNULL(nv.spec, '') + OR ISNULL(p.part_type, '') <> ISNULL(nv.part_type, '') + OR ISNULL(p.umid, '') <> ISNULL(nv.umid, '') + OR ISNULL(p.active, '') <> ISNULL(nv.active, '') + OR ISNULL(p.key_part, '') <> ISNULL(nv.key_part, '') + OR ISNULL(p.qty_box_roll, '') <> ISNULL(nv.qty_box_roll, '') + OR ISNULL(p.qty_roll, '') <> ISNULL(nv.qty_roll, '') + OR ISNULL(p.cinv_source_code, '') <> ISNULL(nv.cinvSourceCode, '') + OR ISNULL(p.sourceBu, '') <> ISNULL(nv.sourceBu, '') + OR ISNULL(p.control_mes, '') <> ISNULL(nv.control_mes, '') + OR ISNULL(CONVERT(decimal(18, 4), p.standard_box_qty), 0) <> ISNULL(nv.standard_box_qty, 0) + OR ISNULL(p.sku, '') <> ISNULL(nv.sku, '') + OR ISNULL(p.cinvcname, '') <> ISNULL(nv.cinvname, '') + OR ISNULL(p.part_desce_en, '') <> ISNULL(nv.part_desce_en, '') + OR ISNULL(p.DefaultWarehouseID, '') <> ISNULL(nv.cwhcode, '') + OR ISNULL(p.DefaultWarehouseName, '') <> ISNULL(nv.cwhname, '') + OR ISNULL(p.invdefinetype, '') <> ISNULL(nv.invdefinetype, '') + OR ISNULL(CONVERT(decimal(20, 6), p.standard_cost), 0) <> ISNULL(CONVERT(decimal(20, 6), nv.invrcost), 0) + OR ISNULL(CONVERT(decimal(18, 4), p.box_length), 0) <> ISNULL(nv.box_length, 0) + OR ISNULL(CONVERT(decimal(18, 4), p.box_width), 0) <> ISNULL(nv.box_width, 0) + OR ISNULL(CONVERT(decimal(18, 4), p.box_height), 0) <> ISNULL(nv.box_height, 0) + ORDER BY p.id + ) + UPDATE p + SET p.part_desc = c.part_description, + p.spec = c.spec, + p.part_type = c.part_type, + p.umid = c.umid, + p.active = c.active, + p.key_part = c.key_part, + p.qty_box_roll = c.qty_box_roll, + p.qty_roll = c.qty_roll, + p.cinv_source_code = c.cinvSourceCode, + p.sourceBu = c.sourceBu, + p.control_mes = c.control_mes, + p.standard_box_qty = c.standard_box_qty, + p.sku = c.sku, + p.cinvcname = c.cinvname, + p.part_desce_en = c.part_desce_en, + p.DefaultWarehouseID = c.cwhcode, + p.DefaultWarehouseName = c.cwhname, + p.invdefinetype = c.invdefinetype, + p.standard_cost = c.invrcost, + p.box_length = c.box_length, + p.box_width = c.box_width, + p.box_height = c.box_height, + p.update_date = GETDATE(), + p.update_by = 'SYSTEM_SYNC' + FROM part p WITH (ROWLOCK, UPDLOCK, READPAST) + INNER JOIN changed_part c ON p.id = c.id + + + + + INSERT INTO part ( + site, + part_no, + part_desc, + spec, + part_type, + umid, + active, + key_part, + qty_box_roll, + qty_roll, + cinv_source_code, + sourceBu, + control_mes, + standard_box_qty, + sku, + cinvcname, + part_desce_en, + DefaultWarehouseID, + DefaultWarehouseName, + invdefinetype, + standard_cost, + box_length, + box_width, + box_height, + created_by, + creation_date, + update_by, + update_date + ) + SELECT TOP (#{batchSize}) + ISNULL(v.site, '5'), + v.part_no, + CASE WHEN v.part_description IS NULL THEN NULL ELSE LEFT(v.part_description, 500) END, + CASE WHEN v.spec IS NULL THEN NULL ELSE LEFT(v.spec, 300) END, + CASE WHEN v.part_type IS NULL THEN NULL ELSE LEFT(v.part_type, 100) END, + CASE WHEN v.umid IS NULL THEN NULL ELSE LEFT(v.umid, 8) END, + CASE WHEN v.active IS NULL THEN NULL ELSE LEFT(v.active, 1) END, + CASE WHEN v.key_part IS NULL THEN NULL ELSE LEFT(CONVERT(varchar(10), v.key_part), 1) END, + CASE WHEN v.qty_box_roll IS NULL THEN NULL ELSE CONVERT(varchar(50), v.qty_box_roll) END, + CASE WHEN v.qty_roll IS NULL THEN NULL ELSE CONVERT(varchar(50), v.qty_roll) END, + CASE WHEN v.cinvSourceCode IS NULL THEN NULL ELSE LEFT(v.cinvSourceCode, 50) END, + CASE WHEN v.sourceBu IS NULL THEN NULL ELSE LEFT(v.sourceBu, 50) END, + CASE WHEN v.control_mes IS NULL THEN NULL ELSE LEFT(v.control_mes, 1) END, + v.standard_box_qty, + CASE + WHEN v.sku IS NULL OR UPPER(LTRIM(RTRIM(v.sku))) = 'NULL' THEN LEFT(v.part_no, 50) + ELSE LEFT(v.sku, 50) + END, + CASE WHEN v.cinvname IS NULL THEN NULL ELSE LEFT(v.cinvname, 50) END, + CASE WHEN v.part_desce_en IS NULL THEN NULL ELSE LEFT(v.part_desce_en, 1000) END, + CASE WHEN v.cwhcode IS NULL THEN NULL ELSE LEFT(v.cwhcode, 20) END, + CASE WHEN v.cwhname IS NULL THEN NULL ELSE LEFT(v.cwhname, 50) END, + CASE WHEN v.invdefinetype IS NULL THEN NULL ELSE LEFT(v.invdefinetype, 100) END, + v.invrcost, + v.box_length, + v.box_width, + v.box_height, + 'SYSTEM_SYNC', + GETDATE(), + 'SYSTEM_SYNC', + GETDATE() + FROM view_custdev_mes_inventory v WITH (NOLOCK) + LEFT JOIN part p WITH (READPAST) + ON p.site = ISNULL(v.site, '5') + AND p.part_no = v.part_no + WHERE p.id IS NULL + AND v.part_no IS NOT NULL + AND v.part_no != '' + ORDER BY ISNULL(v.site, '5'), v.part_no + +