From 6c0fd2cb801554f7999d14e45a94ef0f0d0dfb8d Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 16 十月 2026 17:45:33 +0800
Subject: [PATCH] feat(mes): 二维码大批量导出改为后台任务,取消数量上限

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportTaskStatusEnum.java                         |   65 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/qrexport/MesProQrExportTaskDO.java               |   85 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java           |   47 
 sql/mysql/mes_pro_qr_export_task_init.sql                                                                                      |   26 
 yudao-server/src/main/resources/application.yaml                                                                               |   17 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSourceFactory.java                |  180 ++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java                      |  223 +++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/SystemCpuLoadMonitor.java                        |  148 +++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProQrExportCleanupTask.java                              |   63 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelAssembler.java                    |  130 +++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskRespVO.java      |   51 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java                 |   81 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/qrexport/MesProQrExportTaskMapper.java                |   88 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportBizTypeEnum.java                            |   42 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java             |   38 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java                       |   38 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskSubmitReqVO.java |   22 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java                    |   90 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java                   |   20 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java               |   48 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java                              |   24 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java                      |   52 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportExecutorConfig.java                |   42 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportCountRespVO.java     |   30 
 scripts/qr-export-big-batch.cjs                                                                                                |  286 ++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java                                       |    7 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java             |  297 ++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java     |  178 ++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSource.java                       |   34 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportTaskRegistry.java                  |  113 ++
 30 files changed, 2,466 insertions(+), 99 deletions(-)

diff --git a/scripts/qr-export-big-batch.cjs b/scripts/qr-export-big-batch.cjs
new file mode 100644
index 0000000..cef7498
--- /dev/null
+++ b/scripts/qr-export-big-batch.cjs
@@ -0,0 +1,286 @@
+/**
+ * 浜岀淮鐮佸ぇ鎵归噺瀵煎嚭鈥斺�旂鍒扮瀹炴祴
+ *
+ * 瑕嗙洊锛氬垎娴佸垽鏂� 鈫� 鍚庡彴浠诲姟鐢熸垚锛堢湡瀹炶繘搴︼級鈫� 闅愯棌涓烘诞鏍� 鈫� 瀹屾垚鍚庝笅杞� ZIP
+ *
+ * 鐢ㄦ硶锛歯ode qr-export-big-batch.cjs [--headed] [--uuid=<鐢熸垚缁�>] [--cancel-after=<绉�>]
+ *   --cancel-after 璁惧畾鍚庯紝鐢熸垚鍒拌绉掓暟鏃剁偣鈥滃彇娑堚�濓紝鍙獙璇佸彇娑堥摼璺紙涓嶇瓑瀹屾垚锛�
+ *
+ * 璇存槑锛氱湡瀹炰娇鐢ㄦ椂鍓嶇浼氬厛寮广�屼繚瀛樹綅缃�嶅璇濇锛團ile System Access API锛夛紝杈逛笅杞借竟鍐欑洏锛�
+ *      鑷姩鍖栦笅璇ュ璇濇浼氶樆濉炴祦绋嬶紝鏁呮湰鑴氭湰鎶婂畠闄嶇骇锛岃蛋娴忚鍣ㄥ師鐢熶笅杞戒互渚挎柇瑷�浜х墿銆�
+ */
+const { chromium } = require('playwright');
+const path = require('node:path');
+const fs = require('node:fs');
+
+const BASE = process.env.QR_TEST_BASE || 'http://127.0.0.1:5666';
+const API = process.env.QR_TEST_API || 'http://127.0.0.1:48080';
+const USERNAME = process.env.QR_TEST_USER || 'admin';
+const PASSWORD = process.env.QR_TEST_PWD || 'admin123';
+const DEFAULT_UUID = 'ae1229655a3e48d7b4bc842e3e74e15c';
+
+const args = process.argv.slice(2);
+const HEADED = args.includes('--headed');
+/** 璺戝畬鍚庝繚鐣欐祻瑙堝櫒绐楀彛涓嶅叧锛屼緵浜哄伐鏌ョ湅浜岀淮鐮� */
+const KEEP_OPEN = args.includes('--keep-open');
+const uuidArg = args.find((a) => a.startsWith('--uuid='));
+const UUID = uuidArg ? uuidArg.slice('--uuid='.length) : DEFAULT_UUID;
+const cancelArg = args.find((a) => a.startsWith('--cancel-after='));
+const CANCEL_AFTER_SEC = cancelArg ? Number(cancelArg.split('=')[1]) : 0;
+
+const OUT_DIR = path.resolve(__dirname, 'qr-export-test-out');
+
+/**
+ * 杩涘害娴眰涓庢诞鏍囩殑閫夋嫨鍣�
+ * 鐢ㄧ粨鏋勭壒寰佽�岄潪 .last() 涔嬬被鐨勬ā绯婂畾浣嶏細椤甸潰閲屽叾瀹冨厓绱狅紙濡� Ant 鐨� message 鎻愮ず锛�
+ * 鍚屾牱鍚�滀簩缁寸爜瀵煎嚭鈥濇枃妗堬紝妯$硦瀹氫綅浼氱偣鍒版彁绀烘潯涓婏紝瀵艰嚧璇垽銆�
+ */
+const PANEL = 'div.fixed.bottom-6.w-80';
+const PILL = 'div.fixed.bottom-6.cursor-pointer';
+const DOWNLOAD_DIR = path.join(OUT_DIR, 'downloads');
+const LOG_FILE = path.join(OUT_DIR, 'run.log');
+
+fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
+fs.writeFileSync(LOG_FILE, '');
+
+const started = Date.now();
+function log(message) {
+  const line = `[${((Date.now() - started) / 1000).toFixed(1)}s] ${message}`;
+  console.log(line);
+  fs.appendFileSync(LOG_FILE, line + '\n');
+}
+function step(m) {
+  log(`\n=== ${m} ===`);
+}
+function fail(m) {
+  log(`FAIL: ${m}`);
+  throw new Error(m);
+}
+
+/**
+ * 鎸夋枃鏈偣鎸夐挳锛氬蹇� Ant Design 鍦ㄤ袱涓眽瀛椾箣闂磋嚜鍔ㄦ彃鍏ョ殑绌烘牸锛堚�滄悳绱⑩�濅細琚覆鏌撴垚鈥滄悳 绱⑩�濓級
+ */
+async function clickButton(page, text, options = {}) {
+  const re = new RegExp(text.split('').join('\\s*'));
+  const locator = page.locator('button').filter({ hasText: re });
+  const count = await locator.count();
+  if (count === 0) fail(`鎵句笉鍒版寜閽細${text}`);
+  await locator.first().click(options);
+}
+
+/** 椤甸潰杩涘害娴眰涓婄殑鎸夐挳锛堟垜鐨勭粍浠剁敤 2 瀛椾腑鏂囷紝鍚屾牱浼氳鎻掔┖鏍硷級 */
+async function clickPanelButton(page, text) {
+  const re = new RegExp(text.split('').join('\\s*'));
+  const locator = page.locator('button').filter({ hasText: re });
+  if ((await locator.count()) === 0) fail(`杩涘害娴眰涓婃壘涓嶅埌銆�${text}銆嶆寜閽甡);
+  await locator.first().click();
+}
+
+async function apiPreflight() {
+  step('闃舵 0锛氭帴鍙i妫�锛堢‘璁ゅ悗绔凡鍚湰娆℃敼鍔級');
+  const loginRes = await fetch(`${API}/admin-api/system/auth/login`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
+  }).then((r) => r.json());
+  if (loginRes.code !== 0) fail(`鐧诲綍澶辫触锛�${JSON.stringify(loginRes)}`);
+  log(`鎺ュ彛鐧诲綍鎴愬姛锛寀serId=${loginRes.data.userId}`);
+
+  const countRes = await fetch(
+    `${API}/admin-api/mes/pro/qr-export/count?bizType=BAG_LABEL&uuid=${UUID}`,
+    { headers: { Authorization: `Bearer ${loginRes.data.accessToken}` } },
+  ).then((r) => r.json());
+  if (countRes.code === 404) fail('鏂版帴鍙h繑鍥� 404 鈥斺�� 鍚庣鏈惈鏈鏀瑰姩');
+  if (countRes.code !== 0) fail(`缁熻鎺ュ彛寮傚父锛�${JSON.stringify(countRes)}`);
+  const { totalCount, syncThreshold, mode } = countRes.data;
+  log(`鎺ュ彛缁熻锛�${totalCount} 寮狅紝鍒嗙晫绾� ${syncThreshold}锛屾柟寮� ${mode}`);
+  return { totalCount, mode, syncThreshold };
+}
+
+/** 鏍¢獙涓嬭浇浜х墿锛氫綋绉�乑IP 澶村熬绛惧悕 */
+async function verifyDownload(download) {
+  const saved = path.join(DOWNLOAD_DIR, download.suggestedFilename());
+  await download.saveAs(saved);
+  const size = fs.statSync(saved).size;
+  log(`涓嬭浇瀹屾垚锛�${download.suggestedFilename()}锛�${(size / 1024 / 1024).toFixed(2)} MB`);
+  const fd = fs.openSync(saved, 'r');
+  const head = Buffer.alloc(4);
+  fs.readSync(fd, head, 0, 4, 0);
+  const tail = Buffer.alloc(22);
+  fs.readSync(fd, tail, 0, 22, size - 22);
+  fs.closeSync(fd);
+  const headOk = head.readUInt32LE(0) === 0x04034b50;
+  const tailOk = tail.readUInt32LE(0) === 0x06054b50;
+  log(`ZIP 澶寸鍚� PK\\x03\\x04锛�${headOk}`);
+  log(`ZIP 灏� EOCD 绛惧悕 PK\\x05\\x06锛�${tailOk}`);
+  if (!headOk || !tailOk) fail('涓嬭浇浜х墿涓嶆槸缁撴瀯瀹屾暣鐨� ZIP');
+  return { saved, size };
+}
+
+async function main() {
+  const { totalCount, mode, syncThreshold } = await apiPreflight();
+
+  step('闃舵 1锛氬惎鍔ㄦ祻瑙堝櫒骞剁櫥褰�');
+  const browser = await chromium.launch({
+    headless: !HEADED,
+    channel: HEADED ? 'msedge' : undefined,
+  });
+  const context = await browser.newContext({
+    acceptDownloads: true,
+    viewport: { width: 1600, height: 900 },
+  });
+  const page = await context.newPage();
+  // 闄嶇骇 File System Access API锛屼娇涓嬭浇璧版祻瑙堝櫒鍘熺敓閫氶亾锛屼究浜庢柇瑷�浜х墿
+  await page.addInitScript(() => {
+    Object.defineProperty(window, 'showSaveFilePicker', {
+      value: undefined,
+      configurable: true,
+    });
+  });
+  const pageErrors = [];
+  page.on('pageerror', (e) => {
+    pageErrors.push(e.message);
+    log(`!! 椤甸潰 JS 閿欒: ${e.message}`);
+  });
+
+  try {
+    await page.goto(`${BASE}/auth/login`, { waitUntil: 'domcontentloaded' });
+    await page.locator('input[name=username]').waitFor({ timeout: 60_000 });
+    await page.locator('input[name=username]').fill(USERNAME);
+    await page.locator('input[name=password]').fill(PASSWORD);
+    await page.locator('button:has-text("鐧诲綍")').last().click();
+    await page.waitForURL((u) => !u.pathname.includes('/auth/login'), { timeout: 60_000 });
+    log('鐧诲綍鎴愬姛锛�' + page.url());
+
+    step('闃舵 2锛氳繘鍏ヨ鐮佸垪琛ㄩ〉骞舵寜鐢熸垚缁勭瓫閫�');
+    await page.goto(`${BASE}/bagcode/bag-code`, { waitUntil: 'domcontentloaded' });
+    await page.getByRole('button', { name: '瀵煎嚭浜岀淮鐮�' }).first().waitFor({ timeout: 60_000 });
+    await page.screenshot({ path: path.join(OUT_DIR, '01-鍒楄〃椤�.png') });
+
+    await page.getByPlaceholder('璇疯緭鍏ョ敓鎴愮粍缂栧彿').fill(UUID);
+    log(`宸插~鍐欑敓鎴愮粍锛�${UUID}`);
+    await clickButton(page, '鎼滅储');
+    await page.waitForTimeout(4000);
+    await page.screenshot({ path: path.join(OUT_DIR, '02-绛涢�夊悗.png') });
+
+    step(`闃舵 3锛氱偣鍑汇�屽鍑轰簩缁寸爜銆嶏紙${totalCount} 寮狅紝鍒嗙晫绾� ${syncThreshold}锛岄鏈熻蛋${mode === 'SYNC' ? '鍚屾涓嬭浇' : '鍚庡彴浠诲姟'}锛塦);
+    // 涓嬭浇浜嬩欢蹇呴』鍏堟寕涓婏紝閬垮厤浜嬩欢鏃╀簬鐩戝惉鑰屾紡鎺�
+    const downloadPromise = page.waitForEvent('download', { timeout: 60 * 60 * 1000 });
+    await page.getByRole('button', { name: '瀵煎嚭浜岀淮鐮�' }).first().click();
+
+    if (mode === 'SYNC') {
+      // 灏忔壒閲忥細绉掔骇鍚屾涓嬭浇锛屼笉杩涘悗鍙颁换鍔°�佷笉鎺掗槦銆佷笉鍙� CPU 闂搁棬绾︽潫
+      await page
+        .locator(PANEL)
+        .waitFor({ timeout: 30_000 })
+        .catch(() => log('锛堝悓姝ヨ矾寰勬湭灞曠ず娴眰锛岀洿鎺ヤ笅杞斤級'));
+      const download = await downloadPromise;
+      await verifyDownload(download);
+      await page.screenshot({ path: path.join(OUT_DIR, 'sync-瀹屾垚.png') });
+      if (pageErrors.length > 0) log(`娉ㄦ剰锛氭祴璇曟湡闂村嚭鐜� ${pageErrors.length} 鏉¢〉闈� JS 閿欒`);
+      log(`\nPASS锛�${totalCount} 寮犳湭瓒呰繃鍒嗙晫绾� ${syncThreshold}锛岃蛋鍚屾涓嬭浇锛屼骇鐗╁畬鏁碻);
+      return;
+    }
+
+    await page.locator(PANEL).waitFor({ timeout: 90_000 });
+    log('杩涘害娴眰宸插嚭鐜�');
+    await page.waitForTimeout(3000);
+
+    // 鎻愪氦鍙兘琚苟鍙戦椄闂ㄦ嫆缁濓紙宸叉湁浠诲姟鍦ㄨ窇锛夛紱鏄庣‘鎶ュ嚭鏉ワ紝閬垮厤鍚庣画姝ラ浠ヨ浼犺
+    const afterClick = await page.locator('body').innerText();
+    if (/瀵煎嚭澶辫触/.test(afterClick)) {
+      const busy = afterClick.match(/宸叉湁浜岀淮鐮佸鍑轰换鍔℃鍦ㄨ繘琛孾^\n]*/);
+      fail(busy ? `琚苟鍙戦椄闂ㄦ嫆缁濓細${busy[0]}` : '瀵煎嚭澶辫触锛堣椤甸潰鎻愮ず锛�');
+    }
+
+    await page.screenshot({ path: path.join(OUT_DIR, '03-杩涘害娴眰.png') });
+    const cancelBtn = page.locator('button').filter({ hasText: /鍙朶s*娑�/ });
+    if ((await cancelBtn.count()) === 0) fail('杩涘害娴眰涓婃壘涓嶅埌銆屽彇娑堛�嶆寜閽�');
+    log('杩涘害娴眰宸插嚭鐜帮紝銆屽彇娑堛�嶆寜閽瓨鍦� 鉁�');
+
+    step('闃舵 4锛氶獙璇併�岄殣钘忋�嶁啋 鏀舵垚娴爣 鈫� 鐐规诞鏍囧啀灞曞紑');
+    await page.screenshot({ path: path.join(OUT_DIR, '04a-闅愯棌鍓�.png') });
+    await clickPanelButton(page, '闅愯棌');
+    await page.waitForTimeout(1500);
+    if ((await page.locator(PILL).count()) === 0) fail('鐐瑰嚮銆岄殣钘忋�嶅悗娴爣鏈嚭鐜�');
+    log('宸叉敹璧蜂负娴爣 鉁�');
+    await page.screenshot({ path: path.join(OUT_DIR, '04b-闅愯棌鍚庢诞鏍�.png') });
+
+    await page.locator(PILL).first().click();
+    await page.waitForTimeout(1200);
+    if ((await page.locator(PANEL).count()) === 0) fail('鐐瑰嚮娴爣鍚庤繘搴﹂潰鏉挎湭鎭㈠');
+    log('鐐瑰嚮娴爣鍚庤繘搴﹂潰鏉挎仮澶� 鉁�');
+    await page.screenshot({ path: path.join(OUT_DIR, '05-鎭㈠闈㈡澘.png') });
+
+    if (CANCEL_AFTER_SEC > 0) {
+      step(`闃舵 5锛氱瓑寰� ${CANCEL_AFTER_SEC}s 鍚庨獙璇併�屽彇娑堛�嶉摼璺痐);
+      await page.waitForTimeout(CANCEL_AFTER_SEC * 1000);
+      await clickPanelButton(page, '鍙栨秷');
+      await page.waitForTimeout(5000);
+      await page.screenshot({ path: path.join(OUT_DIR, '06-鍙栨秷鍚�.png') });
+      const canceled = await page
+        .locator('text=/宸插彇娑�/')
+        .first()
+        .isVisible()
+        .catch(() => false);
+      log(`鍙栨秷鍚庣晫闈㈡彁绀恒�屽凡鍙栨秷銆嶏細${canceled}`);
+      if (!canceled) fail('鐐瑰嚮鍙栨秷鍚庣晫闈㈡湭杩涘叆宸插彇娑堢姸鎬�');
+      log('鍙栨秷閾捐矾 鉁�');
+      return;
+    }
+
+    step(`闃舵 5锛氱瓑寰呭悗鍙扮敓鎴愬畬鎴愶紙鍏� ${totalCount} 寮狅級`);
+    let lastLogged = '';
+    for (let i = 0; i < 3600; i++) {
+      await page.waitForTimeout(2000);
+      const body = await page.locator('body').innerText().catch(() => '');
+      const m = body.match(/姝e湪鐢熸垚锛�(\d+)\s*\/\s*(\d+)锛�/);
+      if (m) {
+        const cur = `${m[1]}/${m[2]} (${((Number(m[1]) / Number(m[2])) * 100).toFixed(1)}%)`;
+        if (cur !== lastLogged) {
+          log(`鐢熸垚杩涘害 ${cur}`);
+          lastLogged = cur;
+        }
+      }
+      const phase = body.match(/姝e湪涓嬭浇|瀵煎嚭瀹屾垚|瀵煎嚭澶辫触/);
+      if (phase) {
+        log(`杩涘叆涓嬩竴闃舵锛�${phase[0]}`);
+        break;
+      }
+    }
+    await page.screenshot({ path: path.join(OUT_DIR, '06-鐢熸垚瀹屾垚.png') });
+
+    step('闃舵 6锛氱瓑寰呬笅杞借惤鐩樺苟鏍¢獙浜х墿');
+    const download = await downloadPromise;
+    await verifyDownload(download);
+    await page.screenshot({ path: path.join(OUT_DIR, '07-涓嬭浇瀹屾垚.png') });
+
+    if (pageErrors.length > 0) log(`娉ㄦ剰锛氭祴璇曟湡闂村嚭鐜� ${pageErrors.length} 鏉¢〉闈� JS 閿欒`);
+    log(`\nPASS锛氬垎娴� 鈫� 鍚庡彴鐢熸垚 ${totalCount} 寮� 鈫� 鐪熷疄杩涘害 鈫� 闅愯棌/鎭㈠ 鈫� 涓嬭浇 ZIP 鍏ㄩ摼璺�氳繃`);
+  } catch (error) {
+    await page.screenshot({ path: path.join(OUT_DIR, 'error.png') }).catch(() => {});
+    log(`澶辫触鎴浘锛�${path.join(OUT_DIR, 'error.png')}`);
+    throw error;
+  } finally {
+    if (KEEP_OPEN) {
+      // 淇濇寔娴忚鍣ㄦ墦寮�渚涗汉宸ユ煡鐪嬩簩缁寸爜锛涚瓑寰呴〉闈㈣鎵嬪姩鍏抽棴鍚庤繘绋嬫墠閫�鍑�
+      log('锛�--keep-open锛氭祻瑙堝櫒淇濇寔鎵撳紑锛岃鑷鏌ョ湅锛屽叧闂獥鍙e悗鏈繘绋嬮��鍑猴級');
+      await page
+        .waitForEvent('close', { timeout: 0 })
+        .catch(() => {});
+      await browser.close().catch(() => {});
+      return;
+    }
+    if (HEADED) {
+      // 鐣欏嚭鍑犵璁╀汉鐪嬫竻鏈�缁堢晫闈紝鍐嶈嚜鍔ㄦ敹灏撅紝閬垮厤鐣欎笅鎮寕鐨勬祻瑙堝櫒涓� node 杩涚▼
+      log('锛坔eaded 妯″紡锛�10 绉掑悗鑷姩鍏抽棴娴忚鍣級');
+      await page.waitForTimeout(10_000).catch(() => {});
+    }
+    await browser.close().catch(() => {});
+  }
+}
+
+main().catch((e) => {
+  log(`\nFAILED: ${e.message}`);
+  process.exit(1);
+});
diff --git a/sql/mysql/mes_pro_qr_export_task_init.sql b/sql/mysql/mes_pro_qr_export_task_init.sql
new file mode 100644
index 0000000..b6f1232
--- /dev/null
+++ b/sql/mysql/mes_pro_qr_export_task_init.sql
@@ -0,0 +1,26 @@
+-- 浜岀淮鐮佸ぇ鎵归噺瀵煎嚭浠诲姟锛氫复鏃� ZIP 钀界洏 + 杩涘害/鍙栨秷
+-- 璇存槑锛氬ぇ鎵归噺瀵煎嚭涓嶅湪璇锋眰绾跨▼鍐呭畬鎴愶紝鏀逛负钀戒换鍔″悗鐢卞悗鍙扮嚎绋嬫祦寮忕敓鎴� ZIP锛�
+--       鍓嶇杞鐪熷疄杩涘害骞舵敮鎸佸彇娑堬紱ZIP 涓嬭浇瀹屾垚鍚庣珛鍗冲垹闄わ紝寮傚父娈嬬暀鐢卞畾鏃朵换鍔℃竻鐞嗐��
+
+CREATE TABLE IF NOT EXISTS `mes_pro_qr_export_task` (
+    `id`             BIGINT       NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+    `biz_type`       VARCHAR(32)  NOT NULL COMMENT '涓氬姟绫诲瀷锛圔AG_LABEL/BAG_BUSINESS/PALLET_LABEL/PALLET_BUSINESS/BATCH_LABEL/BATCH_BUSINESS锛�',
+    `uuid`           VARCHAR(64)  DEFAULT NULL COMMENT '鐢熸垚缁� UUID锛堜笌鎵规缂栧彿浜岄�変竴锛�',
+    `batch_id`       BIGINT       DEFAULT NULL COMMENT '鐢熶骇鎵规缂栧彿锛堜笌 UUID 浜岄�変竴锛�',
+    `task_name`      VARCHAR(128) DEFAULT NULL COMMENT '浠诲姟鍚嶇О锛堝睍绀虹敤锛�',
+    `status`         TINYINT      NOT NULL COMMENT '鐘舵�侊紙10 鎺掗槦涓� 20 鐢熸垚涓� 30 寰呬笅杞� 40 宸插彇娑� 50 宸插け璐� 60 宸蹭笅杞斤級',
+    `total_count`    INT          NOT NULL DEFAULT 0 COMMENT '鏍囩鎬诲紶鏁�',
+    `finished_count` INT          NOT NULL DEFAULT 0 COMMENT '宸茬敓鎴愬紶鏁�',
+    `file_path`      VARCHAR(512) DEFAULT NULL COMMENT '涓存椂 ZIP 缁濆璺緞锛堜负绌鸿〃绀烘湭钀界洏鎴栧凡鍒犻櫎锛�',
+    `file_size`      BIGINT       DEFAULT NULL COMMENT '涓存椂 ZIP 澶у皬锛堝瓧鑺傦級',
+    `error_msg`      VARCHAR(512) 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`),
+    KEY `idx_status` (`status`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE = InnoDB COMMENT = 'MES 浜岀淮鐮佸鍑轰换鍔�';
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 1e28b26..dc7c482 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
@@ -22,12 +22,12 @@
 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.framework.qrcode.MesProQrLabelBuilder;
 import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
-import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
+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.MesProBagCodeService;
 import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
+import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrLabelAssembler;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -42,11 +42,9 @@
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -56,7 +54,7 @@
 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BAG_CODE_NOT_EXISTS;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_PARAMS_INVALID;
-import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TOO_MANY;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TOO_MANY_FOR_SYNC;
 
 @Tag(name = "绠$悊鍚庡彴 - MES 琚嬬爜璧嬬爜锛堜竴琚嬩竴鐮侊級")
 @RestController
@@ -72,6 +70,10 @@
     private AdminUserApi adminUserApi;
     @Resource
     private MesMdmItemService mdmItemService;
+    @Resource
+    private MesProQrLabelAssembler labelAssembler;
+    @Resource
+    private MesProQrExportProperties qrExportProperties;
 
     @PostMapping("/generate")
     @Operation(summary = "鐢熸垚琚嬬爜鏁版嵁鍖咃紙涓�琚嬩竴鐮侊級锛岀疮璁¤揪鍒伴槇鍊兼椂鑷姩鐢熸垚鎵樼洏鐮�")
@@ -211,10 +213,8 @@
         if (CollUtil.isEmpty(list)) {
             throw exception(PRO_BAG_CODE_NOT_EXISTS);
         }
-        if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) {
-            throw exception(PRO_QR_EXPORT_TOO_MANY);
-        }
-        QrCodeImageUtils.writeZip(response, zipName, buildBagCodeQrLabels(list));
+        assertSyncExportAllowed(list.size());
+        QrCodeImageUtils.writeZip(response, zipName, labelAssembler.buildBagCodeLabels(list));
     }
 
     // ==================== 鎷兼帴 VO ====================
@@ -239,9 +239,9 @@
         else if (batchId != null) list = bagCodeService.getPrintableBagCodeListByBatchId(batchId);
         else throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
         if (CollUtil.isEmpty(list)) throw exception(PRO_BAG_CODE_NOT_EXISTS);
-        if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY);
-        List<QrLabelInfo> labels = list.stream().map(MesProQrLabelBuilder::buildBagBusinessCodeLabel).toList();
-        QrCodeImageUtils.writeZip(response, "琚嬬爜涓氬姟浜岀淮鐮�-" + (StrUtil.isNotBlank(uuid) ? uuid : batchId), labels);
+        assertSyncExportAllowed(list.size());
+        QrCodeImageUtils.writeZip(response, "琚嬬爜涓氬姟浜岀淮鐮�-" + (StrUtil.isNotBlank(uuid) ? uuid : batchId),
+                labelAssembler.buildBagBusinessLabels(list));
     }
 
     private List<MesProBagCodeRespVO> buildBagCodeRespVOList(List<MesProBagCodeDO> list) {
@@ -303,24 +303,15 @@
     }
 
     /**
-     * 鏋勫缓琚嬬爜浜岀淮鐮佹爣绛惧垪琛紙浜岀淮鐮佺紪鐮佺函鐮佸�硷紝鍥剧墖鍙犲姞鍏抽敭淇℃伅鏂囧瓧锛�
+     * 鍚屾瀵煎嚭鍙壙鎺ュ皬鎵归噺
+     * <p>
+     * 鏁伴噺瓒呰繃鍒嗙晫绾挎椂蹇呴』璧板悗鍙板鍑轰换鍔★細鍚屾鍝嶅簲浼氶殢寮犳暟绾挎�у彉闀匡紙涓婁竾寮犲彲杈炬暟鍒嗛挓銆佸搷搴斾綋涓� GB锛夛紝
+     * 蹇呯劧琚綉鍏虫垨娴忚鍣ㄦ埅鏂紝鐢ㄦ埛鐪嬪埌鐨勫皢鏄�屼笅杞藉け璐ャ�嶈�屼笉鏄彲鎿嶄綔鐨勬彁绀恒��
      */
-    private List<QrLabelInfo> buildBagCodeQrLabels(List<MesProBagCodeDO> list) {
-        // 1. 鑾峰緱鍏宠仈鏁版嵁锛堟壒娆°�佸搧绉嶃�佽矗浠讳汉锛夛紱鍝佺闆嗗悎鍚鐮佽嚜韬搧绉嶄笌鍘嗗彶鎵规鍝佺
-        Map<Long, MesProBatchDO> batchMap = convertMap(
-                batchService.getBatchList(convertSet(list, MesProBagCodeDO::getBatchId)), MesProBatchDO::getId);
-        Set<Long> itemIds = convertSet(list, MesProBagCodeDO::getItemId);
-        itemIds.addAll(convertSet(batchMap.values(), MesProBatchDO::getItemId));
-        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
-        // 2. 鏋勫缓鏍囩
-        List<QrLabelInfo> labels = new ArrayList<>(list.size());
-        for (MesProBagCodeDO bagCode : list) {
-            labels.add(MesProQrLabelBuilder.buildBagCodeLabel(
-                    bagCode, batchMap.get(bagCode.getBatchId()), itemMap, userMap));
+    private void assertSyncExportAllowed(int count) {
+        if (count > qrExportProperties.getSyncThreshold()) {
+            throw exception(PRO_QR_EXPORT_TOO_MANY_FOR_SYNC, count, qrExportProperties.getSyncThreshold());
         }
-        return labels;
     }
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
index bde13d7..a83a15b 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
@@ -23,13 +23,12 @@
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
-import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
 import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
-import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
 import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
 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.pallet.MesProPalletService;
+import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrLabelAssembler;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -44,7 +43,6 @@
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -58,13 +56,20 @@
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BATCH_NOT_EXISTS;
-import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TOO_MANY;
 
 @Tag(name = "绠$悊鍚庡彴 - MES 鐢熶骇鎵规")
 @RestController
 @RequestMapping("/mes/pro/batch")
 @Validated
 public class MesProBatchController {
+
+    /**
+     * 鎵规杩芥函鍝嶅簲鍐呭祵鐨勮鐮佸垪琛ㄤ笂闄�
+     * <p>
+     * 杩欐槸鍝嶅簲浣撶Н鐨勪繚鎶わ紝涓庝簩缁寸爜瀵煎嚭鏃犲叧锛氳鐮佹�婚噺鐢� bagTotal 杩斿洖锛屽垪琛ㄦ湰韬彧浣滃睍绀虹敤閫斻��
+     * 瀵煎嚭璺緞涓嶅啀鏈夋暟閲忎笂闄愩��
+     */
+    private static final int MAX_TRACE_BAG_LIST_SIZE = 2000;
 
     @Resource
     private MesProBatchService batchService;
@@ -76,6 +81,8 @@
     private MesProPalletService palletService;
     @Resource
     private MesProBagCodeService bagCodeService;
+    @Resource
+    private MesProQrLabelAssembler labelAssembler;
 
     @PostMapping("/create")
     @Operation(summary = "鍒涘缓鐢熶骇鎵规锛堜竴娆″綍鍏ヤ笟鍔′俊鎭紝鑷姩鐢熸垚鎵规鍙凤級")
@@ -200,11 +207,8 @@
             throw exception(PRO_BATCH_NOT_EXISTS);
         }
         List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId);
-        if (pallets.size() + 1 > QrCodeImageUtils.MAX_QR_EXPORT) {
-            throw exception(PRO_QR_EXPORT_TOO_MANY);
-        }
         String qrCode = StrUtil.blankToDefault(batch.getCode(), batch.getTempCode());
-        QrCodeImageUtils.writeZip(response, "鎵规浜岀淮鐮�-" + qrCode, buildBatchQrLabels(batch, pallets));
+        QrCodeImageUtils.writeZip(response, "鎵规浜岀淮鐮�-" + qrCode, labelAssembler.buildBatchLabels(batch, pallets));
     }
 
     // ==================== 鎷兼帴 VO ====================
@@ -229,11 +233,8 @@
             throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "宸蹭綔搴熸壒娆′笉鍙鍑轰笟鍔′簩缁寸爜");
         List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId).stream()
                 .filter(record -> !cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())).toList();
-        if (pallets.size() + 1 > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY);
-        List<QrLabelInfo> labels = new java.util.ArrayList<>();
-        labels.add(MesProQrLabelBuilder.buildBatchBusinessCodeLabel(batch));
-        pallets.forEach(pallet -> labels.add(MesProQrLabelBuilder.buildPalletBusinessCodeLabel(pallet)));
-        QrCodeImageUtils.writeZip(response, "鎵规涓氬姟浜岀淮鐮�-" + batch.getBusinessCode(), labels);
+        QrCodeImageUtils.writeZip(response, "鎵规涓氬姟浜岀淮鐮�-" + batch.getBusinessCode(),
+                labelAssembler.buildBatchBusinessLabels(batch, pallets));
     }
 
     private List<MesProBatchRespVO> buildBatchRespVOList(List<MesProBatchDO> list) {
@@ -282,12 +283,12 @@
         respVO.setWorkers(buildWorkerRespVOList(batchService.getBatchWorkerListByBatchId(batch.getId())));
         // 鎵樼洏鍒楄〃
         respVO.setPallets(BeanUtils.toBean(palletService.getPalletListByBatchId(batch.getId()), MesProPalletRespVO.class));
-        // 琚嬬爜鍒楄〃锛堝皝椤� 2000锛宐agTotal 璁板綍鎬绘暟锛�
+        // 琚嬬爜鍒楄〃锛堝皝椤� MAX_TRACE_BAG_LIST_SIZE锛宐agTotal 璁板綍鎬绘暟锛�
         List<MesProBagCodeDO> bags = bagCodeService.getBagCodeListByBatchId(batch.getId());
         respVO.setPendingBagQuantity((int) bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() == null).count());
         respVO.setBagTotal(bags.size());
-        if (bags.size() > QrCodeImageUtils.MAX_QR_EXPORT) {
-            bags = bags.subList(0, QrCodeImageUtils.MAX_QR_EXPORT);
+        if (bags.size() > MAX_TRACE_BAG_LIST_SIZE) {
+            bags = bags.subList(0, MAX_TRACE_BAG_LIST_SIZE);
         }
         respVO.setBags(buildBagCodeRespVOList(bags));
         return respVO;
@@ -303,21 +304,6 @@
         return BeanUtils.toBean(list, MesProBagCodeRespVO.class, vo ->
                 MapUtils.findAndThen(batchMap, vo.getBatchId(), batch ->
                         vo.setLineCode(batch.getLineCode()).setLineName(batch.getLineName())));
-    }
-
-    private List<QrLabelInfo> buildBatchQrLabels(MesProBatchDO batch, List<MesProPalletDO> pallets) {
-        // 鑾峰緱鍏宠仈鏁版嵁锛堝搧绉嶃�佽矗浠讳汉锛夆�斺�旀壒娆′笌鎵樼洏鍚屽睘涓�鎵癸紝鍏辩敤鍚屼竴浠�
-        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
-                convertSet(Collections.singletonList(batch), MesProBatchDO::getItemId));
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSet(Collections.singletonList(batch), MesProBatchDO::getOwnerUserId));
-        // 鏋勫缓鏍囩锛氭壒娆″浘 + 鍏ㄩ儴鎵樼洏鍥�
-        List<QrLabelInfo> labels = new ArrayList<>(pallets.size() + 1);
-        labels.add(MesProQrLabelBuilder.buildBatchLabel(batch, itemMap, userMap));
-        for (MesProPalletDO pallet : pallets) {
-            labels.add(MesProQrLabelBuilder.buildPalletLabel(pallet, batch, itemMap, userMap));
-        }
-        return labels;
     }
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java
index 3892fab..511b68e 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java
@@ -21,13 +21,12 @@
 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.framework.qrcode.MesProQrLabelBuilder;
 import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
-import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
 import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
 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.pallet.MesProPalletService;
+import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrLabelAssembler;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -41,7 +40,6 @@
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -53,7 +51,6 @@
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_PALLET_NOT_EXISTS;
-import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TOO_MANY;
 
 @Tag(name = "绠$悊鍚庡彴 - MES 鎵樼洏鐮�")
 @RestController
@@ -71,6 +68,8 @@
     private MesMdmItemService mdmItemService;
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private MesProQrLabelAssembler labelAssembler;
 
     @PostMapping("/batch-create")
     @Operation(summary = "鎵归噺鐢熸垚鎵樼洏鐮侊紙涓�娆″綍鍏ユ壒娆$敓鎴愭寮忔墭鐩樼爜锛�")
@@ -106,10 +105,7 @@
         if (CollUtil.isEmpty(list)) {
             throw exception(PRO_PALLET_NOT_EXISTS);
         }
-        if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) {
-            throw exception(PRO_QR_EXPORT_TOO_MANY);
-        }
-        QrCodeImageUtils.writeZip(response, "鎵樼洏鐮佷簩缁寸爜", buildPalletQrLabels(list));
+        QrCodeImageUtils.writeZip(response, "鎵樼洏鐮佷簩缁寸爜", labelAssembler.buildPalletLabels(list));
     }
 
     @DeleteMapping("/delete")
@@ -190,10 +186,7 @@
         if (CollUtil.isEmpty(list)) {
             throw exception(PRO_PALLET_NOT_EXISTS);
         }
-        if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) {
-            throw exception(PRO_QR_EXPORT_TOO_MANY);
-        }
-        QrCodeImageUtils.writeZip(response, "鎵樼洏鐮佷簩缁寸爜-" + batchId, buildPalletQrLabels(list));
+        QrCodeImageUtils.writeZip(response, "鎵樼洏鐮佷簩缁寸爜-" + batchId, labelAssembler.buildPalletLabels(list));
     }
 
     // ==================== 鎷兼帴 VO ====================
@@ -215,9 +208,7 @@
         List<MesProPalletDO> list = palletService.getPalletListByIds(reqVO.getIds()).stream()
                 .filter(record -> !cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())).toList();
         if (CollUtil.isEmpty(list)) throw exception(PRO_PALLET_NOT_EXISTS);
-        if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY);
-        QrCodeImageUtils.writeZip(response, "鎵樼洏涓氬姟浜岀淮鐮�", list.stream()
-                .map(MesProQrLabelBuilder::buildPalletBusinessCodeLabel).toList());
+        QrCodeImageUtils.writeZip(response, "鎵樼洏涓氬姟浜岀淮鐮�", labelAssembler.buildPalletBusinessLabels(list));
     }
 
     private List<MesProPalletRespVO> buildPalletRespVOList(List<MesProPalletDO> list) {
@@ -275,23 +266,6 @@
         return BeanUtils.toBean(list, MesProBagCodeRespVO.class, vo ->
                 MapUtils.findAndThen(batchMap, vo.getBatchId(), batch ->
                         vo.setLineCode(batch.getLineCode()).setLineName(batch.getLineName())));
-    }
-
-    private List<QrLabelInfo> buildPalletQrLabels(List<MesProPalletDO> list) {
-        // 鑾峰緱鍏宠仈鏁版嵁锛堟壒娆°�佸搧绉嶃�佽矗浠讳汉锛�
-        Map<Long, MesProBatchDO> batchMap = convertMap(
-                batchService.getBatchList(convertSet(list, MesProPalletDO::getBatchId)), MesProBatchDO::getId);
-        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
-                convertSet(batchMap.values(), MesProBatchDO::getItemId));
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
-        // 鏋勫缓鏍囩
-        List<QrLabelInfo> labels = new ArrayList<>(list.size());
-        for (MesProPalletDO pallet : list) {
-            labels.add(MesProQrLabelBuilder.buildPalletLabel(
-                    pallet, batchMap.get(pallet.getBatchId()), itemMap, userMap));
-        }
-        return labels;
     }
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java
new file mode 100644
index 0000000..bb0f151
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java
@@ -0,0 +1,178 @@
+package cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo.MesProQrExportCountRespVO;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo.MesProQrExportTaskRespVO;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo.MesProQrExportTaskSubmitReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.qrexport.MesProQrExportTaskDO;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportBizTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportProperties;
+import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrExportTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_PARAMS_INVALID;
+
+/**
+ * 浜岀淮鐮佸ぇ鎵归噺瀵煎嚭浠诲姟鎺ュ彛
+ * <p>
+ * 鏁伴噺涓嶈秴杩囧垎鐣岀嚎鐨勫鍑轰笉璧拌繖閲岋紙璋冪敤鏂圭洿鎺ヨ皟鍘熶笅杞芥帴鍙o紝绉掔骇杩斿洖銆佷笉鎺掗槦锛夛紱
+ * 瓒呰繃鍒嗙晫绾挎墠鎻愪氦浠诲姟銆佽疆璇㈣繘搴︺�佸畬鎴愬悗涓嬭浇銆�
+ * <p>
+ * 璇存槑锛氫笅杞戒笌鍙栨秷鏈澶栭檺鍒躲�屽彧鑳芥搷浣滆嚜宸卞垱寤虹殑浠诲姟銆嶃�傝兘杩涘叆鏈帴鍙g殑鐢ㄦ埛宸叉寔鏈夊搴旀ā鍧楃殑瀵煎嚭鏉冮檺锛�
+ * 涔熷氨宸茬粡鍙互鐩存帴璋冪敤鍚屾瀵煎嚭鎺ュ彛鎷垮埌鍚屾牱鐨� ZIP 鍐呭锛屽啀鍔犱竴灞傚垱寤轰汉鏍¢獙骞朵笉鑳芥彁渚涘疄闄呬繚鎶ゃ��
+ */
+@Tag(name = "绠$悊鍚庡彴 - MES 浜岀淮鐮佸鍑轰换鍔�")
+@RestController
+@RequestMapping("/mes/pro/qr-export")
+@Validated
+public class MesProQrExportTaskController {
+
+    /**
+     * 浠讳竴妯″潡鐨勫鍑烘潈闄愬嵆鍙闂湰鎺ュ彛锛涘叿浣撲笟鍔$被鍨嬪啀閫愰」鏍¢獙
+     */
+    private static final String ANY_EXPORT_PERMISSION =
+            "@ss.hasAnyPermissions('mes:pro-bag-code:export','mes:pro-pallet:export','mes:pro-batch:export')";
+
+    @Resource
+    private MesProQrExportTaskService taskService;
+    @Resource
+    private MesProQrExportProperties properties;
+    @Resource
+    private SecurityFrameworkService securityFrameworkService;
+
+    @GetMapping("/count")
+    @Operation(summary = "缁熻浜岀淮鐮佸鍑哄紶鏁板苟缁欏嚭瀵煎嚭鏂瑰紡锛圫YNC 鐩存帴涓嬭浇 / ASYNC 鍚庡彴浠诲姟锛�")
+    @PreAuthorize(ANY_EXPORT_PERMISSION)
+    public CommonResult<MesProQrExportCountRespVO> count(@RequestParam("bizType") String bizType,
+                                                         @RequestParam(value = "uuid", required = false) String uuid,
+                                                         @RequestParam(value = "batchId", required = false) Long batchId) {
+        MesProQrExportBizTypeEnum bizTypeEnum = resolveBizType(bizType);
+        assertBizPermission(bizTypeEnum);
+        long total = taskService.countLabels(bizTypeEnum, uuid, batchId);
+        MesProQrExportCountRespVO respVO = new MesProQrExportCountRespVO();
+        respVO.setTotalCount(total);
+        respVO.setSyncThreshold(properties.getSyncThreshold());
+        respVO.setMode(total > properties.getSyncThreshold()
+                ? MesProQrExportCountRespVO.MODE_ASYNC : MesProQrExportCountRespVO.MODE_SYNC);
+        return success(respVO);
+    }
+
+    @PostMapping("/task")
+    @Operation(summary = "鎻愪氦浜岀淮鐮佸悗鍙板鍑轰换鍔★紙澶ф壒閲忥級")
+    @PreAuthorize(ANY_EXPORT_PERMISSION)
+    public CommonResult<MesProQrExportTaskRespVO> submit(@Valid @RequestBody MesProQrExportTaskSubmitReqVO reqVO) {
+        MesProQrExportBizTypeEnum bizTypeEnum = resolveBizType(reqVO.getBizType());
+        assertBizPermission(bizTypeEnum);
+        MesProQrExportTaskDO task = taskService.submit(bizTypeEnum, reqVO.getUuid(), reqVO.getBatchId());
+        return success(convert(task));
+    }
+
+    @GetMapping("/task/get")
+    @Operation(summary = "鑾峰緱浜岀淮鐮佸鍑轰换鍔★紙鐢ㄤ簬杞杩涘害锛�")
+    @Parameter(name = "id", description = "浠诲姟缂栧彿", required = true, example = "1024")
+    @PreAuthorize(ANY_EXPORT_PERMISSION)
+    public CommonResult<MesProQrExportTaskRespVO> get(@RequestParam("id") Long id) {
+        return success(convert(taskService.getTask(id)));
+    }
+
+    @PostMapping("/task/cancel")
+    @Operation(summary = "鍙栨秷浜岀淮鐮佸鍑轰换鍔�")
+    @Parameter(name = "id", description = "浠诲姟缂栧彿", required = true, example = "1024")
+    @PreAuthorize(ANY_EXPORT_PERMISSION)
+    public CommonResult<Boolean> cancel(@RequestParam("id") Long id) {
+        taskService.cancel(id);
+        return success(true);
+    }
+
+    @GetMapping("/task/download")
+    @Operation(summary = "涓嬭浇浜岀淮鐮佸鍑轰骇鐗╋紙涓嬭浇瀹屾垚鍚庣郴缁熺珛鍗冲垹闄や复鏃舵枃浠讹級")
+    @Parameter(name = "id", description = "浠诲姟缂栧彿", required = true, example = "1024")
+    @PreAuthorize(ANY_EXPORT_PERMISSION)
+    public void download(@RequestParam("id") Long id, HttpServletResponse response) throws IOException {
+        taskService.download(id, response);
+    }
+
+    // ==================== 绉佹湁鏂规硶 ====================
+
+    private MesProQrExportBizTypeEnum resolveBizType(String bizType) {
+        for (MesProQrExportBizTypeEnum value : MesProQrExportBizTypeEnum.values()) {
+            if (value.getType().equals(bizType)) {
+                return value;
+            }
+        }
+        throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
+    }
+
+    /**
+     * 鏍¢獙涓氬姟绫诲瀷瀵瑰簲鐨勬ā鍧楀鍑烘潈闄�
+     * <p>
+     * 鍗曢潬銆屼换涓�瀵煎嚭鏉冮檺銆嶄細璁╁彧鏈夋墭鐩樺鍑烘潈闄愮殑鐢ㄦ埛涔熻兘瀵煎嚭琚嬬爜锛屾晠鎸夌被鍨嬪啀鏍¢獙涓�娆°��
+     */
+    private void assertBizPermission(MesProQrExportBizTypeEnum bizType) {
+        String permission = switch (bizType) {
+            case BAG_LABEL, BAG_BUSINESS -> "mes:pro-bag-code:export";
+            case PALLET_LABEL, PALLET_BUSINESS -> "mes:pro-pallet:export";
+            case BATCH_LABEL, BATCH_BUSINESS -> "mes:pro-batch:export";
+        };
+        if (!securityFrameworkService.hasAnyPermissions(permission)) {
+            throw exception(FORBIDDEN);
+        }
+    }
+
+    private MesProQrExportTaskRespVO convert(MesProQrExportTaskDO task) {
+        MesProQrExportTaskRespVO respVO = new MesProQrExportTaskRespVO();
+        respVO.setId(task.getId());
+        respVO.setBizType(task.getBizType());
+        respVO.setTaskName(task.getTaskName());
+        respVO.setStatus(task.getStatus());
+        MesProQrExportTaskStatusEnum statusEnum = parseStatus(task.getStatus());
+        respVO.setStatusName(statusEnum == null ? null : statusEnum.getName());
+        respVO.setTotalCount(task.getTotalCount());
+        respVO.setFinishedCount(task.getFinishedCount());
+        respVO.setProgress(calcProgress(task));
+        respVO.setFileSize(task.getFileSize());
+        respVO.setErrorMsg(task.getErrorMsg());
+        respVO.setDownloadable(MesProQrExportTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())
+                && task.getFilePath() != null && !task.getFilePath().isBlank());
+        respVO.setCreateTime(task.getCreateTime());
+        respVO.setFinishTime(task.getFinishTime());
+        return respVO;
+    }
+
+    private Integer calcProgress(MesProQrExportTaskDO 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);
+    }
+
+    private MesProQrExportTaskStatusEnum parseStatus(Integer status) {
+        if (status == null) {
+            return null;
+        }
+        for (MesProQrExportTaskStatusEnum value : MesProQrExportTaskStatusEnum.values()) {
+            if (value.getStatus().equals(status)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportCountRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportCountRespVO.java
new file mode 100644
index 0000000..ed79bfb
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportCountRespVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 浜岀淮鐮佸鍑烘暟閲忎笌鏂瑰紡 Response VO")
+@Data
+public class MesProQrExportCountRespVO {
+
+    /**
+     * 鍚屾鐩存帴涓嬭浇锛氭暟閲忎笉瓒呰繃鍒嗙晫绾匡紝璋冪敤鏂规寜鍘熸湁鐨勪笅杞芥帴鍙e彇 ZIP 鍗冲彲锛堢绾ц繑鍥烇紝涓嶆帓闃燂級
+     */
+    public static final String MODE_SYNC = "SYNC";
+
+    /**
+     * 鍚庡彴浠诲姟锛氭暟閲忚秴杩囧垎鐣岀嚎锛岄渶鎻愪氦浠诲姟鍚庤疆璇㈣繘搴︺�佸畬鎴愬悗涓嬭浇
+     */
+    public static final String MODE_ASYNC = "ASYNC";
+
+    @Schema(description = "寰呭鍑烘爣绛惧紶鏁�", requiredMode = Schema.RequiredMode.REQUIRED, example = "52872")
+    private Long totalCount;
+
+    @Schema(description = "鍚屾/鍚庡彴鐨勫垎鐣岀嚎锛堝紶锛�", example = "2000")
+    private Integer syncThreshold;
+
+    @Schema(description = "瀵煎嚭鏂瑰紡锛圫YNC 鍚屾鐩存帴涓嬭浇 / ASYNC 鍚庡彴浠诲姟锛�", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "ASYNC")
+    private String mode;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskRespVO.java
new file mode 100644
index 0000000..c11c33f
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskRespVO.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 浜岀淮鐮佸鍑轰换鍔� Response VO")
+@Data
+public class MesProQrExportTaskRespVO {
+
+    @Schema(description = "浠诲姟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "涓氬姟绫诲瀷", example = "BAG_LABEL")
+    private String bizType;
+
+    @Schema(description = "浠诲姟鍚嶇О", example = "琚嬬爜浜岀淮鐮佹爣绛�-20260818")
+    private String taskName;
+
+    @Schema(description = "鐘舵�侊紙10 鎺掗槦涓� 20 鐢熸垚涓� 30 寰呬笅杞� 40 宸插彇娑� 50 宸插け璐� 60 宸蹭笅杞斤級", example = "20")
+    private Integer status;
+
+    @Schema(description = "鐘舵�佸悕绉�", example = "鐢熸垚涓�")
+    private String statusName;
+
+    @Schema(description = "鏍囩鎬诲紶鏁�", example = "52872")
+    private Integer totalCount;
+
+    @Schema(description = "宸茬敓鎴愬紶鏁�", example = "12000")
+    private Integer finishedCount;
+
+    @Schema(description = "杩涘害鐧惧垎姣旓紙0~100锛�", example = "23")
+    private Integer progress;
+
+    @Schema(description = "ZIP 鏂囦欢澶у皬锛堝瓧鑺傦級", example = "1536000")
+    private Long fileSize;
+
+    @Schema(description = "澶辫触鍘熷洜")
+    private String errorMsg;
+
+    @Schema(description = "鏄惁鍙笅杞斤紙宸茬敓鎴愬畬鎴愪笖鏂囦欢浠嶅瓨鍦級", example = "true")
+    private Boolean downloadable;
+
+    @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/qrexport/vo/MesProQrExportTaskSubmitReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskSubmitReqVO.java
new file mode 100644
index 0000000..ad91fe3
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/vo/MesProQrExportTaskSubmitReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.mes.controller.admin.pro.qrexport.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 浜岀淮鐮佸鍑轰换鍔℃彁浜� Request VO")
+@Data
+public class MesProQrExportTaskSubmitReqVO {
+
+    @Schema(description = "涓氬姟绫诲瀷锛圔AG_LABEL/BAG_BUSINESS/PALLET_LABEL/PALLET_BUSINESS/BATCH_LABEL/BATCH_BUSINESS锛�",
+            requiredMode = Schema.RequiredMode.REQUIRED, example = "BAG_LABEL")
+    @NotEmpty(message = "涓氬姟绫诲瀷涓嶈兘涓虹┖")
+    private String bizType;
+
+    @Schema(description = "鐢熸垚缁� UUID锛堜笌鐢熶骇鎵规缂栧彿浜岄�変竴锛�", example = "20260818A01")
+    private String uuid;
+
+    @Schema(description = "鐢熶骇鎵规缂栧彿锛堜笌鐢熸垚缁� UUID 浜岄�変竴锛�", example = "1024")
+    private Long batchId;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/qrexport/MesProQrExportTaskDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/qrexport/MesProQrExportTaskDO.java
new file mode 100644
index 0000000..3306057
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/qrexport/MesProQrExportTaskDO.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.pro.qrexport;
+
+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>
+ * 澶ф壒閲忎簩缁寸爜瀵煎嚭涓嶅湪璇锋眰绾跨▼鍐呭畬鎴愶紝鑰屾槸钀芥垚浠诲姟鍚庣敱鍚庡彴绾跨▼娴佸紡鐢熸垚 ZIP锛�
+ * 鍓嶇鎹� {@link #finishedCount}/{@link #totalCount} 灞曠ず鐪熷疄杩涘害锛屽苟鏀寔涓�斿彇娑堛��
+ */
+@TableName("mes_pro_qr_export_task")
+@KeySequence("mes_pro_qr_export_task_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MesProQrExportTaskDO extends BaseDO {
+
+    @TableId
+    private Long id;
+
+    /**
+     * 涓氬姟绫诲瀷锛岃 {@link cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportBizTypeEnum}
+     */
+    private String bizType;
+
+    /**
+     * 鐢熸垚缁� UUID锛堜笌 batchId 浜岄�変竴锛�
+     */
+    private String uuid;
+
+    /**
+     * 鐢熶骇鎵规缂栧彿锛堜笌 uuid 浜岄�変竴锛�
+     */
+    private Long batchId;
+
+    /**
+     * 浠诲姟鍚嶇О锛堝睍绀虹敤锛屽舰濡傘�岃鐮佷簩缁寸爜鏍囩-PC20260818L01001銆嶏級
+     */
+    private String taskName;
+
+    /**
+     * 鐘舵�侊紝瑙� {@link cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportTaskStatusEnum}
+     */
+    private Integer status;
+
+    /**
+     * 鏍囩鎬诲紶鏁�
+     */
+    private Integer totalCount;
+
+    /**
+     * 宸茬敓鎴愬紶鏁�
+     */
+    private Integer finishedCount;
+
+    /**
+     * 涓存椂 ZIP 鏂囦欢缁濆璺緞锛涗负绌鸿〃绀哄皻鏈惤鐩樻垨宸插垹闄�
+     */
+    private String filePath;
+
+    /**
+     * 涓存椂 ZIP 鏂囦欢澶у皬锛堝瓧鑺傦級
+     */
+    private Long fileSize;
+
+    /**
+     * 澶辫触鍘熷洜
+     */
+    private String errorMsg;
+
+    /**
+     * 瀹屾垚锛堝畬鎴�/鍙栨秷/澶辫触锛夋椂闂�
+     */
+    private LocalDateTime finishTime;
+
+}
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 6830e19..bc35f78 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
@@ -108,6 +108,58 @@
                 .orderByAsc(MesProBagCodeDO::getBagNo));
     }
 
+    /**
+     * 缁熻鐢熸垚缁勪笅鍙嵃鍒风殑琚嬬爜鏁伴噺锛堜緵瀵煎嚭鍒嗘祦鍒ゆ柇锛岄伩鍏嶄负浜嗘嬁鏁伴噺鑰屽叏閲忓姞杞斤級
+     */
+    default Long selectPrintableCountByUuid(String uuid) {
+        return selectCount(new LambdaQueryWrapperX<MesProBagCodeDO>()
+                .eq(MesProBagCodeDO::getUuid, uuid)
+                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus()));
+    }
+
+    /**
+     * 缁熻鎵规涓嬪彲鍗板埛鐨勮鐮佹暟閲忥紙渚涘鍑哄垎娴佸垽鏂級
+     */
+    default Long selectPrintableCountByBatchId(Long batchId) {
+        return selectCount(new LambdaQueryWrapperX<MesProBagCodeDO>()
+                .eq(MesProBagCodeDO::getBatchId, batchId)
+                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus()));
+    }
+
+    /**
+     * 鍒嗛〉鏌ヨ鐢熸垚缁勪笅鍙嵃鍒风殑琚嬬爜锛堝悗鍙板鍑轰换鍔$敤锛岄�愰〉鍙栦互鎺у埗鍐呭瓨锛�
+     * <p>
+     * 鎺掑簭涓庡叏閲忔煡璇繚鎸佷竴鑷达紙鎸夎搴忓彿鍗囧簭锛夛紝淇濊瘉 ZIP 鍐呯殑鏍囩椤哄簭涓嶅彉銆�
+     */
+    default List<MesProBagCodeDO> selectPrintablePageByUuid(String uuid, int pageNo, int pageSize) {
+        return selectPageByWrapper(new LambdaQueryWrapperX<MesProBagCodeDO>()
+                .eq(MesProBagCodeDO::getUuid, uuid)
+                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus())
+                .orderByAsc(MesProBagCodeDO::getBagNo), pageNo, pageSize);
+    }
+
+    /**
+     * 鍒嗛〉鏌ヨ鎵规涓嬪彲鍗板埛鐨勮鐮侊紙鍚庡彴瀵煎嚭浠诲姟鐢級
+     */
+    default List<MesProBagCodeDO> selectPrintablePageByBatchId(Long batchId, int pageNo, int pageSize) {
+        return selectPageByWrapper(new LambdaQueryWrapperX<MesProBagCodeDO>()
+                .eq(MesProBagCodeDO::getBatchId, batchId)
+                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus())
+                .orderByAsc(MesProBagCodeDO::getBagNo), pageNo, pageSize);
+    }
+
+    /**
+     * 鎸夋潯浠跺垎椤垫煡璇紝涓嶆墽琛� count 鏌ヨ锛堣皟鐢ㄦ柟閫氬父宸茬煡鎬绘暟锛�
+     */
+    private List<MesProBagCodeDO> selectPageByWrapper(
+            com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProBagCodeDO> wrapper,
+            int pageNo, int pageSize) {
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<MesProBagCodeDO> page =
+                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNo, pageSize);
+        page.setSearchCount(false);
+        return selectPage(page, wrapper).getRecords();
+    }
+
     default Long selectCountByBatchId(Long batchId) {
         return selectCount(MesProBagCodeDO::getBatchId, batchId);
     }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/qrexport/MesProQrExportTaskMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/qrexport/MesProQrExportTaskMapper.java
new file mode 100644
index 0000000..e654020
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/qrexport/MesProQrExportTaskMapper.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.pro.qrexport;
+
+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.qrexport.MesProQrExportTaskDO;
+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 MesProQrExportTaskMapper extends BaseMapperX<MesProQrExportTaskDO> {
+
+    default List<MesProQrExportTaskDO> selectListByStatus(Integer status) {
+        return selectList(MesProQrExportTaskDO::getStatus, status);
+    }
+
+    default List<MesProQrExportTaskDO> selectListByStatusIn(Collection<Integer> statuses) {
+        return selectList(new LambdaQueryWrapperX<MesProQrExportTaskDO>()
+                .in(MesProQrExportTaskDO::getStatus, statuses)
+                .orderByAsc(MesProQrExportTaskDO::getId));
+    }
+
+    default Long selectCountByStatusIn(Collection<Integer> statuses) {
+        return selectCount(new LambdaQueryWrapperX<MesProQrExportTaskDO>()
+                .in(MesProQrExportTaskDO::getStatus, statuses));
+    }
+
+    /**
+     * 鏌ヨ宸茬粓鎬佷笖瀹屾垚鏃堕棿鏃╀簬 deadline 鐨勪换鍔★紝鐢ㄤ簬娓呯悊涓存椂鏂囦欢涓庝换鍔¤褰�
+     */
+    default List<MesProQrExportTaskDO> selectExpiredFinalList(Collection<Integer> finalStatuses,
+                                                              LocalDateTime deadline) {
+        return selectList(new LambdaQueryWrapperX<MesProQrExportTaskDO>()
+                .in(MesProQrExportTaskDO::getStatus, finalStatuses)
+                .and(wrapper -> wrapper.isNull(MesProQrExportTaskDO::getFinishTime)
+                        .or().lt(MesProQrExportTaskDO::getFinishTime, deadline)));
+    }
+
+    /**
+     * 鏌ヨ鎵�鏈夐潪缁堟�佷换鍔★紝鐢ㄤ簬鍥炴敹杩涚▼閲嶅惎鍚庢畫鐣欑殑璁板綍
+     * <p>
+     * 涓嶆寜鏃堕棿杩囨护锛氭槸鍚︿粛鍦ㄦ墽琛岀敱杩愯鎬佹敞鍐岃〃鍒ゅ畾锛岃〃閲屾煡鍑烘潵鐨勯潪缁堟�佽褰曡嫢涓嶅湪娉ㄥ唽琛ㄤ腑锛�
+     * 灏变竴瀹氭病鏈夋墽琛岀嚎绋嬪湪鎺ㄨ繘瀹冦��
+     */
+    default List<MesProQrExportTaskDO> selectUnfinishedList(Collection<Integer> statuses) {
+        return selectListByStatusIn(statuses);
+    }
+
+    /**
+     * 浠呮洿鏂拌繘搴︼紝閬垮厤鎶婃暣琛屽瓧娈甸兘鍐欏洖锛堢敓鎴愯繃绋嬩腑楂橀璋冪敤锛�
+     */
+    default int updateProgress(Long id, Integer finishedCount) {
+        return update(null, new LambdaUpdateWrapper<MesProQrExportTaskDO>()
+                .eq(MesProQrExportTaskDO::getId, id)
+                .set(MesProQrExportTaskDO::getFinishedCount, finishedCount));
+    }
+
+    /**
+     * 鏇存柊浠诲姟缁堟�侊紱null 瀛楁涓嶈鐩�
+     */
+    default int updateStatus(Long id, Integer status, String filePath, Long fileSize,
+                             String errorMsg, LocalDateTime finishTime) {
+        return update(null, new LambdaUpdateWrapper<MesProQrExportTaskDO>()
+                .eq(MesProQrExportTaskDO::getId, id)
+                .set(MesProQrExportTaskDO::getStatus, status)
+                .set(filePath != null, MesProQrExportTaskDO::getFilePath, filePath)
+                .set(fileSize != null, MesProQrExportTaskDO::getFileSize, fileSize)
+                .set(errorMsg != null, MesProQrExportTaskDO::getErrorMsg, errorMsg)
+                .set(finishTime != null, MesProQrExportTaskDO::getFinishTime, finishTime));
+    }
+
+    /**
+     * 娓呯┖涓存椂鏂囦欢璺緞锛堟枃浠跺凡鍒犻櫎锛屼繚鐣欎换鍔¤褰曠敤浜庡睍绀哄巻鍙诧級
+     */
+    default int clearFilePath(Long id) {
+        return update(null, new LambdaUpdateWrapper<MesProQrExportTaskDO>()
+                .eq(MesProQrExportTaskDO::getId, id)
+                .set(MesProQrExportTaskDO::getFilePath, null)
+                .set(MesProQrExportTaskDO::getFileSize, null));
+    }
+
+}
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 728ef9f..de13be9 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
@@ -838,11 +838,16 @@
     ErrorCode PRO_BATCH_STATUS_INVALID = new ErrorCode(1_040_750_004, "鐢熶骇鎵规鐘舵�佹祦杞笉鍚堟硶");
     ErrorCode PRO_BATCH_WORKER_EMPTY = new ErrorCode(1_040_750_005, "鐢熶骇鎵规蹇呴』閰嶇疆鑷冲皯涓�鍚嶇敓浜т汉鍛�");
     ErrorCode PRO_BATCH_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_040_750_006, "鍙湁鑽夌鎴栧凡浣滃簾鐨勭敓浜ф壒娆℃墠鍏佽鍒犻櫎");
-    ErrorCode PRO_QR_EXPORT_TOO_MANY = new ErrorCode(1_040_750_007, "浜岀淮鐮佹壒閲忓鍑烘暟閲忚秴杩囦笂闄�");
     ErrorCode PRO_QR_IMAGE_GENERATE_ERROR = new ErrorCode(1_040_750_008, "浜岀淮鐮佸浘鐗囩敓鎴愬け璐�");
     ErrorCode PRO_QR_EXPORT_PARAMS_INVALID = new ErrorCode(1_040_750_009, "浜岀淮鐮佹壒閲忓鍑虹己灏戞煡璇㈡潯浠�");
     ErrorCode PRO_BATCH_ALREADY_FILLED = new ErrorCode(1_040_750_012, "鐢熶骇鎵规宸茶ˉ褰曟寮忎俊鎭紝涓嶅厑璁搁噸澶嶈ˉ褰�");
     ErrorCode PRO_BATCH_NOT_FILLED = new ErrorCode(1_040_750_013, "鐢熶骇鎵规灏氭湭琛ュ綍姝e紡淇℃伅");
+    ErrorCode PRO_QR_EXPORT_TASK_NOT_EXISTS = new ErrorCode(1_040_750_010, "浜岀淮鐮佸鍑轰换鍔′笉瀛樺湪");
+    ErrorCode PRO_QR_EXPORT_TASK_BUSY = new ErrorCode(1_040_750_011, "宸叉湁浜岀淮鐮佸鍑轰换鍔℃鍦ㄨ繘琛岋紙{}%锛夛紝璇风瓑寰呭叾瀹屾垚鎴栧彇娑堝悗鍐嶈瘯");
+    ErrorCode PRO_QR_EXPORT_SERVER_BUSY = new ErrorCode(1_040_750_014, "鏈嶅姟鍣� CPU 璐熻浇杩囬珮锛堝綋鍓� {}%锛岄槇鍊� {}%锛夛紝宸叉殏鍋滃ぇ鎵归噺瀵煎嚭锛岃绋嶅悗鍐嶈瘯");
+    ErrorCode PRO_QR_EXPORT_TOO_MANY_FOR_SYNC = new ErrorCode(1_040_750_015, "鏈闇�瀵煎嚭 {} 寮狅紝瓒呰繃鍚屾瀵煎嚭涓婇檺 {} 寮狅紝璇锋敼鐢ㄥ悗鍙板鍑轰换鍔�");
+    ErrorCode PRO_QR_EXPORT_TASK_FILE_MISSING = new ErrorCode(1_040_750_016, "瀵煎嚭鏂囦欢涓嶅瓨鍦ㄦ垨宸茶鍒犻櫎锛岃閲嶆柊瀵煎嚭");
+    ErrorCode PRO_QR_EXPORT_TASK_STATUS_INVALID = new ErrorCode(1_040_750_017, "褰撳墠瀵煎嚭浠诲姟鐘舵�佷笉鍏佽璇ユ搷浣�");
 
     // ========== MES 鐢熶骇绠$悊-琚嬬爜璧嬬爜锛�1-040-752-000锛� ==========
     ErrorCode PRO_BAG_CODE_NOT_EXISTS = new ErrorCode(1_040_752_000, "琚嬬爜涓嶅瓨鍦�");
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportBizTypeEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportBizTypeEnum.java
new file mode 100644
index 0000000..e6969a8
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportBizTypeEnum.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 浜岀淮鐮佸鍑轰笟鍔$被鍨嬫灇涓�
+ * <p>
+ * 瀵煎嚭鍙傛暟缁熶竴涓恒�岀敓鎴愮粍 UUID銆嶆垨銆岀敓浜ф壒娆$紪鍙枫�嶄簩閫変竴锛屾晠涓嶅尯鍒嗗叿浣撳叆鍙f潵婧愩��
+ */
+@Getter
+@AllArgsConstructor
+public enum MesProQrExportBizTypeEnum implements ArrayValuable<String> {
+
+    BAG_LABEL("BAG_LABEL", "琚嬬爜浜岀淮鐮佹爣绛�"),
+    BAG_BUSINESS("BAG_BUSINESS", "琚嬬爜涓氬姟浜岀淮鐮�"),
+    PALLET_LABEL("PALLET_LABEL", "鎵樼洏浜岀淮鐮佹爣绛�"),
+    PALLET_BUSINESS("PALLET_BUSINESS", "鎵樼洏涓氬姟浜岀淮鐮�"),
+    BATCH_LABEL("BATCH_LABEL", "鎵规浜岀淮鐮佹爣绛�"),
+    BATCH_BUSINESS("BATCH_BUSINESS", "鎵规涓氬姟浜岀淮鐮�");
+
+    public static final String[] ARRAYS = Arrays.stream(values())
+            .map(MesProQrExportBizTypeEnum::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/MesProQrExportTaskStatusEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportTaskStatusEnum.java
new file mode 100644
index 0000000..fc83758
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProQrExportTaskStatusEnum.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 MesProQrExportTaskStatusEnum implements ArrayValuable<Integer> {
+
+    /**
+     * 宸插垱寤哄緟鎵ц锛堝彈骞跺彂闂搁棬闄愬埗鏃剁煭鏆傚仠鐣欒鐘舵�侊級
+     */
+    PENDING(10, "鎺掗槦涓�"),
+    /**
+     * 鐢熸垚涓�
+     */
+    RUNNING(20, "鐢熸垚涓�"),
+    /**
+     * 鐢熸垚瀹屾垚锛岀瓑寰呬笅杞�
+     */
+    FINISHED(30, "寰呬笅杞�"),
+    /**
+     * 鐢ㄦ埛鍙栨秷
+     */
+    CANCELED(40, "宸插彇娑�"),
+    /**
+     * 鐢熸垚澶辫触
+     */
+    FAILED(50, "宸插け璐�"),
+    /**
+     * 宸蹭笅杞藉苟鍒犻櫎涓存椂鏂囦欢锛堢粓鎬侊級
+     */
+    DOWNLOADED(60, "宸蹭笅杞�");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values())
+            .map(MesProQrExportTaskStatusEnum::getStatus).toArray(Integer[]::new);
+
+    /**
+     * 鐘舵��
+     */
+    private final Integer status;
+    /**
+     * 鐘舵�佸悕绉�
+     */
+    private final String name;
+
+    /**
+     * 鏄惁涓虹粓鎬侊紙涓嶅啀鍙樺寲锛屽彲琚竻鐞嗭級
+     */
+    public boolean isFinal() {
+        return this == FINISHED || this == CANCELED || this == FAILED || this == DOWNLOADED;
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
index c35c287..93bb1d1 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
@@ -33,9 +33,6 @@
     private QrCodeImageUtils() {
     }
 
-    /** 鎵归噺瀵煎嚭浜岀淮鐮佹暟閲忎笂闄愶紙闃叉涓�娆℃�х敓鎴愯繃澶氬鑷村唴瀛�/鍝嶅簲杩囧ぇ锛� */
-    public static final int MAX_QR_EXPORT = 2000;
-
     /** 浜岀淮鐮佽竟闀� */
     private static final int QR_SIZE = 260;
     /** 鍥剧墖鍥涘懆鐣欑櫧 */
@@ -120,10 +117,7 @@
         ZipOutputStream zip = new ZipOutputStream(response.getOutputStream());
         try {
             for (QrLabelInfo label : labels) {
-                BufferedImage image = createQrLabel(label.getCode(), label.getFileName());
-                zip.putNextEntry(new ZipEntry(label.getFileName() + ".png"));
-                ImageIO.write(image, "png", zip);
-                zip.closeEntry();
+                writeZipEntry(zip, label);
             }
             zip.finish();
             zip.flush();
@@ -132,6 +126,22 @@
         }
     }
 
+    /**
+     * 鐢熸垚鍗曞紶鏍囩骞跺啓鍏� ZIP 鏉$洰
+     * <p>
+     * 渚涘悗鍙板鍑轰换鍔¤嚜寤哄啓鍏ュ惊鐜椂澶嶇敤锛氳皟鐢ㄦ柟闇�瑕佽嚜琛屽湪姣忓紶涔嬮棿妫�鏌ュ彇娑堛�侀噰鏍� CPU 骞跺洖鍐欒繘搴︼紝
+     * 鍥犳杩欓噷鍙礋璐�"涓�寮�"鐨勫鐞嗭紝涓嶆帴绠″惊鐜��
+     *
+     * @param zip   ZIP 杈撳嚭娴�
+     * @param label 鏍囩淇℃伅
+     */
+    public static void writeZipEntry(ZipOutputStream zip, QrLabelInfo label) throws IOException {
+        BufferedImage image = createQrLabel(label.getCode(), label.getFileName());
+        zip.putNextEntry(new ZipEntry(label.getFileName() + ".png"));
+        ImageIO.write(image, "png", zip);
+        zip.closeEntry();
+    }
+
     // ==================== 绉佹湁鏂规硶 ====================
 
     /**
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportExecutorConfig.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportExecutorConfig.java
new file mode 100644
index 0000000..3d63db8
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportExecutorConfig.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.mes.framework.qrexport;
+
+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>
+ * 鍗曠嫭寤烘睜鑰屼笉鏄鐢ㄥ叕鍏卞紓姝ユ睜锛氫簩缁寸爜鐢熸垚鏄暱鏃堕棿銆丆PU 瀵嗛泦鐨勪换鍔★紝娣疯繘鍏叡姹犱細鎶婂叾浠栧紓姝ヤ笟鍔′竴璧峰牭浣忋��
+ * 姹犲ぇ灏忓彇骞跺彂闂搁棬閰嶇疆锛岄槦鍒楃暀灏戦噺鍐椾綑浠ュ惛鏀躲�屾牎楠岄�氳繃浣嗙嚎绋嬪皻鏈氨缁�嶇殑鐬椂绐楀彛锛涚湡姝h秴鍑虹殑鎻愪氦
+ * 鐢� {@link MesProQrExportTaskRegistry} 鐨勫苟鍙戣鍙嫤鍦ㄦ洿鍓嶉潰锛屼笉浼氬爢鍒拌繖閲屻��
+ */
+@Configuration(proxyBeanMethods = false)
+public class MesProQrExportExecutorConfig {
+
+    /**
+     * 绾跨▼鍚嶏紝渚夸簬绾夸笂瀹氫綅瀵煎嚭绾跨▼
+     */
+    private static final String THREAD_NAME_PREFIX = "mes-qr-export-";
+
+    @Resource
+    private MesProQrExportProperties properties;
+
+    @Bean(name = "mesQrExportExecutor")
+    public ThreadPoolTaskExecutor mesQrExportExecutor() {
+        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/qrexport/MesProQrExportProperties.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java
new file mode 100644
index 0000000..9a46de4
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.mes.framework.qrexport;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * MES 浜岀淮鐮佸鍑洪厤缃」
+ * <p>
+ * 鎵归噺瀵煎嚭浜岀淮鐮佹槸 CPU 瀵嗛泦鍨嬫搷浣滐紙閫愬紶鐢熸垚浜岀淮鐮� + 鍚堟垚 PNG锛夛紝涓斿崟鎵规鍙揪鎴愬崈涓婁竾寮狅紝
+ * 鍥犳瀵煎嚭琚媶鎴愪袱鏉¤矾寰勶細
+ * <ul>
+ *     <li>灏忔壒閲忥紙涓嶈秴杩� {@link #syncThreshold}锛夎蛋鍚屾娴佸紡涓嬭浇锛岀绾ц繑鍥烇紝涓嶅彈浠讳綍闂搁棬闄愬埗锛�</li>
+ *     <li>澶ф壒閲忚浆鍚庡彴浠诲姟锛屽彈骞跺彂闂搁棬涓� CPU 闂搁棬绠℃帶锛岄伩鍏嶆妸鍚屾満閮ㄧ讲鐨勪笟鍔¢〉闈㈡嫋鍨��</li>
+ * </ul>
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "mes.qr-export")
+public class MesProQrExportProperties {
+
+    /**
+     * 鍚屾/鍚庡彴鐨勫垎鐣岀嚎锛堝紶锛�
+     * <p>
+     * 涓嶈秴杩囪鏁伴噺鐩存帴鍚屾娴佸紡涓嬭浇锛岃秴杩囧垯杞悗鍙板鍑轰换鍔°�傚悓姝ヨ矾寰勪笉鎺掗槦銆佷笉鏍¢獙 CPU锛�
+     * 淇濊瘉銆屽鍑犵櫨寮犮�嶆案杩滄槸绉掑嚭锛屼笉浼氳姝e湪璺戠殑澶ф壒閲忎换鍔℃尅浣忋��
+     */
+    private int syncThreshold = 2000;
+
+    /**
+     * 鎻愪氦鍚庡彴瀵煎嚭浠诲姟鏃剁殑绯荤粺 CPU 浣跨敤鐜囨嫆缁濋槇鍊硷紙0~1锛�
+     * <p>
+     * 杈惧埌璇ュ�煎嵆鎷掔粷鏂板缓浠诲姟锛堟彁绀烘湇鍔″櫒绻佸繖锛夛紝閬垮厤鍦ㄦ湇鍔″櫒宸茬粡寰堝繖鏃跺啀鍙犲姞瀵煎嚭璐熻浇銆�
+     */
+    private double cpuRejectThreshold = 0.80D;
+
+    /**
+     * 鐢熸垚杩囩▼涓殑绯荤粺 CPU 鑺傛祦闃堝�硷紙0~1锛�
+     * <p>
+     * 浠诲姟涓�鏃﹀紑濮嬪氨蹇呴』璺戝畬锛屼粎闈犳彁浜ゆ椂鎷掔粷鏃犳硶瑕嗙洊銆屾彁浜ゆ椂 CPU 浣庛�佽窇璧锋潵鎶婁笟鍔℃嫋鍗°�嶇殑鎯呭喌锛�
+     * 鍥犳鐢熸垚杩囩▼涓寔缁噰鏍凤紝瓒呰繃璇ユ按浣嶅嵆璁╁嚭 CPU锛堥檷閫熶絾涓嶄腑鏂級銆�
+     */
+    private double cpuThrottleThreshold = 0.85D;
+
+    /**
+     * CPU 楂樹簬鑺傛祦闃堝�兼椂锛屾瘡娆¤鍑� CPU 鐨勬绉掓暟
+     */
+    private long cpuThrottleSleepMillis = 300L;
+
+    /**
+     * 鐢熸垚杩囩▼涓鏌ュ彇娑堛�侀噰鏍� CPU 鐨勯棿闅旓紙姣忓灏戝紶涓�娆★級
+     */
+    private int progressCheckInterval = 200;
+
+    /**
+     * 鍚屾椂鍏佽杩愯鐨勫悗鍙板鍑轰换鍔℃暟
+     * <p>
+     * 浠呯害鏉熷ぇ鎵归噺浠诲姟锛屽皬鎵归噺鍚屾瀵煎嚭涓嶅彈姝ら檺鍒躲��
+     */
+    private int maxConcurrentTasks = 1;
+
+    /**
+     * 鍗曚釜鍚庡彴浠诲姟鐨勬渶闀胯繍琛屾椂闂达紙鍒嗛挓锛夛紝瓒呮椂鑷姩鍙栨秷骞舵竻鐞嗭紝閬垮厤寮傚父浠诲姟闀挎湡鍗犵敤
+     */
+    private int maxRuntimeMinutes = 30;
+
+    /**
+     * 涓存椂 ZIP 瀛樻斁鐩綍锛屼负绌烘椂浣跨敤 {@code ${java.io.tmpdir}/mes-qr-export}
+     */
+    private String tempDir = "";
+
+    /**
+     * 涓存椂 ZIP 鐨勪繚鐣欐椂闂达紙鍒嗛挓锛�
+     * <p>
+     * 姝e父涓嬭浇瀹屾垚鍚庣珛鍗冲垹闄わ紱浠呭湪銆屼笅杞戒腑鏂�嶆垨銆岀敓鎴愬畬鎴愬悗濮嬬粓鏈笅杞姐�嶆椂淇濈暀涓�灏忔鏃堕棿锛�
+     * 鍒版湡鐢辨竻鐞嗕换鍔″垹闄ゃ�傚惁鍒欐瘡娆′腑鏂兘瑕侀噸鏂扮敓鎴愪笂涓囧紶鏍囩锛屾垨鑰呬笂 GB 鐨勪复鏃舵枃浠朵竴鐩村崰鐫�纾佺洏銆�
+     */
+    private int tempFileRetentionMinutes = 10;
+
+    /**
+     * 瑙f瀽涓存椂鐩綍锛屾湭閰嶇疆鏃跺洖閫�鍒扮郴缁熶复鏃剁洰褰�
+     */
+    public String resolveTempDir() {
+        if (tempDir == null || tempDir.isBlank()) {
+            return System.getProperty("java.io.tmpdir") + java.io.File.separator + "mes-qr-export";
+        }
+        return tempDir;
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportTaskRegistry.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportTaskRegistry.java
new file mode 100644
index 0000000..922946c
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportTaskRegistry.java
@@ -0,0 +1,113 @@
+package cn.iocoder.yudao.module.mes.framework.qrexport;
+
+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 MesProQrExportTaskRegistry {
+
+    @Resource
+    private MesProQrExportProperties 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()));
+    }
+
+    /**
+     * 灏濊瘯鍗犵敤涓�涓繍琛屽悕棰�
+     *
+     * @return true 琛ㄧず鍗犵敤鎴愬姛锛屽彲寮�濮嬬敓鎴�
+     */
+    public boolean tryAcquire() {
+        return permits.tryAcquire();
+    }
+
+    /**
+     * 閲婃斁杩愯鍚嶉锛堢敓鎴愮粨鏉熴�佸け璐ャ�佸彇娑堝潎闇�璋冪敤锛�
+     */
+    public void release() {
+        permits.release();
+    }
+
+    /**
+     * 褰撳墠鐢辨湰杩涚▼鎸佹湁鐨勪换鍔℃暟锛堝惈鍒氬缓濂姐�佸皻鏈绾跨▼鎺ョ鐨勶級
+     */
+    public int getRunningCount() {
+        return runningTasks.size();
+    }
+
+    /**
+     * 娉ㄥ唽浠诲姟骞惰繑鍥炲叾鍙栨秷鏍囪
+     * <p>
+     * 蹇呴』鍦ㄤ换鍔¤惤搴撳悗銆佹姇閫掔嚎绋嬫睜鍓嶈皟鐢細鍚﹀垯浠庛�岃惤搴撱�嶅埌銆岀嚎绋嬪紑濮嬭繍琛屻�嶄箣闂村瓨鍦ㄤ竴涓獥鍙o紝
+     * 娓呯悊浠诲姟浼氭妸杩欐潯灏氭湭娉ㄥ唽鐨勯潪缁堟�佽褰曡鍒や负杩涚▼閲嶅惎鍚庣殑娈嬬暀鑰屽洖鏀舵帀銆�
+     */
+    public AtomicBoolean register(Long taskId) {
+        AtomicBoolean flag = new AtomicBoolean(false);
+        runningTasks.put(taskId, flag);
+        return flag;
+    }
+
+    /**
+     * 鑾峰彇浠诲姟鐨勫彇娑堟爣璁�
+     *
+     * @return 鍙栨秷鏍囪锛涗换鍔′笉灞炰簬鏈繘绋嬫椂杩斿洖 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);
+    }
+
+    /**
+     * 璇锋眰鍙栨秷锛氫换鍔″湪璺戝垯缃綅鍙栨秷鏍囪锛岃繑鍥� true
+     *
+     * @return true 琛ㄧず浠诲姟姝e湪鐢熸垚銆佸彇娑堣姹傚凡閫佽揪锛沠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/framework/qrexport/SystemCpuLoadMonitor.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/SystemCpuLoadMonitor.java
new file mode 100644
index 0000000..2cfc3cc
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/SystemCpuLoadMonitor.java
@@ -0,0 +1,148 @@
+package cn.iocoder.yudao.module.mes.framework.qrexport;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.lang.management.ManagementFactory;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 绯荤粺 CPU 浣跨敤鐜囩洃鎺�
+ * <p>
+ * 鐢� JDK 鑷甫鐨� {@code com.sun.management.OperatingSystemMXBean#getCpuLoad()} 鍙栧�硷紝涓嶅紩鍏ラ澶栦緷璧栥��
+ * 涔嬫墍浠ヨ鍗曠嫭璧烽噰鏍风嚎绋嬶紝鏄洜涓鸿鎺ュ彛杩斿洖鐨勬槸銆岃窛涓婃璋冪敤涔嬮棿銆嶇殑骞冲潎鍊硷細鑻ュ彧鍦ㄦ彁浜ゅ鍑烘椂璋冪敤涓�娆★紝
+ * 閲囨牱鍖洪棿鍙兘闀胯揪鏁板皬鏃讹紝浼氭妸褰撳墠鐨勯珮璐熻浇灏栧嘲骞虫粦鎺夛紝浠庤�屾斁杩囨湰璇ユ嫆缁濈殑璇锋眰銆傚浐瀹氬懆鏈熼噰鏍峰悗锛�
+ * 鎻愪氦闂搁棬涓庤繍琛岃妭娴佽鍒扮殑閮芥槸鏈�杩戞暟绉掔殑鐪熷疄璐熻浇銆�
+ * <p>
+ * 涓変釜蹇呴』澶勭悊鐨勮竟鐣岋細
+ * <ol>
+ *     <li>棣栨閲囨牱鏃犳晥 鈥斺�� 璇ユ帴鍙i渶瑕佸墠鍚庝袱娆¢噰鏍锋墠鑳界畻鍑虹粨鏋滐紝鏁呭彇鏈�杩戣嫢骞叉鐨勬粦鍔ㄥ钩鍧囷紝閬垮厤鎶栧姩璇垽锛�</li>
+ *     <li>鍙栦笉鍒板�� 鈥斺�� 杩斿洖璐熸暟鏃惰涓恒�屾湭鐭ャ�嶏紝鐢辫皟鐢ㄦ柟鎸夋斁琛屽鐞嗭紝缁濅笉鑳藉洜涓鸿涓嶅埌鎸囨爣灏遍樆鏂笟鍔★紱</li>
+ *     <li>瀹瑰櫒閮ㄧ讲 鈥斺�� 璇ユ寚鏍囧弽鏄犵殑鏄涓绘満鑰岄潪瀹瑰櫒閰嶉锛岃嫢閮ㄧ讲鍦ㄥ甫 CPU limit 鐨勫鍣ㄤ腑浼氬け鐪燂紝
+ *         姝ゆ椂闇�鏀逛负 cgroup 鎰熺煡鐨勫彛寰勩��</li>
+ * </ol>
+ */
+@Slf4j
+@Component
+public class SystemCpuLoadMonitor {
+
+    /**
+     * 閲囨牱鍛ㄦ湡锛堢锛夛細瓒冲鐭互鍙嶆槧褰撳墠璐熻浇锛岃冻澶熼暱浠ラ伩鍏嶉噰鏍锋湰韬甫鏉ュ紑閿�
+     */
+    private static final long SAMPLE_PERIOD_SECONDS = 2L;
+
+    /**
+     * 婊戝姩骞冲潎绐楀彛澶у皬锛氬彇鏈�杩� 3 娆¢噰鏍风殑鍧囧�硷紝鎶规帀鍗曟鎶栧姩
+     */
+    private static final int WINDOW_SIZE = 3;
+
+    /**
+     * 閲囨牱绾跨▼鍚嶏紝渚夸簬绾夸笂鎺掓煡
+     */
+    private static final String THREAD_NAME = "mes-cpu-load-monitor";
+
+    /**
+     * 绯荤粺 CPU 鎺㈤拡锛涘綋鍓� JVM 涓嶆敮鎸佹椂淇濇寔涓� {@code null}
+     */
+    private final com.sun.management.OperatingSystemMXBean osBean = resolveOsBean();
+
+    private final Deque<Double> samples = new ArrayDeque<>(WINDOW_SIZE);
+
+    private ScheduledExecutorService executor;
+
+    @PostConstruct
+    public void start() {
+        if (osBean == null) {
+            log.warn("[start][褰撳墠 JVM 涓嶆敮鎸佺郴缁� CPU 閲囨牱锛屽ぇ鎵归噺浜岀淮鐮佸鍑虹殑 CPU 闂搁棬灏嗗け鏁堬紙鎸夋斁琛屽鐞嗭級]");
+            return;
+        }
+        executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
+            Thread thread = new Thread(runnable, THREAD_NAME);
+            thread.setDaemon(true);
+            return thread;
+        });
+        executor.scheduleWithFixedDelay(this::sampleQuietly, SAMPLE_PERIOD_SECONDS, SAMPLE_PERIOD_SECONDS,
+                TimeUnit.SECONDS);
+    }
+
+    @PreDestroy
+    public void stop() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    /**
+     * 鑾峰彇绯荤粺 CPU 浣跨敤鐜囷紙鏈�杩戣嫢骞叉閲囨牱鐨勬粦鍔ㄥ钩鍧囷級
+     *
+     * @return 0~1 涔嬮棿琛ㄧず鍗犵敤鐜囷紱杩斿洖璐熸暟琛ㄧず褰撳墠鐜鏃犳硶鍙栧�硷紙璋冪敤鏂瑰簲鎸夋斁琛屽鐞嗭級
+     */
+    public double getCpuLoad() {
+        synchronized (samples) {
+            if (samples.isEmpty()) {
+                // 閲囨牱绾跨▼灏氭湭浜у嚭缁撴灉锛堝鏈嶅姟鍒氬惎鍔級锛氬悓姝ュ彇涓�娆★紝鎷夸笉鍒板氨浜ょ粰璋冪敤鏂规寜鏀捐澶勭悊
+                double current = sample();
+                return current;
+            }
+            double sum = 0D;
+            for (Double value : samples) {
+                sum += value;
+            }
+            return sum / samples.size();
+        }
+    }
+
+    /**
+     * 鏄惁鍙噰闆嗙郴缁� CPU锛堜笉鍙噰闆嗘椂涓婂眰搴旇烦杩囩‖闂搁棬锛�
+     */
+    public boolean isAvailable() {
+        return osBean != null;
+    }
+
+    private void sampleQuietly() {
+        try {
+            double value = sample();
+            if (value < 0) {
+                return;
+            }
+            synchronized (samples) {
+                if (samples.size() >= WINDOW_SIZE) {
+                    samples.pollFirst();
+                }
+                samples.addLast(value);
+            }
+        } catch (Exception e) {
+            // 閲囨牱澶辫触涓嶅簲褰卞搷浠讳綍涓氬姟锛屼粎璁板綍涓�娆�
+            log.warn("[sampleQuietly][绯荤粺 CPU 閲囨牱澶辫触]", e);
+        }
+    }
+
+    /**
+     * 閲囨牱涓�娆$郴缁� CPU 浣跨敤鐜�
+     *
+     * @return 0~1 琛ㄧず鍗犵敤鐜囷紱璐熸暟琛ㄧず涓嶅彲鐢�
+     */
+    private double sample() {
+        if (osBean == null) {
+            return -1D;
+        }
+        double value = osBean.getCpuLoad();
+        // 璇ユ帴鍙e湪涓嶅彲鐢ㄦ垨棣栨閲囨牱鏃惰繑鍥炶礋鏁帮紝缁熶竴鎸夈�屾湭鐭ャ�嶅鐞�
+        return value < 0 ? -1D : Math.min(value, 1D);
+    }
+
+    private static com.sun.management.OperatingSystemMXBean resolveOsBean() {
+        java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
+        if (bean instanceof com.sun.management.OperatingSystemMXBean operatingSystemMXBean) {
+            return operatingSystemMXBean;
+        }
+        return null;
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProQrExportCleanupTask.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProQrExportCleanupTask.java
new file mode 100644
index 0000000..7cd61b5
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/pro/MesProQrExportCleanupTask.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.mes.job.pro;
+
+import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrExportTaskService;
+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>
+ * 澶ф壒閲忓鍑虹殑涓存椂 ZIP 鍙揪涓� GB锛屾甯镐笅杞藉畬鎴愬悗鍗冲垹锛屾湰浠诲姟鍙厹搴曚袱绫绘畫鐣欙細
+ * <ol>
+ *     <li>涓嬭浇涓柇鎴栫敓鎴愬畬鎴愬悗濮嬬粓鏈笅杞界殑鏂囦欢锛岃秴杩囦繚鐣欐湡鍚庡垹闄わ紱</li>
+ *     <li>鍥犳湇鍔¢噸鍚�屽け鍘绘墽琛岀嚎绋嬨�佹案杩滃仠鍦ㄣ�岀敓鎴愪腑銆嶇殑浠诲姟璁板綍銆�</li>
+ * </ol>
+ * 鐢� Spring 鑷甫鐨� {@code @Scheduled} 鑰岄潪 Quartz JobHandler锛氬悗鑰呴渶瑕佸湪绠$悊鍚庡彴鎵嬪伐鐧昏浠诲姟锛�
+ * 婕忕櫥璁板氨浼氶潤榛樹笉鎵ц锛岃�岃繖閲岀殑娓呯悊灞炰簬銆屼笉鎵ц灏变細鍗犳弧纾佺洏銆嶇殑鍏滃簳閫昏緫锛屽繀椤诲紑绠卞嵆璺戙��
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MesProQrExportCleanupTask {
+
+    private final MesProQrExportTaskService taskService;
+
+    /**
+     * 姣� 5 鍒嗛挓娓呯悊涓�娆★細涓庨粯璁� 10 鍒嗛挓鐨勪复鏃舵枃浠朵繚鐣欐湡鍖归厤锛岄伩鍏嶆畫鐣欐枃浠堕暱鏃堕棿鍗犵敤纾佺洏
+     */
+    @Scheduled(cron = "0 */5 * * * ?")
+    public void cleanup() {
+        try {
+            int files = taskService.cleanupExpiredFiles();
+            int tasks = taskService.recycleStaleTasks();
+            if (files > 0 || tasks > 0) {
+                log.info("[cleanup][浜岀淮鐮佸鍑烘竻鐞嗗畬鎴愶細鍒犻櫎涓存椂鏂囦欢 {} 涓紝鍥炴敹娈嬬暀浠诲姟 {} 涓猐", files, tasks);
+            }
+        } catch (Exception e) {
+            log.error("[cleanup][浜岀淮鐮佸鍑烘竻鐞嗗け璐", e);
+        }
+    }
+
+    /**
+     * 鍚姩鏃剁珛鍒诲洖鏀朵竴娆℃畫鐣欎换鍔�
+     * <p>
+     * 杩涚▼閲嶅惎鍚庤繍琛屾�佹敞鍐岃〃涓虹┖锛屾鏃惰〃涓殑闈炵粓鎬佽褰曞繀瀹氭病鏈夋墽琛岀嚎绋嬪湪鎺ㄨ繘锛�
+     * 绔嬪埢鍥炴敹鍙伩鍏嶅畠浠互銆岀敓鎴愪腑銆嶇殑鐘舵�佷竴鐩存寕鍦ㄧ晫闈笂锛堝惁鍒欒绛夊埌涓嬩竴娆″畾鏃舵竻鐞嗭級銆�
+     */
+    @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/MesProBagCodeService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
index d21ea84..7208c12 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
@@ -165,6 +165,44 @@
     List<MesProBagCodeDO> getPrintableBagCodeListByUuid(String uuid);
 
     /**
+     * 缁熻鏌愮敓鎴愮粍涓嬪彲鍗板埛鐨勮鐮佹暟閲�
+     * <p>
+     * 瀵煎嚭鍓嶆嵁姝ゅ垽鏂蛋鍚屾涓嬭浇杩樻槸鍚庡彴浠诲姟锛岄伩鍏嶄负浜嗘嬁鍒版暟閲忚�屾妸鍏ㄩ儴琚嬬爜鍔犺浇杩涘唴瀛樸��
+     *
+     * @param uuid 鐢熸垚缁� UUID
+     * @return 鍙嵃鍒风殑琚嬬爜鏁伴噺
+     */
+    Long getPrintableBagCodeCountByUuid(String uuid);
+
+    /**
+     * 缁熻鏌愭壒娆′笅鍙嵃鍒风殑琚嬬爜鏁伴噺
+     *
+     * @param batchId 鎵规缂栧彿
+     * @return 鍙嵃鍒风殑琚嬬爜鏁伴噺
+     */
+    Long getPrintableBagCodeCountByBatchId(Long batchId);
+
+    /**
+     * 鍒嗛〉鑾峰緱鏌愮敓鎴愮粍涓嬪彲鍗板埛鐨勮鐮侊紙鍚庡彴澶ф壒閲忓鍑虹敤锛岄�愰〉鍙栦互鎺у埗鍐呭瓨锛�
+     *
+     * @param uuid     鐢熸垚缁� UUID
+     * @param pageNo   椤电爜锛屼粠 1 寮�濮�
+     * @param pageSize 姣忛〉鏁伴噺
+     * @return 鍙嵃鍒风殑琚嬬爜鍒楄〃锛堟寜琚嬪簭鍙峰崌搴忥級
+     */
+    List<MesProBagCodeDO> getPrintableBagCodePageByUuid(String uuid, int pageNo, int pageSize);
+
+    /**
+     * 鍒嗛〉鑾峰緱鏌愭壒娆′笅鍙嵃鍒风殑琚嬬爜锛堝悗鍙板ぇ鎵归噺瀵煎嚭鐢級
+     *
+     * @param batchId  鎵规缂栧彿
+     * @param pageNo   椤电爜锛屼粠 1 寮�濮�
+     * @param pageSize 姣忛〉鏁伴噺
+     * @return 鍙嵃鍒风殑琚嬬爜鍒楄〃锛堟寜琚嬪簭鍙峰崌搴忥級
+     */
+    List<MesProBagCodeDO> getPrintableBagCodePageByBatchId(Long batchId, int pageNo, int pageSize);
+
+    /**
      * 鏍囪琚嬬爜涓哄凡鍑哄簱锛堣褰曢攢鍞嚭搴撳崟缂栧彿鍜屽嚭搴撴椂闂达級
      *
      * @param id      琚嬬爜缂栧彿
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 2310eff..ddd169b 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
@@ -596,6 +596,26 @@
     }
 
     @Override
+    public Long getPrintableBagCodeCountByUuid(String uuid) {
+        return bagCodeMapper.selectPrintableCountByUuid(uuid);
+    }
+
+    @Override
+    public Long getPrintableBagCodeCountByBatchId(Long batchId) {
+        return bagCodeMapper.selectPrintableCountByBatchId(batchId);
+    }
+
+    @Override
+    public List<MesProBagCodeDO> getPrintableBagCodePageByUuid(String uuid, int pageNo, int pageSize) {
+        return bagCodeMapper.selectPrintablePageByUuid(uuid, pageNo, pageSize);
+    }
+
+    @Override
+    public List<MesProBagCodeDO> getPrintableBagCodePageByBatchId(Long batchId, int pageNo, int pageSize) {
+        return bagCodeMapper.selectPrintablePageByBatchId(batchId, pageNo, pageSize);
+    }
+
+    @Override
     public void markOutbound(Long id, Long salesId) {
         bagCodeMapper.markOutbound(id, salesId, LocalDateTime.now());
     }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java
new file mode 100644
index 0000000..c977aca
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java
@@ -0,0 +1,223 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.qrexport.MesProQrExportTaskDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.qrexport.MesProQrExportTaskMapper;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
+import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
+import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportProperties;
+import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportTaskRegistry;
+import cn.iocoder.yudao.module.mes.framework.qrexport.SystemCpuLoadMonitor;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * 浜岀淮鐮佸鍑轰换鍔℃墽琛屽櫒
+ * <p>
+ * 鍦ㄥ悗鍙扮嚎绋嬪唴鎶婃爣绛鹃�愬紶鍐欏叆涓存椂 ZIP锛屽苟鍋氫笁浠朵簨锛氬洖鍐欒繘搴︺�佸搷搴斿彇娑堛�佸湪 CPU 楂樻按浣嶆椂璁╁嚭 CPU銆�
+ * <p>
+ * 杩欓噷鍒绘剰涓嶄竴娆℃�ц閰嶅叏閮ㄦ爣绛撅紝鑰屾槸鎸夐〉鍙栥�侀�愬紶鍐欙細鍗曟壒娆″彲杈炬垚鍗冧笂涓囧紶锛屼竴娆℃�ц閰嶄細璁╁爢鍗犵敤
+ * 闅忓鍑洪噺绾挎�у闀匡紝閲忕骇鍐嶅線涓婂氨浼� OOM銆�
+ */
+@Slf4j
+@Component
+public class MesProQrExportRunner {
+
+    /**
+     * 姣忔鍚� ZIP 鍙栨暟鐨勯〉澶у皬
+     */
+    private static final int PAGE_SIZE = 500;
+
+    @Resource
+    private MesProQrExportTaskMapper taskMapper;
+    @Resource
+    private MesProQrExportProperties properties;
+    @Resource
+    private SystemCpuLoadMonitor cpuMonitor;
+    @Resource
+    private MesProQrExportTaskRegistry registry;
+
+    /**
+     * 鎵ц瀵煎嚭浠诲姟锛堢敱绾跨▼姹犺皟鐢紝鍕垮湪璇锋眰绾跨▼鍐呯洿鎺ヨ皟鐢級
+     *
+     * @param taskId  浠诲姟缂栧彿
+     * @param source  鏍囩鏁版嵁婧�
+     */
+    public void run(Long taskId, MesProQrLabelSource source) {
+        // 鍙栨秷鏍囪鍦ㄦ彁浜ゆ椂宸叉敞鍐岋紝杩欓噷鍙彇鐢紱鍙栦笉鍒拌鏄庢敞鍐岀幆鑺傝缁曡繃锛岃ˉ涓�涓互淇濊瘉鍙栨秷鍙敤
+        AtomicBoolean cancelFlag = registry.get(taskId);
+        if (cancelFlag == null) {
+            cancelFlag = registry.register(taskId);
+        }
+        File target = null;
+        try {
+            taskMapper.updateStatus(taskId, MesProQrExportTaskStatusEnum.RUNNING.getStatus(),
+                    null, null, null, null);
+            target = resolveTargetFile(taskId);
+            int written = writeZip(target, source, taskId, cancelFlag);
+            if (cancelFlag.get()) {
+                deleteQuietly(target);
+                taskMapper.updateProgress(taskId, written);
+                taskMapper.updateStatus(taskId, MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
+                        null, null, null, LocalDateTime.now());
+                log.info("[run][瀵煎嚭浠诲姟 {} 宸插彇娑堬紝宸茬敓鎴� {} 寮燷", taskId, written);
+                return;
+            }
+            // 杩涘害鏄瘡鑻ュ共寮犲洖鍐欎竴娆★紝鏀跺熬鏃跺繀椤昏ˉ涓�娆$湡瀹炵殑宸茬敓鎴愬紶鏁帮紝
+            // 鍚﹀垯浠诲姟宸插畬鎴愬嵈鍋滃湪 83% 杩欑被涓棿鍊间笂锛岀晫闈笌璁板綍閮戒細璇浣跨敤鑰�
+            taskMapper.updateProgress(taskId, written);
+            taskMapper.updateStatus(taskId, MesProQrExportTaskStatusEnum.FINISHED.getStatus(),
+                    target.getAbsolutePath(), target.length(), null, LocalDateTime.now());
+            log.info("[run][瀵煎嚭浠诲姟 {} 鐢熸垚瀹屾垚锛屽叡 {} 寮狅紝鏂囦欢 {} 瀛楄妭]",
+                    taskId, written, target.length());
+        } catch (QrExportCancelledException e) {
+            deleteQuietly(target);
+            taskMapper.updateStatus(taskId, MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
+                    null, null, null, LocalDateTime.now());
+            log.info("[run][瀵煎嚭浠诲姟 {} 宸插彇娑圿", taskId);
+        } catch (Exception e) {
+            deleteQuietly(target);
+            taskMapper.updateStatus(taskId, MesProQrExportTaskStatusEnum.FAILED.getStatus(),
+                    null, null, truncateError(e), LocalDateTime.now());
+            log.error("[run][瀵煎嚭浠诲姟 {} 鐢熸垚澶辫触]", taskId, e);
+        } finally {
+            registry.unregister(taskId);
+            registry.release();
+        }
+    }
+
+    /**
+     * 閫愰〉鍙栨爣绛俱�侀�愬紶鍐欏叆 ZIP
+     *
+     * @return 瀹為檯鍐欏叆鐨勬爣绛惧紶鏁�
+     */
+    private int writeZip(File target, MesProQrLabelSource source, Long taskId,
+                         AtomicBoolean cancelFlag) throws IOException {
+        long deadline = System.currentTimeMillis()
+                + Duration.ofMinutes(Math.max(1, properties.getMaxRuntimeMinutes())).toMillis();
+        int written = 0;
+        int pageNo = 1;
+        try (OutputStream fileOut = new FileOutputStream(target);
+             ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(fileOut))) {
+            while (true) {
+                List<QrLabelInfo> page = source.fetchPage(pageNo, PAGE_SIZE);
+                if (page.isEmpty()) {
+                    break;
+                }
+                for (QrLabelInfo label : page) {
+                    // 鍙栨秷妫�鏌ユ斁鍦ㄦ瘡寮犱箣鍓嶏細鍘熷瓙璇诲紑閿�鍙拷鐣ワ紝鎹㈡潵鍙栨秷鐨勫強鏃跺搷搴�
+                    if (cancelFlag.get()) {
+                        throw new QrExportCancelledException();
+                    }
+                    QrCodeImageUtils.writeZipEntry(zip, label);
+                    written++;
+                    if (written % Math.max(1, properties.getProgressCheckInterval()) == 0) {
+                        afterEachInterval(taskId, written, deadline);
+                    }
+                }
+                pageNo++;
+            }
+            zip.finish();
+        }
+        return written;
+    }
+
+    /**
+     * 姣忕敓鎴愯嫢骞插紶鍚庣殑渚嬭鍔ㄤ綔锛氬洖鍐欒繘搴︺�佹鏌ヨ繍琛岃秴鏃躲�佹寜 CPU 姘翠綅璁╁嚭 CPU
+     */
+    private void afterEachInterval(Long taskId, int written, long deadline) throws IOException {
+        taskMapper.updateProgress(taskId, written);
+        if (System.currentTimeMillis() > deadline) {
+            throw new IOException("瀵煎嚭浠诲姟瓒呰繃鏈�闀胯繍琛屾椂闂达紙" + properties.getMaxRuntimeMinutes() + " 鍒嗛挓锛�");
+        }
+        throttleIfCpuBusy();
+    }
+
+    /**
+     * CPU 楂樹簬楂樻按浣嶆椂璁╁嚭 CPU
+     * <p>
+     * 浠诲姟涓�鏃﹀紑濮嬪氨蹇呴』璺戝畬锛屽彧闈犳彁浜ゆ椂鎷掔粷鎸′笉浣忋�屾彁浜ゆ椂 CPU 浣庛�佽窇璧锋潵鎶婂悓鏈轰笟鍔℃嫋鍗°�嶏紝
+     * 鍥犳鐢熸垚杩囩▼涓寔缁璺細瀵煎嚭鍙樻參鏄彲浠ユ帴鍙楃殑锛屼笟鍔¢〉闈㈠崱椤夸笉鍙帴鍙椼��
+     */
+    private void throttleIfCpuBusy() {
+        if (!cpuMonitor.isAvailable()) {
+            return;
+        }
+        double load = cpuMonitor.getCpuLoad();
+        if (load < properties.getCpuThrottleThreshold()) {
+            return;
+        }
+        try {
+            Thread.sleep(Math.max(1L, properties.getCpuThrottleSleepMillis()));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new QrExportCancelledException();
+        }
+    }
+
+    /**
+     * 瑙f瀽涓存椂鏂囦欢璺緞锛屽苟纭繚鐩綍瀛樺湪
+     */
+    private File resolveTargetFile(Long taskId) throws IOException {
+        File dir = new File(properties.resolveTempDir());
+        if (!dir.exists() && !dir.mkdirs() && !dir.exists()) {
+            throw new IOException("鏃犳硶鍒涘缓浜岀淮鐮佸鍑轰复鏃剁洰褰曪細" + dir.getAbsolutePath());
+        }
+        // 鏂囦欢鍚嶅彧鐢ㄤ换鍔$紪鍙凤紝閬垮厤涓氬姟鐮佸�间腑鐨勭壒娈婂瓧绗﹁繘鍏ユ枃浠剁郴缁�
+        return new File(dir, "qr-export-" + taskId + ".zip");
+    }
+
+    private void deleteQuietly(File file) {
+        if (file == null || !file.exists()) {
+            return;
+        }
+        try {
+            Files.deleteIfExists(file.toPath());
+        } catch (IOException e) {
+            log.warn("[deleteQuietly][鍒犻櫎涓存椂鏂囦欢澶辫触 {}]", file.getAbsolutePath(), 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;
+    }
+
+    /**
+     * 鍙栨秷淇″彿锛氱敓鎴愯繃绋嬩腑妫�娴嬪埌鍙栨秷鏃舵姏鍑猴紝鐢� {@link #run} 缁熶竴鏀跺熬
+     */
+    static class QrExportCancelledException extends RuntimeException {
+
+        QrExportCancelledException() {
+            super("瀵煎嚭浠诲姟宸插彇娑�");
+        }
+
+    }
+
+    /**
+     * 渚涙竻鐞嗕换鍔″鐢細鍒犻櫎浠诲姟閬楃暀鐨勪复鏃舵枃浠�
+     */
+    void deleteTaskFile(MesProQrExportTaskDO task) {
+        if (task.getFilePath() == null || task.getFilePath().isBlank()) {
+            return;
+        }
+        deleteQuietly(new File(task.getFilePath()));
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java
new file mode 100644
index 0000000..c9937f3
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.qrexport.MesProQrExportTaskDO;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportBizTypeEnum;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+/**
+ * MES 浜岀淮鐮佸鍑轰换鍔� Service
+ * <p>
+ * 浠呮壙鎷呫�屽ぇ鎵归噺銆嶈繖涓�鏉¤矾寰勶細鏁伴噺涓嶈秴杩囧垎鐣岀嚎鐨勫鍑轰粛璧板悓姝ユ祦寮忎笅杞斤紝涓嶈繘浠诲姟銆佷笉鎺掗槦銆佷笉鍙� CPU 闂搁棬绾︽潫锛�
+ * 浠ヤ繚璇佸鍑犵櫨寮犳案杩滅鍑恒��
+ */
+public interface MesProQrExportTaskService {
+
+    /**
+     * 缁熻寰呭鍑虹殑鏍囩寮犳暟锛堜緵璋冪敤鏂瑰垽鏂蛋鍚屾杩樻槸鍚庡彴锛�
+     *
+     * @param bizType 涓氬姟绫诲瀷
+     * @param uuid    鐢熸垚缁� UUID
+     * @param batchId 鐢熶骇鎵规缂栧彿
+     * @return 鏍囩寮犳暟
+     */
+    long countLabels(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId);
+
+    /**
+     * 鎻愪氦鍚庡彴瀵煎嚭浠诲姟
+     * <p>
+     * 渚濇缁忚繃骞跺彂闂搁棬涓� CPU 闂搁棬锛屼换涓�涓嶉�氳繃鍗虫嫆缁濇彁浜ゅ苟缁欏嚭鍘熷洜銆�
+     *
+     * @param bizType 涓氬姟绫诲瀷
+     * @param uuid    鐢熸垚缁� UUID
+     * @param batchId 鐢熶骇鎵规缂栧彿
+     * @return 宸插垱寤虹殑浠诲姟
+     */
+    MesProQrExportTaskDO submit(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId);
+
+    /**
+     * 鑾峰緱瀵煎嚭浠诲姟
+     *
+     * @param id 浠诲姟缂栧彿
+     * @return 浠诲姟锛涗笉瀛樺湪鏃舵姏涓氬姟寮傚父
+     */
+    MesProQrExportTaskDO getTask(Long id);
+
+    /**
+     * 鍙栨秷瀵煎嚭浠诲姟
+     * <p>
+     * 浠诲姟姝e湪鐢熸垚鏃朵細灏藉揩涓柇骞跺垹闄や复鏃舵枃浠讹紱宸插浜庣粓鎬佺殑浠诲姟涓嶅厑璁稿彇娑堛��
+     *
+     * @param id 浠诲姟缂栧彿
+     */
+    void cancel(Long id);
+
+    /**
+     * 涓嬭浇瀵煎嚭浜х墿
+     * <p>
+     * 涓嬭浇鎴愬姛鍚庣珛鍗冲垹闄や复鏃� ZIP锛堥伩鍏嶄笂 GB 鐨勪复鏃舵枃浠跺爢绉級锛涗笅杞戒腑鏂垯淇濈暀鑷虫竻鐞嗕换鍔″垹闄わ紝
+     * 浠ヤ究璋冪敤鏂瑰湪鐭椂闂村唴閲嶈瘯鑰屼笉蹇呴噸鏂扮敓鎴愩��
+     *
+     * @param id       浠诲姟缂栧彿
+     * @param response 鍝嶅簲
+     */
+    void download(Long id, HttpServletResponse response) throws IOException;
+
+    /**
+     * 娓呯悊瓒呮湡鐨勪复鏃� ZIP 鏂囦欢
+     *
+     * @return 娓呯悊鐨勬枃浠舵暟
+     */
+    int cleanupExpiredFiles();
+
+    /**
+     * 鍥炴敹鍥犺繘绋嬮噸鍚垨瓒呮椂鑰屾畫鐣欏湪闈炵粓鎬佺殑浠诲姟
+     *
+     * @return 鍥炴敹鐨勪换鍔℃暟
+     */
+    int recycleStaleTasks();
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
new file mode 100644
index 0000000..4bb7f10
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
@@ -0,0 +1,297 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.qrexport.MesProQrExportTaskDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.qrexport.MesProQrExportTaskMapper;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportBizTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportProperties;
+import cn.iocoder.yudao.module.mes.framework.qrexport.MesProQrExportTaskRegistry;
+import cn.iocoder.yudao.module.mes.framework.qrexport.SystemCpuLoadMonitor;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StreamUtils;
+import org.springframework.validation.annotation.Validated;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+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_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BATCH_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_PALLET_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_SERVER_BUSY;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TASK_BUSY;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TASK_FILE_MISSING;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TASK_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_TASK_STATUS_INVALID;
+
+/**
+ * MES 浜岀淮鐮佸鍑轰换鍔� Service 瀹炵幇
+ */
+@Slf4j
+@Service
+@Validated
+public class MesProQrExportTaskServiceImpl implements MesProQrExportTaskService {
+
+    /**
+     * 闈炵粓鎬佺姸鎬侀泦鍚�
+     */
+    private static final List<Integer> UNFINISHED_STATUSES = List.of(
+            MesProQrExportTaskStatusEnum.PENDING.getStatus(),
+            MesProQrExportTaskStatusEnum.RUNNING.getStatus());
+
+    /**
+     * 缁堟�佺姸鎬侀泦鍚�
+     */
+    private static final List<Integer> FINAL_STATUSES = List.of(
+            MesProQrExportTaskStatusEnum.FINISHED.getStatus(),
+            MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
+            MesProQrExportTaskStatusEnum.FAILED.getStatus(),
+            MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus());
+
+    @Resource
+    private MesProQrExportTaskMapper taskMapper;
+    @Resource
+    private MesProQrLabelSourceFactory sourceFactory;
+    @Resource
+    private MesProQrExportRunner runner;
+    @Resource
+    private MesProQrExportProperties properties;
+    @Resource
+    private SystemCpuLoadMonitor cpuMonitor;
+    @Resource
+    private MesProQrExportTaskRegistry registry;
+
+    @Resource(name = "mesQrExportExecutor")
+    private ThreadPoolTaskExecutor executor;
+
+    @Override
+    public long countLabels(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId) {
+        return sourceFactory.count(bizType, uuid, batchId);
+    }
+
+    @Override
+    public MesProQrExportTaskDO submit(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId) {
+        // 1. 瑙f瀽鏁版嵁婧愬苟缁熻鏁伴噺锛氬弬鏁颁笉鍚堟硶鎴栦笟鍔℃暟鎹笉瀛樺湪鏃讹紝鍦ㄦ鐩存帴鎶涘嚭鑰屼笉钀戒换鍔�
+        MesProQrLabelSource source = sourceFactory.create(bizType, uuid, batchId);
+        long total = source.count();
+        if (total <= 0) {
+            throw exception(resolveEmptyErrorCode(bizType));
+        }
+        // 2. 骞跺彂闂搁棬锛氬悓涓�鏃堕棿鍙窇涓�涓ぇ鎵归噺浠诲姟锛涘皬鎵归噺鍚屾瀵煎嚭涓嶈蛋杩欓噷锛屽洜姝や笉浼氳鎸′綇
+        if (!registry.tryAcquire()) {
+            throw exception(PRO_QR_EXPORT_TASK_BUSY, resolveRunningProgress());
+        }
+        // 涓�鏃︿换鍔¤浜ょ粰鎵ц鍣紝骞跺彂璁稿彲灏辫浆鐢辨墽琛岀嚎绋嬪湪鏀跺熬鏃跺綊杩橈紱姝ゅ墠鐨勪换浣曞け璐ラ兘蹇呴』鍦ㄨ繖閲屽綊杩�
+        boolean handedOver = false;
+        try {
+            // 3. CPU 闂搁棬锛氭湇鍔″櫒宸茬粡寰堝繖鏃朵笉鍐嶅彔鍔犲鍑鸿礋杞�
+            assertCpuAllowSubmit();
+            // 4. 钀戒换鍔�
+            MesProQrExportTaskDO task = MesProQrExportTaskDO.builder()
+                    .bizType(bizType.getType())
+                    .uuid(uuid)
+                    .batchId(batchId)
+                    .taskName(source.zipName())
+                    .status(MesProQrExportTaskStatusEnum.PENDING.getStatus())
+                    .totalCount((int) total)
+                    .finishedCount(0)
+                    .build();
+            taskMapper.insert(task);
+            // 5. 鍏堟敞鍐岃繍琛屾�佸啀鎶曢�掞細閬垮厤銆屽凡钀藉簱浣嗘湭娉ㄥ唽銆嶇殑绐楀彛琚竻鐞嗕换鍔¤鍥炴敹
+            registry.register(task.getId());
+            handedOver = true;
+            try {
+                executor.execute(() -> runner.run(task.getId(), source));
+            } catch (RejectedExecutionException e) {
+                registry.unregister(task.getId());
+                handedOver = false;
+                taskMapper.updateStatus(task.getId(), MesProQrExportTaskStatusEnum.FAILED.getStatus(),
+                        null, null, "瀵煎嚭绾跨▼绻佸繖锛岃绋嶅悗閲嶈瘯", LocalDateTime.now());
+                throw exception(PRO_QR_EXPORT_TASK_BUSY, 0);
+            }
+            return task;
+        } finally {
+            if (!handedOver) {
+                registry.release();
+            }
+        }
+    }
+
+    @Override
+    public MesProQrExportTaskDO getTask(Long id) {
+        MesProQrExportTaskDO task = taskMapper.selectById(id);
+        if (task == null) {
+            throw exception(PRO_QR_EXPORT_TASK_NOT_EXISTS);
+        }
+        return task;
+    }
+
+    @Override
+    public void cancel(Long id) {
+        MesProQrExportTaskDO task = getTask(id);
+        if (isFinalStatus(task.getStatus())) {
+            throw exception(PRO_QR_EXPORT_TASK_STATUS_INVALID);
+        }
+        if (registry.requestCancel(id)) {
+            // 鐢熸垚绾跨▼浼氳嚜琛屾敹灏撅細鍒犻櫎鍗婃垚鍝佹枃浠跺苟缃负宸插彇娑�
+            return;
+        }
+        // 涓嶅湪鏈繘绋嬭繍琛岋紙杩涚▼閲嶅惎鍚庣殑娈嬬暀浠诲姟锛夛細鐩存帴鏀跺熬
+        runner.deleteTaskFile(task);
+        taskMapper.updateStatus(id, MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
+                null, null, null, LocalDateTime.now());
+    }
+
+    @Override
+    public void download(Long id, HttpServletResponse response) throws IOException {
+        MesProQrExportTaskDO task = getTask(id);
+        if (!MesProQrExportTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())) {
+            throw exception(PRO_QR_EXPORT_TASK_STATUS_INVALID);
+        }
+        File file = resolveTaskFile(task);
+        response.setContentType("application/zip");
+        response.setHeader("Content-Disposition",
+                "attachment;filename=" + HttpUtils.encodeUtf8(task.getTaskName() + ".zip"));
+        boolean success = false;
+        try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+            StreamUtils.copy(in, response.getOutputStream());
+            response.getOutputStream().flush();
+            success = true;
+        } finally {
+            // 浠呭湪瀹屾暣浼犺緭鍚庡垹闄わ紱涓柇鏃朵繚鐣欙紝浜ょ敱娓呯悊浠诲姟鍦ㄤ繚鐣欐湡鍚庡垹闄わ紝渚夸簬鐭椂闂村唴閲嶈瘯
+            if (success) {
+                deleteAfterDownload(task, file);
+            }
+        }
+    }
+
+    @Override
+    public int cleanupExpiredFiles() {
+        LocalDateTime deadline = LocalDateTime.now()
+                .minusMinutes(Math.max(1, properties.getTempFileRetentionMinutes()));
+        List<MesProQrExportTaskDO> list = taskMapper.selectExpiredFinalList(FINAL_STATUSES, deadline);
+        int count = 0;
+        for (MesProQrExportTaskDO task : list) {
+            if (StrUtil.isBlank(task.getFilePath())) {
+                continue;
+            }
+            runner.deleteTaskFile(task);
+            taskMapper.clearFilePath(task.getId());
+            count++;
+        }
+        return count;
+    }
+
+    @Override
+    public int recycleStaleTasks() {
+        List<MesProQrExportTaskDO> list = taskMapper.selectUnfinishedList(UNFINISHED_STATUSES);
+        int count = 0;
+        for (MesProQrExportTaskDO task : list) {
+            if (registry.isRunning(task.getId())) {
+                continue;
+            }
+            runner.deleteTaskFile(task);
+            taskMapper.clearFilePath(task.getId());
+            taskMapper.updateStatus(task.getId(), MesProQrExportTaskStatusEnum.FAILED.getStatus(),
+                    null, null, "浠诲姟鍥犳湇鍔¢噸鍚垨寮傚父涓柇鑰岀粓姝紝璇烽噸鏂板鍑�", LocalDateTime.now());
+            count++;
+        }
+        return count;
+    }
+
+    // ==================== 绉佹湁鏂规硶 ====================
+
+    /**
+     * 鏍¢獙绯荤粺 CPU 鏄惁鍏佽鎻愪氦澶ф壒閲忓鍑�
+     * <p>
+     * 鍙栦笉鍒� CPU 鎸囨爣鏃舵寜鏀捐澶勭悊锛氫笉鑳藉洜涓鸿涓嶅埌鐩戞帶鍊煎氨闃绘柇涓氬姟銆�
+     */
+    private void assertCpuAllowSubmit() {
+        if (!cpuMonitor.isAvailable()) {
+            return;
+        }
+        double load = cpuMonitor.getCpuLoad();
+        double threshold = properties.getCpuRejectThreshold();
+        if (load >= threshold) {
+            throw exception(PRO_QR_EXPORT_SERVER_BUSY,
+                    Math.round(load * 100), Math.round(threshold * 100));
+        }
+    }
+
+    /**
+     * 鏁版嵁涓虹┖鏃剁殑閿欒鐮侊細鎸変笟鍔$被鍨嬬粰鍑烘洿鍑嗙‘鐨勬彁绀�
+     */
+    private cn.iocoder.yudao.framework.common.exception.ErrorCode resolveEmptyErrorCode(
+            MesProQrExportBizTypeEnum bizType) {
+        return switch (bizType) {
+            case PALLET_LABEL, PALLET_BUSINESS -> PRO_PALLET_NOT_EXISTS;
+            case BATCH_LABEL, BATCH_BUSINESS -> PRO_BATCH_NOT_EXISTS;
+            default -> PRO_BAG_CODE_NOT_EXISTS;
+        };
+    }
+
+    /**
+     * 姝e湪杩愯浠诲姟鐨勮繘搴︾櫨鍒嗘瘮锛岀敤浜庛�屽凡鏈変换鍔¤繘琛屼腑銆嶇殑鎻愮ず
+     */
+    private int resolveRunningProgress() {
+        List<MesProQrExportTaskDO> tasks = taskMapper.selectUnfinishedList(UNFINISHED_STATUSES);
+        return tasks.stream().findFirst().map(this::calcProgress).orElse(0);
+    }
+
+    private int calcProgress(MesProQrExportTaskDO 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);
+    }
+
+    private boolean isFinalStatus(Integer status) {
+        return status != null && FINAL_STATUSES.contains(status);
+    }
+
+    /**
+     * 瀹氫綅浠诲姟鐨勪复鏃� ZIP锛屾枃浠剁己澶辨椂椤烘墜娓呯┖璺緞閬垮厤鍚庣画鍙嶅澶辫触
+     */
+    private File resolveTaskFile(MesProQrExportTaskDO task) {
+        String filePath = task.getFilePath();
+        if (StrUtil.isBlank(filePath)) {
+            throw exception(PRO_QR_EXPORT_TASK_FILE_MISSING);
+        }
+        File file = new File(filePath);
+        if (!file.isFile()) {
+            taskMapper.clearFilePath(task.getId());
+            throw exception(PRO_QR_EXPORT_TASK_FILE_MISSING);
+        }
+        return file;
+    }
+
+    /**
+     * 涓嬭浇瀹屾垚鍗冲垹闄や复鏃舵枃浠讹細鍗曟瀵煎嚭鍙揪涓� GB锛岀暀鐫�浼氳繀閫熸妸纾佺洏鍚冩弧
+     */
+    private void deleteAfterDownload(MesProQrExportTaskDO task, File file) {
+        try {
+            Files.deleteIfExists(file.toPath());
+        } catch (IOException e) {
+            log.warn("[deleteAfterDownload][鍒犻櫎涓存椂鏂囦欢澶辫触 {}]", file.getAbsolutePath(), e);
+        }
+        taskMapper.clearFilePath(task.getId());
+        taskMapper.updateStatus(task.getId(), MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus(),
+                null, null, null, null);
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelAssembler.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelAssembler.java
new file mode 100644
index 0000000..4e45da6
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelAssembler.java
@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+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.framework.qrcode.MesProQrLabelBuilder;
+import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
+import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
+import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+/**
+ * MES 浜岀淮鐮佹爣绛捐閰嶅櫒
+ * <p>
+ * 缁熶竴銆屾煡鍑哄叧鑱旀暟鎹紙鎵规/鍝佺/璐d换浜猴級鈫� 缁勮鏍囩銆嶇殑閫昏緫锛屼緵鍚屾瀵煎嚭鎺ュ彛涓庡悗鍙板鍑轰换鍔″叡鐢紝
+ * 閬垮厤鍚屼竴鏉¤閰嶈鍒欏湪澶氫釜鍏ュ彛鍚勫啓涓�浠借�屽嚭鐜板彛寰勪笉涓�鑷达紙鍘嗗彶涓婃爣绛剧己椤瑰氨鏄繖涔堟潵鐨勶級銆�
+ */
+@Component
+public class MesProQrLabelAssembler {
+
+    @Resource
+    private MesProBatchService batchService;
+    @Resource
+    private MesMdmItemService mdmItemService;
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    /**
+     * 鏋勫缓琚嬬爜浜岀淮鐮佹爣绛撅紙瀵瑰杩芥函鏍囩锛屽惈鍥芥爣鍥涜绱狅級
+     */
+    public List<QrLabelInfo> buildBagCodeLabels(List<MesProBagCodeDO> list) {
+        if (list.isEmpty()) {
+            return Collections.emptyList();
+        }
+        // 1. 鑾峰緱鍏宠仈鏁版嵁锛堟壒娆°�佸搧绉嶃�佽矗浠讳汉锛夛紱鍝佺闆嗗悎鍚鐮佽嚜韬搧绉嶄笌鍘嗗彶鎵规鍝佺
+        Map<Long, MesProBatchDO> batchMap = convertMap(
+                batchService.getBatchList(convertSet(list, MesProBagCodeDO::getBatchId)), MesProBatchDO::getId);
+        Set<Long> itemIds = convertSet(list, MesProBagCodeDO::getItemId);
+        itemIds.addAll(convertSet(batchMap.values(), MesProBatchDO::getItemId));
+        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
+        // 2. 鏋勫缓鏍囩
+        List<QrLabelInfo> labels = new ArrayList<>(list.size());
+        for (MesProBagCodeDO bagCode : list) {
+            labels.add(MesProQrLabelBuilder.buildBagCodeLabel(
+                    bagCode, batchMap.get(bagCode.getBatchId()), itemMap, userMap));
+        }
+        return labels;
+    }
+
+    /**
+     * 鏋勫缓琚嬬爜涓氬姟浜岀淮鐮佹爣绛撅紙浜岀淮鐮佷粎缂栫爜鍐呴儴涓氬姟鐮佸�硷級
+     */
+    public List<QrLabelInfo> buildBagBusinessLabels(List<MesProBagCodeDO> list) {
+        return list.stream().map(MesProQrLabelBuilder::buildBagBusinessCodeLabel).toList();
+    }
+
+    /**
+     * 鏋勫缓鎵樼洏浜岀淮鐮佹爣绛�
+     */
+    public List<QrLabelInfo> buildPalletLabels(List<MesProPalletDO> list) {
+        if (list.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, MesProBatchDO> batchMap = convertMap(
+                batchService.getBatchList(convertSet(list, MesProPalletDO::getBatchId)), MesProBatchDO::getId);
+        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
+                convertSet(batchMap.values(), MesProBatchDO::getItemId));
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
+        List<QrLabelInfo> labels = new ArrayList<>(list.size());
+        for (MesProPalletDO pallet : list) {
+            labels.add(MesProQrLabelBuilder.buildPalletLabel(
+                    pallet, batchMap.get(pallet.getBatchId()), itemMap, userMap));
+        }
+        return labels;
+    }
+
+    /**
+     * 鏋勫缓鎵樼洏涓氬姟浜岀淮鐮佹爣绛�
+     */
+    public List<QrLabelInfo> buildPalletBusinessLabels(List<MesProPalletDO> list) {
+        return list.stream().map(MesProQrLabelBuilder::buildPalletBusinessCodeLabel).toList();
+    }
+
+    /**
+     * 鏋勫缓鎵规浜岀淮鐮佹爣绛撅細鎵规鍥� + 璇ユ壒娆″叏閮ㄦ墭鐩樺浘
+     * <p>
+     * 鎵规涓庢墭鐩樺悓灞炰竴鎵癸紝鍝佺/璐d换浜哄叡鐢ㄥ悓涓�浠藉叧鑱旀暟鎹��
+     */
+    public List<QrLabelInfo> buildBatchLabels(MesProBatchDO batch, List<MesProPalletDO> pallets) {
+        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
+                convertSet(Collections.singletonList(batch), MesProBatchDO::getItemId));
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(Collections.singletonList(batch), MesProBatchDO::getOwnerUserId));
+        List<QrLabelInfo> labels = new ArrayList<>(pallets.size() + 1);
+        labels.add(MesProQrLabelBuilder.buildBatchLabel(batch, itemMap, userMap));
+        for (MesProPalletDO pallet : pallets) {
+            labels.add(MesProQrLabelBuilder.buildPalletLabel(pallet, batch, itemMap, userMap));
+        }
+        return labels;
+    }
+
+    /**
+     * 鏋勫缓鎵规涓氬姟浜岀淮鐮佹爣绛撅細鎵规涓氬姟鐮� + 鍏ㄩ儴鎵樼洏涓氬姟鐮�
+     */
+    public List<QrLabelInfo> buildBatchBusinessLabels(MesProBatchDO batch, List<MesProPalletDO> pallets) {
+        List<QrLabelInfo> labels = new ArrayList<>(pallets.size() + 1);
+        labels.add(MesProQrLabelBuilder.buildBatchBusinessCodeLabel(batch));
+        for (MesProPalletDO pallet : pallets) {
+            labels.add(MesProQrLabelBuilder.buildPalletBusinessCodeLabel(pallet));
+        }
+        return labels;
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSource.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSource.java
new file mode 100644
index 0000000..d33141d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSource.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
+
+import java.util.List;
+
+/**
+ * 浜岀淮鐮佹爣绛炬暟鎹簮
+ * <p>
+ * 鍚庡彴瀵煎嚭浠诲姟鎸夐〉鍙栨爣绛撅紝鑰屼笉鏄竴娆℃�ф妸鍏ㄩ儴鏍囩瑁呴厤杩涘唴瀛橈細鍗曟壒娆″彲杈炬垚鍗冧笂涓囧紶锛�
+ * 涓�娆℃�ц閰嶄細闅忓鍑洪噺绾挎�у崰鐢ㄥ爢鍐呭瓨锛岄噺绾у啀寰�涓婂氨浼� OOM銆�
+ */
+public interface MesProQrLabelSource {
+
+    /**
+     * 鏍囩鎬诲紶鏁帮紙鐢ㄤ簬杩涘害璁$畻涓庡垎娴佸垽鏂級
+     */
+    long count();
+
+    /**
+     * 鍒嗛〉鑾峰彇鏍囩
+     *
+     * @param pageNo   椤电爜锛屼粠 1 寮�濮�
+     * @param pageSize 姣忛〉鏁伴噺
+     * @return 璇ラ〉鏍囩锛涜繑鍥炵┖鍒楄〃琛ㄧず宸插彇瀹�
+     */
+    List<QrLabelInfo> fetchPage(int pageNo, int pageSize);
+
+    /**
+     * ZIP 鏂囦欢鍚嶏紙涓嶅惈 {@code .zip} 鍚庣紑锛�
+     */
+    String zipName();
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSourceFactory.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSourceFactory.java
new file mode 100644
index 0000000..8ca9555
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrLabelSourceFactory.java
@@ -0,0 +1,180 @@
+package cn.iocoder.yudao.module.mes.service.pro.qrexport;
+
+import cn.hutool.core.util.StrUtil;
+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.enums.pro.MesProPalletStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProQrExportBizTypeEnum;
+import cn.iocoder.yudao.module.mes.framework.qrcode.QrLabelInfo;
+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.pallet.MesProPalletService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BATCH_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_PALLET_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_EXPORT_PARAMS_INVALID;
+
+/**
+ * 浜岀淮鐮佹爣绛炬暟鎹簮宸ュ巶
+ * <p>
+ * 鎶娿�屼笟鍔$被鍨� + 瀵煎嚭鍙傛暟銆嶇炕璇戞垚缁熶竴鐨勬暟鎹簮锛屼娇鍚庡彴瀵煎嚭浠诲姟涓嶅繀鍏冲績鏍囩鏄�庝箞鏉ョ殑锛�
+ * 涔熻鍚屾瀵煎嚭璺緞涓庡悗鍙拌矾寰勫叡鐢ㄥ悓涓�濂楀彇鏁颁笌瑁呴厤鍙e緞銆�
+ */
+@Component
+public class MesProQrLabelSourceFactory {
+
+    @Resource
+    private MesProBagCodeService bagCodeService;
+    @Resource
+    private MesProPalletService palletService;
+    @Resource
+    private MesProBatchService batchService;
+    @Resource
+    private MesProQrLabelAssembler assembler;
+
+    /**
+     * 鍒涘缓鏍囩鏁版嵁婧愶紱鍙傛暟涓嶅悎娉曟椂鐩存帴鎶涘嚭涓氬姟寮傚父
+     *
+     * @param bizType 涓氬姟绫诲瀷
+     * @param uuid    鐢熸垚缁� UUID锛堣鐮佺被瀵煎嚭鍙敤锛�
+     * @param batchId 鐢熶骇鎵规缂栧彿锛堣鐮佺被鍙敤锛屽叾浣欑被鍨嬪繀濉級
+     */
+    public MesProQrLabelSource create(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId) {
+        if (bizType == null) {
+            throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
+        }
+        return switch (bizType) {
+            case BAG_LABEL -> bagSource(uuid, batchId, false);
+            case BAG_BUSINESS -> bagSource(uuid, batchId, true);
+            case PALLET_LABEL -> palletSource(requireBatchId(batchId), false);
+            case PALLET_BUSINESS -> palletSource(requireBatchId(batchId), true);
+            case BATCH_LABEL -> batchSource(requireBatchId(batchId), false);
+            case BATCH_BUSINESS -> batchSource(requireBatchId(batchId), true);
+        };
+    }
+
+    /**
+     * 缁熻鏍囩寮犳暟锛堝垎娴佸垽鏂敤锛屼笉瑁呴厤鏍囩鍐呭锛�
+     */
+    public long count(MesProQrExportBizTypeEnum bizType, String uuid, Long batchId) {
+        return create(bizType, uuid, batchId).count();
+    }
+
+    private Long requireBatchId(Long batchId) {
+        if (batchId == null) {
+            throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
+        }
+        return batchId;
+    }
+
+    // ==================== 鍚勪笟鍔$被鍨嬬殑鏁版嵁婧� ====================
+
+    /**
+     * 琚嬬爜绫绘暟鎹簮锛堟爣绛� / 涓氬姟鐮侊級
+     */
+    private MesProQrLabelSource bagSource(String uuid, Long batchId, boolean business) {
+        if (StrUtil.isBlank(uuid) && batchId == null) {
+            throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
+        }
+        boolean byUuid = StrUtil.isNotBlank(uuid);
+        String zipName = (business ? "琚嬬爜涓氬姟浜岀淮鐮�-" : "琚嬬爜浜岀淮鐮�-") + (byUuid ? uuid : batchId);
+        return new MesProQrLabelSource() {
+
+            @Override
+            public long count() {
+                return byUuid ? bagCodeService.getPrintableBagCodeCountByUuid(uuid)
+                        : bagCodeService.getPrintableBagCodeCountByBatchId(batchId);
+            }
+
+            @Override
+            public List<QrLabelInfo> fetchPage(int pageNo, int pageSize) {
+                List<MesProBagCodeDO> bags = byUuid
+                        ? bagCodeService.getPrintableBagCodePageByUuid(uuid, pageNo, pageSize)
+                        : bagCodeService.getPrintableBagCodePageByBatchId(batchId, pageNo, pageSize);
+                return business ? assembler.buildBagBusinessLabels(bags) : assembler.buildBagCodeLabels(bags);
+            }
+
+            @Override
+            public String zipName() {
+                return zipName;
+            }
+        };
+    }
+
+    /**
+     * 鎵樼洏绫绘暟鎹簮
+     * <p>
+     * 涓�涓壒娆′笅鐨勬墭鐩樻暟閲忔湁涓婄晫锛堝彈姣忔墭琚嬫暟绾︽潫锛夛紝鏁呬竴娆℃�у彇鍑哄悗鎸夐〉鍒囩墖锛屾棤闇�鐪熷垎椤垫煡璇€��
+     */
+    private MesProQrLabelSource palletSource(Long batchId, boolean business) {
+        List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId);
+        if (business) {
+            // 涓氬姟鐮佸鍑洪渶鎺掗櫎宸蹭綔搴熸墭鐩橈紝閬垮厤浣滃簾鐮佽鍗板嚭
+            pallets = pallets.stream()
+                    .filter(record -> !MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus()))
+                    .toList();
+        }
+        if (pallets.isEmpty()) {
+            throw exception(PRO_PALLET_NOT_EXISTS);
+        }
+        List<QrLabelInfo> labels = business ? assembler.buildPalletBusinessLabels(pallets)
+                : assembler.buildPalletLabels(pallets);
+        String zipName = business ? "鎵樼洏涓氬姟浜岀淮鐮�" : "鎵樼洏鐮佷簩缁寸爜-" + batchId;
+        return fixedSource(labels, zipName);
+    }
+
+    /**
+     * 鎵规绫绘暟鎹簮锛氭壒娆″浘 + 璇ユ壒娆″叏閮ㄦ墭鐩樺浘
+     */
+    private MesProQrLabelSource batchSource(Long batchId, boolean business) {
+        MesProBatchDO batch = batchService.getBatch(batchId);
+        if (batch == null) {
+            throw exception(PRO_BATCH_NOT_EXISTS);
+        }
+        List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId);
+        if (business) {
+            pallets = pallets.stream()
+                    .filter(record -> !MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus()))
+                    .toList();
+        }
+        List<QrLabelInfo> labels = business ? assembler.buildBatchBusinessLabels(batch, pallets)
+                : assembler.buildBatchLabels(batch, pallets);
+        String zipName = business ? "鎵规涓氬姟浜岀淮鐮�-" + batch.getBusinessCode()
+                : "鎵规浜岀淮鐮�-" + StrUtil.blankToDefault(batch.getCode(), batch.getTempCode());
+        return fixedSource(labels, zipName);
+    }
+
+    /**
+     * 鐢卞凡瑁呴厤濂界殑鏍囩鍒楄〃鏋勯�犳暟鎹簮锛堥�傜敤浜庢�婚噺鏈変笂鐣岀殑鍦烘櫙锛�
+     */
+    private MesProQrLabelSource fixedSource(List<QrLabelInfo> labels, String zipName) {
+        return new MesProQrLabelSource() {
+
+            @Override
+            public long count() {
+                return labels.size();
+            }
+
+            @Override
+            public List<QrLabelInfo> fetchPage(int pageNo, int pageSize) {
+                int from = (pageNo - 1) * pageSize;
+                if (from >= labels.size()) {
+                    return List.of();
+                }
+                return labels.subList(from, Math.min(from + pageSize, labels.size()));
+            }
+
+            @Override
+            public String zipName() {
+                return zipName;
+            }
+        };
+    }
+
+}
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index 2600196..60212d2 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -13,6 +13,23 @@
   # 鏄惁鍏佽浜岀淮鐮佺紪鐮佺函鐮佸�硷紙涓嶅甫婧簮 URL锛夛紝浠呬緵鏈湴鍏煎鍘嗗彶鐮侊紝鐢熶骇鐜蹇呴』涓� false
   allow-plain-qr-code: false
 
+  # 浜岀淮鐮佹壒閲忓鍑猴細灏忔壒閲忓悓姝ョ鍑猴紝澶ф壒閲忚浆鍚庡彴浠诲姟锛堟棤鏁伴噺涓婇檺锛�
+  qr-export:
+    # 鍚屾/鍚庡彴鍒嗙晫绾匡紙寮狅級銆備笉瓒呰繃鍒欏悓姝ヤ笅杞斤紝涓嶆帓闃熴�佷笉鏍¢獙 CPU锛屼繚璇佸鍑犵櫨寮犳案杩滅鍑�
+    sync-threshold: 2000
+    # 鎻愪氦鍚庡彴浠诲姟鏃剁殑绯荤粺 CPU 鎷掔粷闃堝�硷紙0~1锛夛紝杈惧埌鍗虫嫆缁濇柊寤轰换鍔�
+    cpu-reject-threshold: 0.80
+    # 鐢熸垚杩囩▼涓殑绯荤粺 CPU 鑺傛祦闃堝�硷紙0~1锛夛紝杈惧埌鍗宠鍑� CPU锛堥檷閫熶絾涓嶄腑鏂换鍔★級
+    cpu-throttle-threshold: 0.85
+    # 鍚屾椂鍏佽杩愯鐨勫悗鍙板鍑轰换鍔℃暟锛堜粎绾︽潫澶ф壒閲忥紝灏忔壒閲忓悓姝ュ鍑轰笉鍙楅檺锛�
+    max-concurrent-tasks: 1
+    # 鍗曚釜鍚庡彴浠诲姟鏈�闀胯繍琛屾椂闂达紙鍒嗛挓锛夛紝瓒呮椂鑷姩鍙栨秷骞舵竻鐞�
+    max-runtime-minutes: 30
+    # 涓存椂 ZIP 淇濈暀鏃堕棿锛堝垎閽燂級銆備笅杞藉畬鎴愬悗绔嬪嵆鍒犻櫎锛屾澶勪粎鍏滃簳銆屼笅杞戒腑鏂�/濮嬬粓鏈笅杞姐�嶇殑娈嬬暀
+    temp-file-retention-minutes: 10
+    # 涓存椂 ZIP 鐩綍锛岀暀绌哄垯浣跨敤 ${java.io.tmpdir}/mes-qr-export
+    temp-dir: ''
+
 spring:
   application:
     name: yudao-server

--
Gitblit v1.9.3