From 02a1ae81eba7b48223052fb833859f0a65dd7d21 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 17:31:08 +0800
Subject: [PATCH] feat(mes): 实现袋码大批量生成异步任务功能
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProBagCodeGenerateCleanupTask.java | 55 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateTaskStatusEnum.java | 65 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskService.java | 54 +
db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz | 0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java | 41 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateSpec.java | 30
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java | 4
yudao-server/src/main/resources/application.yaml | 18
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java | 5
sql/mysql/mes_pro_bag_code_bag_no_index.sql | 18
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeGenerateTaskDO.java | 105 ++
sql/mysql/mes_pro_bag_code_generate_task.sql | 35
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateBizTypeEnum.java | 42 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeChunkWriter.java | 51 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeGenerateTaskMapper.java | 55 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateProperties.java | 56 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskServiceImpl.java | 216 +++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeBatchGenerator.java | 186 ++++
TempDbAdmin.java | 34
TempDbCleanup.java | 232 +++++
db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz.manifest | 125 +++
sql/mysql/mes_pro_pallet_generate_uuid.sql | 29
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateTaskRegistry.java | 108 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java | 58 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateExecutorConfig.java | 38
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java | 5
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateCleaner.java | 46 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java | 151 ++-
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateRunner.java | 90 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java | 12
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/BagBusinessCodeTest.java | 45
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateTaskRespVO.java | 63 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java | 4
TempDbAdmin.class | 0
TempDbCleanup.class | 0
docs/bag_code_async_generate_plan.md | 309 +++++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java | 11
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java | 13
38 files changed, 2,341 insertions(+), 68 deletions(-)
diff --git a/TempDbAdmin.class b/TempDbAdmin.class
new file mode 100644
index 0000000..385e56d
--- /dev/null
+++ b/TempDbAdmin.class
Binary files differ
diff --git a/TempDbAdmin.java b/TempDbAdmin.java
new file mode 100644
index 0000000..9da9ea5
--- /dev/null
+++ b/TempDbAdmin.java
@@ -0,0 +1,34 @@
+import java.nio.file.*;
+import java.sql.*;
+import java.util.*;
+
+public class TempDbAdmin {
+ public static void main(String[] args) throws Exception {
+ String yaml = Files.readString(Path.of("yudao-server/src/main/resources/application-thzy.yaml"));
+ String block = yaml.substring(yaml.indexOf("master:"));
+ String[] lines = block.split("\\R");
+ String user = null, pass = null;
+ boolean master = false;
+ for (String line : lines) {
+ if (line.trim().equals("master:")) { master = true; continue; }
+ if (!master) continue;
+ if (line.trim().startsWith("username:")) user = line.trim().substring(9).trim().split("\\s+#", 2)[0];
+ if (line.trim().startsWith("password:")) { pass = line.trim().substring(9).trim().split("\\s+#", 2)[0]; break; }
+ }
+ if (user == null || pass == null) throw new IllegalStateException("Missing database credentials");
+ String url = "jdbc:mysql://1.95.78.124:9000/mom-thzynew?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
+ try (Connection conn = DriverManager.getConnection(url, user, pass)) {
+ System.out.println("DATABASE=" + conn.getCatalog());
+ if (args.length == 0) return;
+ try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(args[0])) {
+ ResultSetMetaData md = rs.getMetaData();
+ for (int i = 1; i <= md.getColumnCount(); i++) System.out.print((i > 1 ? "\t" : "") + md.getColumnLabel(i));
+ System.out.println();
+ while (rs.next()) {
+ for (int i = 1; i <= md.getColumnCount(); i++) System.out.print((i > 1 ? "\t" : "") + Objects.toString(rs.getObject(i), "NULL"));
+ System.out.println();
+ }
+ }
+ }
+ }
+}
diff --git a/TempDbCleanup.class b/TempDbCleanup.class
new file mode 100644
index 0000000..d2a7b01
--- /dev/null
+++ b/TempDbCleanup.class
Binary files differ
diff --git a/TempDbCleanup.java b/TempDbCleanup.java
new file mode 100644
index 0000000..4be2ea3
--- /dev/null
+++ b/TempDbCleanup.java
@@ -0,0 +1,232 @@
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.security.MessageDigest;
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.zip.GZIPOutputStream;
+
+/** Temporary, scoped administrator for the requested mom-thzynew data cleanup. */
+public class TempDbCleanup {
+ private static final String DB = "mom-thzynew";
+ private static final Set<String> PRO_TABLES = Set.of(
+ "mes_pro_bag_code", "mes_pro_bag_code_replace", "mes_pro_batch",
+ "mes_pro_batch_worker", "mes_pro_pallet", "mes_pro_qr_export_task",
+ "mes_pro_scan_event");
+ private static final Set<String> KEEP = Set.of("mes_wm_warehouse", "mes_wm_warehouse_area",
+ "mes_wm_warehouse_location", "mes_pro_line");
+
+ public static void main(String[] args) throws Exception {
+ if (args.length < 1 || !Set.of("inventory", "backup", "clean").contains(args[0])
+ || (args[0].equals("clean") ? args.length != 2 : args.length != 1)) {
+ throw new IllegalArgumentException("Usage: TempDbCleanup inventory|backup|clean <backup.sql.gz>");
+ }
+ String yaml = Files.readString(Path.of("yudao-server/src/main/resources/application-thzy.yaml"));
+ String block = yaml.substring(yaml.indexOf("master:"));
+ String user = null, pass = null;
+ for (String line : block.split("\\R")) {
+ if (line.trim().startsWith("username:")) user = value(line);
+ if (line.trim().startsWith("password:")) { pass = value(line); break; }
+ }
+ if (user == null || pass == null) throw new IllegalStateException("Missing database credentials");
+ String url = "jdbc:mysql://1.95.78.124:9000/" + DB
+ + "?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useCursorFetch=true";
+ try (Connection conn = DriverManager.getConnection(url, user, pass)) {
+ if (!DB.equals(conn.getCatalog())) throw new IllegalStateException("Wrong database: " + conn.getCatalog());
+ List<String> tables = targetTables(conn);
+ System.out.println("DATABASE=" + DB + " TARGET_TABLES=" + tables.size());
+ if (args[0].equals("inventory")) inventory(conn, tables);
+ if (args[0].equals("backup")) backup(conn, tables);
+ if (args[0].equals("clean")) clean(conn, tables, Path.of(args[1]));
+ }
+ }
+
+ private static String value(String line) {
+ return line.trim().split(":", 2)[1].trim().split("\\s+#", 2)[0];
+ }
+
+ private static List<String> targetTables(Connection conn) throws SQLException {
+ List<String> names = new ArrayList<>();
+ try (Statement s = conn.createStatement(); ResultSet r = s.executeQuery(
+ "SELECT table_name FROM information_schema.tables WHERE table_schema='" + DB + "' "
+ + "AND table_type='BASE TABLE' ORDER BY table_name")) {
+ while (r.next()) {
+ String t = r.getString(1);
+ if (KEEP.contains(t)) continue;
+ if (PRO_TABLES.contains(t) || t.startsWith("mes_qc_") || t.startsWith("mes_wm_")
+ || t.startsWith("mes_trace_") || t.startsWith("crm_")) names.add(t);
+ }
+ }
+ return names;
+ }
+
+ private static long count(Connection conn, String table) throws SQLException {
+ try (Statement s = conn.createStatement(); ResultSet r = s.executeQuery("SELECT COUNT(*) FROM `" + table + "`")) {
+ r.next(); return r.getLong(1);
+ }
+ }
+
+ private static void inventory(Connection conn, List<String> tables) throws SQLException {
+ for (String t : tables) {
+ long n = count(conn, t);
+ if (n > 0) System.out.println(t + "=" + n);
+ }
+ for (String t : KEEP) System.out.println("KEEP " + t + "=" + count(conn, t));
+ try (Statement s = conn.createStatement(); ResultSet r = s.executeQuery(
+ "SELECT status, COUNT(*), SUM(total_count-finished_count) FROM mes_pro_qr_export_task "
+ + "WHERE status IN (10,20) GROUP BY status")) {
+ while (r.next()) System.out.println("ACTIVE_EXPORT status=" + r.getInt(1)
+ + " tasks=" + r.getLong(2) + " remaining=" + r.getLong(3));
+ }
+ }
+
+ private static void requireIdle(Connection conn) throws SQLException {
+ try (Statement s = conn.createStatement(); ResultSet r = s.executeQuery(
+ "SELECT COUNT(*) FROM mes_pro_qr_export_task WHERE status IN (10,20)")) {
+ r.next();
+ if (r.getLong(1) != 0) throw new IllegalStateException("QR export task still active; aborting");
+ }
+ }
+
+ private static void backup(Connection conn, List<String> tables) throws Exception {
+ requireIdle(conn);
+ Path dir = Path.of("db-backups");
+ Files.createDirectories(dir);
+ String stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
+ Path dest = dir.resolve(DB + "-business-cleanup-" + stamp + ".sql.gz");
+ Path temp = dir.resolve(dest.getFileName() + ".part");
+ long total = 0;
+ Properties manifest = new Properties();
+ manifest.setProperty("database", DB);
+ manifest.setProperty("table.count", Integer.toString(tables.size()));
+ conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
+ conn.setAutoCommit(false);
+ try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
+ new GZIPOutputStream(Files.newOutputStream(temp), 1024 * 1024), StandardCharsets.UTF_8), 1024 * 1024)) {
+ out.write("-- mom-thzynew requested business-data backup; restore only to the intended database\n");
+ out.write("SET NAMES utf8mb4;\nSET sql_mode='';\n");
+ for (String table : tables) {
+ long rows = 0;
+ try (Statement s = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
+ s.setFetchSize(1000);
+ try (ResultSet r = s.executeQuery("SELECT * FROM `" + table + "`")) {
+ ResultSetMetaData md = r.getMetaData();
+ int cols = md.getColumnCount();
+ StringJoiner columns = new StringJoiner(",");
+ for (int i = 1; i <= cols; i++) columns.add("`" + md.getColumnName(i) + "`");
+ String prefix = "INSERT INTO `" + table + "` (" + columns + ") VALUES (";
+ while (r.next()) {
+ out.write(prefix);
+ for (int i = 1; i <= cols; i++) {
+ if (i > 1) out.write(',');
+ writeSqlValue(out, r, md, i);
+ }
+ out.write(");\n");
+ rows++;
+ }
+ }
+ }
+ total += rows;
+ manifest.setProperty("row." + table, Long.toString(rows));
+ if (rows > 0) System.out.println("BACKED_UP " + table + "=" + rows);
+ }
+ out.write("-- backup complete: " + total + " rows\n");
+ conn.rollback();
+ } catch (Exception e) {
+ conn.rollback();
+ Files.deleteIfExists(temp);
+ throw e;
+ } finally {
+ conn.setAutoCommit(true);
+ }
+ Files.move(temp, dest, StandardCopyOption.ATOMIC_MOVE);
+ manifest.setProperty("sha256", sha256(dest));
+ manifest.setProperty("rows.total", Long.toString(total));
+ try (OutputStream m = Files.newOutputStream(Path.of(dest + ".manifest"))) {
+ manifest.store(m, "mom-thzynew business data backup manifest");
+ }
+ System.out.println("BACKUP_FILE=" + dest.toAbsolutePath() + " ROWS=" + total
+ + " BYTES=" + Files.size(dest));
+ }
+
+ private static void writeSqlValue(Writer out, ResultSet r, ResultSetMetaData md, int i) throws Exception {
+ int type = md.getColumnType(i);
+ if (type == Types.BINARY || type == Types.VARBINARY || type == Types.LONGVARBINARY
+ || type == Types.BLOB || type == Types.BIT) {
+ byte[] bytes = r.getBytes(i);
+ if (bytes == null) { out.write("NULL"); return; }
+ out.write("X'");
+ for (byte b : bytes) out.write(String.format("%02X", b & 0xff));
+ out.write("'");
+ return;
+ }
+ String value = r.getString(i);
+ if (value == null) { out.write("NULL"); return; }
+ if (type == Types.INTEGER || type == Types.BIGINT || type == Types.SMALLINT
+ || type == Types.TINYINT || type == Types.DECIMAL || type == Types.NUMERIC
+ || type == Types.DOUBLE || type == Types.FLOAT || type == Types.REAL) {
+ out.write(value); return;
+ }
+ out.write('\'');
+ for (int p = 0; p < value.length(); p++) {
+ char c = value.charAt(p);
+ switch (c) {
+ case '\\' -> out.write("\\\\");
+ case '\'' -> out.write("\\'");
+ case '\n' -> out.write("\\n");
+ case '\r' -> out.write("\\r");
+ case '\0' -> out.write("\\0");
+ case 26 -> out.write("\\Z");
+ default -> out.write(c);
+ }
+ }
+ out.write('\'');
+ }
+
+ private static String sha256(Path path) throws Exception {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ try (InputStream in = Files.newInputStream(path)) {
+ byte[] buf = new byte[1024 * 1024];
+ int n;
+ while ((n = in.read(buf)) != -1) digest.update(buf, 0, n);
+ }
+ return HexFormat.of().formatHex(digest.digest());
+ }
+
+ private static void clean(Connection conn, List<String> tables, Path backup) throws Exception {
+ requireIdle(conn);
+ Properties manifest = new Properties();
+ try (InputStream in = Files.newInputStream(Path.of(backup + ".manifest"))) { manifest.load(in); }
+ if (!DB.equals(manifest.getProperty("database"))
+ || tables.size() != Integer.parseInt(manifest.getProperty("table.count"))
+ || !sha256(backup).equals(manifest.getProperty("sha256"))) {
+ throw new IllegalStateException("Backup integrity or target mismatch");
+ }
+ // Verify every table before any destructive statement. New writes cause a hard abort.
+ Map<String, Long> expected = new LinkedHashMap<>();
+ for (String table : tables) {
+ String raw = manifest.getProperty("row." + table);
+ if (raw == null) throw new IllegalStateException("Missing backup count: " + table);
+ long n = Long.parseLong(raw);
+ long now = count(conn, table);
+ if (n != now) throw new IllegalStateException("Table changed since backup: " + table
+ + " backup=" + n + " current=" + now);
+ expected.put(table, n);
+ }
+ for (String table : List.of("mes_pro_line", "mes_wm_warehouse", "mes_wm_warehouse_area",
+ "mes_wm_warehouse_location")) {
+ System.out.println("PRESERVE " + table + "=" + count(conn, table));
+ }
+ for (Map.Entry<String, Long> entry : expected.entrySet()) {
+ if (entry.getValue() == 0) continue;
+ requireIdle(conn);
+ try (Statement s = conn.createStatement()) {
+ s.executeUpdate("TRUNCATE TABLE `" + entry.getKey() + "`");
+ }
+ System.out.println("CLEARED " + entry.getKey() + "=" + entry.getValue());
+ }
+ inventory(conn, tables);
+ }
+}
diff --git a/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz b/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz
new file mode 100644
index 0000000..4183c92
--- /dev/null
+++ b/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz
Binary files differ
diff --git a/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz.manifest b/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz.manifest
new file mode 100644
index 0000000..0b8a743
--- /dev/null
+++ b/db-backups/mom-thzynew-business-cleanup-20261019-171745.sql.gz.manifest
@@ -0,0 +1,125 @@
+#mom-thzynew business data backup manifest
+#Mon Oct 19 17:23:57 CST 2026
+database=mom-thzynew
+row.crm_business=0
+row.crm_business_product=0
+row.crm_business_status=0
+row.crm_business_status_type=0
+row.crm_clue=0
+row.crm_contact=0
+row.crm_contact_business=0
+row.crm_contract=0
+row.crm_contract_config=0
+row.crm_contract_product=0
+row.crm_customer=0
+row.crm_customer_limit_config=0
+row.crm_customer_pool_config=0
+row.crm_follow_up_record=0
+row.crm_invoice=0
+row.crm_permission=0
+row.crm_receivable=0
+row.crm_receivable_plan=0
+row.crm_sale_quotation=0
+row.crm_sale_quotation_product=0
+row.crm_sales_target=0
+row.mes_pro_bag_code=1436003
+row.mes_pro_bag_code_replace=0
+row.mes_pro_batch=0
+row.mes_pro_batch_worker=0
+row.mes_pro_pallet=25600
+row.mes_pro_qr_export_task=16
+row.mes_pro_scan_event=0
+row.mes_qc_approver_config=0
+row.mes_qc_defect=0
+row.mes_qc_defect_record=0
+row.mes_qc_indicator=0
+row.mes_qc_indicator_result=0
+row.mes_qc_indicator_result_detail=0
+row.mes_qc_ipqc=0
+row.mes_qc_ipqc_line=0
+row.mes_qc_iqc=0
+row.mes_qc_iqc_line=0
+row.mes_qc_ncr=0
+row.mes_qc_oqc=0
+row.mes_qc_oqc_action=0
+row.mes_qc_oqc_line=0
+row.mes_qc_rqc=0
+row.mes_qc_rqc_line=0
+row.mes_qc_seed_indicator_range=0
+row.mes_qc_seed_quality=0
+row.mes_qc_seed_quality_action=0
+row.mes_qc_seed_quality_indicator=0
+row.mes_qc_seed_quality_source=0
+row.mes_qc_template=0
+row.mes_qc_template_indicator=0
+row.mes_qc_template_item=0
+row.mes_trace_anti_config=1
+row.mes_trace_consumer_scan_event=62
+row.mes_trace_cross_region_warning=0
+row.mes_trace_event=0
+row.mes_trace_integrity_record=62
+row.mes_wm_arrival_notice=0
+row.mes_wm_arrival_notice_line=0
+row.mes_wm_barcode=0
+row.mes_wm_barcode_config=0
+row.mes_wm_batch=0
+row.mes_wm_item_consume=0
+row.mes_wm_item_consume_detail=0
+row.mes_wm_item_consume_line=0
+row.mes_wm_item_receipt=0
+row.mes_wm_item_receipt_detail=0
+row.mes_wm_item_receipt_line=0
+row.mes_wm_material_stock=0
+row.mes_wm_material_stock_bag=0
+row.mes_wm_misc_issue=0
+row.mes_wm_misc_issue_detail=0
+row.mes_wm_misc_issue_line=0
+row.mes_wm_misc_receipt=0
+row.mes_wm_misc_receipt_detail=0
+row.mes_wm_misc_receipt_line=0
+row.mes_wm_outsource_issue=0
+row.mes_wm_outsource_issue_detail=0
+row.mes_wm_outsource_issue_line=0
+row.mes_wm_outsource_receipt=0
+row.mes_wm_outsource_receipt_detail=0
+row.mes_wm_outsource_receipt_line=0
+row.mes_wm_package=0
+row.mes_wm_package_line=0
+row.mes_wm_product_issue=0
+row.mes_wm_product_issue_detail=0
+row.mes_wm_product_issue_line=0
+row.mes_wm_product_produce=0
+row.mes_wm_product_produce_detail=0
+row.mes_wm_product_produce_line=0
+row.mes_wm_product_receipt=0
+row.mes_wm_product_receipt_detail=0
+row.mes_wm_product_receipt_line=0
+row.mes_wm_product_sales=0
+row.mes_wm_product_sales_detail=0
+row.mes_wm_product_sales_line=0
+row.mes_wm_return_issue=0
+row.mes_wm_return_issue_detail=0
+row.mes_wm_return_issue_line=0
+row.mes_wm_return_sales=0
+row.mes_wm_return_sales_detail=0
+row.mes_wm_return_sales_line=0
+row.mes_wm_return_vendor=0
+row.mes_wm_return_vendor_detail=0
+row.mes_wm_return_vendor_line=0
+row.mes_wm_sales_notice=0
+row.mes_wm_sales_notice_line=0
+row.mes_wm_sn=0
+row.mes_wm_stock_reserve=0
+row.mes_wm_stock_taking_plan=0
+row.mes_wm_stock_taking_plan_param=0
+row.mes_wm_stock_taking_task=0
+row.mes_wm_stock_taking_task_line=0
+row.mes_wm_stock_taking_task_result=0
+row.mes_wm_stock_warning=0
+row.mes_wm_transaction=0
+row.mes_wm_transfer=0
+row.mes_wm_transfer_detail=0
+row.mes_wm_transfer_line=0
+rows.total=1461744
+sha256=13f2a900bfcfac80d6aec291b23750a2482ae988d3e8888dacd9f41e46eaa6f5
+table.count=119
diff --git a/docs/bag_code_async_generate_plan.md b/docs/bag_code_async_generate_plan.md
new file mode 100644
index 0000000..b702faa
--- /dev/null
+++ b/docs/bag_code_async_generate_plan.md
@@ -0,0 +1,309 @@
+# 璧嬬爜绠$悊澶ф壒閲忕敓鎴愭敼閫犳柟妗堬紙寮傛浠诲姟鍖栵級
+
+## 涓�銆佽儗鏅�
+
+銆岀敓鎴愪簩缁寸爜鏁版嵁鍖呫�嶏紙`POST /mes/pro/bag-code/generate`锛変笌銆岀敓鎴愬師濮嬫暟鎹�嶏紙`POST /mes/pro/bag-code/raw-generate`锛夊湪鍑犲崄涓囨潯鏁版嵁閲忎笅鐩存帴瓒呮椂锛屽墠绔敹涓嶅埌鍝嶅簲銆�
+
+缁忎唬鐮佸畾浣嶏紝瓒呮椂涓嶆槸鍗曚竴鍘熷洜锛岃�屾槸涓夊鍙犲姞锛�
+
+| 鐡堕 | 浣嶇疆 | 璇存槑 |
+|------|------|------|
+| 閫愭潯鍚� Redis 瑕佺爜 | `MesProBagCodeServiceImpl.java:143-144`銆乣176-177` | 姣忎釜琚嬬爜鐢熸垚銆岃拷婧爜 + 涓氬姟鐮併�嶄袱娆★紝`MesCodeRedisDAO.generate()` 姣忔鎵ц涓�娆� Redis `SADD` 寰�杩旓紙`MesCodeRedisDAO.java:53`锛夈��30 涓囪 = 60 涓囨缃戠粶寰�杩旓紝浠呮涓�姝ュ嵆鍒嗛挓绾� |
+| 鍗曚簨鍔″法閲忔彃鍏� | `MesProBagCodeServiceImpl.java:154`銆乣185` | `insertBatch` 榛樿 1000 鏉�/鎵癸紝浣嗘暣鎵瑰湪鍚屼竴涓� `@Transactional` 鍐呭畬鎴愶紝鍑犲崄涓囪褰㈡垚瓒呭ぇ浜嬪姟 |
+| 鍚屾闀胯姹� | Controller 鐩存帴杩斿洖鐢熸垚缁撴灉 | 璇锋眰绾跨▼浠庡ご鎵ц鍒板熬锛涚綉鍏�/娴忚鍣ㄨ秴鏃跺悗杩炴帴鏂紑锛屼絾鏈嶅姟绔粛缁х画鍐欏簱銆傜敤鎴烽噸璇曚細鍐嶇敓鎴愪竴鏁村寘閲嶅鏁版嵁锛屼笖鏃犲箓绛変繚鎶� |
+
+姝ゅ锛宍selectMaxBagNo()` 瀵� `mes_pro_bag_code` 鍋� `MAX(bag_no)`锛屽綋鍓嶈〃娌℃湁 `bag_no` 绱㈠紩锛屾暟鎹噺涓婃潵鍚庢瘡娆$敓鎴愰兘瑕佸叏琛ㄦ壂鎻忋��
+
+## 浜屻�佺洰鏍囦笌闈炵洰鏍�
+
+### 鐩爣
+
+1. 鍑犲崄涓囨潯锛堝崟娆′笂闄� 100 涓囷級鐢熸垚涓嶅啀渚濊禆鍗曟 HTTP 璇锋眰鏃堕暱锛屾彁浜ゅ悗绔嬪嵆杩斿洖浠诲姟缂栧彿銆�
+2. 鐢熸垚杩囩▼鍙杩涘害銆佸彲鍙栨秷锛涘け璐�/鍙栨秷涓嶇暀涓嬪崐鎴愬搧鏁版嵁鍖呫��
+3. 娑堥櫎閫愭潯 Redis 寰�杩斾笌瓒呭ぇ浜嬪姟锛岀敓鎴愬悶鍚愪笌鏁版嵁閲忚В鑰︺��
+4. 灏忔壒閲忎繚鎸佸悓姝ョ鍥烇紝涓嶆敼鍙樹娇鐢ㄤ綋楠屻��
+5. 鎺ュ彛鍏峰骞傜瓑鎬э紝缃戠粶閲嶈瘯涓嶄細閲嶅鐢熸垚銆�
+
+### 闈炵洰鏍�
+
+- 涓嶆敼閫犮�岀牬鎹熸浛鎹€�嶃�岀粦瀹氭壒娆�/鎵樼洏銆嶇瓑鍏朵粬璧嬬爜鎿嶄綔锛涘畠浠湰韬槸灏戦噺鏁版嵁鐨勭煭浜嬪姟銆�
+- 涓嶅仛鏈嶅姟閲嶅惎鍚庣殑鏂偣缁窇锛堣 5.7 鍙栬垗璇存槑锛夈��
+- 涓嶈皟鏁� 16 浣嶉殢鏈虹爜鐨勭敓鎴愯鍒欎笌 `biz:code:used` 闆嗗悎鏈哄埗銆�
+
+## 涓夈�佹�讳綋鏂规
+
+閲囩敤涓庣幇鏈変簩缁寸爜瀵煎嚭浠诲姟锛坄MesProQrExportTask`锛変竴鑷寸殑銆屽皬鎵归噺鍚屾銆佸ぇ鎵归噺寮傛銆嶅弻璺緞锛�
+
+```
+鍓嶇鎻愪氦鐢熸垚
+ 鈹�
+ 鈹溾攢 琚嬬爜鎬绘暟 鈮� sync-threshold锛堥粯璁� 20000锛�
+ 鈹� 鈹斺攢 璇锋眰绾跨▼鍐呭畬鎴愶細鎵归噺鍙栫爜 鈫� 鍒嗘钀藉簱 鈫� 杩斿洖鍘熸湁缁撴灉锛坅sync=false锛�
+ 鈹�
+ 鈹斺攢 琚嬬爜鎬绘暟 > sync-threshold
+ 鈹溾攢 钀戒换鍔¤褰曪紝娉ㄥ唽杩愯鎬侊紝鎶曢�掑悗鍙扮嚎绋嬫睜锛岀珛鍗宠繑鍥� taskId + uuid锛坅sync=true锛�
+ 鈹斺攢 鍓嶇鎸� taskId 杞杩涘害锛涘畬鎴愬悗鍒锋柊鍒楄〃锛涘彇娑堝垯鍒犻櫎宸茬敓鎴愮殑閮ㄥ垎鏁版嵁
+```
+
+鍚屾涓庡紓姝ュ叡鐢ㄥ悓涓�濂椼�屽垎娈电敓鎴愩�嶆墽琛屽櫒锛岄伩鍏嶄袱浠介�昏緫銆�
+
+## 鍥涖�佷换鍔℃ā鍨�
+
+### 4.1 鐘舵�佹満
+
+| 鐘舵�佸�� | 鍚嶇О | 璇存槑 |
+|--------|------|------|
+| 10 | 鎺掗槦涓� | 宸茶惤搴擄紝绛夊緟绾跨▼姹犳帴绠� |
+| 20 | 鐢熸垚涓� | 鍚庡彴绾跨▼鎵ц涓� |
+| 25 | 鍙栨秷涓� | 宸叉敹鍒板彇娑堣姹傦紝姝e湪鍋滄骞舵竻鐞嗛儴鍒嗘暟鎹� |
+| 30 | 宸插畬鎴� | 鍏ㄩ儴琚嬬爜锛堝強鎵樼洏锛夊凡钀藉簱 |
+| 40 | 宸插彇娑� | 鐢ㄦ埛鍙栨秷锛岄儴鍒嗘暟鎹凡娓呯悊 |
+| 50 | 宸插け璐� | 寮傚父缁堟锛岄儴鍒嗘暟鎹凡娓呯悊 |
+
+缁堟�侊細30 / 40 / 50銆�
+
+### 4.2 琛ㄧ粨鏋�
+
+鏂板 `mes_pro_bag_code_generate_task`锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | BIGINT | 涓婚敭 |
+| request_id | VARCHAR(64) | 骞傜瓑閿紝鍞竴绱㈠紩锛涘墠绔瘡娆℃彁浜ょ敓鎴愪竴涓� UUID |
+| biz_type | VARCHAR(16) | `PACKAGE`锛堟暟鎹寘锛�/ `RAW`锛堝師濮嬫暟鎹級 |
+| uuid | VARCHAR(32) | 鏈鐢熸垚缁� UUID锛堟彁浜ゆ椂鍗崇敓鎴愶紝澶辫触/鍙栨秷鍚庣敤浜庢竻鐞嗭級 |
+| item_id / item_code / item_name | BIGINT / VARCHAR | 鍝佺蹇収 |
+| unit_type | VARCHAR(32) | 璁¢噺鍗曚綅绫诲瀷锛堝惃 / 48琚� / 100琚嬶級 |
+| per_pallet | INT | 姣忔墭琚嬫暟锛涘惃绫诲瀷涓� 0 |
+| pallet_count | INT | 鎵樼洏鏁帮紱鍘熷鏁版嵁涓� 0 |
+| total_count | INT | 琚嬬爜鎬绘暟 |
+| finished_count | INT | 宸茬敓鎴愯鐮佹暟 |
+| status | TINYINT | 鐘舵�侊紝瑙� 4.1 |
+| error_msg | VARCHAR(500) | 澶辫触鍘熷洜 |
+| finish_time | DATETIME | 瀹屾垚/缁堟鏃堕棿 |
+| creator / create_time / updater / update_time / deleted | 鈥� | 娌跨敤 `BaseDO` |
+
+绱㈠紩锛�
+
+- `UNIQUE KEY uk_request_id (request_id)`
+- `KEY idx_status (status)`
+- `KEY idx_uuid (uuid)`
+
+杩佺Щ鑴氭湰锛歚sql/mysql/mes_pro_bag_code_generate_task.sql`銆�
+
+### 4.3 琚嬬爜琛ㄧ储寮�
+
+鏂板 `KEY idx_bag_no (bag_no)`锛岀敤浜� `selectMaxBagNo()` 涓庤搴忓彿鎺掑簭銆�
+
+杩佺Щ鑴氭湰锛歚sql/mysql/mes_pro_bag_code_bag_no_index.sql`锛堝厛鏌� `information_schema.statistics` 鍐嶅缓锛屽吋瀹归噸澶嶆墽琛岋級銆�
+
+## 浜斻�佽缁嗚璁�
+
+### 5.1 鎺ュ彛
+
+#### 1锛夌敓鎴愭暟鎹寘 / 鍘熷鏁版嵁锛堟部鐢ㄥ師璺緞锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | `/mes/pro/bag-code/generate` | 鍚堝苟鍚庣殑鏁版嵁鍖呯敓鎴愬叆鍙� |
+| POST | `/mes/pro/bag-code/raw-generate` | 鍘熷琚嬬爜鐢熸垚鍏ュ彛 |
+
+璇锋眰鍙傛暟涓嶅彉銆傚搷搴旀墿灞曪細
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| async | Boolean | true=宸茶浆鍚庡彴浠诲姟锛沠alse=鏈鍚屾瀹屾垚 |
+| taskId | Long | 寮傛浠诲姟缂栧彿锛涘悓姝ユ椂涓� null |
+| uuid | String | 鐢熸垚缁� UUID锛屼袱绉嶆ā寮忛兘杩斿洖 |
+| type / perPallet | String / Integer | 鏁版嵁鍖呮ā寮忎笅杩斿洖 |
+| totalCount | Integer | 琚嬬爜鎬绘暟 |
+| palletCount | Integer | 鎵樼洏鎬绘暟锛涘師濮嬫暟鎹负 0 |
+| palletIds | List\<Long\> | 浠呭悓姝ュ畬鎴愭椂杩斿洖锛涘紓姝ユā寮忎负 null锛岄伩鍏嶄笂鍗冧釜 ID 鎾戝ぇ鍝嶅簲 |
+
+鍚庣鎸� `totalCount` 鑷姩鍒嗘祦锛屽墠绔笉闇�瑕佸悇鑷绠楅槇鍊笺��
+
+#### 2锛変换鍔℃煡璇紙杞锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | `/mes/pro/bag-code/generate-task/get?id=` | 鏌ヨ浠诲姟鐘舵�佷笌杩涘害 |
+
+鍝嶅簲瀛楁锛歚id`銆乣bizType`銆乣status`銆乣statusName`銆乣progress`锛�0~100锛夈�乣totalCount`銆乣finishedCount`銆乣palletCount`銆乣itemId`銆乣itemCode`銆乣itemName`銆乣unitType`銆乣perPallet`銆乣uuid`銆乣errorMsg`銆乣createTime`銆乣finishTime`銆�
+
+#### 3锛変换鍔″彇娑�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | `/mes/pro/bag-code/generate-task/cancel?id=` | 璇锋眰鍙栨秷锛涘凡缁堟�佷换鍔℃嫆缁� |
+
+鏉冮檺锛氭彁浜�/鍙栨秷鐢� `mes:pro-bag-code:create`锛屾煡璇㈢敤 `mes:pro-bag-code:query`銆�
+
+### 5.2 鎻愪氦闃舵
+
+1. 鏍¢獙鍝佺銆佽閲忓崟浣嶇被鍨嬶紝鎹㈢畻琚嬬爜鎬绘暟锛堜笌鍘熼�昏緫涓�鑷达級銆�
+2. 鏁伴噺鏍¢獙锛歚totalCount 鈮� maxTaskCount`锛堥粯璁� 1,000,000锛夛紝瓒呭嚭鐩存帴鎶ラ敊銆�
+3. 骞傜瓑澶勭悊锛歚requestId` 闈炵┖鏃跺厛鏌ヤ换鍔¤〃锛涘懡涓垯鐩存帴杩斿洖宸叉湁浠诲姟锛屼笉閲嶅鐢熸垚銆傚苟鍙戦噸澶嶆彁浜ょ敱 `uk_request_id` 鍞竴绱㈠紩鍏滃簳锛屾崟鑾峰啿绐佸悗鍥炴煡銆�
+4. 鏁伴噺 鈮� `syncThreshold`锛氳蛋鍚屾鎵ц锛岃繑鍥� `async=false`銆�
+5. 鏁伴噺 > `syncThreshold`锛�
+ - 鐢熸垚浠诲姟 UUID锛�
+ - 鎻掑叆浠诲姟璁板綍锛堢姸鎬�=10锛夛紱
+ - 鍏堟敞鍐屽唴瀛樿繍琛屾�侊紝鍐嶆姇閫掔嚎绋嬫睜锛堜笌瀵煎嚭浠诲姟涓�鑷达紝閬垮厤娓呯悊浠诲姟璇洖鏀讹級锛�
+ - 杩斿洖 `async=true`銆乣taskId`銆乣uuid`銆乣totalCount`銆乣palletCount`銆�
+6. 骞跺彂闂搁棬锛氬悓涓�鏃堕棿鏈�澶� `maxConcurrentTasks`锛堥粯璁� 1锛変釜鐢熸垚浠诲姟銆傚崰鐢ㄥけ璐ユ椂杩斿洖銆屽凡鏈夌敓鎴愪换鍔¤繘琛屼腑銆嶅苟甯︿笂褰撳墠浠诲姟杩涘害銆�
+7. 绾跨▼姹犳嫆缁濇椂鎶婁换鍔$疆涓哄け璐ュ苟褰掕繕骞跺彂璁稿彲銆�
+
+### 5.3 鎵归噺鍙栫爜锛堟牳蹇冧紭鍖栵級
+
+灏嗐�岄�愭潯 `SADD`銆嶆敼涓恒�屾壒閲忚剼鏈啓鍏ャ�嶏細
+
+1. Java 渚ф寜 5000 涓竴鎵圭敓鎴愬�欓�夌爜锛坄ThreadLocalRandom` 绮剧‘鐢熸垚 16 浣嶆暟瀛楋級銆�
+2. 涓�娆� Lua 鑴氭湰鍘熷瓙瀹屾垚锛氶�愪釜 `SADD` 鍒� `biz:code:used`锛岃繑鍥炴湰鎵瑰疄闄呭啓鍏ユ垚鍔熺殑鐮併��
+3. 鑻ユ垚鍔熸暟涓嶈冻锛堥殢鏈虹鎾烇紝姒傜巼鏋佷綆锛夛紝鍙缂洪缁х画鐢熸垚涓嬩竴鎵癸紝鐩村埌鍑戦綈銆�
+4. 杩芥函鐮佷笌涓氬姟鐮佸悇璧颁竴娆℃壒閲忔祦绋嬨��
+
+鏁堟灉锛�30 涓囪浠� 60 涓囨缃戠粶寰�杩旈檷鍒扮害 120 娆★紙姣忔 5000 涓級銆�
+
+> 琚嬬爜涓庢墭鐩樼爜閮借蛋璇ユ壒閲忔柟娉曪紱`generate()` 淇濈暀缁欏叾浠栧皯閲忓満鏅��
+
+### 5.4 鍒嗘钀藉簱
+
+1. **涓�娆℃�у垎閰嶈搴忓彿**锛氫换鍔″紑濮嬫椂璋冪敤涓�娆� `allocateBagNoStart(totalCount, selectMaxBagNo())`锛屽悗缁悇娈电敤 `startNo + offset` 鎺ㄧ畻銆傚彇娑堜骇鐢熺殑搴忓彿绌烘礊鍏佽瀛樺湪锛堝簭鍙峰彧瑕佹眰鍞竴銆佷笉鍥為��锛夛紝鍥犳涓嶈鎸夋閲嶅鍒嗛厤銆�
+2. **鎵樼洏**锛堜粎鏁版嵁鍖呮ā寮忥級锛氭寜 1000 涓�/鎵规彃鍏ワ紝淇濈暀 `palletId` 涓� `tempCode` 涓や釜鏁扮粍渚涜鐮佹寮曠敤锛岄伩鍏嶅爢绉暣琛� DO銆�
+3. **琚嬬爜**锛氭寜 `chunkSize`锛堥粯璁� 5000锛夊垎娈佃惤搴擄紝姣忔钀藉簱鍚庡洖鍐欎竴娆� `finished_count`銆�
+4. **浜嬪姟杈圭晫**锛氭瘡涓垎娈电殑鎻掑叆鍦ㄧ嫭绔嬬殑 `REQUIRES_NEW` 浜嬪姟涓畬鎴愶紝閬垮厤鍑犲崄涓囪鍗曚簨鍔°�傚垎娈垫柟娉曞繀椤荤敱鎵ц鍣ㄨ法 Bean 璋冪敤锛屼繚璇佷簨鍔′唬鐞嗙敓鏁堛��
+5. 姣忔寮�濮嬪墠妫�鏌ュ彇娑堟爣璁颁笌鏈�闀胯繍琛屾椂闂达紝娈靛唴涓嶅啀鍋氭暟鎹簱鏌ヨ銆�
+
+### 5.5 杩涘害涓庤妭娴�
+
+- `finished_count` 姣忔锛�5000 鏉★級鏇存柊涓�娆★紝30 涓囨潯绾� 60 娆℃洿鏂帮紝寮�閿�鍙拷鐣ャ��
+- 鐢熸垚杩囩▼浠ユ暟鎹簱鍐欏叆涓轰富锛屼笉鍙犲姞 CPU 瀵嗛泦鎿嶄綔锛屽洜姝や笉鍋� CPU 鑺傛祦锛涗粎淇濈暀鏈�闀胯繍琛屾椂闂达紙榛樿 30 鍒嗛挓锛夊厹搴曘��
+- 姣忔鍐欏叆鍓嶆鏌ュ彇娑堟爣璁般��
+
+### 5.6 鍙栨秷
+
+1. 鍙栨秷鎺ュ彛鎶婂唴瀛樹腑鐨勫彇娑堟爣璁扮疆浣嶏紝浠诲姟杩涘叆銆屽彇娑堜腑銆嶃��
+2. 鎵ц绾跨▼鍦ㄥ綋鍓嶅垎娈电粨鏉熷悗鍋滄锛岄殢鍚庯細
+ - 鐗╃悊鍒犻櫎璇� `uuid` 涓嬪凡鐢熸垚鐨勮鐮侊紙`DELETE ... WHERE uuid = ? LIMIT 5000` 寰幆锛夛紱
+ - 鐗╃悊鍒犻櫎鏈宸插垱寤虹殑鎵樼洏锛氭墭鐩樿〃鏂板 `generate_uuid` 璁板綍鐢熸垚缁勶紝鎸� `DELETE ... WHERE generate_uuid = ? AND batch_id IS NULL AND code IS NULL LIMIT 5000` 寰幆銆傝鏉′欢纭繚鍙垹浠庢湭鍏ユ壒娆$殑涓存椂鎵樼洏锛屼笉浼氳鍒犵敤鎴峰悗缁凡缁戝畾鐨勬墭鐩橈紱
+ - 浠诲姟缃负銆屽凡鍙栨秷銆嶃��
+3. 浠诲姟涓嶅湪鏈繘绋嬭繍琛岋紙杩涚▼閲嶅惎鍚庣殑娈嬬暀锛夋椂锛屽彇娑堟帴鍙g洿鎺ユ竻鐞嗘畫鐣欐暟鎹苟缃负銆屽凡鍙栨秷銆嶃��
+
+> 璇存槑锛氱敓鎴愪竴鍗婄殑鏁版嵁鍖呬笉鍙敤锛屽繀椤绘竻鐞嗐�傝繖閲岄噰鐢ㄧ墿鐞嗗垹闄よ�岄潪閫昏緫鍒犻櫎锛屾槸鍥犱负杩欎簺琛屼粠鏈澶栧睍绀猴紝閫昏緫鍒犻櫎鍙細鐣欎笅鍑犲崄涓囨潯鍨冨溇骞舵氮璐圭储寮曠┖闂达紱鐢ㄦ埛涓诲姩鍒犻櫎宸茬敓鎴愭暟鎹寘浠嶆部鐢ㄥ師鏈夐�昏緫鍒犻櫎閫昏緫銆�
+
+### 5.7 澶辫触涓庨噸鍚洖鏀�
+
+- 鎵ц寮傚父锛氭竻鐞嗚 `uuid` 鐨勯儴鍒嗘暟鎹紝浠诲姟缃负銆屽凡澶辫触銆嶏紝`error_msg` 鎴柇鍒� 500 瀛楃銆�
+- 杩涚▼閲嶅惎锛氬唴瀛樿繍琛屾�佷涪澶便�傚惎鍔ㄥ悗涓庢瘡 5 鍒嗛挓涓�娆$殑娓呯悊浠诲姟鎵弿闈炵粓鎬佷换鍔★紝鍑′笉鍦ㄨ繍琛屾�佹敞鍐岃〃涓殑锛屼竴寰嬬疆涓恒�屽凡澶辫触銆嶅苟娓呯悊閮ㄥ垎鏁版嵁銆�
+- **涓嶅疄鐜版柇鐐圭画璺�**锛氱画璺戦渶瑕佺簿纭鐞嗗垎娈典簨鍔¤竟鐣屻�佹墭鐩樻槧灏勪笌鍙栨秷绌烘礊锛屽鏉傚害楂橈紱鑰岄噸鏂扮敓鎴愪竴娆$殑鎴愭湰杩滀綆浜庣淮鎶ょ画璺戠姸鎬佹満銆俇I 鏄庣‘鎻愮ず鈥滀换鍔′腑鏂紝璇烽噸鏂扮敓鎴愨�濄��
+
+### 5.8 骞傜瓑涓庨噸澶嶆彁浜�
+
+- 鍓嶇姣忔鎵撳紑鐢熸垚寮圭獥鐢熸垚涓�涓� `requestId`锛屽悓涓�娆℃彁浜ょ殑閲嶈瘯澶嶇敤鍚屼竴涓�硷紙濡傜綉缁滈噸璇曪級銆�
+- 鍚庣浠� `requestId` 鍞竴绱㈠紩淇濊瘉鍚屼竴璇锋眰鍙惤涓�涓换鍔★紱閲嶅鎻愪氦鐩存帴杩斿洖宸叉湁浠诲姟銆�
+- 涓嶄紶 `requestId` 鏃朵粛鍙敤锛屼絾澶卞幓缃戠粶绾у箓绛変繚鎶わ紱鍓嶇蹇呴』浼犮��
+
+### 5.9 閰嶇疆椤�
+
+鏂板 `mes.bag-code.generate` 鍓嶇紑閰嶇疆锛�
+
+| 閰嶇疆 | 榛樿鍊� | 璇存槑 |
+|------|--------|------|
+| sync-threshold | 20000 | 涓嶈秴杩囪鏁伴噺鍚屾鐢熸垚 |
+| chunk-size | 5000 | 姣忔琚嬬爜/姣忔壒鍙栫爜鏁伴噺 |
+| max-concurrent-tasks | 1 | 鍚屾椂杩愯鐨勫悗鍙扮敓鎴愪换鍔℃暟 |
+| max-task-count | 1000000 | 鍗曟鐢熸垚琚嬬爜涓婇檺 |
+| max-runtime-minutes | 30 | 鍗曚换鍔℃渶闀胯繍琛屾椂闂� |
+| poll-interval-seconds | 2 | 浠呬綔涓哄墠绔疆璇㈠缓璁�间笅鍙戯紙鍙�夛級 |
+
+## 鍏�佸墠绔仈璋冭鐐�
+
+> 鏈妭鍙弿杩拌涓轰笌瀛楁锛屼笉鎻愪緵鍓嶇浠g爜銆�
+
+### 6.1 娑夊強椤甸潰
+
+- 璧嬬爜绠$悊鍒楄〃椤碉紙`mes/pro/bagCode`锛�
+- 銆岀敓鎴愪簩缁寸爜鏁版嵁鍖呫�嶅脊绐楋紙`generate-form.vue`锛�
+- 銆岀敓鎴愬師濮嬫暟鎹�嶅叆鍙o紙濡傚瓨鍦ㄧ嫭绔嬪脊绐楋紝鍚屾牱澶勭悊锛�
+
+### 6.2 鎻愪氦娴佺▼
+
+1. 鎵撳紑鐢熸垚寮圭獥鏃剁敓鎴愪竴涓� `requestId`锛圲UID锛夛紝鍏抽棴閲嶅紑鍒欓噸鏂扮敓鎴愶紱鍚屼竴娆℃彁浜ょ殑鑷姩閲嶈瘯澶嶇敤鍚屼竴涓� `requestId`銆�
+2. 璋冪敤鍘熺敓鎴愭帴鍙o紝璇锋眰浣撳鍔� `requestId`銆�
+3. 鍝嶅簲 `async=false`锛氱淮鎸佺幇鏈夋垚鍔熸彁绀轰笌鍒楄〃鍒锋柊琛屼负銆�
+4. 鍝嶅簲 `async=true`锛氬脊绐椾笉绔嬪嵆鍏抽棴锛屽睍绀鸿繘搴︽潯锛屾寜 `poll-interval-seconds`锛堝缓璁� 2 绉掞級杞浠诲姟鏌ヨ鎺ュ彛锛岀洿鍒扮姸鎬佽繘鍏ョ粓鎬併��
+ - 鐘舵�� 30锛氭彁绀衡�滅敓鎴愬畬鎴愶紝鍏� N 鏉¤鐮佲�濓紝鍏抽棴寮圭獥骞跺埛鏂板垪琛ㄣ��
+ - 鐘舵�� 40锛氭彁绀衡�滀换鍔″凡鍙栨秷鈥濄��
+ - 鐘舵�� 50锛氬睍绀� `errorMsg`锛屽厑璁哥敤鎴烽噸鏂版彁浜わ紙閲嶆柊鐢熸垚 `requestId`锛夈��
+5. 杞鏈熼棿鎻愪緵鈥滃彇娑堢敓鎴愨�濇寜閽紝璋冪敤鍙栨秷鎺ュ彛锛涙寜閽偣鍑诲悗缃伆锛岀瓑寰呬换鍔¤繘鍏ョ粓鎬併��
+6. 椤甸潰鍏抽棴鎴栫粍浠跺嵏杞芥椂鍋滄杞锛涢噸鏂拌繘鍏ラ〉闈㈡椂锛屽彲閫氳繃浠诲姟缂栧彿缁х画鏌ヨ锛堜换鍔$紪鍙峰彲鏆傚瓨鍦ㄦ湰鍦扮姸鎬侊級銆�
+
+### 6.3 瀛楁灞曠ず
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| progress | 鐢熸垚寮圭獥 | 杩涘害鏉$櫨鍒嗘瘮 |
+| finishedCount / totalCount | 鐢熸垚寮圭獥 | 鏂囨鈥滃凡鐢熸垚 x / y 鏉♀�� |
+| statusName | 鐢熸垚寮圭獥 | 浠诲姟鐘舵�佹爣绛� |
+| palletCount | 瀹屾垚鎻愮ず | 鏁版嵁鍖呮ā寮忔彁绀鸿嚜鍔ㄧ敓鎴愮殑鎵樼洏鏁� |
+| errorMsg | 鐢熸垚寮圭獥 | 澶辫触鍘熷洜 |
+
+### 6.4 娉ㄦ剰浜嬮」
+
+- 寮傛妯″紡鍝嶅簲涓� `palletIds` 涓� null锛屽墠绔笉瑕佸啀鐢� `palletIds.length` 鍒ゆ柇鎵樼洏鏁帮紝鏀圭敤 `palletCount`銆�
+- 鍚屼竴鐢ㄦ埛鍚屾椂鍙兘杩愯涓�涓悗鍙扮敓鎴愪换鍔★紱琚嫆缁濇椂鍚庣浼氳繑鍥炲綋鍓嶄换鍔¤繘搴︼紝鍓嶇鍙洿鎺ュ垏鎹负鏌ヨ璇ヤ换鍔°��
+- 涓嶈鐢ㄢ�滆姹傛槸鍚﹁秴鏃垛�濇潵鍒ゆ柇浠诲姟澶辫触锛涙彁浜ゆ帴鍙e湪杞紓姝ュ悗搴斿揩閫熻繑鍥炪��
+
+## 涓冦�佹�ц兘浼扮畻涓庨獙鏀舵爣鍑�
+
+### 7.1 浼扮畻
+
+| 鐜妭 | 鏀归�犲墠锛�30 涓囪锛� | 鏀归�犲悗 |
+|------|------------------|--------|
+| Redis 鍙栫爜 | 60 涓囨 RTT锛屽垎閽熺骇 | 绾� 120 娆� RTT锛岀绾� |
+| 鏁版嵁搴撳啓鍏� | 1 涓秴澶т簨鍔� | 60 涓� 5000 琛屼簨鍔� |
+| HTTP 鍝嶅簲 | 绛夊緟鍏ㄩ儴瀹屾垚锛屽繀鐒惰秴鏃� | 鎻愪氦鍚� 1 绉掑唴杩斿洖 taskId |
+| 鐢ㄦ埛閲嶈瘯 | 浜х敓閲嶅鏁版嵁鍖� | 骞傜瓑閿幓閲� |
+
+### 7.2 楠屾敹鏍囧噯
+
+1. 30 涓囪鐮� + 瀵瑰簲鎵樼洏鍙畬鏁寸敓鎴愶紝杩涘害鍙粠 0 鎺ㄨ繘鍒� 100銆�
+2. 鎻愪氦鎺ュ彛鍦� 1 绉掑唴杩斿洖锛涚敓鎴愯繃绋嬩腑鍒楄〃鍙甯告煡璇㈠叾浠栨暟鎹��
+3. 鍙栨秷鍚庢暟鎹簱涓 `uuid` 鏃犳畫鐣欒鐮佷笌鎵樼洏锛屼换鍔$姸鎬佷负宸插彇娑堛��
+4. 妯℃嫙杩涚▼閲嶅惎锛氭畫鐣欎换鍔″湪涓�娆℃竻鐞嗗懆鏈熷唴鍙樹负宸插け璐ワ紝閮ㄥ垎鏁版嵁琚竻鐞嗐��
+5. 鐩稿悓 `requestId` 閲嶅鎻愪氦鍙骇鐢熶竴涓换鍔°�佷竴涓敓鎴愮粍銆�
+6. 2 涓囨潯浠ュ唴浠嶄负鍚屾杩斿洖锛屽搷搴旂粨鏋勪笌鏃х増鍏煎锛堟柊澧炲瓧娈典笉褰卞搷鏃у墠绔В鏋愶級銆�
+7. 鐜版湁銆岀牬鎹熸浛鎹� / 缁戝畾鎵规 / 缁戝畾鎵樼洏 / 瀵煎嚭銆嶅洖褰掗�氳繃銆�
+
+## 鍏�侀闄╀笌鍙栬垗
+
+| 椋庨櫓 | 搴斿 |
+|------|------|
+| Redis `biz:code:used` 闆嗗悎鎸佺画澧為暱锛堝嚑鍗佷竾绾�/娆★級 | 鐜扮綉宸叉湁璇ユ満鍒讹紝闆嗗悎鍙瓨 16 瀛楃鐭覆锛屽崟娆″嚑鍗佷竾鏉$害鍑犲崄 MB锛涘悗缁彲璇勪及鎸夊墠缂�鍒嗙墖锛屾湰娆′笉鍔� |
+| 娓呯悊 30 涓囪閮ㄥ垎鏁版嵁鑰楁椂 | 鍒嗘 `DELETE ... LIMIT 5000`锛岄伩鍏嶉暱浜嬪姟涓庝富浠庡欢杩熷皷宄� |
+| `bag_no` 涓� INT锛岀悊璁轰笂闄愮害 21 浜� | 鍗曟涓婇檺 100 涓囷紱鎸夊綋鍓嶉噺绾у彲鏀拺澶氬勾锛屽悗缁闇�闀挎湡浣跨敤鍐嶈縼绉� BIGINT |
+| 寮傛浠诲姟鏈熼棿鏈嶅姟閲嶅惎 | 涓嶇画璺戯紝鏍囪澶辫触骞舵竻鐞嗭紝鎻愮ず閲嶆柊鐢熸垚 |
+| 鍚屾璺緞浠嶅彈缃戝叧瓒呮椂绾︽潫 | 闃堝�煎唴鍗曟鏈�澶� 2 涓囨潯锛屾壒閲忓彇鐮佸悗棰勮 1~3 绉掞紝鐣欒冻浣欓噺 |
+
+## 涔濄�佸疄鏂芥楠や笌鐘舵�佽拷韪�
+
+| 搴忓彿 | 鍐呭 | 鐘舵�� |
+|------|------|------|
+| 1 | 鏂规鏂囨。纭 | 宸插畬鎴� |
+| 2 | 鏂板浠诲姟琛ㄣ�佽鐮� `bag_no` 绱㈠紩銆佹墭鐩� `generate_uuid` 杩佺Щ鑴氭湰 | 宸插畬鎴� |
+| 3 | 鏂板浠诲姟 DO / Mapper / 鐘舵�佹灇涓� / 閰嶇疆椤� | 宸插畬鎴� |
+| 4 | `MesCodeRedisDAO` 澧炲姞鎵归噺鍙栫爜鏂规硶 | 宸插畬鎴� |
+| 5 | 鎶藉彇鍒嗘鐢熸垚鎵ц鍣紙鍚屾/寮傛鍏辩敤锛� | 宸插畬鎴� |
+| 6 | 鏂板浠诲姟 Service锛堟彁浜ゃ�佹煡璇€�佸彇娑堛�佸洖鏀讹級涓庣嚎绋嬫睜閰嶇疆 | 宸插畬鎴� |
+| 7 | 鏀归�犱袱涓敓鎴愭帴鍙g殑鍚屾/寮傛鍒嗘祦涓庡搷搴斿瓧娈� | 宸插畬鎴� |
+| 8 | 鏂板浠诲姟鏌ヨ/鍙栨秷鎺ュ彛涓庢潈闄� | 宸插畬鎴� |
+| 9 | 鍚姩涓庡畾鏃舵竻鐞嗕换鍔� | 宸插畬鎴� |
+| 10 | 鍓嶇鑱旇皟锛堝脊绐楄繘搴︺�佽疆璇€�佸彇娑堬級 | 宸插畬鎴� |
+| 11 | 缂栬瘧妫�鏌ヤ笌澶ф壒閲忓疄娴� | 宸插畬鎴愶紙鏈湴 mom-thzy锛�30 涓囨潯绾� 76 绉掔敓鎴愬畬姣曪紝鎻愪氦鍝嶅簲 0.18 绉掞紱鍙栨秷娓呯悊鏃犳畫鐣欙紱骞傜瓑涓庡苟鍙戦椄闂ㄥ潎楠岃瘉閫氳繃锛� |
+
+### 杩佺Щ鑴氭湰
+
+| 鑴氭湰 | 璇存槑 |
+|------|------|
+| `sql/mysql/mes_pro_bag_code_generate_task.sql` | 浠诲姟琛� |
+| `sql/mysql/mes_pro_bag_code_bag_no_index.sql` | 琚嬬爜 `bag_no` 绱㈠紩 |
+| `sql/mysql/mes_pro_pallet_generate_uuid.sql` | 鎵樼洏 `generate_uuid` 鍒椾笌绱㈠紩 |
diff --git a/sql/mysql/mes_pro_bag_code_bag_no_index.sql b/sql/mysql/mes_pro_bag_code_bag_no_index.sql
new file mode 100644
index 0000000..a3a72e0
--- /dev/null
+++ b/sql/mysql/mes_pro_bag_code_bag_no_index.sql
@@ -0,0 +1,18 @@
+-- =============================================
+-- 璧嬬爜绠$悊 - 琚嬬爜琛� bag_no 绱㈠紩
+-- selectMaxBagNo() 渚濊禆 MAX(bag_no)锛涘ぇ鎵归噺鐢熸垚鍚庢棤绱㈠紩浼氬叏琛ㄦ壂鎻�
+-- MySQL 8.4 閫傜敤
+-- =============================================
+
+SET NAMES utf8mb4;
+
+SET @exists = (SELECT COUNT(*) FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+ AND table_name = 'mes_pro_bag_code'
+ AND index_name = 'idx_bag_no');
+SET @sql = IF(@exists = 0,
+ 'ALTER TABLE mes_pro_bag_code ADD INDEX idx_bag_no (bag_no)',
+ 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/sql/mysql/mes_pro_bag_code_generate_task.sql b/sql/mysql/mes_pro_bag_code_generate_task.sql
new file mode 100644
index 0000000..c1f6e10
--- /dev/null
+++ b/sql/mysql/mes_pro_bag_code_generate_task.sql
@@ -0,0 +1,35 @@
+-- =============================================
+-- 璧嬬爜绠$悊 - 澶ф壒閲忕敓鎴愪换鍔¤〃
+-- 鐢ㄤ簬鎵挎帴鍑犲崄涓囨潯琚嬬爜鐨勫悗鍙扮敓鎴愩�佽繘搴﹀睍绀恒�佸彇娑堜笌澶辫触鍥炴敹
+-- MySQL 8.4 閫傜敤
+-- =============================================
+
+SET NAMES utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `mes_pro_bag_code_generate_task` (
+ `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `request_id` VARCHAR(64) DEFAULT NULL COMMENT '骞傜瓑閿紙鍓嶇姣忔鎻愪氦鐢熸垚涓�涓� UUID锛岄噸璇曞鐢級',
+ `biz_type` VARCHAR(16) NOT NULL COMMENT '涓氬姟绫诲瀷锛圥ACKAGE 鏁版嵁鍖� / RAW 鍘熷鏁版嵁锛�',
+ `uuid` VARCHAR(32) NOT NULL COMMENT '鐢熸垚缁� UUID锛堟彁浜ゆ椂鐢熸垚锛屽け璐�/鍙栨秷鍚庣敤浜庢竻鐞嗭級',
+ `item_id` BIGINT NOT NULL COMMENT '鍝佺锛堢墿鏂欙級缂栧彿锛屽叧鑱� mdm_item.id',
+ `item_code` VARCHAR(64) DEFAULT NULL COMMENT '鍝佺缂栫爜锛堝揩鐓э級',
+ `item_name` VARCHAR(255) DEFAULT NULL COMMENT '鍝佺鍚嶇О锛堝揩鐓э級',
+ `unit_type` VARCHAR(32) DEFAULT NULL COMMENT '璁¢噺鍗曚綅绫诲瀷锛堝惃 / 48琚� / 100琚嬶級',
+ `per_pallet` INT NOT NULL DEFAULT 0 COMMENT '姣忔墭鐩樿鏁帮紙鍚ㄧ被鍨嬩负 0锛�',
+ `pallet_count` INT NOT NULL DEFAULT 0 COMMENT '鎵樼洏鏁帮紙鍘熷鏁版嵁涓� 0锛�',
+ `total_count` INT NOT NULL COMMENT '琚嬬爜鎬绘暟',
+ `finished_count` INT NOT NULL DEFAULT 0 COMMENT '宸茬敓鎴愯鐮佹暟',
+ `status` TINYINT NOT NULL DEFAULT 10 COMMENT '鐘舵�侊紙10 鎺掗槦涓� 20 鐢熸垚涓� 25 鍙栨秷涓� 30 宸插畬鎴� 40 宸插彇娑� 50 宸插け璐ワ級',
+ `remark` VARCHAR(255) DEFAULT NULL COMMENT '澶囨敞锛堝啓鍏ョ敓鎴愮殑琚嬬爜锛�',
+ `error_msg` VARCHAR(500) DEFAULT NULL COMMENT '澶辫触鍘熷洜',
+ `finish_time` DATETIME DEFAULT NULL COMMENT '瀹屾垚/缁堟鏃堕棿',
+ `creator` VARCHAR(64) DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` VARCHAR(64) DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_request_id` (`request_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_uuid` (`uuid`)
+) ENGINE = InnoDB COMMENT = 'MES 琚嬬爜鎵归噺鐢熸垚浠诲姟';
diff --git a/sql/mysql/mes_pro_pallet_generate_uuid.sql b/sql/mysql/mes_pro_pallet_generate_uuid.sql
new file mode 100644
index 0000000..1f5afd0
--- /dev/null
+++ b/sql/mysql/mes_pro_pallet_generate_uuid.sql
@@ -0,0 +1,29 @@
+-- =============================================
+-- 璧嬬爜绠$悊 - 鎵樼洏琛ㄦ柊澧炵敓鎴愮粍鏍囪
+-- 鎵归噺鐢熸垚琚嬬爜鏁版嵁鍖呮椂鍐欏叆锛岀敤浜庝换鍔″け璐�/鍙栨秷鍚庢竻鐞嗘湭缁戝畾鐨勪复鏃舵墭鐩�
+-- MySQL 8.4 閫傜敤锛圓DD COLUMN 涓嶆敮鎸� IF NOT EXISTS锛屽厛鏌� information_schema锛�
+-- =============================================
+
+SET NAMES utf8mb4;
+
+SET @exists = (SELECT COUNT(*) FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+ AND table_name = 'mes_pro_pallet'
+ AND column_name = 'generate_uuid');
+SET @sql = IF(@exists = 0,
+ 'ALTER TABLE mes_pro_pallet ADD COLUMN generate_uuid VARCHAR(32) DEFAULT NULL COMMENT ''鐢熸垚缁� UUID锛堟壒閲忕敓鎴愭椂鍐欏叆锛岀敤浜庡け璐�/鍙栨秷娓呯悊锛�'' AFTER temp_code',
+ 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @indexExists = (SELECT COUNT(*) FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+ AND table_name = 'mes_pro_pallet'
+ AND index_name = 'idx_generate_uuid');
+SET @sql = IF(@indexExists = 0,
+ 'ALTER TABLE mes_pro_pallet ADD INDEX idx_generate_uuid (generate_uuid)',
+ 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
index 1cb2a19..c5523de 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateTaskRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
@@ -21,10 +22,13 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeTraceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeGenerateTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateTaskStatusEnum;
import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportProperties;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
+import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeGenerateTaskService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrLabelAssembler;
@@ -65,6 +69,8 @@
@Resource
private MesProBagCodeService bagCodeService;
@Resource
+ private MesProBagCodeGenerateTaskService generateTaskService;
+ @Resource
private MesProBatchService batchService;
@Resource
private AdminUserApi adminUserApi;
@@ -88,6 +94,23 @@
public CommonResult<MesProBagCodeGenerateRespVO> generateRawBagCodes(
@Valid @RequestBody MesProBagCodeRawGenerateReqVO rawGenerateReqVO) {
return success(bagCodeService.generateRawBagCodes(rawGenerateReqVO));
+ }
+
+ @GetMapping("/generate-task/get")
+ @Operation(summary = "鑾峰緱琚嬬爜鐢熸垚浠诲姟锛堝ぇ鎵归噺鐢熸垚鐨勮繘搴﹁疆璇級")
+ @Parameter(name = "id", description = "浠诲姟缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')")
+ public CommonResult<MesProBagCodeGenerateTaskRespVO> getGenerateTask(@RequestParam("id") Long id) {
+ return success(buildGenerateTaskRespVO(generateTaskService.getTask(id)));
+ }
+
+ @PostMapping("/generate-task/cancel")
+ @Operation(summary = "鍙栨秷琚嬬爜鐢熸垚浠诲姟锛堝仠姝㈢敓鎴愬苟娓呯悊鍗婃垚鍝侊級")
+ @Parameter(name = "id", description = "浠诲姟缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')")
+ public CommonResult<Boolean> cancelGenerateTask(@RequestParam("id") Long id) {
+ generateTaskService.cancel(id);
+ return success(true);
}
@PostMapping("/replace")
@@ -251,6 +274,24 @@
labelAssembler.buildBagBusinessLabels(list));
}
+ private MesProBagCodeGenerateTaskRespVO buildGenerateTaskRespVO(MesProBagCodeGenerateTaskDO task) {
+ MesProBagCodeGenerateTaskRespVO respVO = BeanUtils.toBean(task, MesProBagCodeGenerateTaskRespVO.class);
+ for (MesProBagCodeGenerateTaskStatusEnum value : MesProBagCodeGenerateTaskStatusEnum.values()) {
+ if (value.getStatus().equals(task.getStatus())) {
+ respVO.setStatusName(value.getName());
+ break;
+ }
+ }
+ Integer total = task.getTotalCount();
+ if (total == null || total <= 0) {
+ respVO.setProgress(0);
+ } else {
+ int finished = task.getFinishedCount() == null ? 0 : task.getFinishedCount();
+ respVO.setProgress((int) Math.min(100L, (long) finished * 100 / total));
+ }
+ return respVO;
+ }
+
private List<MesProBagCodeRespVO> buildBagCodeRespVOList(List<MesProBagCodeDO> list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
index 0c05073..e564a59 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "绠$悊鍚庡彴 - MES 琚嬬爜锛堜簩缁寸爜鏁版嵁鍖咃級鐢熸垚 Request VO")
@@ -24,4 +25,8 @@
@Schema(description = "澶囨敞")
private String remark;
+ @Schema(description = "骞傜瓑閿細鍓嶇姣忔鎻愪氦鐢熸垚涓�涓� UUID锛岀綉缁滈噸璇曞鐢ㄥ悓涓�涓��", example = "a1b2c3")
+ @Size(max = 64, message = "骞傜瓑閿暱搴︿笉鑳借秴杩� 64 涓瓧绗�")
+ private String requestId;
+
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
index f1ede9b..91961a0 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
@@ -21,6 +21,15 @@
@Schema(description = "鏈鐢熸垚鐨勮鐮佹�绘暟", example = "240")
private Integer totalCount;
- @Schema(description = "鑷姩鐢熸垚鐨勬墭鐩樼紪鍙峰垪琛紙绫诲瀷涓� 鍚� 鏃朵负绌猴級")
+ @Schema(description = "鑷姩鐢熸垚鐨勬墭鐩樼紪鍙峰垪琛紙浠呭悓姝ュ畬鎴愭椂杩斿洖锛岀被鍨嬩负 鍚� 鏃朵负绌猴級")
private List<Long> palletIds;
+
+ @Schema(description = "鏄惁宸茶浆涓哄悗鍙颁换鍔�", example = "true")
+ private Boolean async;
+
+ @Schema(description = "鍚庡彴浠诲姟缂栧彿锛堝紓姝ユā寮忚繑鍥烇級", example = "1024")
+ private Long taskId;
+
+ @Schema(description = "鎵樼洏鎬绘暟锛堝師濮嬫暟鎹负 0锛�", example = "5")
+ private Integer palletCount;
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateTaskRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateTaskRespVO.java
new file mode 100644
index 0000000..ebc8825
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateTaskRespVO.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 琚嬬爜鐢熸垚浠诲姟 Response VO")
+@Data
+public class MesProBagCodeGenerateTaskRespVO {
+
+ @Schema(description = "浠诲姟缂栧彿", example = "1024")
+ private Long id;
+
+ @Schema(description = "涓氬姟绫诲瀷锛歅ACKAGE锛堟暟鎹寘锛�/ RAW锛堝師濮嬫暟鎹級", example = "PACKAGE")
+ private String bizType;
+
+ @Schema(description = "浠诲姟鐘舵��", example = "20")
+ private Integer status;
+
+ @Schema(description = "浠诲姟鐘舵�佸悕绉�", example = "鐢熸垚涓�")
+ private String statusName;
+
+ @Schema(description = "杩涘害鐧惧垎姣旓紙0~100锛�", example = "35")
+ private Integer progress;
+
+ @Schema(description = "鐢熸垚缁� UUID")
+ private String uuid;
+
+ @Schema(description = "鍝佺缂栧彿", example = "1")
+ private Long itemId;
+
+ @Schema(description = "鍝佺缂栫爜", example = "ITEM001")
+ private String itemCode;
+
+ @Schema(description = "鍝佺鍚嶇О", example = "绉嶅瓙")
+ private String itemName;
+
+ @Schema(description = "璁¢噺鍗曚綅绫诲瀷", example = "48琚�")
+ private String unitType;
+
+ @Schema(description = "姣忔墭鐩樿鏁帮紙鍚ㄧ被鍨嬩负 0锛�", example = "48")
+ private Integer perPallet;
+
+ @Schema(description = "鎵樼洏鏁帮紙鍘熷鏁版嵁涓� 0锛�", example = "5")
+ private Integer palletCount;
+
+ @Schema(description = "琚嬬爜鎬绘暟", example = "240")
+ private Integer totalCount;
+
+ @Schema(description = "宸茬敓鎴愯鐮佹暟", example = "120")
+ private Integer finishedCount;
+
+ @Schema(description = "澶辫触鍘熷洜")
+ private String errorMsg;
+
+ @Schema(description = "鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "瀹屾垚/缁堟鏃堕棿")
+ private LocalDateTime finishTime;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java
index 0d18c50..32fda42 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "绠$悊鍚庡彴 - MES 鍘熷琚嬬爜鐢熸垚 Request VO锛堜粎鍝佺+鏁伴噺锛屼笉鍏宠仈璁¢噺鍗曚綅绫诲瀷銆佷笉鐢熸垚鎵樼洏锛�")
@@ -21,4 +22,8 @@
@Schema(description = "澶囨敞")
private String remark;
+ @Schema(description = "骞傜瓑閿細鍓嶇姣忔鎻愪氦鐢熸垚涓�涓� UUID锛岀綉缁滈噸璇曞鐢ㄥ悓涓�涓��", example = "a1b2c3")
+ @Size(max = 64, message = "骞傜瓑閿暱搴︿笉鑳借秴杩� 64 涓瓧绗�")
+ private String requestId;
+
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeGenerateTaskDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeGenerateTaskDO.java
new file mode 100644
index 0000000..cf55097
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeGenerateTaskDO.java
@@ -0,0 +1,105 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * MES 琚嬬爜鎵归噺鐢熸垚浠诲姟 DO
+ * <p>
+ * 鍑犲崄涓囨潯琚嬬爜涓嶅湪璇锋眰绾跨▼鍐呭畬鎴愶細鎻愪氦鍚庤惤浠诲姟锛岀敱鍚庡彴绾跨▼鍒嗘钀藉簱锛�
+ * 鍓嶇鎹� {@link #finishedCount}/{@link #totalCount} 灞曠ず杩涘害锛屽苟鏀寔鍙栨秷銆�
+ */
+@TableName("mes_pro_bag_code_generate_task")
+@KeySequence("mes_pro_bag_code_generate_task_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MesProBagCodeGenerateTaskDO extends BaseDO {
+
+ @TableId
+ private Long id;
+
+ /**
+ * 骞傜瓑閿細鍓嶇姣忔鎻愪氦鐢熸垚涓�涓� UUID锛岀綉缁滈噸璇曞鐢ㄥ悓涓�涓��
+ */
+ private String requestId;
+
+ /**
+ * 涓氬姟绫诲瀷锛岃 {@link cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateBizTypeEnum}
+ */
+ private String bizType;
+
+ /**
+ * 鐢熸垚缁� UUID锛堟彁浜ゆ椂鐢熸垚锛屽け璐�/鍙栨秷鍚庢嵁姝ゆ竻鐞嗛儴鍒嗘暟鎹級
+ */
+ private String uuid;
+
+ /**
+ * 鍝佺缂栧彿
+ */
+ private Long itemId;
+
+ /**
+ * 鍝佺缂栫爜锛堝揩鐓э級
+ */
+ private String itemCode;
+
+ /**
+ * 鍝佺鍚嶇О锛堝揩鐓э級
+ */
+ private String itemName;
+
+ /**
+ * 璁¢噺鍗曚綅绫诲瀷锛堝惃 / 48琚� / 100琚嬶級
+ */
+ private String unitType;
+
+ /**
+ * 姣忔墭鐩樿鏁帮紙鍚ㄧ被鍨嬩负 0锛�
+ */
+ private Integer perPallet;
+
+ /**
+ * 鎵樼洏鏁帮紙鍘熷鏁版嵁涓� 0锛�
+ */
+ private Integer palletCount;
+
+ /**
+ * 琚嬬爜鎬绘暟
+ */
+ private Integer totalCount;
+
+ /**
+ * 宸茬敓鎴愯鐮佹暟
+ */
+ private Integer finishedCount;
+
+ /**
+ * 鐘舵�侊紝瑙� {@link cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateTaskStatusEnum}
+ */
+ private Integer status;
+
+ /**
+ * 澶囨敞锛堝啓鍏ョ敓鎴愮殑琚嬬爜锛�
+ */
+ private String remark;
+
+ /**
+ * 澶辫触鍘熷洜
+ */
+ private String errorMsg;
+
+ /**
+ * 瀹屾垚/缁堟鏃堕棿
+ */
+ private LocalDateTime finishTime;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java
index 57aabc3..67aeb69 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java
@@ -41,6 +41,10 @@
* 涓存椂鎵樼洏鐮侊紙鐮佸�硷細PL-8浣嶅叏灞�搴忓彿锛岄鐢熸垚浜岀淮鐮侀樁娈典娇鐢紱缁戝畾鎵规鍚庝粛鍙壂鐮佽拷婧級
*/
private String tempCode;
+ /**
+ * 鐢熸垚缁� UUID锛堟壒閲忕敓鎴愯鐮佹暟鎹寘鏃跺啓鍏ワ紝鐢ㄤ簬浠诲姟澶辫触/鍙栨秷鍚庢竻鐞嗘湭缁戝畾鐨勪复鏃舵墭鐩橈級
+ */
+ private String generateUuid;
/** 鎵樼洏鍝佺缂栧彿锛岀敓鎴愭垨棣栨瑁呰鏃跺甫鍏ャ�� */
private Long itemId;
/**
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeGenerateTaskMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeGenerateTaskMapper.java
new file mode 100644
index 0000000..6bf690e
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeGenerateTaskMapper.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeGenerateTaskDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * MES 琚嬬爜鎵归噺鐢熸垚浠诲姟 Mapper
+ */
+@Mapper
+public interface MesProBagCodeGenerateTaskMapper extends BaseMapperX<MesProBagCodeGenerateTaskDO> {
+
+ default MesProBagCodeGenerateTaskDO selectByRequestId(String requestId) {
+ return selectOne(MesProBagCodeGenerateTaskDO::getRequestId, requestId);
+ }
+
+ /**
+ * 浠呮洿鏂拌繘搴︼紝閬垮厤鎶婃暣琛屽瓧娈甸兘鍐欏洖锛堢敓鎴愯繃绋嬩腑楂橀璋冪敤锛�
+ */
+ default int updateProgress(Long id, Integer finishedCount) {
+ return update(null, new LambdaUpdateWrapper<MesProBagCodeGenerateTaskDO>()
+ .eq(MesProBagCodeGenerateTaskDO::getId, id)
+ .set(MesProBagCodeGenerateTaskDO::getFinishedCount, finishedCount));
+ }
+
+ /**
+ * 鏇存柊浠诲姟缁堟�侊紱null 瀛楁涓嶈鐩�
+ */
+ default int updateStatus(Long id, Integer status, String errorMsg, LocalDateTime finishTime) {
+ return update(null, new LambdaUpdateWrapper<MesProBagCodeGenerateTaskDO>()
+ .eq(MesProBagCodeGenerateTaskDO::getId, id)
+ .set(MesProBagCodeGenerateTaskDO::getStatus, status)
+ .set(errorMsg != null, MesProBagCodeGenerateTaskDO::getErrorMsg, errorMsg)
+ .set(finishTime != null, MesProBagCodeGenerateTaskDO::getFinishTime, finishTime));
+ }
+
+ /**
+ * 鏌ヨ鎵�鏈夐潪缁堟�佷换鍔★紝鐢ㄤ簬鍥炴敹杩涚▼閲嶅惎鍚庢畫鐣欑殑璁板綍
+ * <p>
+ * 鏄惁浠嶅湪鎵ц鐢辫繍琛屾�佹敞鍐岃〃鍒ゅ畾锛氳〃閲岀殑闈炵粓鎬佽褰曡嫢涓嶅湪娉ㄥ唽琛ㄤ腑锛�
+ * 灏变竴瀹氭病鏈夋墽琛岀嚎绋嬪湪鎺ㄨ繘瀹冦��
+ */
+ default List<MesProBagCodeGenerateTaskDO> selectUnfinishedList(Collection<Integer> statuses) {
+ return selectList(new LambdaQueryWrapperX<MesProBagCodeGenerateTaskDO>()
+ .in(MesProBagCodeGenerateTaskDO::getStatus, statuses)
+ .orderByAsc(MesProBagCodeGenerateTaskDO::getId));
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
index a921465..a4f9fb0 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
@@ -275,6 +275,18 @@
"WHERE deleted = 0 AND status <> 30 AND batch_id = #{batchId} AND item_id IS NOT NULL")
List<Long> selectDistinctItemIdsByBatchId(@Param("batchId") Long batchId);
+ /**
+ * 鐗╃悊鍒犻櫎鐢熸垚缁勪笅鐨勪竴鎵硅鐮侊紙浠呯敤浜庣敓鎴愬け璐�/鍙栨秷鍚庣殑鍗婃垚鍝佹竻鐞嗭級
+ * <p>
+ * 鍗婃垚鍝佷粠鏈澶栧睍绀猴紝閫昏緫鍒犻櫎鍙細鐣欎笅鍑犲崄涓囨潯鍨冨溇骞跺崰鐢ㄧ储寮曠┖闂达紝鍥犳杩欓噷鐩存帴鐗╃悊鍒犻櫎銆�
+ *
+ * @param uuid 鐢熸垚缁� UUID
+ * @param limit 鍗曟鏈�澶氬垹闄よ鏁�
+ * @return 瀹為檯鍒犻櫎琛屾暟
+ */
+ @org.apache.ibatis.annotations.Delete("DELETE FROM mes_pro_bag_code WHERE uuid = #{uuid} LIMIT #{limit}")
+ int deletePhysicallyByUuidChunk(@Param("uuid") String uuid, @Param("limit") int limit);
+
default List<MesProBagCodeDO> selectListByIdsForUpdate(Collection<Long> ids) {
return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
.in(MesProBagCodeDO::getId, ids)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
index 6c1b01f..280a9cc 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
@@ -96,6 +96,19 @@
@Select("SELECT IFNULL(MAX(pallet_no), 0) FROM mes_pro_pallet WHERE batch_id = #{batchId}")
Integer selectMaxPalletNo(@Param("batchId") Long batchId);
+ /**
+ * 鐗╃悊鍒犻櫎鏌愪釜鐢熸垚缁勪笅灏氭湭缁戝畾鎵规鐨勪复鏃舵墭鐩橈紙浠呯敤浜庣敓鎴愬け璐�/鍙栨秷鍚庣殑鍗婃垚鍝佹竻鐞嗭級
+ * <p>
+ * 宸茬粦瀹氭壒娆$殑鎵樼洏鍙兘宸茶鍚庣画涓氬姟浣跨敤锛屽嵆浣垮甫鏈夌敓鎴愮粍鏍囪涔熶笉鑳藉垹銆�
+ *
+ * @param generateUuid 鐢熸垚缁� UUID
+ * @param limit 鍗曟鏈�澶氬垹闄よ鏁�
+ * @return 瀹為檯鍒犻櫎琛屾暟
+ */
+ @org.apache.ibatis.annotations.Delete("DELETE FROM mes_pro_pallet WHERE generate_uuid = #{generateUuid} "
+ + "AND batch_id IS NULL AND code IS NULL LIMIT #{limit}")
+ int deletePhysicallyByGenerateUuidChunk(@Param("generateUuid") String generateUuid, @Param("limit") int limit);
+
@org.apache.ibatis.annotations.Update("UPDATE mes_pro_pallet SET sales_id = #{salesId}, outbound_time = #{outboundTime}, update_time = NOW() "
+ "WHERE id = #{id} AND deleted = 0")
void markOutbound(@Param("id") Long id, @Param("salesId") Long salesId,
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
index 5a805eb..dd24ca3 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
@@ -7,6 +7,7 @@
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Repository;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -22,6 +23,29 @@
* 琚嬪簭鍙疯嚜澧� KEY锛堝叏灞�琚嬪簭鍙凤紝淇濊瘉鍚屼竴鎵�/璺ㄦ壒鐢熸垚鏃朵笉閲嶅锛�
*/
private static final String BAG_NO_SEQ_KEY = "mes:bag_no:seq";
+
+ /**
+ * 鍗曟鎵归噺鍙栫爜鐨勬渶澶т釜鏁�
+ * <p>
+ * 鍙� Redis 鍗曟潯鍛戒护鍙傛暟瑙勬ā涓庡搷搴斾綋澶у皬绾︽潫锛岃繃澶у弽鑰屽鍔犲崟娆¢樆濉炴椂闂淬��
+ */
+ public static final int MAX_BATCH_SIZE = 5000;
+
+ /**
+ * 鎵归噺鍐欏叆骞惰繑鍥炲疄闄呯敓鏁堢殑缂栫爜銆�
+ * <p>
+ * 閫愪釜 SADD 鍦ㄥ悓涓�鑴氭湰鍐呭畬鎴愶細涓庤皟鐢ㄦ柟閫愭潯 SADD 鐩告瘮锛岀綉缁滃線杩斾粠 N 娆¢檷涓� 1 娆★紱
+ * 鍚屾椂鍥犱负鑴氭湰鍘熷瓙鎵ц锛屽苟鍙戣皟鐢ㄤ箣闂翠篃涓嶄細浜х敓閲嶅缂栫爜銆�
+ */
+ private static final RedisScript<List> ADD_UNIQUE_CODES_SCRIPT = new DefaultRedisScript<>(
+ "local res = {} "
+ + "for i = 1, #ARGV do "
+ + " if redis.call('SADD', KEYS[1], ARGV[i]) == 1 then "
+ + " res[#res + 1] = ARGV[i] "
+ + " end "
+ + "end "
+ + "return res",
+ List.class);
/**
* 鍘熷瓙鍒嗛厤琚嬪簭鍙峰尯闂寸殑 Lua 鑴氭湰銆�
@@ -59,6 +83,40 @@
}
/**
+ * 鎵归噺鐢熸垚 count 涓敮涓�鐨� 16 浣嶇函鏁板瓧涓氬姟缂栫爜锛堝叏绯荤粺鍞竴锛�
+ * <p>
+ * 澶ф壒閲忕敓鎴愭椂涓嶈寰幆璋冪敤 {@link #generate()}锛氶偅浼氫骇鐢� N 娆� Redis 寰�杩旓紝
+ * 鍑犲崄涓囨潯鍗冲彲鎶婃帴鍙f嫋鍒板垎閽熺骇銆傝繖閲屾瘡鎵规渶澶� {@link #MAX_BATCH_SIZE} 涓�欓�夌爜
+ * 涓�娆¤剼鏈啓鍏ワ紝纰版挒锛堟鐜囨瀬浣庯級鐨勭己棰濆啀琛ヤ竴鎵广��
+ *
+ * @param count 闇�瑕佺殑缂栫爜涓暟
+ * @return 鍞竴缂栫爜鍒楄〃锛岄『搴忔棤淇濊瘉浣嗘暟閲忕瓑浜� count
+ */
+ public List<String> generateBatch(int count) {
+ if (count <= 0) {
+ return List.of();
+ }
+ List<String> result = new ArrayList<>(Math.min(count, 1024));
+ // 16 浣嶉殢鏈虹爜绌洪棿涓� 10^16锛屾甯告儏鍐典笅涓�杞嵆鍙叏閮ㄥ啓鍏ワ紱闄愬埗杞鍙敤浜庡厹搴曞紓甯�
+ for (int round = 0; round < 100 && result.size() < count; round++) {
+ int batchSize = Math.min(count - result.size(), MAX_BATCH_SIZE);
+ List<String> candidates = new ArrayList<>(batchSize);
+ for (int i = 0; i < batchSize; i++) {
+ candidates.add(BizNoUtils.randomCode());
+ }
+ List<?> added = stringRedisTemplate.execute(ADD_UNIQUE_CODES_SCRIPT,
+ List.of(BizNoUtils.USED_SET_KEY), candidates.toArray(new String[0]));
+ if (added != null) {
+ added.forEach(code -> result.add(String.valueOf(code)));
+ }
+ }
+ if (result.size() < count) {
+ throw new IllegalStateException("鎵归噺鐢熸垚鍞竴涓氬姟缂栫爜澶辫触锛岃閲嶈瘯");
+ }
+ return result;
+ }
+
+ /**
* 鍘熷瓙鍒嗛厤涓�娈佃繛缁殑琚嬪簭鍙凤紝杩斿洖鍖洪棿璧峰鍊笺��
* <p>
* 琚嬪簭鍙峰師鍏堢敱銆屾煡搴撳唴鏈�澶у�煎啀 +1銆嶅緱鍒帮紝骞跺彂鐢熸垚鏃朵袱涓姹備細璇诲埌鍚屼竴涓渶澶у�硷紝
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
index ecb296a..7bac19f 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -869,6 +869,10 @@
ErrorCode PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED = new ErrorCode(1_040_752_012, "鍝佺涓昏閲忓崟浣嶆湭閰嶇疆绫诲瀷锛屼粎鏀寔 48琚�/100琚�/鍚�");
ErrorCode PRO_BAG_CODE_PALLET_COUNT_REQUIRED = new ErrorCode(1_040_752_013, "璇ュ搧绉嶆寜鎵樼洏鐢熸垚琚嬬爜锛屾墭鐩樻暟蹇呴』澶т簬 0");
ErrorCode PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH = new ErrorCode(1_040_752_014, "褰曞叆鎵规鐨勮鐮佸繀椤诲睘浜庡悓涓�鍝佺");
+ ErrorCode PRO_BAG_CODE_GENERATE_COUNT_TOO_MANY = new ErrorCode(1_040_752_015, "鍗曟鐢熸垚琚嬬爜鏁伴噺涓嶈兘瓒呰繃 {} 鏉�");
+ ErrorCode PRO_BAG_CODE_GENERATE_TASK_BUSY = new ErrorCode(1_040_752_016, "宸叉湁鐢熸垚浠诲姟杩涜涓紙{}%锛夛紝璇风瓑寰呭畬鎴愭垨鍏堝彇娑�");
+ ErrorCode PRO_BAG_CODE_GENERATE_TASK_NOT_EXISTS = new ErrorCode(1_040_752_017, "鐢熸垚浠诲姟涓嶅瓨鍦�");
+ ErrorCode PRO_BAG_CODE_GENERATE_TASK_STATUS_INVALID = new ErrorCode(1_040_752_018, "褰撳墠鐢熸垚浠诲姟鐘舵�佷笉鍏佽璇ユ搷浣�");
// ========== MES 绉嶅瓙鏍囩鍥芥爣鍥涜绱狅紙1-040-750-020锛� ==========
ErrorCode PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED = new ErrorCode(1_040_750_020, "鏈厤缃拷婧綉鍧�锛屾棤娉曠敓鎴愪簩缁寸爜鏍囩");
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateBizTypeEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateBizTypeEnum.java
new file mode 100644
index 0000000..443a5a5
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateBizTypeEnum.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.mes.enums.pro;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * MES 琚嬬爜鎵归噺鐢熸垚涓氬姟绫诲瀷鏋氫妇
+ */
+@Getter
+@AllArgsConstructor
+public enum MesProBagCodeGenerateBizTypeEnum implements ArrayValuable<String> {
+
+ /**
+ * 鏁版嵁鍖咃細鎸夎閲忓崟浣嶇被鍨嬫崲绠楋紝N 琚嬬被鍨嬪悓鏃剁敓鎴愭墭鐩�
+ */
+ PACKAGE("PACKAGE", "鏁版嵁鍖�"),
+ /**
+ * 鍘熷鏁版嵁锛氫粎鎸夋暟閲忕敓鎴愶紝涓嶇敓鎴愭墭鐩�
+ */
+ RAW("RAW", "鍘熷鏁版嵁");
+
+ public static final String[] ARRAYS = Arrays.stream(values())
+ .map(MesProBagCodeGenerateBizTypeEnum::getType).toArray(String[]::new);
+
+ /**
+ * 绫诲瀷鏍囪瘑
+ */
+ private final String type;
+ /**
+ * 绫诲瀷鍚嶇О
+ */
+ private final String name;
+
+ @Override
+ public String[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateTaskStatusEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateTaskStatusEnum.java
new file mode 100644
index 0000000..e3bad57
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeGenerateTaskStatusEnum.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.mes.enums.pro;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * MES 琚嬬爜鎵归噺鐢熸垚浠诲姟鐘舵�佹灇涓�
+ */
+@Getter
+@AllArgsConstructor
+public enum MesProBagCodeGenerateTaskStatusEnum implements ArrayValuable<Integer> {
+
+ /**
+ * 宸插垱寤哄緟鎵ц锛堝彈骞跺彂闂搁棬闄愬埗鏃剁煭鏆傚仠鐣欒鐘舵�侊級
+ */
+ PENDING(10, "鎺掗槦涓�"),
+ /**
+ * 鐢熸垚涓�
+ */
+ RUNNING(20, "鐢熸垚涓�"),
+ /**
+ * 宸叉敹鍒板彇娑堣姹傦紝姝e湪鍋滄骞舵竻鐞嗗凡鐢熸垚鏁版嵁
+ */
+ CANCELING(25, "鍙栨秷涓�"),
+ /**
+ * 鍏ㄩ儴鏁版嵁鐢熸垚瀹屾垚
+ */
+ FINISHED(30, "宸插畬鎴�"),
+ /**
+ * 鐢ㄦ埛鍙栨秷锛岄儴鍒嗘暟鎹凡娓呯悊
+ */
+ CANCELED(40, "宸插彇娑�"),
+ /**
+ * 寮傚父缁堟锛岄儴鍒嗘暟鎹凡娓呯悊
+ */
+ FAILED(50, "宸插け璐�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(MesProBagCodeGenerateTaskStatusEnum::getStatus).toArray(Integer[]::new);
+
+ /**
+ * 鐘舵��
+ */
+ private final Integer status;
+ /**
+ * 鐘舵�佸悕绉�
+ */
+ private final String name;
+
+ /**
+ * 鏄惁涓虹粓鎬侊紙涓嶅啀鍙樺寲锛�
+ */
+ public boolean isFinal() {
+ return this == FINISHED || this == CANCELED || this == FAILED;
+ }
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateExecutorConfig.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateExecutorConfig.java
new file mode 100644
index 0000000..db9c15d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateExecutorConfig.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.mes.framework.bagcode;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 琚嬬爜鍚庡彴鐢熸垚绾跨▼姹�
+ * <p>
+ * 鍗曠嫭寤烘睜鑰屼笉鏄鐢ㄥ叕鍏卞紓姝ユ睜锛氬ぇ鎵归噺钀藉簱鏄暱鏃堕棿銆佹暟鎹簱瀵嗛泦鐨勪换鍔★紝
+ * 娣疯繘鍏叡姹犱細鎶婂叾浠栧紓姝ヤ笟鍔′竴璧峰牭浣忋��
+ */
+@Configuration(proxyBeanMethods = false)
+public class MesProBagCodeGenerateExecutorConfig {
+
+ private static final String THREAD_NAME_PREFIX = "mes-bag-code-generate-";
+
+ @Resource
+ private MesProBagCodeGenerateProperties properties;
+
+ @Bean(name = "mesBagCodeGenerateExecutor")
+ public ThreadPoolTaskExecutor mesBagCodeGenerateExecutor() {
+ int concurrency = Math.max(1, properties.getMaxConcurrentTasks());
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(concurrency);
+ executor.setMaxPoolSize(concurrency);
+ executor.setQueueCapacity(concurrency);
+ executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
+ // 闃熷垪婊″嵆鎷掔粷锛岀敱璋冪敤鏂规妸浠诲姟缃负澶辫触锛岄伩鍏嶄换鍔℃棤澹板湴鍫嗗湪闃熷垪閲�
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
+ executor.initialize();
+ return executor;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateProperties.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateProperties.java
new file mode 100644
index 0000000..ba6409d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateProperties.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.mes.framework.bagcode;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * MES 琚嬬爜鎵归噺鐢熸垚閰嶇疆椤�
+ * <p>
+ * 灏忔壒閲忎粛鍦ㄨ姹傜嚎绋嬪唴鍚屾瀹屾垚锛屼繚璇佺鍥烇紱澶ф壒閲忚浆鍚庡彴浠诲姟锛屽垎娈电敓鎴愩�佸彲鍙栨秷銆�
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "mes.bag-code.generate")
+public class MesProBagCodeGenerateProperties {
+
+ /**
+ * 鍚屾/寮傛鐨勫垎鐣岀嚎锛堣鐮佹暟锛�
+ * <p>
+ * 涓嶈秴杩囪鏁伴噺鍦ㄨ姹傜嚎绋嬪唴瀹屾垚锛涜秴杩囧垯钀戒换鍔¤浆鍚庡彴鐢熸垚銆�
+ */
+ private int syncThreshold = 20000;
+
+ /**
+ * 姣忔钀藉簱鐨勮鐮佹暟閲忥紙鍚屾椂涔熸槸姣忔壒鍙栫爜鏁伴噺锛�
+ */
+ private int chunkSize = 5000;
+
+ /**
+ * 鍚屾椂鍏佽杩愯鐨勫悗鍙扮敓鎴愪换鍔℃暟
+ */
+ private int maxConcurrentTasks = 1;
+
+ /**
+ * 鍗曟鐢熸垚琚嬬爜涓婇檺
+ */
+ private int maxTaskCount = 1000000;
+
+ /**
+ * 鍗曚釜鍚庡彴浠诲姟鐨勬渶闀胯繍琛屾椂闂达紙鍒嗛挓锛夛紝瓒呮椂鑷姩缁堟骞舵竻鐞�
+ */
+ private int maxRuntimeMinutes = 30;
+
+ /**
+ * 鍓嶇杞寤鸿闂撮殧锛堢锛夛紝浠呯敤浜庝笅鍙戠粰鍓嶇
+ */
+ private int pollIntervalSeconds = 2;
+
+ /**
+ * 娓呯悊浠诲姟姣忚疆鏈�澶氬垹闄ょ殑琛屾暟锛岄伩鍏嶉暱鏃堕棿鍗犵敤鏁版嵁搴�
+ * <p>
+ * 鍒嗘壒鍒犻櫎锛屽崟娆℃竻鐞嗕笂闄愮敤浜庢帶鍒跺垹闄や换鍔″绾夸笂鍐欏叆鐨勫奖鍝嶃��
+ */
+ private int cleanupBatchSize = 5000;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateTaskRegistry.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateTaskRegistry.java
new file mode 100644
index 0000000..d06bd84
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/bagcode/MesProBagCodeGenerateTaskRegistry.java
@@ -0,0 +1,108 @@
+package cn.iocoder.yudao.module.mes.framework.bagcode;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 琚嬬爜鐢熸垚浠诲姟杩愯鎬佹敞鍐岃〃
+ * <p>
+ * 淇濆瓨銆屽摢浜涗换鍔℃鍦ㄧ敓鎴愩�嶄笌銆屾槸鍚﹀凡璇锋眰鍙栨秷銆嶃�傚彇娑堟爣璁版斁鍐呭瓨鑰岄潪鏁版嵁搴擄紝
+ * 鏄洜涓虹敓鎴愮嚎绋嬮渶瑕佹瘡娈佃惤妫�鏌ヤ竴娆″彇娑堬紝璧版暟鎹簱浼氫骇鐢熷ぇ閲忔棤璋撴煡璇紱鏈」鐩负鍗曟満閮ㄧ讲锛屼袱鑰呭悓杩涚▼銆�
+ * <p>
+ * 杩涚▼閲嶅惎浼氳鎵�鏈夎繍琛屾�佷涪澶憋紝姝ゆ椂鐢辨竻鐞嗕换鍔℃妸娈嬬暀鐨勯潪缁堟�佷换鍔″洖鏀朵负澶辫触銆�
+ */
+@Component
+public class MesProBagCodeGenerateTaskRegistry {
+
+ @Resource
+ private MesProBagCodeGenerateProperties properties;
+
+ /**
+ * 姝e湪杩愯鐨勪换鍔★細浠诲姟缂栧彿 鈫� 鍙栨秷鏍囪
+ */
+ private final Map<Long, AtomicBoolean> runningTasks = new ConcurrentHashMap<>();
+
+ /**
+ * 骞跺彂璁稿彲锛氱害鏉熷悗鍙扮敓鎴愪换鍔℃暟
+ */
+ private Semaphore permits;
+
+ @PostConstruct
+ public void init() {
+ permits = new Semaphore(Math.max(1, properties.getMaxConcurrentTasks()));
+ }
+
+ /**
+ * 灏濊瘯鍗犵敤涓�涓繍琛屽悕棰�
+ */
+ public boolean tryAcquire() {
+ return permits.tryAcquire();
+ }
+
+ /**
+ * 閲婃斁杩愯鍚嶉锛堝畬鎴愩�佸け璐ャ�佸彇娑堛�佹彁浜ゅけ璐ュ潎闇�璋冪敤锛�
+ */
+ public void release() {
+ permits.release();
+ }
+
+ /**
+ * 褰撳墠鐢辨湰杩涚▼鎸佹湁鐨勪换鍔℃暟锛堝惈鍒氬缓濂姐�佸皻鏈绾跨▼鎺ョ鐨勶級
+ */
+ public int getRunningCount() {
+ return runningTasks.size();
+ }
+
+ /**
+ * 娉ㄥ唽浠诲姟骞惰繑鍥炲叾鍙栨秷鏍囪
+ * <p>
+ * 蹇呴』鍦ㄤ换鍔¤惤搴撳悗銆佹姇閫掔嚎绋嬫睜鍓嶈皟鐢紝閬垮厤娓呯悊浠诲姟鎶婂皻鏈敞鍐岀殑璁板綍璇垽涓洪噸鍚畫鐣欍��
+ */
+ public AtomicBoolean register(Long taskId) {
+ AtomicBoolean flag = new AtomicBoolean(false);
+ runningTasks.put(taskId, flag);
+ return flag;
+ }
+
+ /**
+ * 鑾峰彇浠诲姟鐨勫彇娑堟爣璁帮紱浠诲姟涓嶅睘浜庢湰杩涚▼鏃惰繑鍥� null
+ */
+ public AtomicBoolean get(Long taskId) {
+ return runningTasks.get(taskId);
+ }
+
+ /**
+ * 浠诲姟鏄惁鐢辨湰杩涚▼鎸佹湁
+ */
+ public boolean isRunning(Long taskId) {
+ return runningTasks.containsKey(taskId);
+ }
+
+ /**
+ * 娉ㄩ攢浠诲姟
+ */
+ public void unregister(Long taskId) {
+ runningTasks.remove(taskId);
+ }
+
+ /**
+ * 璇锋眰鍙栨秷锛氫换鍔″湪璺戝垯缃綅鍙栨秷鏍囪
+ *
+ * @return true 琛ㄧず鍙栨秷璇锋眰宸查�佽揪锛沠alse 琛ㄧず浠诲姟涓嶅湪鏈繘绋嬭繍琛�
+ */
+ public boolean requestCancel(Long taskId) {
+ AtomicBoolean flag = runningTasks.get(taskId);
+ if (flag == null) {
+ return false;
+ }
+ flag.set(true);
+ return true;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProBagCodeGenerateCleanupTask.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProBagCodeGenerateCleanupTask.java
new file mode 100644
index 0000000..456fd96
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProBagCodeGenerateCleanupTask.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.mes.job.pro;
+
+import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeGenerateTaskService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 琚嬬爜鐢熸垚浠诲姟娓呯悊
+ * <p>
+ * 杩涚▼閲嶅惎鍚庡唴瀛樿繍琛屾�佷涪澶憋紝琛ㄤ腑鍋滃湪銆屾帓闃熶腑/鐢熸垚涓�/鍙栨秷涓�嶇殑璁板綍涓嶄細鍐嶆湁绾跨▼鎺ㄨ繘锛�
+ * 鐢辨湰浠诲姟鍥炴敹涓哄け璐ュ苟娓呯悊鍏跺崐鎴愬搧鏁版嵁銆傜敤 Spring 鑷甫鐨� {@code @Scheduled} 鑰岄潪 Quartz JobHandler锛�
+ * 鍚庤�呴渶瑕佸湪绠$悊鍚庡彴鎵嬪伐鐧昏锛屾紡鐧昏灏变細闈欓粯涓嶆墽琛岋紝杩欓噷鐨勬竻鐞嗗睘浜庡繀椤诲紑绠卞嵆璺戠殑鍏滃簳閫昏緫銆�
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MesProBagCodeGenerateCleanupTask {
+
+ private final MesProBagCodeGenerateTaskService taskService;
+
+ /**
+ * 姣� 5 鍒嗛挓鍥炴敹涓�娆″紓甯镐腑鏂殑浠诲姟
+ */
+ @Scheduled(cron = "0 */5 * * * ?")
+ public void cleanup() {
+ try {
+ int tasks = taskService.recycleStaleTasks();
+ if (tasks > 0) {
+ log.info("[cleanup][鍥炴敹娈嬬暀琚嬬爜鐢熸垚浠诲姟 {} 涓猐", tasks);
+ }
+ } catch (Exception e) {
+ log.error("[cleanup][鍥炴敹娈嬬暀琚嬬爜鐢熸垚浠诲姟澶辫触]", e);
+ }
+ }
+
+ /**
+ * 鍚姩鏃剁珛鍒诲洖鏀朵竴娆★紝閬垮厤娈嬬暀浠诲姟浠ャ�岀敓鎴愪腑銆嶇姸鎬佷竴鐩存寕鍦ㄧ晫闈笂
+ */
+ @EventListener(ApplicationReadyEvent.class)
+ public void recycleOnStartup() {
+ try {
+ int tasks = taskService.recycleStaleTasks();
+ if (tasks > 0) {
+ log.info("[recycleOnStartup][鍚姩鍥炴敹娈嬬暀琚嬬爜鐢熸垚浠诲姟 {} 涓猐", tasks);
+ }
+ } catch (Exception e) {
+ log.error("[recycleOnStartup][鍚姩鍥炴敹琚嬬爜鐢熸垚浠诲姟澶辫触]", e);
+ }
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeBatchGenerator.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeBatchGenerator.java
new file mode 100644
index 0000000..cc6213c
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeBatchGenerator.java
@@ -0,0 +1,186 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeGenerateTaskMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
+import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateBizTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateProperties;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 琚嬬爜鎵归噺鐢熸垚鎵ц鍣紙鍚屾涓庡紓姝ュ叡鐢級
+ * <p>
+ * 涓庢棫瀹炵幇鐨勫叧閿樊寮傦細
+ * <ul>
+ * <li>缂栫爜鎵归噺鍚� Redis 鐢宠锛岄伩鍏嶆瘡涓鐮佷袱娆$綉缁滃線杩旓紱</li>
+ * <li>琚嬪簭鍙蜂竴娆℃�у垎閰嶏紝鍒嗘鍙仛鍋忕Щ璁$畻锛�</li>
+ * <li>姣忔鐙珛浜嬪姟钀藉簱锛屽苟鍦ㄦ闂村洖鍐欒繘搴︺�佹鏌ュ彇娑堜笌瓒呮椂銆�</li>
+ * </ul>
+ */
+@Slf4j
+@Component
+public class MesProBagCodeBatchGenerator {
+
+ @Resource
+ private MesProBagCodeMapper bagCodeMapper;
+ @Resource
+ private MesProBagCodeGenerateTaskMapper taskMapper;
+ @Resource
+ private MesCodeRedisDAO codeRedisDao;
+ @Resource
+ private MesProBagCodeChunkWriter chunkWriter;
+ @Resource
+ private MesProBagCodeGenerateProperties properties;
+
+ /**
+ * 鎵ц鐢熸垚
+ *
+ * @param spec 鐢熸垚鍙傛暟
+ * @param taskId 浠诲姟缂栧彿锛涗负 null 琛ㄧず鍚屾鐢熸垚锛屼笉鍥炲啓杩涘害
+ * @param cancelFlag 鍙栨秷鏍囪
+ * @return 鐢熸垚缁撴灉锛堣鐮佹暟 + 鎵樼洏缂栧彿锛�
+ */
+ public GenerateResult generate(MesProBagCodeGenerateSpec spec, Long taskId, AtomicBoolean cancelFlag) {
+ assertNotCancelled(cancelFlag);
+ long deadline = System.currentTimeMillis()
+ + Duration.ofMinutes(Math.max(1, properties.getMaxRuntimeMinutes())).toMillis();
+ // 1. 鍏ㄥ眬琚嬪簭鍙蜂竴娆℃�у垎閰嶏細鍚庣画鍒嗘鍙仛鍋忕Щ锛岄伩鍏嶆瘡娈甸兘鏌ュ簱鍐呮渶澶у��
+ int startNo = codeRedisDao.allocateBagNoStart(spec.totalCount(), bagCodeMapper.selectMaxBagNo());
+ // 2. 鎵樼洏锛堜粎鏁版嵁鍖呮ā寮忎笖鎸� N 琚嬫崲绠楁椂鐢熸垚锛�
+ PalletRefs palletRefs = createPallets(spec, cancelFlag, deadline);
+ // 3. 琚嬬爜鍒嗘钀藉簱
+ int chunkSize = Math.max(1, properties.getChunkSize());
+ int finished = 0;
+ while (finished < spec.totalCount()) {
+ assertNotCancelled(cancelFlag);
+ assertNotTimeout(deadline);
+ int size = Math.min(chunkSize, spec.totalCount() - finished);
+ // 姣忎釜琚嬬爜闇�瑕佽拷婧爜 + 涓氬姟鐮�
+ List<String> codes = codeRedisDao.generateBatch(size * 2);
+ List<MesProBagCodeDO> chunk = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ int index = finished + i;
+ PalletRef pallet = palletRefs.resolve(spec, index);
+ chunk.add(new MesProBagCodeDO()
+ .setUuid(spec.uuid())
+ .setCode(codes.get(i * 2))
+ .setBusinessCode(codes.get(i * 2 + 1))
+ .setBagNo(startNo + index)
+ .setItemId(spec.itemId())
+ .setItemCode(spec.itemCode())
+ .setItemName(spec.itemName())
+ .setPalletId(pallet == null ? null : pallet.id())
+ .setPalletCode(pallet == null ? null : pallet.tempCode())
+ .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
+ .setRemark(spec.remark()));
+ }
+ chunkWriter.insertBagCodes(chunk);
+ finished += size;
+ if (taskId != null) {
+ taskMapper.updateProgress(taskId, finished);
+ }
+ }
+ return new GenerateResult(finished, palletRefs.ids());
+ }
+
+ /**
+ * 鎸夐渶鍒涘缓鎵樼洏锛氭暟鎹寘妯″紡涓嬭鐮佹寜銆屾瘡鎵� N 琚嬨�嶅潎鍒嗙粦瀹�
+ */
+ private PalletRefs createPallets(MesProBagCodeGenerateSpec spec,
+ AtomicBoolean cancelFlag, long deadline) {
+ if (spec.bizType() != MesProBagCodeGenerateBizTypeEnum.PACKAGE || spec.palletCount() <= 0) {
+ return PalletRefs.EMPTY;
+ }
+ int chunkSize = Math.max(1, properties.getChunkSize());
+ List<Long> ids = new ArrayList<>(spec.palletCount());
+ List<String> tempCodes = new ArrayList<>(spec.palletCount());
+ int created = 0;
+ while (created < spec.palletCount()) {
+ assertNotCancelled(cancelFlag);
+ assertNotTimeout(deadline);
+ int size = Math.min(chunkSize, spec.palletCount() - created);
+ // 姣忎釜鎵樼洏闇�瑕佷复鏃剁爜 + 涓氬姟鐮�
+ List<String> codes = codeRedisDao.generateBatch(size * 2);
+ List<MesProPalletDO> pallets = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ pallets.add(new MesProPalletDO()
+ .setTempCode(codes.get(i * 2))
+ .setBusinessCode(codes.get(i * 2 + 1))
+ .setBagCount(spec.perPallet())
+ .setAutoGenerated(true)
+ .setItemId(spec.itemId())
+ .setGenerateUuid(spec.uuid())
+ .setStatus(MesProPalletStatusEnum.IN_USE.getStatus()));
+ }
+ chunkWriter.insertPallets(pallets);
+ pallets.forEach(pallet -> {
+ ids.add(pallet.getId());
+ tempCodes.add(pallet.getTempCode());
+ });
+ created += size;
+ }
+ return new PalletRefs(ids, tempCodes);
+ }
+
+ private void assertNotCancelled(AtomicBoolean cancelFlag) {
+ if (cancelFlag != null && cancelFlag.get()) {
+ throw new CancelledException();
+ }
+ }
+
+ private void assertNotTimeout(long deadline) {
+ if (System.currentTimeMillis() > deadline) {
+ throw new IllegalStateException("鐢熸垚浠诲姟瓒呰繃鏈�闀胯繍琛屾椂闂达紙"
+ + properties.getMaxRuntimeMinutes() + " 鍒嗛挓锛�");
+ }
+ }
+
+ /**
+ * 鎵樼洏寮曠敤锛氬彧淇濈暀琚嬬爜闇�瑕佸洖鍐欑殑涓や釜瀛楁锛岄伩鍏嶅湪鍐呭瓨涓爢绉暣琛� DO
+ */
+ private record PalletRefs(List<Long> ids, List<String> tempCodes) {
+
+ private static final PalletRefs EMPTY = new PalletRefs(List.of(), List.of());
+
+ private PalletRef resolve(MesProBagCodeGenerateSpec spec, int bagIndex) {
+ if (ids.isEmpty() || spec.perPallet() <= 0) {
+ return null;
+ }
+ int palletIndex = bagIndex / spec.perPallet();
+ return new PalletRef(ids.get(palletIndex), tempCodes.get(palletIndex));
+ }
+
+ }
+
+ private record PalletRef(Long id, String tempCode) {
+ }
+
+ /**
+ * 鐢熸垚缁撴灉锛氬悓姝ヨ矾寰勯渶瑕佹墭鐩樼紪鍙风敤浜庡搷搴旓紝寮傛璺緞鍙叧蹇冨畬鎴愭暟閲�
+ */
+ public record GenerateResult(int finishedCount, List<Long> palletIds) {
+ }
+
+ /**
+ * 鍙栨秷淇″彿锛氱敓鎴愮嚎绋嬫娴嬪埌鍙栨秷鏃舵姏鍑猴紝鐢辫皟鐢ㄦ柟缁熶竴娓呯悊鍗婃垚鍝�
+ */
+ public static class CancelledException extends RuntimeException {
+
+ public CancelledException() {
+ super("鐢熸垚浠诲姟宸插彇娑�");
+ }
+
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeChunkWriter.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeChunkWriter.java
new file mode 100644
index 0000000..efdcfe8
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeChunkWriter.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 琚嬬爜/鎵樼洏鍒嗘鍐欏叆鍣�
+ * <p>
+ * 姣忔浣跨敤鐙珛浜嬪姟锛圧EQUIRES_NEW锛夛細鍑犲崄涓囪鏀惧湪鍚屼竴涓簨鍔¢噷浼氬舰鎴愯秴澶т簨鍔★紝
+ * 闀挎椂闂村崰鐢� undo/redo 涓庤閿侊紝澶辫触鏃跺洖婊氭垚鏈篃鏋侀珮銆�
+ * <p>
+ * 鐙珛鎴� Bean 鏄负浜嗚 {@code REQUIRES_NEW} 缁� Spring 浠g悊鐢熸晥锛岄伩鍏嶅悓绫昏嚜璋冪敤缁曡繃浜嬪姟銆�
+ */
+@Component
+public class MesProBagCodeChunkWriter {
+
+ /**
+ * 鍗曟潯 INSERT 璇彞鐨勮鏁帮紱涓庡垎娈靛ぇ灏忓垎寮�锛屾帶鍒跺崟鏉� SQL 鐨勫寘浣撳ぇ灏�
+ */
+ private static final int INSERT_BATCH_SIZE = 1000;
+
+ @Resource
+ private MesProBagCodeMapper bagCodeMapper;
+ @Resource
+ private MesProPalletMapper palletMapper;
+
+ /**
+ * 鍐欏叆涓�娈佃鐮�
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+ public void insertBagCodes(List<MesProBagCodeDO> bagCodes) {
+ bagCodeMapper.insertBatch(bagCodes, INSERT_BATCH_SIZE);
+ }
+
+ /**
+ * 鍐欏叆涓�娈垫墭鐩橈紙鎻掑叆鍚� ID 浼氬洖濉埌瀹炰綋涓婏級
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+ public void insertPallets(List<MesProPalletDO> pallets) {
+ palletMapper.insertBatch(pallets, INSERT_BATCH_SIZE);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateCleaner.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateCleaner.java
new file mode 100644
index 0000000..a9979cb
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateCleaner.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateProperties;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 琚嬬爜鐢熸垚鍗婃垚鍝佹竻鐞嗗櫒
+ * <p>
+ * 鐢熸垚澶辫触鎴栧彇娑堟椂浼氱暀涓嬮儴鍒嗚鐮佷笌鎵樼洏銆傝繖浜涙暟鎹粠鏈澶栧睍绀猴紝涓嶅仛閫昏緫鍒犻櫎锛�
+ * 鐩存帴鎸� {@code uuid} 鍒嗘鐗╃悊鍒犻櫎锛岄伩鍏嶇暀涓嬪嚑鍗佷竾鏉″瀮鍦捐銆�
+ * <p>
+ * 鍗曟潯 DELETE 鑷甫鍘熷瓙鎬э紝涓斿凡鐢� LIMIT 鎺у埗鍗曟褰卞搷琛屾暟锛屽洜姝や笉闇�瑕侀澶栦簨鍔°��
+ */
+@Slf4j
+@Component
+public class MesProBagCodeGenerateCleaner {
+
+ @Resource
+ private MesProBagCodeMapper bagCodeMapper;
+ @Resource
+ private MesProPalletMapper palletMapper;
+ @Resource
+ private MesProBagCodeGenerateProperties properties;
+
+ /**
+ * 娓呯悊鏌愪釜鐢熸垚缁勭殑鎵�鏈夊崐鎴愬搧鏁版嵁
+ *
+ * @param uuid 鐢熸垚缁� UUID
+ */
+ public void cleanup(String uuid) {
+ int batchSize = Math.max(1, properties.getCleanupBatchSize());
+ int deleted;
+ do {
+ deleted = bagCodeMapper.deletePhysicallyByUuidChunk(uuid, batchSize);
+ } while (deleted >= batchSize);
+ do {
+ deleted = palletMapper.deletePhysicallyByGenerateUuidChunk(uuid, batchSize);
+ } while (deleted >= batchSize);
+ log.info("[cleanup][鐢熸垚缁� {} 鍗婃垚鍝佹竻鐞嗗畬鎴怾", uuid);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateRunner.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateRunner.java
new file mode 100644
index 0000000..094afac
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateRunner.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeGenerateTaskMapper;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateTaskRegistry;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 琚嬬爜鐢熸垚浠诲姟鎵ц鍣�
+ * <p>
+ * 鍦ㄥ悗鍙扮嚎绋嬪唴瀹屾垚鍒嗘鐢熸垚锛屽苟璐熻矗锛氬洖鍐欒繘搴︺�佸搷搴斿彇娑堛�佸け璐�/鍙栨秷鍚庢竻鐞嗗崐鎴愬搧銆�
+ */
+@Slf4j
+@Component
+public class MesProBagCodeGenerateRunner {
+
+ @Resource
+ private MesProBagCodeGenerateTaskMapper taskMapper;
+ @Resource
+ private MesProBagCodeBatchGenerator generator;
+ @Resource
+ private MesProBagCodeGenerateCleaner cleaner;
+ @Resource
+ private MesProBagCodeGenerateTaskRegistry registry;
+
+ /**
+ * 鎵ц鐢熸垚浠诲姟锛堢敱绾跨▼姹犺皟鐢紝鍕垮湪璇锋眰绾跨▼鍐呯洿鎺ヨ皟鐢級
+ *
+ * @param taskId 浠诲姟缂栧彿
+ * @param spec 鐢熸垚鍙傛暟
+ */
+ public void run(Long taskId, MesProBagCodeGenerateSpec spec) {
+ // 鍙栨秷鏍囪鍦ㄦ彁浜ゆ椂宸叉敞鍐岋紝杩欓噷鍙彇鐢紱鍙栦笉鍒拌鏄庢敞鍐岀幆鑺傝缁曡繃锛岃ˉ涓�涓互淇濊瘉鍙栨秷鍙敤
+ AtomicBoolean cancelFlag = registry.get(taskId);
+ if (cancelFlag == null) {
+ cancelFlag = registry.register(taskId);
+ }
+ try {
+ taskMapper.updateStatus(taskId, MesProBagCodeGenerateTaskStatusEnum.RUNNING.getStatus(),
+ null, null);
+ int finished = generator.generate(spec, taskId, cancelFlag).finishedCount();
+ // 鏈�鍚庝竴娈靛啓鍏ュ悗銆佷换鍔℃敹灏惧墠鎵嶆敹鍒板彇娑堬細浠嶆寜鍙栨秷澶勭悊锛屼繚璇�"鍙栨秷鍚庢棤鍗婃垚鍝�"
+ if (cancelFlag.get()) {
+ throw new MesProBagCodeBatchGenerator.CancelledException();
+ }
+ taskMapper.updateProgress(taskId, finished);
+ taskMapper.updateStatus(taskId, MesProBagCodeGenerateTaskStatusEnum.FINISHED.getStatus(),
+ null, LocalDateTime.now());
+ log.info("[run][鐢熸垚浠诲姟 {} 瀹屾垚锛屽叡 {} 鏉¤鐮乚", taskId, finished);
+ } catch (MesProBagCodeBatchGenerator.CancelledException e) {
+ cleanup(taskId, spec.uuid());
+ taskMapper.updateStatus(taskId, MesProBagCodeGenerateTaskStatusEnum.CANCELED.getStatus(),
+ null, LocalDateTime.now());
+ log.info("[run][鐢熸垚浠诲姟 {} 宸插彇娑圿", taskId);
+ } catch (Exception e) {
+ cleanup(taskId, spec.uuid());
+ taskMapper.updateStatus(taskId, MesProBagCodeGenerateTaskStatusEnum.FAILED.getStatus(),
+ truncateError(e), LocalDateTime.now());
+ log.error("[run][鐢熸垚浠诲姟 {} 鐢熸垚澶辫触]", taskId, e);
+ } finally {
+ registry.unregister(taskId);
+ registry.release();
+ }
+ }
+
+ /**
+ * 娓呯悊鍗婃垚鍝侊細娓呯悊澶辫触鍙褰曟棩蹇楋紝涓嶈兘褰卞搷浠诲姟缁堟�佸洖鍐欙紝鍚﹀垯浠诲姟浼氫竴鐩存寕鍦�"鐢熸垚涓�"
+ */
+ private void cleanup(Long taskId, String uuid) {
+ try {
+ cleaner.cleanup(uuid);
+ } catch (Exception e) {
+ log.error("[cleanup][鐢熸垚浠诲姟 {} 鍗婃垚鍝佹竻鐞嗗け璐ワ紝uuid={}]", taskId, uuid, e);
+ }
+ }
+
+ private String truncateError(Exception e) {
+ String message = e.getMessage();
+ if (message == null || message.isBlank()) {
+ message = e.getClass().getSimpleName();
+ }
+ return message.length() > 500 ? message.substring(0, 500) : message;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateSpec.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateSpec.java
new file mode 100644
index 0000000..4f609e7
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateSpec.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateBizTypeEnum;
+
+/**
+ * 琚嬬爜鎵归噺鐢熸垚鍙傛暟锛堝悓姝ヤ笌寮傛鍏辩敤锛�
+ *
+ * @param bizType 涓氬姟绫诲瀷
+ * @param uuid 鐢熸垚缁� UUID
+ * @param itemId 鍝佺缂栧彿
+ * @param itemCode 鍝佺缂栫爜
+ * @param itemName 鍝佺鍚嶇О
+ * @param unitType 璁¢噺鍗曚綅绫诲瀷
+ * @param perPallet 姣忔墭鐩樿鏁帮紙鍚ㄧ被鍨嬩负 0锛�
+ * @param palletCount 鎵樼洏鏁帮紙鍘熷鏁版嵁涓� 0锛�
+ * @param totalCount 琚嬬爜鎬绘暟
+ * @param remark 澶囨敞
+ */
+public record MesProBagCodeGenerateSpec(
+ MesProBagCodeGenerateBizTypeEnum bizType,
+ String uuid,
+ Long itemId,
+ String itemCode,
+ String itemName,
+ String unitType,
+ int perPallet,
+ int palletCount,
+ int totalCount,
+ String remark) {
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskService.java
new file mode 100644
index 0000000..c918f7a
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskService.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeGenerateTaskDO;
+
+/**
+ * 琚嬬爜鎵归噺鐢熸垚浠诲姟 Service
+ * <p>
+ * 鍙壙鎷呰秴鍑哄悓姝ラ槇鍊肩殑鐢熸垚璇锋眰锛氬皬鎵归噺浠嶅湪璇锋眰绾跨▼鍐呭悓姝ュ畬鎴愶紝涓嶈繘浠诲姟銆佷笉鎺掗槦銆�
+ */
+public interface MesProBagCodeGenerateTaskService {
+
+ /**
+ * 鎻愪氦鍚庡彴鐢熸垚浠诲姟
+ *
+ * @param spec 鐢熸垚鍙傛暟
+ * @param requestId 骞傜瓑閿紝鍙负绌猴紱闈炵┖鏃跺悓涓�閿彧浼氬垱寤轰竴涓换鍔�
+ * @return 宸插垱寤烘垨宸插瓨鍦ㄧ殑浠诲姟
+ */
+ MesProBagCodeGenerateTaskDO submit(MesProBagCodeGenerateSpec spec, String requestId);
+
+ /**
+ * 鑾峰緱鐢熸垚浠诲姟
+ *
+ * @param id 浠诲姟缂栧彿
+ * @return 浠诲姟锛涗笉瀛樺湪鏃舵姏涓氬姟寮傚父
+ */
+ MesProBagCodeGenerateTaskDO getTask(Long id);
+
+ /**
+ * 鎸夊箓绛夐敭鑾峰緱鐢熸垚浠诲姟
+ *
+ * @param requestId 骞傜瓑閿�
+ * @return 浠诲姟锛涗笉瀛樺湪鏃惰繑鍥� null
+ */
+ MesProBagCodeGenerateTaskDO getTaskByRequestId(String requestId);
+
+ /**
+ * 鍙栨秷鐢熸垚浠诲姟
+ * <p>
+ * 浠诲姟姝e湪鏈繘绋嬫墽琛屾椂缃綅鍙栨秷鏍囪锛岀敱鎵ц绾跨▼鍋滄骞舵竻鐞嗛儴鍒嗘暟鎹紱
+ * 宸插浜庣粓鎬佺殑浠诲姟涓嶅厑璁稿彇娑堛��
+ *
+ * @param id 浠诲姟缂栧彿
+ */
+ void cancel(Long id);
+
+ /**
+ * 鍥炴敹鍥犳湇鍔¢噸鍚垨寮傚父涓柇鑰屾畫鐣欏湪闈炵粓鎬佺殑浠诲姟
+ *
+ * @return 鍥炴敹鐨勪换鍔℃暟
+ */
+ int recycleStaleTasks();
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskServiceImpl.java
new file mode 100644
index 0000000..9a24bd8
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeGenerateTaskServiceImpl.java
@@ -0,0 +1,216 @@
+package cn.iocoder.yudao.module.mes.service.pro.bagcode;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeGenerateTaskDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeGenerateTaskMapper;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateTaskRegistry;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BAG_CODE_GENERATE_TASK_BUSY;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BAG_CODE_GENERATE_TASK_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BAG_CODE_GENERATE_TASK_STATUS_INVALID;
+
+/**
+ * 琚嬬爜鎵归噺鐢熸垚浠诲姟 Service 瀹炵幇
+ */
+@Slf4j
+@Service
+@Validated
+public class MesProBagCodeGenerateTaskServiceImpl implements MesProBagCodeGenerateTaskService {
+
+ /**
+ * 闈炵粓鎬佺姸鎬侀泦鍚�
+ */
+ private static final List<Integer> UNFINISHED_STATUSES = List.of(
+ MesProBagCodeGenerateTaskStatusEnum.PENDING.getStatus(),
+ MesProBagCodeGenerateTaskStatusEnum.RUNNING.getStatus(),
+ MesProBagCodeGenerateTaskStatusEnum.CANCELING.getStatus());
+
+ /**
+ * 缁堟�佺姸鎬侀泦鍚�
+ */
+ private static final List<Integer> FINAL_STATUSES = List.of(
+ MesProBagCodeGenerateTaskStatusEnum.FINISHED.getStatus(),
+ MesProBagCodeGenerateTaskStatusEnum.CANCELED.getStatus(),
+ MesProBagCodeGenerateTaskStatusEnum.FAILED.getStatus());
+
+ @Resource
+ private MesProBagCodeGenerateTaskMapper taskMapper;
+ @Resource
+ private MesProBagCodeGenerateRunner runner;
+ @Resource
+ private MesProBagCodeGenerateCleaner cleaner;
+ @Resource
+ private MesProBagCodeGenerateTaskRegistry registry;
+
+ @Resource(name = "mesBagCodeGenerateExecutor")
+ private ThreadPoolTaskExecutor executor;
+
+ @Override
+ public MesProBagCodeGenerateTaskDO submit(MesProBagCodeGenerateSpec spec, String requestId) {
+ // 1. 骞傜瓑锛氱綉缁滈噸璇曞鐢ㄥ悓涓�涓� requestId 鏃剁洿鎺ヨ繑鍥炲凡鏈変换鍔★紝涓嶅啀鐢熸垚
+ MesProBagCodeGenerateTaskDO existing = findExisting(requestId);
+ if (existing != null) {
+ return existing;
+ }
+ // 2. 骞跺彂闂搁棬锛氬悓涓�鏃堕棿鍙窇涓�涓悗鍙扮敓鎴愪换鍔★紱琚尅浣忔椂甯︿笂褰撳墠浠诲姟杩涘害
+ if (!registry.tryAcquire()) {
+ // 骞跺彂閲嶅鎻愪氦鏃讹紝鍙︿竴璇锋眰鍙兘宸叉姠鍏堣惤搴�
+ existing = findExisting(requestId);
+ if (existing != null) {
+ return existing;
+ }
+ throw exception(PRO_BAG_CODE_GENERATE_TASK_BUSY, resolveRunningProgress());
+ }
+ // 鏉冮檺涓�鏃︿氦缁欐墽琛屽櫒锛屽氨鐢辨墽琛岀嚎绋嬪湪鏀跺熬鏃跺綊杩橈紱姝ゅ墠浠讳綍澶辫触閮藉繀椤诲湪杩欓噷褰掕繕
+ boolean handedOver = false;
+ try {
+ MesProBagCodeGenerateTaskDO task = MesProBagCodeGenerateTaskDO.builder()
+ .requestId(requestId)
+ .bizType(spec.bizType().getType())
+ .uuid(spec.uuid())
+ .itemId(spec.itemId())
+ .itemCode(spec.itemCode())
+ .itemName(spec.itemName())
+ .unitType(spec.unitType())
+ .perPallet(spec.perPallet())
+ .palletCount(spec.palletCount())
+ .totalCount(spec.totalCount())
+ .finishedCount(0)
+ .status(MesProBagCodeGenerateTaskStatusEnum.PENDING.getStatus())
+ .remark(spec.remark())
+ .build();
+ try {
+ taskMapper.insert(task);
+ } catch (DuplicateKeyException e) {
+ // 鍞竴绱㈠紩鍏滃簳骞跺彂閲嶅鎻愪氦
+ existing = findExisting(requestId);
+ if (existing != null) {
+ return existing;
+ }
+ throw e;
+ }
+ // 3. 鍏堟敞鍐岃繍琛屾�佸啀鎶曢�掞細閬垮厤銆屽凡钀藉簱浣嗘湭娉ㄥ唽銆嶇殑绐楀彛琚竻鐞嗕换鍔¤鍥炴敹
+ registry.register(task.getId());
+ handedOver = true;
+ try {
+ executor.execute(() -> runner.run(task.getId(), spec));
+ } catch (RejectedExecutionException e) {
+ registry.unregister(task.getId());
+ handedOver = false;
+ taskMapper.updateStatus(task.getId(),
+ MesProBagCodeGenerateTaskStatusEnum.FAILED.getStatus(),
+ "鐢熸垚绾跨▼绻佸繖锛岃绋嶅悗閲嶈瘯", LocalDateTime.now());
+ throw exception(PRO_BAG_CODE_GENERATE_TASK_BUSY, 0);
+ }
+ return task;
+ } finally {
+ if (!handedOver) {
+ registry.release();
+ }
+ }
+ }
+
+ @Override
+ public MesProBagCodeGenerateTaskDO getTask(Long id) {
+ MesProBagCodeGenerateTaskDO task = taskMapper.selectById(id);
+ if (task == null) {
+ throw exception(PRO_BAG_CODE_GENERATE_TASK_NOT_EXISTS);
+ }
+ return task;
+ }
+
+ @Override
+ public MesProBagCodeGenerateTaskDO getTaskByRequestId(String requestId) {
+ return findExisting(requestId);
+ }
+
+ @Override
+ public void cancel(Long id) {
+ MesProBagCodeGenerateTaskDO task = getTask(id);
+ if (FINAL_STATUSES.contains(task.getStatus())) {
+ throw exception(PRO_BAG_CODE_GENERATE_TASK_STATUS_INVALID);
+ }
+ if (registry.requestCancel(id)) {
+ // 鎵ц绾跨▼浼氬湪褰撳墠鍒嗘缁撴潫鍚庡仠姝€�佹竻鐞嗗崐鎴愬搧骞剁疆涓哄凡鍙栨秷
+ taskMapper.updateStatus(id, MesProBagCodeGenerateTaskStatusEnum.CANCELING.getStatus(),
+ null, null);
+ return;
+ }
+ // 涓嶅湪鏈繘绋嬭繍琛岋紙杩涚▼閲嶅惎鍚庣殑娈嬬暀浠诲姟锛夛細鐩存帴娓呯悊娈嬬暀骞舵敹灏�
+ cleanupQuietly(task);
+ taskMapper.updateStatus(id, MesProBagCodeGenerateTaskStatusEnum.CANCELED.getStatus(),
+ null, LocalDateTime.now());
+ }
+
+ @Override
+ public int recycleStaleTasks() {
+ List<MesProBagCodeGenerateTaskDO> list = taskMapper.selectUnfinishedList(UNFINISHED_STATUSES);
+ // 鎻愪氦绾跨▼銆岃惤搴� 鈫� 娉ㄥ唽杩愯鎬併�嶄箣闂村瓨鍦ㄦ瀬鐭獥鍙o紝璺宠繃鍒氬垱寤虹殑浠诲姟閬垮厤璇洖鏀�
+ LocalDateTime recent = LocalDateTime.now().minusMinutes(1);
+ int count = 0;
+ for (MesProBagCodeGenerateTaskDO task : list) {
+ if (task.getCreateTime() != null && task.getCreateTime().isAfter(recent)) {
+ continue;
+ }
+ if (registry.isRunning(task.getId())) {
+ continue;
+ }
+ cleanupQuietly(task);
+ taskMapper.updateStatus(task.getId(), MesProBagCodeGenerateTaskStatusEnum.FAILED.getStatus(),
+ "浠诲姟鍥犳湇鍔¢噸鍚垨寮傚父涓柇鑰岀粓姝紝璇烽噸鏂扮敓鎴�", LocalDateTime.now());
+ count++;
+ }
+ return count;
+ }
+
+ // ==================== 绉佹湁鏂规硶 ====================
+
+ private MesProBagCodeGenerateTaskDO findExisting(String requestId) {
+ if (StrUtil.isBlank(requestId)) {
+ return null;
+ }
+ return taskMapper.selectByRequestId(requestId);
+ }
+
+ /**
+ * 娓呯悊鍗婃垚鍝侊細娓呯悊澶辫触涓嶈兘闃绘柇浠诲姟缁堟�佸洖鍐欙紝鍚﹀垯浠诲姟浼氫竴鐩存寕鍦ㄩ潪缁堟��
+ */
+ private void cleanupQuietly(MesProBagCodeGenerateTaskDO task) {
+ try {
+ cleaner.cleanup(task.getUuid());
+ } catch (Exception e) {
+ log.error("[cleanupQuietly][鐢熸垚浠诲姟 {} 鍗婃垚鍝佹竻鐞嗗け璐ワ紝uuid={}]",
+ task.getId(), task.getUuid(), e);
+ }
+ }
+
+ /**
+ * 姝e湪杩愯浠诲姟鐨勮繘搴︾櫨鍒嗘瘮锛岀敤浜庛�屽凡鏈変换鍔¤繘琛屼腑銆嶇殑鎻愮ず
+ */
+ private int resolveRunningProgress() {
+ List<MesProBagCodeGenerateTaskDO> tasks = taskMapper.selectUnfinishedList(UNFINISHED_STATUSES);
+ return tasks.stream().findFirst().map(this::calcProgress).orElse(0);
+ }
+
+ private int calcProgress(MesProBagCodeGenerateTaskDO task) {
+ Integer total = task.getTotalCount();
+ if (total == null || total <= 0) {
+ return 0;
+ }
+ int finished = task.getFinishedCount() == null ? 0 : task.getFinishedCount();
+ return (int) Math.min(100L, (long) finished * 100 / total);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
index 1990cd8..8180e42 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -18,6 +18,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeGenerateTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
@@ -26,9 +27,11 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateProperties;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import jakarta.annotation.Resource;
@@ -46,6 +49,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -92,8 +96,16 @@
@Resource
private cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService integrityService;
+ @Resource
+ private MesProBagCodeBatchGenerator batchGenerator;
+ @Resource
+ private MesProBagCodeGenerateCleaner generateCleaner;
+ @Resource
+ private MesProBagCodeGenerateTaskService generateTaskService;
+ @Resource
+ private MesProBagCodeGenerateProperties generateProperties;
+
@Override
- @Transactional(rollbackFor = Exception.class)
public MesProBagCodeGenerateRespVO generateBagCodes(MesProBagCodeGenerateReqVO generateReqVO) {
// 1. 鏍¢獙鍝佺骞惰鍙栦富璁¢噺鍗曚綅绫诲瀷
MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
@@ -102,7 +114,7 @@
// 2. 鎸夌被鍨嬫崲绠楄鐮佹暟閲忥細N琚� = 姣忔墭 N 琚� 脳 鎵樼洏鏁帮紱鍚� = 鐩存帴杈撳叆琚嬬爜鏁帮紙鏃犳墭鐩橈級
boolean ton = UNIT_TYPE_TON.equals(type);
int perPallet = ton ? 0 : parseBagsPerPallet(type);
- int totalCount;
+ long totalCount;
int palletCount = 0;
if (ton) {
if (generateReqVO.getBagCount() == null || generateReqVO.getBagCount() <= 0) {
@@ -114,79 +126,94 @@
throw exception(PRO_BAG_CODE_PALLET_COUNT_REQUIRED);
}
palletCount = generateReqVO.getPalletCount();
- totalCount = perPallet * palletCount;
+ // 鐢� long 璁$畻锛岄伩鍏嶅ぇ鎵樼洏鏁扮浉涔樻孩鍑� int
+ totalCount = (long) perPallet * palletCount;
}
+ assertGenerateCountAllowed(totalCount);
// 3. 鐢熸垚闃舵锛氳鐮佺洿鎺ョ粦瀹氬搧绉嶏紱鐢熶骇鎵规鍦ㄥ悗缁綍鍏�
- String uuid = IdUtil.fastSimpleUUID();
- List<MesProPalletDO> pallets = new ArrayList<>(palletCount);
- if (!ton) {
- for (int i = 0; i < palletCount; i++) {
- pallets.add(new MesProPalletDO()
- .setTempCode(codeRedisDao.generate())
- .setBusinessCode(codeRedisDao.generate())
- .setBagCount(perPallet)
- .setAutoGenerated(true)
- .setItemId(item.getId())
- .setStatus(MesProPalletStatusEnum.IN_USE.getStatus()));
- }
- palletMapper.insertBatch(pallets);
+ MesProBagCodeGenerateSpec spec = new MesProBagCodeGenerateSpec(
+ MesProBagCodeGenerateBizTypeEnum.PACKAGE, IdUtil.fastSimpleUUID(),
+ item.getId(), item.getCode(), item.getName(), type,
+ perPallet, palletCount, (int) totalCount, generateReqVO.getRemark());
+
+ // 4. 灏忔壒閲忓悓姝ュ畬鎴愶紝澶ф壒閲忚浆鍚庡彴浠诲姟
+ if (totalCount <= generateProperties.getSyncThreshold()) {
+ MesProBagCodeBatchGenerator.GenerateResult result = runSyncGenerate(spec);
+ return new MesProBagCodeGenerateRespVO()
+ .setUuid(spec.uuid())
+ .setType(type)
+ .setPerPallet(perPallet)
+ .setTotalCount(result.finishedCount())
+ .setPalletCount(result.palletIds().size())
+ .setPalletIds(result.palletIds())
+ .setAsync(false);
}
- // 琚嬪簭鍙峰師瀛愬垎閰嶏細鍏堝彇搴撳唴鏈�澶у簭鍙蜂綔鍩哄噯锛屽啀鐢� Redis 鍘熷瓙鑷锛岄伩鍏嶅苟鍙戠敓鎴愪骇鐢熼噸澶嶈搴忓彿
- int startNo = codeRedisDao.allocateBagNoStart(totalCount, bagCodeMapper.selectMaxBagNo());
- List<MesProBagCodeDO> bagCodes = new ArrayList<>(totalCount);
- for (int i = 0; i < totalCount; i++) {
- int bagNo = startNo + i;
- MesProPalletDO pallet = ton ? null : pallets.get(i / perPallet);
- bagCodes.add(new MesProBagCodeDO()
- .setUuid(uuid)
- .setCode(codeRedisDao.generate())
- .setBusinessCode(codeRedisDao.generate())
- .setBagNo(bagNo)
- .setItemId(item.getId())
- .setItemCode(item.getCode())
- .setItemName(item.getName())
- .setPalletId(pallet == null ? null : pallet.getId())
- .setPalletCode(pallet == null ? null : pallet.getTempCode())
- .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
- .setRemark(generateReqVO.getRemark()));
- }
- bagCodeMapper.insertBatch(bagCodes);
+ MesProBagCodeGenerateTaskDO task = generateTaskService.submit(spec, generateReqVO.getRequestId());
return new MesProBagCodeGenerateRespVO()
- .setUuid(uuid)
- .setType(type)
- .setPerPallet(perPallet)
- .setTotalCount(totalCount)
- .setPalletIds(pallets.stream().map(MesProPalletDO::getId).toList());
+ .setUuid(task.getUuid())
+ .setType(task.getUnitType())
+ .setPerPallet(task.getPerPallet())
+ .setTotalCount(task.getTotalCount())
+ .setPalletCount(task.getPalletCount())
+ .setAsync(true)
+ .setTaskId(task.getId());
}
@Override
- @Transactional(rollbackFor = Exception.class)
public MesProBagCodeGenerateRespVO generateRawBagCodes(MesProBagCodeRawGenerateReqVO generateReqVO) {
// 鍘熷琚嬬爜涓嶄緷璧栬閲忓崟浣嶇被鍨嬩笌鎵樼洏锛氫粎鎸夋暟閲忕敓鎴愶紝琚嬬爜甯﹀搧绉嶅啑浣欙紝鍚庣画鍙甯稿綍鍏ユ壒娆�/缁戝畾鎵樼洏
MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
- String uuid = IdUtil.fastSimpleUUID();
- // 琚嬪簭鍙峰師瀛愬垎閰嶏細鍏堝彇搴撳唴鏈�澶у簭鍙蜂綔鍩哄噯锛屽啀鐢� Redis 鍘熷瓙鑷锛岄伩鍏嶅苟鍙戠敓鎴愪骇鐢熼噸澶嶈搴忓彿
- int startNo = codeRedisDao.allocateBagNoStart(generateReqVO.getCount(), bagCodeMapper.selectMaxBagNo());
- List<MesProBagCodeDO> bagCodes = new ArrayList<>(generateReqVO.getCount());
- for (int i = 0; i < generateReqVO.getCount(); i++) {
- int bagNo = startNo + i;
- bagCodes.add(new MesProBagCodeDO()
- .setUuid(uuid)
- .setCode(codeRedisDao.generate())
- .setBusinessCode(codeRedisDao.generate())
- .setBagNo(bagNo)
- .setItemId(item.getId())
- .setItemCode(item.getCode())
- .setItemName(item.getName())
- .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
- .setRemark(generateReqVO.getRemark()));
+ long totalCount = generateReqVO.getCount();
+ assertGenerateCountAllowed(totalCount);
+
+ MesProBagCodeGenerateSpec spec = new MesProBagCodeGenerateSpec(
+ MesProBagCodeGenerateBizTypeEnum.RAW, IdUtil.fastSimpleUUID(),
+ item.getId(), item.getCode(), item.getName(), null,
+ 0, 0, (int) totalCount, generateReqVO.getRemark());
+
+ if (totalCount <= generateProperties.getSyncThreshold()) {
+ MesProBagCodeBatchGenerator.GenerateResult result = runSyncGenerate(spec);
+ return new MesProBagCodeGenerateRespVO()
+ .setUuid(spec.uuid())
+ .setTotalCount(result.finishedCount())
+ .setPalletCount(0)
+ .setPalletIds(List.of())
+ .setAsync(false);
}
- bagCodeMapper.insertBatch(bagCodes);
+ MesProBagCodeGenerateTaskDO task = generateTaskService.submit(spec, generateReqVO.getRequestId());
return new MesProBagCodeGenerateRespVO()
- .setUuid(uuid)
- .setTotalCount(bagCodes.size())
- .setPalletIds(List.of());
+ .setUuid(task.getUuid())
+ .setTotalCount(task.getTotalCount())
+ .setPalletCount(task.getPalletCount())
+ .setAsync(true)
+ .setTaskId(task.getId());
+ }
+
+ /**
+ * 鍚屾鐢熸垚锛氬け璐ユ椂娓呯悊宸插啓鍏ョ殑鍒嗘锛岄伩鍏嶇暀涓嬩笉瀹屾暣鐨勬暟鎹寘
+ */
+ private MesProBagCodeBatchGenerator.GenerateResult runSyncGenerate(MesProBagCodeGenerateSpec spec) {
+ try {
+ return batchGenerator.generate(spec, null, new AtomicBoolean(false));
+ } catch (RuntimeException e) {
+ try {
+ generateCleaner.cleanup(spec.uuid());
+ } catch (Exception cleanupException) {
+ log.error("[runSyncGenerate][鐢熸垚缁� {} 鍗婃垚鍝佹竻鐞嗗け璐", spec.uuid(), cleanupException);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 鏍¢獙鍗曟鐢熸垚琚嬬爜鏁伴噺涓嶈秴杩囦笂闄�
+ */
+ private void assertGenerateCountAllowed(long totalCount) {
+ int maxCount = generateProperties.getMaxTaskCount();
+ if (totalCount > maxCount) {
+ throw exception(PRO_BAG_CODE_GENERATE_COUNT_TOO_MANY, maxCount);
+ }
}
/**
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/BagBusinessCodeTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/BagBusinessCodeTest.java
index 514b59a..98d92f2 100644
--- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/BagBusinessCodeTest.java
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/BagBusinessCodeTest.java
@@ -8,10 +8,13 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeGenerateTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeReplaceMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
+import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateProperties;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
@@ -20,6 +23,7 @@
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -35,6 +39,9 @@
private final MesProBatchMapper batches = mock(MesProBatchMapper.class);
private final MesProBagCodeReplaceMapper replacements = mock(MesProBagCodeReplaceMapper.class);
private final MdmUnitMeasureApi units = mock(MdmUnitMeasureApi.class);
+ private final MesProBagCodeChunkWriter writer = mock(MesProBagCodeChunkWriter.class);
+ private final MesProBagCodeBatchGenerator generator = new MesProBagCodeBatchGenerator();
+ private final MesProBagCodeGenerateProperties properties = new MesProBagCodeGenerateProperties();
@BeforeEach void setup() {
ReflectionTestUtils.setField(service, "bagCodeMapper", bags);
@@ -43,9 +50,39 @@
ReflectionTestUtils.setField(service, "batchMapper", batches);
ReflectionTestUtils.setField(service, "replaceMapper", replacements);
ReflectionTestUtils.setField(service, "unitMeasureApi", units);
+ ReflectionTestUtils.setField(service, "batchGenerator", generator);
+ ReflectionTestUtils.setField(service, "generateProperties", properties);
+ ReflectionTestUtils.setField(service, "generateCleaner", mock(MesProBagCodeGenerateCleaner.class));
+ ReflectionTestUtils.setField(service, "generateTaskService", mock(MesProBagCodeGenerateTaskService.class));
+ // 鍒嗘鎵ц鍣ㄤ娇鐢ㄧ湡瀹炲疄鐜帮紝浠呮妸浜嬪姟鍐欏叆鍣ㄤ笌 Redis 鍙栫爜鏇挎崲涓� mock
+ ReflectionTestUtils.setField(generator, "bagCodeMapper", bags);
+ ReflectionTestUtils.setField(generator, "taskMapper", mock(MesProBagCodeGenerateTaskMapper.class));
+ ReflectionTestUtils.setField(generator, "codeRedisDao", codes);
+ ReflectionTestUtils.setField(generator, "chunkWriter", writer);
+ ReflectionTestUtils.setField(generator, "properties", properties);
+ doAnswer(invocation -> {
+ bags.insertBatch(invocation.getArgument(0));
+ return null;
+ }).when(writer).insertBagCodes(anyList());
+ doAnswer(invocation -> {
+ List<MesProPalletDO> pallets = invocation.getArgument(0);
+ for (int i = 0; i < pallets.size(); i++) {
+ pallets.get(i).setId((long) (i + 1));
+ }
+ return null;
+ }).when(writer).insertPallets(anyList());
+
when(items.validateItemExists(1L)).thenReturn(new MesMdmItemDO().setId(1L).setUnitMeasureId(2L));
AtomicLong sequence = new AtomicLong(1000000000000000L);
when(codes.generate()).thenAnswer(invocation -> String.valueOf(sequence.incrementAndGet()));
+ when(codes.generateBatch(anyInt())).thenAnswer(invocation -> {
+ int count = invocation.getArgument(0);
+ List<String> codes = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ codes.add(String.valueOf(sequence.incrementAndGet()));
+ }
+ return codes;
+ });
when(codes.allocateBagNoStart(anyInt(), any())).thenReturn(1);
}
@@ -65,13 +102,11 @@
}
@Test @SuppressWarnings({"unchecked", "rawtypes"}) void automaticPalletCarriesVariety() {
- var pallets=mock(cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper.class);
- ReflectionTestUtils.setField(service,"palletMapper",pallets);
- when(units.getUnitMeasure(2L)).thenReturn(CommonResult.success(new MdmUnitMeasureRespDTO().setType("2鍗冨厠")));
+ when(units.getUnitMeasure(2L)).thenReturn(CommonResult.success(new MdmUnitMeasureRespDTO().setType("48琚�")));
var req=new MesProBagCodeGenerateReqVO();req.setItemId(1L);req.setPalletCount(1);
service.generateBagCodes(req);
- ArgumentCaptor<Collection<cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO>> saved=ArgumentCaptor.forClass((Class)Collection.class);
- verify(pallets).insertBatch(saved.capture());assertEquals(1L,saved.getValue().iterator().next().getItemId());
+ ArgumentCaptor<List<MesProPalletDO>> saved=ArgumentCaptor.forClass((Class)List.class);
+ verify(writer).insertPallets(saved.capture());assertEquals(1L,saved.getValue().iterator().next().getItemId());
}
@Test void replacementIssuesNewPairInsteadOfReusingOldBusinessCode() {
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index d351052..86c4308 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -33,6 +33,24 @@
# 涓存椂 ZIP 鐩綍锛岀暀绌哄垯浣跨敤 ${java.io.tmpdir}/mes-qr-export
temp-dir: ''
+ # 琚嬬爜鎵归噺鐢熸垚锛氬皬鎵归噺鍚屾瀹屾垚锛屽ぇ鎵归噺杞悗鍙颁换鍔″苟鍒嗘钀藉簱
+ bag-code:
+ generate:
+ # 鍚屾/鍚庡彴鍒嗙晫绾匡紙琚嬬爜鏁帮級銆備笉瓒呰繃鍒欒姹傜嚎绋嬪唴瀹屾垚
+ sync-threshold: 20000
+ # 姣忔琚嬬爜/姣忔壒鍙栫爜鏁伴噺
+ chunk-size: 5000
+ # 鍚屾椂鍏佽杩愯鐨勫悗鍙扮敓鎴愪换鍔℃暟
+ max-concurrent-tasks: 1
+ # 鍗曟鐢熸垚琚嬬爜涓婇檺
+ max-task-count: 1000000
+ # 鍗曚釜鍚庡彴浠诲姟鏈�闀胯繍琛屾椂闂达紙鍒嗛挓锛夛紝瓒呮椂鑷姩缁堟骞舵竻鐞�
+ max-runtime-minutes: 30
+ # 鍓嶇杞寤鸿闂撮殧锛堢锛�
+ poll-interval-seconds: 2
+ # 鍗婃垚鍝佹竻鐞嗘瘡杞垹闄よ鏁�
+ cleanup-batch-size: 5000
+
spring:
application:
name: yudao-server
--
Gitblit v1.9.3