From a7cf1913c376b53f487f0ef8dfff084c5bc010a7 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 17 十月 2026 11:19:12 +0800
Subject: [PATCH] feat(mes): 支持二维码导出任务的分段下载功能
---
sql/mysql/mdm_unit_measure_type_bag.sql | 5 +
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskDownloadTest.java | 62 ++++++++++++
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtilsZipTest.java | 41 ++++++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java | 6
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java | 2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java | 6
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java | 18 +-
yudao-server/src/main/resources/application.yaml | 4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java | 5
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java | 24 ++++
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java | 4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java | 2
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java | 4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java | 105 +++++++++++++++------
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java | 2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java | 8 +
16 files changed, 238 insertions(+), 60 deletions(-)
diff --git a/sql/mysql/mdm_unit_measure_type_bag.sql b/sql/mysql/mdm_unit_measure_type_bag.sql
new file mode 100644
index 0000000..9463777
--- /dev/null
+++ b/sql/mysql/mdm_unit_measure_type_bag.sql
@@ -0,0 +1,5 @@
+-- 璁¢噺鍗曚綅绫诲瀷鐢扁�淣鍗冨厠鈥濇敼涓衡�淣琚嬧�濄�傛暟瀛楄〃绀烘瘡鎵樿鏁帮紝閲嶉噺浠嶇敱 weight 瀛楁琛ㄧず銆�
+-- 鍙噸澶嶆墽琛岋紱鍙慨鏀圭鍚堣鏍煎紡鐨勭被鍨嬶紝涓嶆敼鍗曚綅鍚嶇О鍙婇噸閲忋��
+UPDATE mdm_unit_measure
+SET type = CONCAT(LEFT(type, CHAR_LENGTH(type) - 2), '琚�')
+WHERE type REGEXP '^[0-9]{1,5}鍗冨厠$';
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
index ef2f075..afe34c9 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
@@ -18,7 +18,7 @@
@Schema(description = "鍗曚綅鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "涓�")
private String name;
- @Schema(description = "绫诲瀷锛�48鍗冨厠/100鍗冨厠/鍚級锛岄潪蹇呭~锛涜鐮佺敓鎴愪緷璧栬瀛楁锛屼负绌烘椂鏃犳硶鐢熸垚琚嬬爜", example = "48鍗冨厠")
+ @Schema(description = "绫诲瀷锛�48琚�/100琚�/鍚級锛岄潪蹇呭~锛涜鐮佺敓鎴愪緷璧栬瀛楁锛屼负绌烘椂鏃犳硶鐢熸垚琚嬬爜", example = "48琚�")
private String type;
@Schema(description = "閲嶉噺(kg)", example = "25")
@@ -39,4 +39,4 @@
@Schema(description = "澶囨敞", example = "澶囨敞淇℃伅")
private String remark;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
index 2a9ec82..1b85a94 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
@@ -42,7 +42,7 @@
/**
* 绫诲瀷
*
- * 鍙�夊�硷細48鍗冨厠銆�100鍗冨厠銆佸惃
+ * 鍙�夊�硷細48琚嬨��100琚嬨�佸惃
*
* 闈炲繀濉紱MES 琚嬬爜鐢熸垚渚濊禆璇ュ瓧娈垫崲绠楁瘡鎵樿鏁帮紝涓虹┖鏃舵棤娉曠敓鎴愯鐮�
*/
@@ -80,4 +80,4 @@
*/
private String remark;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
index 2199316..0c05073 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
@@ -13,7 +13,7 @@
@NotNull(message = "鍝佺涓嶈兘涓虹┖")
private Long itemId;
- @Schema(description = "鎵樼洏鏁帮紙鍝佺涓诲崟浣嶇被鍨嬩负 48鍗冨厠/100鍗冨厠 鏃跺繀濉紝琚嬬爜鏁� = 姣忔墭琚嬫暟 脳 鎵樼洏鏁帮級", example = "5")
+ @Schema(description = "鎵樼洏鏁帮紙鍝佺涓诲崟浣嶇被鍨嬩负 48琚�/100琚� 鏃跺繀濉紝琚嬬爜鏁� = 姣忔墭琚嬫暟 脳 鎵樼洏鏁帮級", example = "5")
@Positive(message = "鎵樼洏鏁板繀椤诲ぇ浜� 0")
private Integer palletCount;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
index 8d8c484..f1ede9b 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
@@ -12,7 +12,7 @@
@Schema(description = "鐢熸垚缁� UUID")
private String uuid;
- @Schema(description = "鍝佺涓诲崟浣嶇被鍨�", example = "48鍗冨厠")
+ @Schema(description = "鍝佺涓诲崟浣嶇被鍨�", example = "48琚�")
private String type;
@Schema(description = "姣忔墭鐩樿鏁帮紙绫诲瀷涓� 鍚� 鏃朵负 0锛�", example = "48")
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java
index bb0f151..9f90b7d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/qrexport/MesProQrExportTaskController.java
@@ -100,11 +100,13 @@
}
@GetMapping("/task/download")
- @Operation(summary = "涓嬭浇浜岀淮鐮佸鍑轰骇鐗╋紙涓嬭浇瀹屾垚鍚庣郴缁熺珛鍗冲垹闄や复鏃舵枃浠讹級")
+ @Operation(summary = "涓嬭浇浜岀淮鐮佸鍑轰骇鐗╋紙鏀寔 Range 鍒嗘涓嬭浇锛�")
@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);
+ public void download(@RequestParam("id") Long id,
+ @RequestHeader(value = "Range", required = false) String range,
+ HttpServletResponse response) throws IOException {
+ taskService.download(id, range, response);
}
// ==================== 绉佹湁鏂规硶 ====================
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
index de13be9..5d0356b 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -861,7 +861,7 @@
ErrorCode PRO_BAG_CODE_REPLACE_ALREADY_REPLACED = new ErrorCode(1_040_752_009, "琚嬬爜宸插畬鎴愭浛鎹紝涓嶅厑璁搁噸澶嶆浛鎹�");
ErrorCode PRO_BAG_CODE_REPLACE_STATUS_INVALID = new ErrorCode(1_040_752_010, "琚嬬爜鐘舵�佷笉鍏佽鐮存崯鏇挎崲");
ErrorCode PRO_BAG_CODE_REPLACE_PALLET_INVALID = new ErrorCode(1_040_752_011, "琚嬬爜鍘熸墭鐩樹笉瀛樺湪鎴栧凡浣滃簾锛屼笉鍏佽缁ф壙");
- ErrorCode PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED = new ErrorCode(1_040_752_012, "鍝佺涓昏閲忓崟浣嶆湭閰嶇疆绫诲瀷锛屼粎鏀寔 48鍗冨厠/100鍗冨厠/鍚�");
+ ErrorCode PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED = new ErrorCode(1_040_752_012, "鍝佺涓昏閲忓崟浣嶆湭閰嶇疆绫诲瀷锛屼粎鏀寔 48琚�/100琚�/鍚�");
ErrorCode PRO_BAG_CODE_PALLET_COUNT_REQUIRED = new ErrorCode(1_040_752_013, "璇ュ搧绉嶆寜鎵樼洏鐢熸垚琚嬬爜锛屾墭鐩樻暟蹇呴』澶т簬 0");
ErrorCode PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH = new ErrorCode(1_040_752_014, "褰曞叆鎵规鐨勮鐮佸繀椤诲睘浜庡悓涓�鍝佺");
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
index 93bb1d1..9a30eb5 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
@@ -9,6 +9,9 @@
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
@@ -17,6 +20,7 @@
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
+import java.io.FilterOutputStream;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
@@ -138,8 +142,24 @@
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();
+ try {
+ // ImageIO.write(OutputStream) 鍙兘涓烘瘡寮犲浘鐗囧垱寤虹鐩樼紦瀛橈紱澶ф壒閲忓鍑烘敼鐢ㄥ唴瀛樼紦瀛樸��
+ // 鍖呰娴佺殑 close 浠� flush锛屼笉鑳藉叧闂灞� ZIP銆�
+ ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
+ try (ImageOutputStream imageOut = new MemoryCacheImageOutputStream(new FilterOutputStream(zip) {
+ @Override
+ public void close() throws IOException {
+ flush();
+ }
+ })) {
+ writer.setOutput(imageOut);
+ writer.write(image);
+ } finally {
+ writer.dispose();
+ }
+ } finally {
+ zip.closeEntry();
+ }
}
// ==================== 绉佹湁鏂规硶 ====================
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java
index 9a46de4..3cdd097 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrexport/MesProQrExportProperties.java
@@ -72,10 +72,10 @@
/**
* 涓存椂 ZIP 鐨勪繚鐣欐椂闂达紙鍒嗛挓锛�
* <p>
- * 姝e父涓嬭浇瀹屾垚鍚庣珛鍗冲垹闄わ紱浠呭湪銆屼笅杞戒腑鏂�嶆垨銆岀敓鎴愬畬鎴愬悗濮嬬粓鏈笅杞姐�嶆椂淇濈暀涓�灏忔鏃堕棿锛�
- * 鍒版湡鐢辨竻鐞嗕换鍔″垹闄ゃ�傚惁鍒欐瘡娆′腑鏂兘瑕侀噸鏂扮敓鎴愪笂涓囧紶鏍囩锛屾垨鑰呬笂 GB 鐨勪复鏃舵枃浠朵竴鐩村崰鐫�纾佺洏銆�
+ * 瀹屾垚鍚庝繚鐣欎竴娈垫椂闂翠緵鍒嗘涓嬭浇鍜屽け璐ラ噸璇曪紝鍒版湡鐢辨竻鐞嗕换鍔″垹闄ゃ��
+ * 閬垮厤缃戠粶涓柇鍚庨噸鏂扮敓鎴愪笂涓囧紶鏍囩锛屼篃閬垮厤涓存椂鏂囦欢闀挎湡鍗犵敤纾佺洏銆�
*/
- private int tempFileRetentionMinutes = 10;
+ private int tempFileRetentionMinutes = 60;
/**
* 瑙f瀽涓存椂鐩綍锛屾湭閰嶇疆鏃跺洖閫�鍒扮郴缁熶复鏃剁洰褰�
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
index bfcd3af..b60d9f8 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -83,9 +83,9 @@
*/
private static final String UNIT_TYPE_TON = "鍚�";
/**
- * 璁¢噺鍗曚綅绫诲瀷銆孨鍗冨厠銆嶏細姣忔墭鐩樿 N 涓鐮�
+ * 璁¢噺鍗曚綅绫诲瀷銆孨琚嬨�嶏細姣忔墭鐩樿 N 涓鐮�
*/
- private static final Pattern KG_TYPE_PATTERN = Pattern.compile("^(\\d{1,5})鍗冨厠$");
+ private static final Pattern BAG_TYPE_PATTERN = Pattern.compile("^(\\d{1,5})琚�$");
@Resource
private MesCodeRedisDAO codeRedisDao;
@@ -99,9 +99,9 @@
MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
String type = getUnitType(item);
- // 2. 鎸夌被鍨嬫崲绠楄鐮佹暟閲忥細N鍗冨厠 = 姣忔墭 N 琚� 脳 鎵樼洏鏁帮紱鍚� = 鐩存帴杈撳叆琚嬬爜鏁帮紙鏃犳墭鐩橈級
+ // 2. 鎸夌被鍨嬫崲绠楄鐮佹暟閲忥細N琚� = 姣忔墭 N 琚� 脳 鎵樼洏鏁帮紱鍚� = 鐩存帴杈撳叆琚嬬爜鏁帮紙鏃犳墭鐩橈級
boolean ton = UNIT_TYPE_TON.equals(type);
- int perPallet = ton ? 0 : parseKgPerPallet(type);
+ int perPallet = ton ? 0 : parseBagsPerPallet(type);
int totalCount;
int palletCount = 0;
if (ton) {
@@ -190,7 +190,7 @@
}
/**
- * 璇诲彇鍝佺涓昏閲忓崟浣嶇殑绫诲瀷锛�48鍗冨厠/100鍗冨厠/鍚級锛屾湭閰嶇疆鎴栨棤娉曡瘑鍒椂鎶ラ敊
+ * 璇诲彇鍝佺涓昏閲忓崟浣嶇殑绫诲瀷锛�48琚�/100琚�/鍚級锛屾湭閰嶇疆鎴栨棤娉曡瘑鍒椂鎶ラ敊
*/
private String getUnitType(MesMdmItemDO item) {
String type = null;
@@ -203,17 +203,17 @@
if (type == null) {
throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
}
- if (!UNIT_TYPE_TON.equals(type) && !KG_TYPE_PATTERN.matcher(type).matches()) {
+ if (!UNIT_TYPE_TON.equals(type) && !BAG_TYPE_PATTERN.matcher(type).matches()) {
throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
}
return type;
}
/**
- * 瑙f瀽銆孨鍗冨厠銆嶇被鍨嬪緱鍑烘瘡鎵樼洏琚嬫暟
+ * 瑙f瀽銆孨琚嬨�嶇被鍨嬪緱鍑烘瘡鎵樼洏琚嬫暟
*/
- private int parseKgPerPallet(String type) {
- Matcher matcher = KG_TYPE_PATTERN.matcher(type);
+ private int parseBagsPerPallet(String type) {
+ Matcher matcher = BAG_TYPE_PATTERN.matcher(type);
if (!matcher.matches()) {
throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java
index c977aca..5685462 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportRunner.java
@@ -22,6 +22,7 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;
/**
@@ -111,7 +112,9 @@
int written = 0;
int pageNo = 1;
try (OutputStream fileOut = new FileOutputStream(target);
- ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(fileOut))) {
+ ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(fileOut, 256 * 1024))) {
+ // 姣忓紶 PNG 宸插帇缂╋紝鍐嶅仛 ZIP deflate 浼氶噸澶嶆秷鑰� CPU锛涗繚鐣� ZIP 瀹瑰櫒鍜岄�愬紶鏂囦欢缁撴瀯銆�
+ zip.setLevel(Deflater.NO_COMPRESSION);
while (true) {
List<QrLabelInfo> page = source.fetchPage(pageNo, PAGE_SIZE);
if (page.isEmpty()) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java
index c9937f3..43905e3 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskService.java
@@ -56,13 +56,13 @@
/**
* 涓嬭浇瀵煎嚭浜х墿
* <p>
- * 涓嬭浇鎴愬姛鍚庣珛鍗冲垹闄や复鏃� ZIP锛堥伩鍏嶄笂 GB 鐨勪复鏃舵枃浠跺爢绉級锛涗笅杞戒腑鏂垯淇濈暀鑷虫竻鐞嗕换鍔″垹闄わ紝
- * 浠ヤ究璋冪敤鏂瑰湪鐭椂闂村唴閲嶈瘯鑰屼笉蹇呴噸鏂扮敓鎴愩��
+ * 鏀寔 Range 鍒嗘涓嬭浇锛涗复鏃� ZIP 淇濈暀鑷虫竻鐞嗕换鍔″垹闄わ紝浠ヤ究浼犺緭涓柇鍚庣画浼犮��
*
* @param id 浠诲姟缂栧彿
+ * @param range HTTP Range 璇锋眰澶达紝鍙负绌�
* @param response 鍝嶅簲
*/
- void download(Long id, HttpServletResponse response) throws IOException;
+ void download(Long id, String range, HttpServletResponse response) throws IOException;
/**
* 娓呯悊瓒呮湡鐨勪复鏃� ZIP 鏂囦欢
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
index 4bb7f10..e6adab6 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
@@ -14,7 +14,6 @@
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;
@@ -22,10 +21,13 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.file.Files;
+import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
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;
@@ -60,6 +62,11 @@
MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
MesProQrExportTaskStatusEnum.FAILED.getStatus(),
MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus());
+
+ private static final Pattern BYTE_RANGE = Pattern.compile("^bytes=(\\d+)-(\\d*)$");
+
+ /** 闃叉瀹氭椂娓呯悊浠诲姟鍒犻櫎姝e湪浼犺緭鐨� ZIP銆� */
+ private final ConcurrentHashMap<Long, Integer> activeDownloads = new ConcurrentHashMap<>();
@Resource
private MesProQrExportTaskMapper taskMapper;
@@ -156,25 +163,74 @@
}
@Override
- public void download(Long id, HttpServletResponse response) throws IOException {
+ public void download(Long id, String range, 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);
+ activeDownloads.compute(id, (key, count) -> count == null ? 1 : count + 1);
+ try {
+ File file = resolveTaskFile(task);
+ long fileSize = file.length();
+ long[] bounds = resolveRange(range, fileSize);
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("X-Accel-Buffering", "no");
+ response.setHeader("Cache-Control", "private, no-store");
+ response.setHeader("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges");
+ if (bounds == null) {
+ response.setStatus(416);
+ response.setHeader("Content-Range", "bytes */" + fileSize);
+ return;
}
+ long start = bounds[0];
+ long end = bounds[1];
+ long length = end - start + 1;
+ response.setContentType("application/zip");
+ response.setHeader("Content-Disposition",
+ "attachment;filename=" + HttpUtils.encodeUtf8(task.getTaskName() + ".zip"));
+ if (range != null && !range.isBlank()) {
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
+ }
+ response.setContentLengthLong(length);
+ try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+ in.skipNBytes(start);
+ OutputStream out = response.getOutputStream();
+ byte[] buffer = new byte[64 * 1024];
+ long remaining = length;
+ while (remaining > 0) {
+ int read = in.read(buffer, 0, (int) Math.min(buffer.length, remaining));
+ if (read < 0) {
+ throw new IOException("浜岀淮鐮佸鍑烘枃浠跺湪涓嬭浇杩囩▼涓鎴柇");
+ }
+ out.write(buffer, 0, read);
+ remaining -= read;
+ }
+ out.flush();
+ }
+ } finally {
+ activeDownloads.computeIfPresent(id, (key, count) -> count > 1 ? count - 1 : null);
+ }
+ }
+
+ /** 鍙敮鎸佸崟涓棴鍖洪棿锛涗笉鍚堟硶鐨� Range 杩斿洖 416锛岄伩鍏嶈浼犳暣鍖呫�� */
+ private long[] resolveRange(String range, long fileSize) {
+ if (range == null || range.isBlank()) {
+ return new long[]{0, fileSize - 1};
+ }
+ Matcher matcher = BYTE_RANGE.matcher(range);
+ if (!matcher.matches() || fileSize <= 0) {
+ return null;
+ }
+ try {
+ long start = Long.parseLong(matcher.group(1));
+ long end = matcher.group(2).isEmpty() ? fileSize - 1 : Long.parseLong(matcher.group(2));
+ if (start >= fileSize || end < start) {
+ return null;
+ }
+ return new long[]{start, Math.min(end, fileSize - 1)};
+ } catch (NumberFormatException e) {
+ return null;
}
}
@@ -185,6 +241,9 @@
List<MesProQrExportTaskDO> list = taskMapper.selectExpiredFinalList(FINAL_STATUSES, deadline);
int count = 0;
for (MesProQrExportTaskDO task : list) {
+ if (activeDownloads.containsKey(task.getId())) {
+ continue;
+ }
if (StrUtil.isBlank(task.getFilePath())) {
continue;
}
@@ -278,20 +337,6 @@
throw exception(PRO_QR_EXPORT_TASK_FILE_MISSING);
}
return file;
- }
-
- /**
- * 涓嬭浇瀹屾垚鍗冲垹闄や复鏃舵枃浠讹細鍗曟瀵煎嚭鍙揪涓� GB锛岀暀鐫�浼氳繀閫熸妸纾佺洏鍚冩弧
- */
- private void deleteAfterDownload(MesProQrExportTaskDO task, File file) {
- try {
- Files.deleteIfExists(file.toPath());
- } catch (IOException e) {
- log.warn("[deleteAfterDownload][鍒犻櫎涓存椂鏂囦欢澶辫触 {}]", file.getAbsolutePath(), e);
- }
- taskMapper.clearFilePath(task.getId());
- taskMapper.updateStatus(task.getId(), MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus(),
- null, null, null, null);
}
}
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtilsZipTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtilsZipTest.java
new file mode 100644
index 0000000..0c17f2f
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtilsZipTest.java
@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.mes.framework.qrcode;
+
+import cn.hutool.extra.qrcode.QrCodeUtil;
+import org.junit.jupiter.api.Test;
+
+import javax.imageio.ImageIO;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class QrCodeImageUtilsZipTest {
+
+ @Test
+ void writesMultipleReadablePngEntries() throws Exception {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (ZipOutputStream zip = new ZipOutputStream(bytes)) {
+ QrCodeImageUtils.writeZipEntry(zip, QrLabelInfo.builder()
+ .fileName("1234567890123456").code("1234567890123456").build());
+ QrCodeImageUtils.writeZipEntry(zip, QrLabelInfo.builder()
+ .fileName("1234567890123457").code("1234567890123457").build());
+ }
+
+ try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
+ assertEquals("1234567890123456.png", zip.getNextEntry().getName());
+ var first = ImageIO.read(zip);
+ assertNotNull(first);
+ assertEquals("1234567890123456", QrCodeUtil.decode(first));
+ assertEquals("1234567890123457.png", zip.getNextEntry().getName());
+ var second = ImageIO.read(zip);
+ assertNotNull(second);
+ assertEquals("1234567890123457", QrCodeUtil.decode(second));
+ assertNull(zip.getNextEntry());
+ }
+ }
+
+}
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskDownloadTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskDownloadTest.java
new file mode 100644
index 0000000..9ce139b
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskDownloadTest.java
@@ -0,0 +1,62 @@
+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 org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.lang.reflect.Proxy;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MesProQrExportTaskDownloadTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void rangeDownloadReturnsExactBytesAndKeepsFileForRetry() throws Exception {
+ byte[] content = "0123456789".getBytes();
+ Path file = tempDir.resolve("export.zip");
+ Files.write(file, content);
+ MesProQrExportTaskDO task = MesProQrExportTaskDO.builder()
+ .id(13L).taskName("test").status(MesProQrExportTaskStatusEnum.FINISHED.getStatus())
+ .filePath(file.toString()).build();
+ MesProQrExportTaskMapper mapper = (MesProQrExportTaskMapper) Proxy.newProxyInstance(
+ getClass().getClassLoader(), new Class[]{MesProQrExportTaskMapper.class},
+ (proxy, method, args) -> {
+ if (method.getName().equals("selectById")) {
+ return task;
+ }
+ throw new UnsupportedOperationException(method.getName());
+ });
+ MesProQrExportTaskServiceImpl service = new MesProQrExportTaskServiceImpl();
+ ReflectionTestUtils.setField(service, "taskMapper", mapper);
+
+ MockHttpServletResponse partial = new MockHttpServletResponse();
+ service.download(13L, "bytes=3-6", partial);
+ assertEquals(206, partial.getStatus());
+ assertEquals("bytes 3-6/10", partial.getHeader("Content-Range"));
+ assertEquals("4", partial.getHeader("Content-Length"));
+ assertArrayEquals("3456".getBytes(), partial.getContentAsByteArray());
+
+ MockHttpServletResponse invalid = new MockHttpServletResponse();
+ service.download(13L, "bytes=10-11", invalid);
+ assertEquals(416, invalid.getStatus());
+ assertEquals("bytes */10", invalid.getHeader("Content-Range"));
+
+ MockHttpServletResponse full = new MockHttpServletResponse();
+ service.download(13L, null, full);
+ assertEquals(200, full.getStatus());
+ assertArrayEquals(content, full.getContentAsByteArray());
+ assertTrue(Files.exists(file));
+ }
+
+}
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index 3f8c1ca..d351052 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -28,8 +28,8 @@
max-concurrent-tasks: 1
# 鍗曚釜鍚庡彴浠诲姟鏈�闀胯繍琛屾椂闂达紙鍒嗛挓锛夛紝瓒呮椂鑷姩鍙栨秷骞舵竻鐞�
max-runtime-minutes: 30
- # 涓存椂 ZIP 淇濈暀鏃堕棿锛堝垎閽燂級銆備笅杞藉畬鎴愬悗绔嬪嵆鍒犻櫎锛屾澶勪粎鍏滃簳銆屼笅杞戒腑鏂�/濮嬬粓鏈笅杞姐�嶇殑娈嬬暀
- temp-file-retention-minutes: 10
+ # 涓存椂 ZIP 淇濈暀鏃堕棿锛堝垎閽燂級銆傚垎娈典笅杞藉強涓柇閲嶈瘯鏈熼棿淇濈暀锛屽埌鏈熷悗娓呯悊
+ temp-file-retention-minutes: 60
# 涓存椂 ZIP 鐩綍锛岀暀绌哄垯浣跨敤 ${java.io.tmpdir}/mes-qr-export
temp-dir: ''
--
Gitblit v1.9.3