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