Răsfoiți Sursa

Merge remote-tracking branch 'origin/master'

Gogs 3 luni în urmă
părinte
comite
137d33cc80

+ 20 - 28
dtm-admin/src/main/java/com/dtm/web/controller/order/shop/ShopDataImportController.java

@@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Collections;
 
 @Anonymous
 @RestController
@@ -92,9 +93,7 @@ public class ShopDataImportController {
             Map<String, Double> contributionData = shopAnalysisService.getChannelSalesContribution();
 
             if (contributionData == null || contributionData.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No sales data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
             }
 
             response.put("success", true);
@@ -115,9 +114,7 @@ public class ShopDataImportController {
             Map<String, Double> roiData = shopAnalysisService.getChannelRoiValue();
 
             if (roiData == null || roiData.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No sales data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
             }
 
             response.put("success", true);
@@ -137,9 +134,7 @@ public class ShopDataImportController {
         List<Map<String, Object>> data = shopAnalysisService.getUnitContribution();
 
         if (data == null || data.isEmpty()) {
-            response.put("success", false);
-            response.put("message", "No data found.");
-            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
         }
 
         response.put("success", true);
@@ -154,9 +149,7 @@ public class ShopDataImportController {
         List<Map<String, Object>> data = shopAnalysisService.getChannelTotalContribution();
 
         if (data == null || data.isEmpty()) {
-            response.put("success", false);
-            response.put("message", "No data found.");
-            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
         }
 
         response.put("success", true);
@@ -171,9 +164,7 @@ public class ShopDataImportController {
         List<Map<String, Object>> data = shopAnalysisService.getPlatformTotalContribution();
 
         if (data == null || data.isEmpty()) {
-            response.put("success", false);
-            response.put("message", "No data found.");
-            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
         }
 
         response.put("success", true);
@@ -189,9 +180,7 @@ public class ShopDataImportController {
             Map<String, Object> result = shopAnalysisService.getTopProductContribution();
 
             if (result == null || result.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
             }
 
             response.put("success", true);
@@ -212,9 +201,7 @@ public class ShopDataImportController {
             List<Map<String, Object>> data = shopAnalysisService.getCrossSellingProducts();
 
             if (data == null || data.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
             }
 
             response.put("success", true);
@@ -235,9 +222,7 @@ public class ShopDataImportController {
             Map<String, Double> data = shopAnalysisService.getDepartmentOperationalEfficiency();
 
             if (data == null || data.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
             }
 
             response.put("success", true);
@@ -258,9 +243,7 @@ public class ShopDataImportController {
             Map<String, Long> data = shopAnalysisService.getChannelProductDiversity();
 
             if (data == null || data.isEmpty()) {
-                response.put("success", false);
-                response.put("message", "No data found.");
-                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
             }
 
             response.put("success", true);
@@ -273,4 +256,13 @@ public class ShopDataImportController {
             return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
         }
     }
-}
+
+    private Map<String, Object> buildEmptyResponse(String message, Object data) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("success", true);
+        response.put("empty", true);
+        response.put("message", message);
+        response.put("data", data);
+        return response;
+    }
+}

+ 1 - 1
dtm-admin/src/main/java/com/dtm/web/controller/storage/StorageUploadController.java

@@ -1,4 +1,4 @@
-package com.dtm.web.controller.storage;
+package com.dtm.web.controller.storage;
 
 import com.dtm.common.annotation.Anonymous;
 import com.dtm.common.core.domain.AjaxResult;

+ 43 - 1
dtm-admin/src/main/resources/application-druid.yml

@@ -58,4 +58,46 @@ spring:
                     merge-sql: true
                 wall:
                     config:
-                        multi-statement-allow: true
+                        multi-statement-allow: true
+
+# 项目相关配置
+dtm:
+    # 名称
+    name: dtm
+    # 版本
+    version: 3.9.0
+    # 版权年份
+    copyrightYear: 2025
+    # 文件路径 示例( Windows配置D:/dtm/uploadPath,Linux配置 /home/dtm/uploadPath)
+    profile: D:/dtm/uploadPath
+    # 获取ip地址开关
+    addressEnabled: false
+    # 验证码类型 math 数字计算 char 字符验证
+    captchaType: math
+
+# 开发环境配置
+server:
+    # 服务器的HTTP端口,默认为8080
+    port: 8080
+    servlet:
+        # 应用的访问路径
+        context-path: /
+    tomcat:
+        # tomcat的URI编码
+        uri-encoding: UTF-8
+        # 连接数满后的排队数,默认为100
+        accept-count: 1000
+        threads:
+            # tomcat最大线程数,默认为200
+            max: 800
+            # Tomcat启动初始化的线程数,默认值10
+            min-spare: 100
+
+# 订单监测数据目录配置
+data:
+    folder:
+        path: C:/Users/akiby/Documents/dingdandata
+shop:
+    data:
+        folder:
+            path: C:/Users/akiby/Documents/shopdata

+ 13 - 4
dtm-admin/src/main/resources/application-prod.yml

@@ -6,7 +6,7 @@ spring:
         druid:
             # 主库数据源
             master:
-                url: jdbc:mysql://mysql:3306/dtm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
+                url: jdbc:mysql://mysql:3306/dtm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
                 username: root
                 password: dtm@1915116
             # 从库数据源
@@ -102,9 +102,9 @@ server:
       min-spare: 100
 
 # 项目相关配置
-ruoyi:
+dtm:
   # 名称
-  name: RuoYi
+  name: dtm
   # 版本
   version: 3.9.0
   # 版权年份
@@ -114,4 +114,13 @@ ruoyi:
   # 获取ip地址开关
   addressEnabled: false
   # 验证码类型 math 数字计算 char 字符验证
-  captchaType: math
+  captchaType: math
+
+# 订单监测数据目录配置
+data:
+  folder:
+    path: /home/dtm/data  # ✅ Linux 路径
+shop:
+  data:
+    folder:
+      path: /home/dtm/shopdata

+ 1 - 43
dtm-admin/src/main/resources/application.yml

@@ -1,37 +1,4 @@
-# 项目相关配置
-ruoyi:
-  # 名称
-  name: RuoYi
-  # 版本
-  version: 3.9.0
-  # 版权年份
-  copyrightYear: 2025
-  # 文件路径 示例( Windows配置D:/dtm/uploadPath,Linux配置 /home/dtm/uploadPath)
-  profile: D:/dtm/uploadPath
-  # 获取ip地址开关
-  addressEnabled: false
-  # 验证码类型 math 数字计算 char 字符验证
-  captchaType: math
-
-# 开发环境配置
-server:
-  # 服务器的HTTP端口,默认为8080
-  port: 8080
-  servlet:
-    # 应用的访问路径
-    context-path: /
-  tomcat:
-    # tomcat的URI编码
-    uri-encoding: UTF-8
-    # 连接数满后的排队数,默认为100
-    accept-count: 1000
-    threads:
-      # tomcat最大线程数,默认为200
-      max: 800
-      # Tomcat启动初始化的线程数,默认值10
-      min-spare: 100
-
-# 日志配置
+# 日志配置
 logging:
   level:
     com.dtm: debug
@@ -141,15 +108,6 @@ flask:
     # Flask服务基础URL
     base-url: http://localhost:8085
 
-# 订单监测数据目录配置
-data:
-  folder:
-    path: C:/Users/akiby/Documents/dingdandata
-shop:
-  data:
-    folder:
-      path: C:/Users/akiby/Documents/shopdata
-
 # 仓储分析数据目录
 storage:
   data:

+ 1 - 1
dtm-system/src/main/java/com/dtm/storage/service/InventoryService.java

@@ -1,4 +1,4 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import com.dtm.storage.config.StorageSettings;
 import com.dtm.storage.model.AssemblyRecord;

+ 1 - 1
dtm-system/src/main/java/com/dtm/storage/service/InventoryUploadTaskService.java

@@ -1,4 +1,4 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;

+ 138 - 117
dtm-system/src/main/java/com/dtm/storage/service/StorageDataLoader.java

@@ -149,113 +149,140 @@ public class StorageDataLoader {
     }
 
     private List<PurchaseRecord> loadPurchaseRecords() {
-        ExcelSheet sheet = loadPurchaseSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findPurchaseFile();
+        if (file == null) {
             return Collections.emptyList();
         }
 
-        List<String> headers = sheet.getHeaders();
-        int dateIdx = findHeaderIndex(headers, new String[]{"业务日期", "日期"}, 0);
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 1);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 2);
-        int amountIdx = findHeaderIndex(headers, new String[]{"实际金额", "金额"}, 3);
-
         List<PurchaseRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"业务日期", "日期"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 2);
+                indexes[3] = findHeaderIndex(headers, new String[]{"实际金额", "金额"}, 3);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            double amount = toDouble(getValue(row, amountIdx));
-            records.add(new PurchaseRecord(code, date, qty, amount));
-        }
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[1])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[0]));
+                double qty = toDouble(getValue(row, indexes[2]));
+                double amount = toDouble(getValue(row, indexes[3]));
+                records.add(new PurchaseRecord(code, date, qty, amount));
+            }
+        });
         return records;
     }
 
     private List<SalesRecord> loadSalesRecords() {
-        ExcelSheet sheet = loadSalesSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findSalesFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"商家编码", "产品编码", "产品代码", "编码"}, 0);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"购买数量", "数量"}, 1);
-        int dateIdx = findHeaderIndex(headers, new String[]{"订单创建时间", "创建时间", "日期"}, 2);
 
         List<SalesRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"商家编码", "产品编码", "产品代码", "编码"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"购买数量", "数量"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"订单创建时间", "创建时间", "日期"}, 2);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            records.add(new SalesRecord(code, date, qty));
-        }
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[2]));
+                double qty = toDouble(getValue(row, indexes[1]));
+                records.add(new SalesRecord(code, date, qty));
+            }
+        });
         return records;
     }
 
     private List<AssemblyRecord> loadAssemblyRecords() {
-        ExcelSheet sheet = loadAssemblySheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findAssemblyFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品编码", "产品代码", "半成品"}, 0);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 1);
-        int dateIdx = findHeaderIndex(headers, new String[]{"日期", "时间"}, 2);
 
         List<AssemblyRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1};
+        ExcelUtils.processSheet(file, 1, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"产品编码", "产品代码", "半成品"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"日期", "时间"}, 2);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            if (qty <= 0) {
-                continue;
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[2]));
+                double qty = toDouble(getValue(row, indexes[1]));
+                if (qty <= 0) {
+                    return;
+                }
+                records.add(new AssemblyRecord(code, date, qty));
             }
-            records.add(new AssemblyRecord(code, date, qty));
-        }
+        });
         return records;
     }
 
     private List<ProductInfo> loadProductInfo() {
-        ExcelSheet sheet = loadProductInfoSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findProductInfoFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 0);
-        int nameIdx = findHeaderIndex(headers, new String[]{"产品名称", "名称"}, 1);
-        int categoryIdx = findHeaderIndex(headers, new String[]{"分类", "成品", "半成品", "辅料"}, 4);
-        int attributeIdx = findHeaderIndex(headers, new String[]{"属性", "等级", "ABC"}, 10);
-        int spuIdx = findHeaderIndex(headers, new String[]{"SPU"}, 5);
-        int priceIdx = findHeaderIndex(headers, new String[]{"价格", "单价"}, 6);
 
         List<ProductInfo> infos = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1, -1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"产品名称", "名称"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"分类", "成品", "半成品", "辅料"}, 4);
+                indexes[3] = findHeaderIndex(headers, new String[]{"属性", "等级", "ABC"}, 10);
+                indexes[4] = findHeaderIndex(headers, new String[]{"SPU"}, 5);
+                indexes[5] = findHeaderIndex(headers, new String[]{"价格", "单价"}, 6);
             }
-            String name = toText(getValue(row, nameIdx)).trim();
-            String category = toText(getValue(row, categoryIdx)).trim();
-            String attribute = toText(getValue(row, attributeIdx)).trim();
-            String spu = toText(getValue(row, spuIdx)).trim();
-            Double price = null;
-            Object priceObj = getValue(row, priceIdx);
-            if (priceObj != null) {
-                double parsed = toDouble(priceObj);
-                if (parsed > 0) {
-                    price = parsed;
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                String name = toText(getValue(row, indexes[1])).trim();
+                String category = toText(getValue(row, indexes[2])).trim();
+                String attribute = toText(getValue(row, indexes[3])).trim();
+                String spu = toText(getValue(row, indexes[4])).trim();
+                Double price = null;
+                Object priceValue = getValue(row, indexes[5]);
+                if (priceValue != null) {
+                    double parsed = toDouble(priceValue);
+                    if (parsed > 0) {
+                        price = parsed;
+                    }
                 }
+                infos.add(new ProductInfo(code, name, category, attribute, spu, price));
             }
-            infos.add(new ProductInfo(code, name, category, attribute, spu, price));
-        }
+        });
         return infos;
     }
 
@@ -275,38 +302,6 @@ public class StorageDataLoader {
         return readSheet(file, 0);
     }
 
-    private ExcelSheet loadPurchaseSheet() {
-        Path file = findPurchaseFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
-    private ExcelSheet loadSalesSheet() {
-        Path file = findSalesFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
-    private ExcelSheet loadAssemblySheet() {
-        Path file = findAssemblyFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 1);
-    }
-
-    private ExcelSheet loadProductInfoSheet() {
-        Path file = findProductInfoFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
     private ExcelSheet readSheet(Path file, int headerRowIndex) {
         if (tempMode) {
             return ExcelUtils.readSheet(file, headerRowIndex, TEMP_MAX_ROWS);
@@ -314,6 +309,10 @@ public class StorageDataLoader {
         return ExcelUtils.readSheet(file, headerRowIndex);
     }
 
+    private int resolveRowLimit() {
+        return tempMode ? TEMP_MAX_ROWS : -1;
+    }
+
     private Path resolveBasePath() {
         String envPath = Optional.ofNullable(System.getenv("DTM_DATA_PATH"))
                 .filter(v -> !v.trim().isEmpty())
@@ -356,38 +355,45 @@ public class StorageDataLoader {
     }
 
     private Path findPurchaseFile() {
-        Path dir = resolveDir("purchase");
+        Path dir = resolveDir("purchase", "入库数据");
         return findFile(dir, "purchase", "入库", "采购");
     }
 
     private Path findSalesFile() {
-        Path dir = resolveDir("sales");
+        Path dir = resolveDir("sales", "销售数据");
         return findFile(dir, "sales", "订单", "销售");
     }
 
     private Path findAssemblyFile() {
-        Path dir = resolveDir("assembly");
+        Path dir = resolveDir("assembly", "半成品组装");
         return findFile(dir, "assembly", "组装");
     }
 
     private Path findSemiMappingFile() {
-        Path dir = resolveDir("semi-mapping");
+        Path dir = resolveDir("semi-mapping", "半成品组装");
         return findFile(dir, "semi-mapping", "匹配");
     }
 
     private Path findProductInfoFile() {
-        Path dir = resolveDir("product");
+        Path dir = resolveDir("product", "产品资料", "入库数据");
         return findFile(dir, "product", "产品", "资料");
     }
 
-    private Path resolveDir(String dirName) {
+    private Path resolveDir(String... dirNames) {
         ensureTempValid();
         if (basePath == null) {
             return null;
         }
-        Path direct = basePath.resolve(dirName);
-        if (Files.exists(direct)) {
-            return direct;
+        if (dirNames != null) {
+            for (String dirName : dirNames) {
+                if (dirName == null || dirName.trim().isEmpty()) {
+                    continue;
+                }
+                Path candidate = basePath.resolve(dirName);
+                if (Files.exists(candidate)) {
+                    return candidate;
+                }
+            }
         }
         return basePath;
     }
@@ -427,7 +433,7 @@ public class StorageDataLoader {
         try (Stream<Path> stream = Files.list(dir)) {
             List<Path> candidates = stream
                     .filter(Files::isRegularFile)
-                    .filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".xlsx"))
+                    .filter(this::isExcelFile)
                     .sorted(Comparator.comparing(Path::getFileName))
                     .collect(Collectors.toList());
 
@@ -452,6 +458,14 @@ public class StorageDataLoader {
         }
     }
 
+    private boolean isExcelFile(Path path) {
+        if (path == null || path.getFileName() == null) {
+            return false;
+        }
+        String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
+        return name.endsWith(".xlsx") || name.endsWith(".xls");
+    }
+
     private int findHeaderIndex(List<String> headers, String[] keywords, int fallback) {
         if (headers == null || headers.isEmpty()) {
             return fallback;
@@ -531,6 +545,16 @@ public class StorageDataLoader {
             return null;
         }
         text = text.replace('/', '-').replace('.', '-');
+        if (text.matches("\\d+(\\.0+)?")) {
+            try {
+                double numeric = Double.parseDouble(text);
+                if (DateUtil.isValidExcelDate(numeric)) {
+                    Date date = DateUtil.getJavaDate(numeric);
+                    return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDate();
+                }
+            } catch (Exception ignored) {
+            }
+        }
         List<String> patterns = new ArrayList<>();
         patterns.add("yyyy-M-d");
         patterns.add("yyyy-MM-dd");
@@ -582,6 +606,3 @@ public class StorageDataLoader {
         }
     }
 }
-
-
-

+ 1 - 1
dtm-system/src/main/java/com/dtm/storage/service/StorageUploadService.java

@@ -1,4 +1,4 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;

+ 202 - 17
dtm-system/src/main/java/com/dtm/storage/util/ExcelUtils.java

@@ -1,5 +1,7 @@
 package com.dtm.storage.util;
 
+import org.apache.poi.ooxml.util.SAXHelper;
+import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.CellType;
 import org.apache.poi.ss.usermodel.DataFormatter;
@@ -9,57 +11,114 @@ import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
+import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
+import org.apache.poi.xssf.eventusermodel.XSSFReader;
+import org.apache.poi.xssf.model.StylesTable;
+import org.apache.poi.xssf.usermodel.XSSFComment;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
 
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 
 public final class ExcelUtils {
     private ExcelUtils() {
     }
 
+    public interface SheetHandler {
+        void onHeader(List<String> headers);
+
+        void onRow(List<Object> row);
+    }
+
     public static ExcelSheet readSheet(Path filePath, int headerRowIndex) {
         return readSheet(filePath, headerRowIndex, -1);
     }
 
     public static ExcelSheet readSheet(Path filePath, int headerRowIndex, int maxRows) {
         if (filePath == null || !Files.exists(filePath)) {
-            return new ExcelSheet(new ArrayList<>(), new ArrayList<>());
+            return emptySheet();
         }
 
         List<String> headers = new ArrayList<>();
         List<List<Object>> rows = new ArrayList<>();
-        DataFormatter formatter = new DataFormatter();
+        processSheet(filePath, headerRowIndex, maxRows, new SheetHandler() {
+            @Override
+            public void onHeader(List<String> headerRow) {
+                headers.clear();
+                headers.addAll(headerRow);
+            }
+
+            @Override
+            public void onRow(List<Object> row) {
+                rows.add(row);
+            }
+        });
+        return new ExcelSheet(headers, rows);
+    }
+
+    public static void processSheet(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) {
+        if (filePath == null || !Files.exists(filePath) || handler == null) {
+            return;
+        }
+
+        String fileName = filePath.getFileName() == null ? "" : filePath.getFileName().toString().toLowerCase(Locale.ROOT);
+        if (fileName.endsWith(".xlsx")) {
+            try {
+                processXlsx(filePath, headerRowIndex, maxRows, handler);
+                return;
+            } catch (StopProcessingException ignored) {
+                return;
+            } catch (Exception ignored) {
+            }
+        }
+
+        try {
+            processWorkbook(filePath, headerRowIndex, maxRows, handler);
+        } catch (StopProcessingException ignored) {
+        } catch (Exception ignored) {
+        }
+    }
 
+    private static void processWorkbook(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) throws Exception {
+        DataFormatter formatter = new DataFormatter();
         try (InputStream inputStream = Files.newInputStream(filePath);
              Workbook workbook = WorkbookFactory.create(inputStream)) {
             Sheet sheet = workbook.getNumberOfSheets() > 0 ? workbook.getSheetAt(0) : null;
             if (sheet == null) {
-                return new ExcelSheet(headers, rows);
+                return;
             }
 
             FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
             Row headerRow = sheet.getRow(headerRowIndex);
             if (headerRow == null) {
-                return new ExcelSheet(headers, rows);
+                return;
             }
 
-            int maxCol = headerRow.getLastCellNum();
-            if (maxCol < 0) {
-                return new ExcelSheet(headers, rows);
+            int maxCol = Math.max(0, headerRow.getLastCellNum());
+            if (maxCol == 0) {
+                return;
             }
 
+            List<String> headers = new ArrayList<>(maxCol);
             for (int c = 0; c < maxCol; c++) {
                 Cell cell = headerRow.getCell(c);
                 String raw = formatter.formatCellValue(cell, evaluator);
                 headers.add(raw == null ? "" : raw.trim());
             }
+            handler.onHeader(headers);
 
+            int emitted = 0;
             for (int r = headerRowIndex + 1; r <= sheet.getLastRowNum(); r++) {
-                if (maxRows > 0 && rows.size() >= maxRows) {
+                if (maxRows > 0 && emitted >= maxRows) {
                     break;
                 }
                 Row row = sheet.getRow(r);
@@ -69,22 +128,39 @@ public final class ExcelUtils {
                 List<Object> values = new ArrayList<>(maxCol);
                 boolean hasValue = false;
                 for (int c = 0; c < maxCol; c++) {
-                    Cell cell = row.getCell(c);
-                    Object value = readCellValue(cell, formatter, evaluator);
-                    if (value != null && !(value instanceof String && ((String) value).trim().isEmpty())) {
+                    Object value = readCellValue(row.getCell(c), formatter, evaluator);
+                    if (!isBlankValue(value)) {
                         hasValue = true;
                     }
                     values.add(value);
                 }
-                if (hasValue) {
-                    rows.add(values);
+                if (!hasValue) {
+                    continue;
                 }
+                handler.onRow(values);
+                emitted++;
             }
-        } catch (Exception ignored) {
-            return new ExcelSheet(new ArrayList<>(), new ArrayList<>());
         }
+    }
 
-        return new ExcelSheet(headers, rows);
+    private static void processXlsx(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) throws Exception {
+        try (OPCPackage pkg = OPCPackage.open(filePath.toFile())) {
+            ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
+            XSSFReader reader = new XSSFReader(pkg);
+            StylesTable styles = reader.getStylesTable();
+            XSSFReader.SheetIterator iterator = (XSSFReader.SheetIterator) reader.getSheetsData();
+            if (!iterator.hasNext()) {
+                return;
+            }
+            try (InputStream sheetStream = iterator.next()) {
+                XMLReader parser = SAXHelper.newXMLReader();
+                DataFormatter formatter = new DataFormatter();
+                XSSFSheetXMLHandler.SheetContentsHandler sheetHandler = new StreamingSheetHandler(headerRowIndex, maxRows, handler);
+                XSSFSheetXMLHandler xssfHandler = new XSSFSheetXMLHandler(styles, null, strings, sheetHandler, formatter, false);
+                parser.setContentHandler(xssfHandler);
+                parser.parse(new InputSource(sheetStream));
+            }
+        }
     }
 
     private static Object readCellValue(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator) {
@@ -115,6 +191,115 @@ public final class ExcelUtils {
         String value = formatter.formatCellValue(cell, evaluator);
         return value == null ? null : value.trim();
     }
-}
 
+    private static boolean isBlankValue(Object value) {
+        return value == null || (value instanceof String && ((String) value).trim().isEmpty());
+    }
+
+    private static ExcelSheet emptySheet() {
+        return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
+    }
+
+    private static class StreamingSheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
+        private final int headerRowIndex;
+        private final int maxRows;
+        private final SheetHandler delegate;
+        private List<String> headers = Collections.emptyList();
+        private List<Object> currentRow = new ArrayList<>();
+        private int emittedRows;
+
+        private StreamingSheetHandler(int headerRowIndex, int maxRows, SheetHandler delegate) {
+            this.headerRowIndex = headerRowIndex;
+            this.maxRows = maxRows;
+            this.delegate = delegate;
+        }
 
+        @Override
+        public void startRow(int rowNum) {
+            currentRow = new ArrayList<>();
+        }
+
+        @Override
+        public void endRow(int rowNum) {
+            trimTrailingNulls(currentRow);
+            if (rowNum < headerRowIndex) {
+                return;
+            }
+            if (rowNum == headerRowIndex) {
+                headers = toHeaders(currentRow);
+                delegate.onHeader(headers);
+                return;
+            }
+            if (headers.isEmpty()) {
+                return;
+            }
+            if (maxRows > 0 && emittedRows >= maxRows) {
+                throw new StopProcessingException();
+            }
+            List<Object> row = new ArrayList<>(Math.max(headers.size(), currentRow.size()));
+            row.addAll(currentRow);
+            while (row.size() < headers.size()) {
+                row.add(null);
+            }
+            if (!hasValue(row)) {
+                return;
+            }
+            delegate.onRow(row);
+            emittedRows++;
+            if (maxRows > 0 && emittedRows >= maxRows) {
+                throw new StopProcessingException();
+            }
+        }
+
+        @Override
+        public void cell(String cellReference, String formattedValue, XSSFComment comment) {
+            int columnIndex = resolveColumnIndex(cellReference, currentRow.size());
+            while (currentRow.size() < columnIndex) {
+                currentRow.add(null);
+            }
+            String value = formattedValue == null ? null : formattedValue.trim();
+            currentRow.add(value == null || value.isEmpty() ? null : value);
+        }
+
+        @Override
+        public void headerFooter(String text, boolean isHeader, String tagName) {
+        }
+
+        private int resolveColumnIndex(String cellReference, int fallback) {
+            if (cellReference == null || cellReference.trim().isEmpty()) {
+                return fallback;
+            }
+            return new CellReference(cellReference).getCol();
+        }
+
+        private List<String> toHeaders(List<Object> row) {
+            List<String> result = new ArrayList<>(row.size());
+            for (Object value : row) {
+                result.add(value == null ? "" : String.valueOf(value).trim());
+            }
+            return result;
+        }
+
+        private boolean hasValue(List<Object> row) {
+            for (Object value : row) {
+                if (!isBlankValue(value)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void trimTrailingNulls(List<Object> row) {
+            for (int i = row.size() - 1; i >= 0; i--) {
+                Object value = row.get(i);
+                if (!isBlankValue(value)) {
+                    break;
+                }
+                row.remove(i);
+            }
+        }
+    }
+
+    private static class StopProcessingException extends RuntimeException {
+    }
+}

+ 6 - 6
pom.xml

@@ -211,11 +211,11 @@
                 <version>${ruoyi.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>com.dtm</groupId>
-                <artifactId>dtm-common</artifactId>
-                <version>${ruoyi.version}</version>
-            </dependency>
+            <dependency>
+                <groupId>com.dtm</groupId>
+                <artifactId>dtm-common</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
 
         </dependencies>
     </dependencyManagement>
@@ -224,7 +224,7 @@
         <module>dtm-admin</module>
         <module>dtm-framework</module>
         <module>dtm-system</module>
-        <module>dtm-quartz</module>
+        <module>dtm-quartz</module>
         <module>dtm-generator</module>
         <module>dtm-common</module>
     </modules>