feat(mes): 二维码大批量导出改为后台任务,取消数量上限
原先 8 处二维码导出接口共用 MAX_QR_EXPORT=2000 的硬上限,单批次达 5 万张时被直接拒绝,
且报错既不说明上限也不给出路。现按数量分流:
- 不超过分界线(默认 2000 张)走原同步流式下载,秒级返回,不排队、不校验 CPU,
保证导几百张永远秒出,不会被正在跑的大任务挡住
- 超过分界线则落后台任务,流式生成单个 ZIP 落盘,前端轮询真实进度并支持取消/隐藏
配套改动:
- 抽出 MesProQrLabelAssembler,消除袋码/托盘/批次三份重复的标签装配逻辑
- 后台任务受两道闸门约束:并发(同时仅 1 个)与 CPU(提交 80% 拒绝、生成中 85% 让路降速);
两者都只约束大批量,不影响小批量同步导出
- 临时 ZIP 下载后立即删除;下载中断或始终未下载的,由定时任务按保留期兜底清理
- 服务启动时回收因进程重启而停留在「生成中」的残留任务
- 移除已无引用的 PRO_QR_EXPORT_TOO_MANY 错误码
新增 mes_pro_qr_export_task 表,见 sql/mysql/mes_pro_qr_export_task_init.sql。
实测:1000 张约 6 秒,生成速率约 149 张/秒,5 万张约 5.6 分钟。
Co-Authored-By: Claude <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * äºç»´ç 大æ¹é导åºââ端å°ç«¯å®æµ |
| | | * |
| | | * è¦çï¼åæµå¤æ â åå°ä»»å¡çæï¼çå®è¿åº¦ï¼â éè为浮æ â 宿åä¸è½½ ZIP |
| | | * |
| | | * ç¨æ³ï¼node qr-export-big-batch.cjs [--headed] [--uuid=<çæç»>] [--cancel-after=<ç§>] |
| | | * --cancel-after 设å®åï¼çæå°è¯¥ç§æ°æ¶ç¹âåæ¶âï¼åªéªè¯åæ¶é¾è·¯ï¼ä¸çå®æï¼ |
| | | * |
| | | * 说æï¼çå®ä½¿ç¨æ¶å端ä¼å
å¼¹ãä¿åä½ç½®ãå¯¹è¯æ¡ï¼File 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ï¼æ¥å£é¢æ£ï¼ç¡®è®¤åç«¯å·²å«æ¬æ¬¡æ¹å¨ï¼'); |
| | | 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(`æ¥å£ç»å½æåï¼userId=${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('æ°æ¥å£è¿å 404 ââ å端æªå«æ¬æ¬¡æ¹å¨'); |
| | | if (countRes.code !== 0) fail(`ç»è®¡æ¥å£å¼å¸¸ï¼${JSON.stringify(countRes)}`); |
| | | const { totalCount, syncThreshold, mode } = countRes.data; |
| | | log(`æ¥å£ç»è®¡ï¼${totalCount} å¼ ï¼åç线 ${syncThreshold}ï¼æ¹å¼ ${mode}`); |
| | | return { totalCount, mode, syncThreshold }; |
| | | } |
| | | |
| | | /** æ ¡éªä¸è½½äº§ç©ï¼ä½ç§¯ãZIP 头尾ç¾å */ |
| | | 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(/æ£å¨çæï¼(\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(/æ£å¨ä¸è½½|导åºå®æ|导åºå¤±è´¥/); |
| | | 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ï¼æµè§å¨ä¿ææå¼ï¼è¯·èªè¡æ¥çï¼å
³éçªå£åæ¬è¿ç¨éåºï¼'); |
| | | await page |
| | | .waitForEvent('close', { timeout: 0 }) |
| | | .catch(() => {}); |
| | | await browser.close().catch(() => {}); |
| | | return; |
| | | } |
| | | if (HEADED) { |
| | | // çåºå ç§è®©äººçæ¸
æç»çé¢ï¼åèªå¨æ¶å°¾ï¼é¿å
ç䏿¬æçæµè§å¨ä¸ node è¿ç¨ |
| | | log('ï¼headed 模å¼ï¼10 ç§åèªå¨å
³éæµè§å¨ï¼'); |
| | | await page.waitForTimeout(10_000).catch(() => {}); |
| | | } |
| | | await browser.close().catch(() => {}); |
| | | } |
| | | } |
| | | |
| | | main().catch((e) => { |
| | | log(`\nFAILED: ${e.message}`); |
| | | process.exit(1); |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- äºç»´ç 大æ¹é导åºä»»å¡ï¼ä¸´æ¶ 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 'ä¸å¡ç±»åï¼BAG_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 äºç»´ç 导åºä»»å¡'; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | | private MesMdmItemService mdmItemService; |
| | | @Resource |
| | | private MesProQrLabelAssembler labelAssembler; |
| | | @Resource |
| | | private MesProQrExportProperties qrExportProperties; |
| | | |
| | | @PostMapping("/generate") |
| | | @Operation(summary = "çæè¢ç æ°æ®å
ï¼ä¸è¢ä¸ç ï¼ï¼ç´¯è®¡è¾¾å°é弿¶èªå¨çææçç ") |
| | |
| | | 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 ==================== |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * æå»ºè¢ç äºç»´ç æ ç¾å表ï¼äºç»´ç ç¼ç 纯ç å¼ï¼å¾çå å å
³é®ä¿¡æ¯æåï¼ |
| | | * 忥坼åºåªæ¿æ¥å°æ¹é |
| | | * <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; |
| | | } |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | private MesProPalletService palletService; |
| | | @Resource |
| | | private MesProBagCodeService bagCodeService; |
| | | @Resource |
| | | private MesProQrLabelAssembler labelAssembler; |
| | | |
| | | @PostMapping("/create") |
| | | @Operation(summary = "å建çäº§æ¹æ¬¡ï¼ä¸æ¬¡å½å
¥ä¸å¡ä¿¡æ¯ï¼èªå¨çææ¹æ¬¡å·ï¼") |
| | |
| | | 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 ==================== |
| | |
| | | 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) { |
| | |
| | | respVO.setWorkers(buildWorkerRespVOList(batchService.getBatchWorkerListByBatchId(batch.getId()))); |
| | | // æçå表 |
| | | respVO.setPallets(BeanUtils.toBean(palletService.getPalletListByBatchId(batch.getId()), MesProPalletRespVO.class)); |
| | | // è¢ç å表ï¼å°é¡¶ 2000ï¼bagTotal è®°å½æ»æ°ï¼ |
| | | // è¢ç å表ï¼å°é¡¶ MAX_TRACE_BAG_LIST_SIZEï¼bagTotal è®°å½æ»æ°ï¼ |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | 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 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 |
| | |
| | | private MesMdmItemService mdmItemService; |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | | private MesProQrLabelAssembler labelAssembler; |
| | | |
| | | @PostMapping("/batch-create") |
| | | @Operation(summary = "æ¹éçææçç ï¼ä¸æ¬¡å½å
¥æ¹æ¬¡çææ£å¼æçç ï¼") |
| | |
| | | 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") |
| | |
| | | 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 ==================== |
| | |
| | | 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) { |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * æ°éä¸è¶
è¿åç线ç导åºä¸èµ°è¿éï¼è°ç¨æ¹ç´æ¥è°åä¸è½½æ¥å£ï¼ç§çº§è¿åã䏿éï¼ï¼ |
| | | * è¶
è¿åç线ææäº¤ä»»å¡ã轮询è¿åº¦ã宿åä¸è½½ã |
| | | * <p> |
| | | * 说æï¼ä¸è½½ä¸åæ¶æªé¢å¤éå¶ãåªè½æä½èªå·±å建çä»»å¡ããè½è¿å
¥æ¬æ¥å£çç¨æ·å·²ææå¯¹åºæ¨¡åçå¯¼åºæéï¼ |
| | | * ä¹å°±å·²ç»å¯ä»¥ç´æ¥è°ç¨åæ¥å¯¼åºæ¥å£æ¿å°åæ ·ç 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 = "ç»è®¡äºç»´ç 导åºå¼ æ°å¹¶ç»åºå¯¼åºæ¹å¼ï¼SYNC ç´æ¥ä¸è½½ / 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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 { |
| | | |
| | | /** |
| | | * åæ¥ç´æ¥ä¸è½½ï¼æ°éä¸è¶
è¿åç线ï¼è°ç¨æ¹æåæçä¸è½½æ¥å£å 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 = "å¯¼åºæ¹å¼ï¼SYNC åæ¥ç´æ¥ä¸è½½ / ASYNC åå°ä»»å¡ï¼", requiredMode = Schema.RequiredMode.REQUIRED, |
| | | example = "ASYNC") |
| | | private String mode; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 = "ä¸å¡ç±»åï¼BAG_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; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | } |
| | |
| | | .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); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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)); |
| | | } |
| | | |
| | | } |
| | |
| | | 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, "çäº§æ¹æ¬¡å°æªè¡¥å½æ£å¼ä¿¡æ¯"); |
| | | 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, "è¢ç ä¸åå¨"); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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ãæãçäº§æ¹æ¬¡ç¼å·ãäºéä¸ï¼æ
ä¸åºåå
·ä½å
¥å£æ¥æºã |
| | | */ |
| | | @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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| | |
| | | private QrCodeImageUtils() { |
| | | } |
| | | |
| | | /** æ¹é导åºäºç»´ç æ°éä¸éï¼é²æ¢ä¸æ¬¡æ§çæè¿å¤å¯¼è´å
å/ååºè¿å¤§ï¼ */ |
| | | public static final int MAX_QR_EXPORT = 2000; |
| | | |
| | | /** äºç»´ç è¾¹é¿ */ |
| | | private static final int QR_SIZE = 260; |
| | | /** å¾çåå¨çç½ */ |
| | |
| | | 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(); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * çæåå¼ æ ç¾å¹¶åå
¥ 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(); |
| | | } |
| | | |
| | | // ==================== ç§ææ¹æ³ ==================== |
| | | |
| | | /** |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * åç¬å»ºæ± è䏿¯å¤ç¨å
Œ
±å¼æ¥æ± ï¼äºç»´ç çææ¯é¿æ¶é´ãCPU å¯éçä»»å¡ï¼æ··è¿å
Œ
±æ± 伿å
¶ä»å¼æ¥ä¸å¡ä¸èµ·å µä½ã |
| | | * æ± å¤§å°åå¹¶åé¸é¨é
ç½®ï¼éåçå°éåä½ä»¥å¸æ¶ãæ ¡éªéè¿ä½çº¿ç¨å°æªå°±ç»ªãçç¬æ¶çªå£ï¼çæ£è¶
åºçæäº¤ |
| | | * ç± {@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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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ï¼ |
| | | * ä¿è¯ã导å ç¾å¼ ãæ°¸è¿æ¯ç§åºï¼ä¸ä¼è¢«æ£å¨è·ç大æ¹é任塿¡ä½ã |
| | | */ |
| | | 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> |
| | | * æ£å¸¸ä¸è½½å®æåç«å³å é¤ï¼ä»
å¨ãä¸è½½ä¸æãæãçæå®æåå§ç»æªä¸è½½ãæ¶ä¿çä¸å°æ®µæ¶é´ï¼ |
| | | * å°æç±æ¸
çä»»å¡å é¤ãå¦åæ¯æ¬¡ä¸æé½è¦éæ°çæä¸ä¸å¼ æ ç¾ï¼æè
ä¸ GB çä¸´æ¶æä»¶ä¸ç´å çç£çã |
| | | */ |
| | | private int tempFileRetentionMinutes = 10; |
| | | |
| | | /** |
| | | * è§£æä¸´æ¶ç®å½ï¼æªé
ç½®æ¶åéå°ç³»ç»ä¸´æ¶ç®å½ |
| | | */ |
| | | public String resolveTempDir() { |
| | | if (tempDir == null || tempDir.isBlank()) { |
| | | return System.getProperty("java.io.tmpdir") + java.io.File.separator + "mes-qr-export"; |
| | | } |
| | | return tempDir; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | |
| | | /** |
| | | * æ£å¨è¿è¡çä»»å¡ï¼ä»»å¡ç¼å· â åæ¶æ è®° |
| | | */ |
| | | 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> |
| | | * å¿
é¡»å¨ä»»å¡è½åºåãæéçº¿ç¨æ± åè°ç¨ï¼å¦åä»ãè½åºãå°ã线ç¨å¼å§è¿è¡ãä¹é´åå¨ä¸ä¸ªçªå£ï¼ |
| | | * æ¸
çä»»å¡ä¼æè¿æ¡å°æªæ³¨åçéç»æè®°å½è¯¯å¤ä¸ºè¿ç¨éå¯åçæ®çèåæ¶æã |
| | | */ |
| | | 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 è¡¨ç¤ºä»»å¡æ£å¨çæãåæ¶è¯·æ±å·²éè¾¾ï¼false 表示任å¡ä¸å¨æ¬è¿ç¨è¿è¡ |
| | | */ |
| | | public boolean requestCancel(Long taskId) { |
| | | AtomicBoolean flag = runningTasks.get(taskId); |
| | | if (flag == null) { |
| | | return false; |
| | | } |
| | | flag.set(true); |
| | | return true; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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>馿¬¡éæ ·æ æ ââ 该æ¥å£éè¦ååä¸¤æ¬¡éæ ·æè½ç®åºç»æï¼æ
åæè¿è¥å¹²æ¬¡çæ»å¨å¹³åï¼é¿å
æå¨è¯¯å¤ï¼</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(); |
| | | // 该æ¥å£å¨ä¸å¯ç¨æé¦æ¬¡éæ ·æ¶è¿åè´æ°ï¼ç»ä¸æãæªç¥ãå¤ç |
| | | 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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | 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 è¢ç ç¼å· |
| | |
| | | } |
| | | |
| | | @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()); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è§£æä¸´æ¶æä»¶è·¯å¾ï¼å¹¶ç¡®ä¿ç®å½åå¨ |
| | | */ |
| | | 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())); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * 任塿£å¨çææ¶ä¼å°½å¿«ä¸æå¹¶å é¤ä¸´æ¶æä»¶ï¼å·²å¤äºç»æçä»»å¡ä¸å
è®¸åæ¶ã |
| | | * |
| | | * @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(); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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. è§£ææ°æ®æºå¹¶ç»è®¡æ°éï¼åæ°ä¸åæ³æä¸å¡æ°æ®ä¸å卿¶ï¼å¨æ¤ç´æ¥æåºèä¸è½ä»»å¡ |
| | | 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; |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * æ£å¨è¿è¡ä»»å¡çè¿åº¦ç¾åæ¯ï¼ç¨äºãå·²æä»»å¡è¿è¡ä¸ãçæç¤º |
| | | */ |
| | | 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); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * ç»ä¸ãæ¥åºå
³èæ°æ®ï¼æ¹æ¬¡/åç§/责任人ï¼â ç»è£
æ ç¾ãçé»è¾ï¼ä¾åæ¥å¯¼åºæ¥å£ä¸åå°å¯¼åºä»»å¡å
±ç¨ï¼ |
| | | * é¿å
å䏿¡è£
é
è§åå¨å¤ä¸ªå
¥å£ååä¸ä»½èåºç°å£å¾ä¸ä¸è´ï¼åå²ä¸æ ç¾ç¼ºé¡¹å°±æ¯è¿ä¹æ¥çï¼ã |
| | | */ |
| | | @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> |
| | | * æ¹æ¬¡ä¸æçåå±ä¸æ¹ï¼åç§/责任人å
±ç¨åä¸ä»½å
³èæ°æ®ã |
| | | */ |
| | | 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; |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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(); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |
| | | * æãä¸å¡ç±»å + 导åºåæ°ãç¿»è¯æç»ä¸çæ°æ®æºï¼ä½¿åå°å¯¼åºä»»å¡ä¸å¿
å
³å¿æ ç¾æ¯æä¹æ¥çï¼ |
| | | * ä¹è®©åæ¥å¯¼åºè·¯å¾ä¸åå°è·¯å¾å
±ç¨åä¸å¥åæ°ä¸è£
é
å£å¾ã |
| | | */ |
| | | @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; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | } |
| | |
| | | # æ¯å¦å
许äºç»´ç ç¼ç 纯ç å¼ï¼ä¸å¸¦æº¯æº 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 |