2 Commits e8b57e3bbb ... 2a72e915b6

Autor SHA1 Mensaje Fecha
  Gogs 2a72e915b6 订单监测连接数据库 hace 1 mes
  Gogs 6ab577eeca 订单监测_筛选功能 hace 1 mes
Se han modificado 20 ficheros con 1359 adiciones y 267 borrados
  1. BIN
      data/storage/入库数据/2025采购入库单_1119.xlsx
  2. BIN
      data/storage/入库数据/产品资料.xlsx
  3. BIN
      data/storage/半成品组装/半成品匹配成品编码明细.xlsx
  4. BIN
      data/storage/半成品组装/组装明细 - 1107.xlsx
  5. BIN
      data/storage/销售数据/2025年订单数据.xlsx
  6. 3 2
      dtm-admin/src/main/java/com/dtm/web/controller/order/AnalysisController.java
  7. 82 109
      dtm-admin/src/main/java/com/dtm/web/controller/order/shop/ShopDataImportController.java
  8. 155 0
      dtm-system/src/main/java/com/dtm/order/domain/OrderAnalyticsRecord.java
  9. 40 0
      dtm-system/src/main/java/com/dtm/order/domain/OrderFunnelSummary.java
  10. 22 0
      dtm-system/src/main/java/com/dtm/order/domain/OrderLeakageSummary.java
  11. 38 16
      dtm-system/src/main/java/com/dtm/order/dto/CoPurchaseDTO.java
  12. 3 0
      dtm-system/src/main/java/com/dtm/order/dto/ProductDTO.java
  13. 37 0
      dtm-system/src/main/java/com/dtm/order/mapper/OrderAnalyticsMapper.java
  14. 190 45
      dtm-system/src/main/java/com/dtm/order/service/AnalysisService.java
  15. 36 0
      dtm-system/src/main/java/com/dtm/order/shop/mapper/ShopValueMapper.java
  16. 264 25
      dtm-system/src/main/java/com/dtm/order/shop/service/ShopAnalysisService.java
  17. 71 70
      dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesDataStore.java
  18. 3 0
      dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesRecord.java
  19. 251 0
      dtm-system/src/main/resources/mapper/order/OrderAnalyticsMapper.xml
  20. 164 0
      dtm-system/src/main/resources/mapper/order/shop/ShopValueMapper.xml

BIN
data/storage/入库数据/2025采购入库单_1119.xlsx


BIN
data/storage/入库数据/产品资料.xlsx


BIN
data/storage/半成品组装/半成品匹配成品编码明细.xlsx


BIN
data/storage/半成品组装/组装明细 - 1107.xlsx


BIN
data/storage/销售数据/2025年订单数据.xlsx


+ 3 - 2
dtm-admin/src/main/java/com/dtm/web/controller/order/AnalysisController.java

@@ -89,8 +89,9 @@ public class AnalysisController {
     }
 
     @RequestMapping(value = "/co-purchase", method = RequestMethod.GET)
-    public List<CoPurchaseDTO> getCoPurchaseRules() {
-        return analysisService.findCoPurchaseRules();
+    public List<CoPurchaseDTO> getCoPurchaseRules(
+            @RequestParam(required = false) String skuKeyword) {
+        return analysisService.findCoPurchaseRules(skuKeyword);
     }
 
     @GetMapping("/average-payment-time")

+ 82 - 109
dtm-admin/src/main/java/com/dtm/web/controller/order/shop/ShopDataImportController.java

@@ -13,10 +13,10 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Collections;
 
 @Anonymous
 @RestController
@@ -38,6 +38,11 @@ public class ShopDataImportController {
         return "Shop data store ready. Path: " + shopDataFolderPath;
     }
 
+    @GetMapping("/max-date")
+    public String getMaxSalesDate() {
+        return shopAnalysisService.getMaxSalesDate();
+    }
+
     @GetMapping("/import-sales-data")
     public ResponseEntity<Map<String, Object>> importSalesData() {
         Map<String, Object> response = new HashMap<>();
@@ -87,176 +92,137 @@ public class ShopDataImportController {
     }
 
     @GetMapping("/channel-contribution")
-    public ResponseEntity<Map<String, Object>> getChannelContribution() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> contributionData = shopAnalysisService.getChannelSalesContribution();
-
+            Map<String, Double> contributionData = shopAnalysisService.getChannelSalesContribution(startDate, endDate);
             if (contributionData == null || contributionData.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel contribution ready.");
-            response.put("data", contributionData);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel contribution ready.", contributionData));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/channel-roi-value")
-    public ResponseEntity<Map<String, Object>> getChannelRoiValue() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelRoiValue(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> roiData = shopAnalysisService.getChannelRoiValue();
-
+            Map<String, Double> roiData = shopAnalysisService.getChannelRoiValue(startDate, endDate);
             if (roiData == null || roiData.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel ROI ready.");
-            response.put("data", roiData);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel ROI ready.", roiData));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/unit-contribution")
-    public ResponseEntity<Map<String, Object>> getUnitContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getUnitContribution();
-
+    public ResponseEntity<Map<String, Object>> getUnitContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getUnitContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Unit contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Unit contribution ready.", data));
     }
 
     @GetMapping("/channel-total-contribution")
-    public ResponseEntity<Map<String, Object>> getChannelTotalContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getChannelTotalContribution();
-
+    public ResponseEntity<Map<String, Object>> getChannelTotalContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getChannelTotalContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Channel total contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Channel total contribution ready.", data));
     }
 
     @GetMapping("/platform-total-contribution")
-    public ResponseEntity<Map<String, Object>> getPlatformTotalContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getPlatformTotalContribution();
-
+    public ResponseEntity<Map<String, Object>> getPlatformTotalContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getPlatformTotalContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Platform total contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Platform total contribution ready.", data));
     }
 
     @GetMapping("/top-product-contribution")
-    public ResponseEntity<Map<String, Object>> getTopProductContribution() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getTopProductContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Object> result = shopAnalysisService.getTopProductContribution();
-
+            Map<String, Object> result = shopAnalysisService.getTopProductContribution(startDate, endDate);
             if (result == null || result.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Top product contribution ready.");
-            response.put("data", result);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Top product contribution ready.", result));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/cross-selling-products")
-    public ResponseEntity<Map<String, Object>> getCrossSellingProducts() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getCrossSellingProducts(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
+            @RequestParam(required = false) String skuKeyword) {
         try {
-            List<Map<String, Object>> data = shopAnalysisService.getCrossSellingProducts();
-
+            List<Map<String, Object>> data = shopAnalysisService.getCrossSellingProducts(startDate, endDate, skuKeyword);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 件或调整 SKU 条件", Collections.emptyList()));
             }
-
-            response.put("success", true);
-            response.put("message", "Cross-selling products ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Cross-selling products ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/department-efficiency")
-    public ResponseEntity<Map<String, Object>> getDepartmentOperationalEfficiency() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getDepartmentOperationalEfficiency(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> data = shopAnalysisService.getDepartmentOperationalEfficiency();
-
+            Map<String, Double> data = shopAnalysisService.getDepartmentOperationalEfficiency(startDate, endDate);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Department efficiency ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Department efficiency ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/channel-diversity")
-    public ResponseEntity<Map<String, Object>> getChannelProductDiversity() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelProductDiversity(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Long> data = shopAnalysisService.getChannelProductDiversity();
-
+            Map<String, Long> data = shopAnalysisService.getChannelProductDiversity(startDate, endDate);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel diversity ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel diversity ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
+    private Map<String, Object> buildSuccessResponse(String message, Object data) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("success", true);
+        response.put("message", message);
+        response.put("data", data);
+        return response;
+    }
+
     private Map<String, Object> buildEmptyResponse(String message, Object data) {
         Map<String, Object> response = new HashMap<>();
         response.put("success", true);
@@ -265,4 +231,11 @@ public class ShopDataImportController {
         response.put("data", data);
         return response;
     }
+
+    private ResponseEntity<Map<String, Object>> buildFailureResponse(Exception e) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("success", false);
+        response.put("message", "Analysis failed: " + e.getMessage());
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+    }
 }

+ 155 - 0
dtm-system/src/main/java/com/dtm/order/domain/OrderAnalyticsRecord.java

@@ -0,0 +1,155 @@
+package com.dtm.order.domain;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+public class OrderAnalyticsRecord {
+    private String purchaseId;
+    private String orderId;
+    private String productId;
+    private String productTitle;
+    private String productProperties;
+    private String productMerchantCode;
+    private double orderPrice;
+    private int orderQuantity;
+    private String orderStatus;
+    private double orderPayable;
+    private double orderActualPayment;
+    private String orderRefundStatus;
+    private double orderRefundAmount;
+    private LocalDateTime createdTime;
+    private LocalDateTime paidTime;
+    private String purchasePaymentId;
+
+    public String getPurchaseId() {
+        return purchaseId;
+    }
+
+    public void setPurchaseId(String purchaseId) {
+        this.purchaseId = purchaseId;
+    }
+
+    public String getOrderId() {
+        return orderId;
+    }
+
+    public void setOrderId(String orderId) {
+        this.orderId = orderId;
+    }
+
+    public String getProductId() {
+        return productId;
+    }
+
+    public void setProductId(String productId) {
+        this.productId = productId;
+    }
+
+    public String getProductTitle() {
+        return productTitle;
+    }
+
+    public void setProductTitle(String productTitle) {
+        this.productTitle = productTitle;
+    }
+
+    public String getProductProperties() {
+        return productProperties;
+    }
+
+    public void setProductProperties(String productProperties) {
+        this.productProperties = productProperties;
+    }
+
+    public String getProductMerchantCode() {
+        return productMerchantCode;
+    }
+
+    public void setProductMerchantCode(String productMerchantCode) {
+        this.productMerchantCode = productMerchantCode;
+    }
+
+    public double getOrderPrice() {
+        return orderPrice;
+    }
+
+    public void setOrderPrice(double orderPrice) {
+        this.orderPrice = orderPrice;
+    }
+
+    public int getOrderQuantity() {
+        return orderQuantity;
+    }
+
+    public void setOrderQuantity(int orderQuantity) {
+        this.orderQuantity = orderQuantity;
+    }
+
+    public String getOrderStatus() {
+        return orderStatus;
+    }
+
+    public void setOrderStatus(String orderStatus) {
+        this.orderStatus = orderStatus;
+    }
+
+    public double getOrderPayable() {
+        return orderPayable;
+    }
+
+    public void setOrderPayable(double orderPayable) {
+        this.orderPayable = orderPayable;
+    }
+
+    public double getOrderActualPayment() {
+        return orderActualPayment;
+    }
+
+    public void setOrderActualPayment(double orderActualPayment) {
+        this.orderActualPayment = orderActualPayment;
+    }
+
+    public String getOrderRefundStatus() {
+        return orderRefundStatus;
+    }
+
+    public void setOrderRefundStatus(String orderRefundStatus) {
+        this.orderRefundStatus = orderRefundStatus;
+    }
+
+    public double getOrderRefundAmount() {
+        return orderRefundAmount;
+    }
+
+    public void setOrderRefundAmount(double orderRefundAmount) {
+        this.orderRefundAmount = orderRefundAmount;
+    }
+
+    public LocalDateTime getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(LocalDateTime createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    public LocalDateTime getPaidTime() {
+        return paidTime;
+    }
+
+    public void setPaidTime(LocalDateTime paidTime) {
+        this.paidTime = paidTime;
+    }
+
+    public String getPurchasePaymentId() {
+        return purchasePaymentId;
+    }
+
+    public void setPurchasePaymentId(String purchasePaymentId) {
+        this.purchasePaymentId = purchasePaymentId;
+    }
+
+    public LocalDate getCreatedDate() {
+        return createdTime == null ? null : createdTime.toLocalDate();
+    }
+}

+ 40 - 0
dtm-system/src/main/java/com/dtm/order/domain/OrderFunnelSummary.java

@@ -0,0 +1,40 @@
+package com.dtm.order.domain;
+
+public class OrderFunnelSummary {
+    private Long paidWithin5Mins;
+    private Long paidBetween5And30Mins;
+    private Long paidAfter30Mins;
+    private Long unpaidOrders;
+
+    public Long getPaidWithin5Mins() {
+        return paidWithin5Mins;
+    }
+
+    public void setPaidWithin5Mins(Long paidWithin5Mins) {
+        this.paidWithin5Mins = paidWithin5Mins;
+    }
+
+    public Long getPaidBetween5And30Mins() {
+        return paidBetween5And30Mins;
+    }
+
+    public void setPaidBetween5And30Mins(Long paidBetween5And30Mins) {
+        this.paidBetween5And30Mins = paidBetween5And30Mins;
+    }
+
+    public Long getPaidAfter30Mins() {
+        return paidAfter30Mins;
+    }
+
+    public void setPaidAfter30Mins(Long paidAfter30Mins) {
+        this.paidAfter30Mins = paidAfter30Mins;
+    }
+
+    public Long getUnpaidOrders() {
+        return unpaidOrders;
+    }
+
+    public void setUnpaidOrders(Long unpaidOrders) {
+        this.unpaidOrders = unpaidOrders;
+    }
+}

+ 22 - 0
dtm-system/src/main/java/com/dtm/order/domain/OrderLeakageSummary.java

@@ -0,0 +1,22 @@
+package com.dtm.order.domain;
+
+public class OrderLeakageSummary {
+    private Double totalRefundAmount;
+    private Double totalSuccessAmount;
+
+    public Double getTotalRefundAmount() {
+        return totalRefundAmount;
+    }
+
+    public void setTotalRefundAmount(Double totalRefundAmount) {
+        this.totalRefundAmount = totalRefundAmount;
+    }
+
+    public Double getTotalSuccessAmount() {
+        return totalSuccessAmount;
+    }
+
+    public void setTotalSuccessAmount(Double totalSuccessAmount) {
+        this.totalSuccessAmount = totalSuccessAmount;
+    }
+}

+ 38 - 16
dtm-system/src/main/java/com/dtm/order/dto/CoPurchaseDTO.java

@@ -3,12 +3,14 @@ package com.dtm.order.dto;
 public class CoPurchaseDTO {
 
     private String productA;
-    private String productAId; // <-- 鏂板锛佸晢鍝丄鐨勨€滆韩浠借瘉鍙封€?
+    private String productAId;
     private String productB;
-    private String productBId; // <-- 鏂板锛佸晢鍝丅鐨勨€滆韩浠借瘉鍙封€?
+    private String productBId;
     private Long coPurchaseCount;
 
-    // 鏋勯€犲嚱鏁颁篃瑕佸崌绾э紒
+    public CoPurchaseDTO() {
+    }
+
     public CoPurchaseDTO(String productA, String productAId, String productB, String productBId, Long coPurchaseCount) {
         this.productA = productA;
         this.productAId = productAId;
@@ -17,23 +19,43 @@ public class CoPurchaseDTO {
         this.coPurchaseCount = coPurchaseCount;
     }
 
-    // --- 涓嬮潰鏄墍鏈夊瓧娈电殑Getter鍜孲etter ---
-    // hina甯偍鎶婃柊澧炵殑涔熼兘琛ヤ笂鍟︼紒
+    public String getProductA() {
+        return productA;
+    }
 
-    public String getProductA() { return productA; }
-    public void setProductA(String productA) { this.productA = productA; }
+    public void setProductA(String productA) {
+        this.productA = productA;
+    }
 
-    public String getProductAId() { return productAId; }
-    public void setProductAId(String productAId) { this.productAId = productAId; }
+    public String getProductAId() {
+        return productAId;
+    }
 
-    public String getProductB() { return productB; }
-    public void setProductB(String productB) { this.productB = productB; }
+    public void setProductAId(String productAId) {
+        this.productAId = productAId;
+    }
 
-    public String getProductBId() { return productBId; }
-    public void setProductBId(String productBId) { this.productBId = productBId; }
+    public String getProductB() {
+        return productB;
+    }
 
-    public Long getCoPurchaseCount() { return coPurchaseCount; }
-    public void setCoPurchaseCount(Long coPurchaseCount) { this.coPurchaseCount = coPurchaseCount; }
-}
+    public void setProductB(String productB) {
+        this.productB = productB;
+    }
+
+    public String getProductBId() {
+        return productBId;
+    }
+
+    public void setProductBId(String productBId) {
+        this.productBId = productBId;
+    }
 
+    public Long getCoPurchaseCount() {
+        return coPurchaseCount;
+    }
 
+    public void setCoPurchaseCount(Long coPurchaseCount) {
+        this.coPurchaseCount = coPurchaseCount;
+    }
+}

+ 3 - 0
dtm-system/src/main/java/com/dtm/order/dto/ProductDTO.java

@@ -5,6 +5,9 @@ public class ProductDTO {
     private String sku;
     private Double totalSales;
 
+    public ProductDTO() {
+    }
+
     // 鏋勯€犲嚱鏁?
      public ProductDTO(String sku, String name, Double totalSales) {
         this.sku = sku;

+ 37 - 0
dtm-system/src/main/java/com/dtm/order/mapper/OrderAnalyticsMapper.java

@@ -0,0 +1,37 @@
+package com.dtm.order.mapper;
+
+import com.dtm.order.domain.OrderFunnelSummary;
+import com.dtm.order.domain.OrderAnalyticsRecord;
+import com.dtm.order.domain.OrderLeakageSummary;
+import com.dtm.order.dto.CoPurchaseDTO;
+import com.dtm.order.dto.ProductDTO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface OrderAnalyticsMapper {
+    Long countOrders();
+
+    String selectMaxOrderDate();
+
+    List<OrderAnalyticsRecord> selectOrders(
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate
+    );
+
+    Double selectTotalGmv(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<ProductDTO> selectTop5Products(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    Double selectTop5SalesTotal(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Double> selectPaymentAmounts(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    OrderLeakageSummary selectLeakageSummary(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    Double selectAveragePaymentSeconds(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    OrderFunnelSummary selectPaymentFunnel(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<CoPurchaseDTO> selectCoPurchaseRules(@Param("skuKeyword") String skuKeyword);
+}

+ 190 - 45
dtm-system/src/main/java/com/dtm/order/service/AnalysisService.java

@@ -1,7 +1,11 @@
 package com.dtm.order.service;
 
+import com.dtm.order.domain.OrderAnalyticsRecord;
+import com.dtm.order.domain.OrderFunnelSummary;
+import com.dtm.order.domain.OrderLeakageSummary;
 import com.dtm.order.dto.CoPurchaseDTO;
 import com.dtm.order.dto.ProductDTO;
+import com.dtm.order.mapper.OrderAnalyticsMapper;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -27,32 +31,45 @@ public class AnalysisService {
 
 
     private final OrderDataStore orderDataStore;
+    private final OrderAnalyticsMapper orderAnalyticsMapper;
 
     @Autowired
-    public AnalysisService(OrderDataStore orderDataStore) {
+    public AnalysisService(OrderDataStore orderDataStore, OrderAnalyticsMapper orderAnalyticsMapper) {
         this.orderDataStore = orderDataStore;
+        this.orderAnalyticsMapper = orderAnalyticsMapper;
     }
 
     public Double calculateTotalGMV(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
-        double sum = 0.0;
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (range.contains(order.getCreatedDate())) {
-                sum += order.getOrderActualPayment();
+        if (hasDatabaseOrders()) {
+            try {
+                Double total = orderAnalyticsMapper.selectTotalGmv(startDate, endDate);
+                return total == null ? 0.0 : total;
+            } catch (Exception ignored) {
             }
         }
+
+        double sum = 0.0;
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
+            sum += order.getOrderActualPayment();
+        }
         return sum;
     }
 
     public List<ProductDTO> getTop5Products(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
+        if (hasDatabaseOrders()) {
+            try {
+                List<ProductDTO> products = orderAnalyticsMapper.selectTop5Products(startDate, endDate);
+                if (products != null) {
+                    return products;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+
         Map<String, Double> salesBySku = new HashMap<>();
         Map<String, String> nameBySku = new HashMap<>();
 
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (!range.contains(order.getCreatedDate())) {
-                continue;
-            }
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
             String sku = order.getProductMerchantCode();
             if (sku == null || sku.isEmpty()) {
                 sku = order.getProductId();
@@ -79,19 +96,23 @@ public class AnalysisService {
     }
 
     public Map<String, Double> calculateP80AndRBig(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
-        List<Double> payments = new ArrayList<>();
-
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (!range.contains(order.getCreatedDate())) {
-                continue;
-            }
-            double payment = order.getOrderActualPayment();
-            if (payment <= 0 && order.getOrderPayable() > 0) {
-                payment = order.getOrderPayable();
+        List<Double> payments = null;
+        if (hasDatabaseOrders()) {
+            try {
+                payments = orderAnalyticsMapper.selectPaymentAmounts(startDate, endDate);
+            } catch (Exception ignored) {
             }
-            if (payment > 0) {
-                payments.add(payment);
+        }
+        if (payments == null) {
+            payments = new ArrayList<>();
+            for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
+                double payment = order.getOrderActualPayment();
+                if (payment <= 0 && order.getOrderPayable() > 0) {
+                    payment = order.getOrderPayable();
+                }
+                if (payment > 0) {
+                    payments.add(payment);
+                }
             }
         }
 
@@ -124,14 +145,27 @@ public class AnalysisService {
     }
 
     public Map<String, Double> calculateLeakageRate(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
+        if (hasDatabaseOrders()) {
+            try {
+                OrderLeakageSummary summary = orderAnalyticsMapper.selectLeakageSummary(startDate, endDate);
+                if (summary != null) {
+                    double totalRefundAmount = valueOrZero(summary.getTotalRefundAmount());
+                    double totalSuccessAmount = valueOrZero(summary.getTotalSuccessAmount());
+                    double leakageRate = (totalSuccessAmount > 0) ? (totalRefundAmount / totalSuccessAmount) * 100 : 0.0;
+                    Map<String, Double> result = new HashMap<>();
+                    result.put("totalRefundAmount", totalRefundAmount);
+                    result.put("totalSuccessAmount", totalSuccessAmount);
+                    result.put("leakageRatePercent", leakageRate);
+                    return result;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+
         double totalRefundAmount = 0.0;
         double totalSuccessAmount = 0.0;
 
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (!range.contains(order.getCreatedDate())) {
-                continue;
-            }
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
             double payment = order.getOrderActualPayment();
             if (payment <= 0 && order.getOrderPayable() > 0) {
                 payment = order.getOrderPayable();
@@ -162,7 +196,8 @@ public class AnalysisService {
         long refundAmountCount = 0;
         long refundStatusCount = 0;
 
-        for (OrderRecord order : orderDataStore.getOrders()) {
+        List<OrderAnalyticsRecord> orders = getAnalyticsOrders(null, null);
+        for (OrderAnalyticsRecord order : orders) {
             String refundStatus = normalize(order.getOrderRefundStatus());
             if (!refundStatus.isEmpty()) {
                 statusCounts.merge(refundStatus, 1L, Long::sum);
@@ -185,7 +220,7 @@ public class AnalysisService {
         }
 
         Map<String, Object> result = new HashMap<>();
-        result.put("orders", orderDataStore.getOrders().size());
+        result.put("orders", orders.size());
         result.put("refundAmountSum", refundAmountSum);
         result.put("refundAmountCount", refundAmountCount);
         result.put("refundStatusCount", refundStatusCount);
@@ -197,11 +232,22 @@ public class AnalysisService {
         return result;
     }
 
-    public List<CoPurchaseDTO> findCoPurchaseRules() {
+    public List<CoPurchaseDTO> findCoPurchaseRules(String skuKeyword) {
+        if (hasDatabaseOrders()) {
+            try {
+                List<CoPurchaseDTO> rules = orderAnalyticsMapper.selectCoPurchaseRules(normalize(skuKeyword));
+                if (rules != null) {
+                    return rules;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+
+        String normalizedKeyword = normalize(skuKeyword).toLowerCase();
         Map<String, Set<String>> purchaseProducts = new HashMap<>();
         Map<String, String> productTitle = new HashMap<>();
 
-        for (OrderRecord order : orderDataStore.getOrders()) {
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(null, null)) {
             String purchaseKey = order.getPurchaseId();
             if (purchaseKey == null || purchaseKey.isEmpty()) {
                 purchaseKey = order.getPurchasePaymentId();
@@ -255,6 +301,11 @@ public class AnalysisService {
             String[] ids = sortedPairs.get(i).getKey().split("\\|\\|", 2);
             String aId = ids[0];
             String bId = ids.length > 1 ? ids[1] : "";
+            if (!normalizedKeyword.isEmpty()
+                    && !aId.toLowerCase().contains(normalizedKeyword)
+                    && !bId.toLowerCase().contains(normalizedKeyword)) {
+                continue;
+            }
             result.add(new CoPurchaseDTO(
                     productTitle.getOrDefault(aId, aId),
                     aId,
@@ -267,13 +318,17 @@ public class AnalysisService {
     }
 
     public Double calculateAveragePaymentTime(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
+        if (hasDatabaseOrders()) {
+            try {
+                Double average = orderAnalyticsMapper.selectAveragePaymentSeconds(startDate, endDate);
+                return average == null ? 0.0 : average;
+            } catch (Exception ignored) {
+            }
+        }
+
         double totalSeconds = 0.0;
         long count = 0;
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (!range.contains(order.getCreatedDate())) {
-                continue;
-            }
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
             if (order.getCreatedTime() == null || order.getPaidTime() == null) {
                 continue;
             }
@@ -287,16 +342,27 @@ public class AnalysisService {
     }
 
     public Map<String, Long> analyzePaymentDecisionFunnel(String startDate, String endDate) {
-        LocalDateRange range = parseRange(startDate, endDate);
+        if (hasDatabaseOrders()) {
+            try {
+                OrderFunnelSummary summary = orderAnalyticsMapper.selectPaymentFunnel(startDate, endDate);
+                if (summary != null) {
+                    Map<String, Long> funnelData = new HashMap<>();
+                    funnelData.put("paidWithin5Mins", valueOrZero(summary.getPaidWithin5Mins()));
+                    funnelData.put("paidBetween5And30Mins", valueOrZero(summary.getPaidBetween5And30Mins()));
+                    funnelData.put("paidAfter30Mins", valueOrZero(summary.getPaidAfter30Mins()));
+                    funnelData.put("unpaidOrders", valueOrZero(summary.getUnpaidOrders()));
+                    return funnelData;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+
         long paidWithin5Mins = 0;
         long paidBetween5And30Mins = 0;
         long paidAfter30Mins = 0;
         long unpaidOrders = 0;
 
-        for (OrderRecord order : orderDataStore.getOrders()) {
-            if (!range.contains(order.getCreatedDate())) {
-                continue;
-            }
+        for (OrderAnalyticsRecord order : getAnalyticsOrders(startDate, endDate)) {
             if (order.getCreatedTime() == null) {
                 continue;
             }
@@ -340,6 +406,18 @@ public class AnalysisService {
     }
 
     public Double calculateTop5Percentage(String startDate, String endDate) {
+        if (hasDatabaseOrders()) {
+            try {
+                Double top5Sum = orderAnalyticsMapper.selectTop5SalesTotal(startDate, endDate);
+                Double totalGmv = orderAnalyticsMapper.selectTotalGmv(startDate, endDate);
+                if (totalGmv == null || totalGmv <= 0) {
+                    return 0.0;
+                }
+                return (valueOrZero(top5Sum) / totalGmv) * 100;
+            } catch (Exception ignored) {
+            }
+        }
+
         List<ProductDTO> top5List = getTop5Products(startDate, endDate);
         if (top5List == null || top5List.isEmpty()) {
             return 0.0;
@@ -355,13 +433,72 @@ public class AnalysisService {
     }
 
     public String getMaxOrderDate() {
-        LocalDate maxDate = orderDataStore.getMaxOrderDate();
-        if (maxDate != null) {
-            return maxDate.toString();
+        String maxDate = getDatabaseMaxOrderDate();
+        if (maxDate != null && !maxDate.isEmpty()) {
+            return maxDate;
+        }
+        LocalDate localMaxDate = orderDataStore.getMaxOrderDate();
+        if (localMaxDate != null) {
+            return localMaxDate.toString();
         }
         return LocalDate.now().toString();
     }
 
+    private List<OrderAnalyticsRecord> getAnalyticsOrders(String startDate, String endDate) {
+        if (hasDatabaseOrders()) {
+            List<OrderAnalyticsRecord> databaseOrders = orderAnalyticsMapper.selectOrders(startDate, endDate);
+            if (databaseOrders != null) {
+                return databaseOrders;
+            }
+        }
+        return convertOrders(orderDataStore.getOrders(), startDate, endDate);
+    }
+
+    private String getDatabaseMaxOrderDate() {
+        if (!hasDatabaseOrders()) {
+            return null;
+        }
+        return orderAnalyticsMapper.selectMaxOrderDate();
+    }
+
+    private boolean hasDatabaseOrders() {
+        try {
+            Long count = orderAnalyticsMapper.countOrders();
+            return count != null && count > 0;
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
+    private List<OrderAnalyticsRecord> convertOrders(List<OrderRecord> orders, String startDate, String endDate) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        List<OrderAnalyticsRecord> result = new ArrayList<>();
+        for (OrderRecord order : orders) {
+            if (!range.contains(order.getCreatedDate())) {
+                continue;
+            }
+            OrderAnalyticsRecord item = new OrderAnalyticsRecord();
+            item.setPurchaseId(order.getPurchaseId());
+            item.setOrderId(order.getOrderId());
+            item.setProductId(order.getProductId());
+            item.setProductTitle(order.getProductTitle());
+            item.setProductProperties(order.getProductProperties());
+            item.setProductMerchantCode(order.getProductMerchantCode());
+            item.setOrderPrice(order.getOrderPrice());
+            item.setOrderQuantity(order.getOrderQuantity());
+            item.setOrderStatus(order.getOrderStatus());
+            item.setOrderPayable(order.getOrderPayable());
+            item.setOrderActualPayment(order.getOrderActualPayment());
+            item.setOrderRefundStatus(order.getOrderRefundStatus());
+            item.setOrderRefundAmount(order.getOrderRefundAmount());
+            item.setCreatedTime(order.getCreatedTime());
+            item.setPaidTime(order.getPaidTime());
+            item.setPurchasePaymentId(order.getPurchasePaymentId());
+            result.add(item);
+        }
+        return result;
+    }
+
     private LocalDateRange parseRange(String startDate, String endDate) {
         LocalDate start = parseDate(startDate);
         LocalDate end = parseDate(endDate);
@@ -400,6 +537,14 @@ public class AnalysisService {
         return value == null ? "" : value.trim();
     }
 
+    private double valueOrZero(Double value) {
+        return value == null ? 0.0 : value;
+    }
+
+    private long valueOrZero(Long value) {
+        return value == null ? 0L : value;
+    }
+
     private static class LocalDateRange {
         private final LocalDate start;
         private final LocalDate end;

+ 36 - 0
dtm-system/src/main/java/com/dtm/order/shop/mapper/ShopValueMapper.java

@@ -0,0 +1,36 @@
+package com.dtm.order.shop.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ShopValueMapper {
+    Long countRows();
+
+    String selectMaxStatDate();
+
+    List<Map<String, Object>> selectPlatformOrderCount(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectPlatformSalesAmount(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectUnitContribution(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectChannelContribution(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectPlatformContribution(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectTopProducts(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    Double selectTotalSales(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectCrossSellingProducts(
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate,
+            @Param("skuKeyword") String skuKeyword
+    );
+
+    List<Map<String, Object>> selectDepartmentEfficiency(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<Map<String, Object>> selectChannelDiversity(@Param("startDate") String startDate, @Param("endDate") String endDate);
+}

+ 264 - 25
dtm-system/src/main/java/com/dtm/order/shop/service/ShopAnalysisService.java

@@ -1,9 +1,13 @@
 package com.dtm.order.shop.service;
 
+import com.dtm.order.shop.mapper.ShopValueMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -15,10 +19,12 @@ import java.util.Set;
 public class ShopAnalysisService {
 
     private final ShopSalesDataStore dataStore;
+    private final ShopValueMapper shopValueMapper;
 
     @Autowired
-    public ShopAnalysisService(ShopSalesDataStore dataStore) {
+    public ShopAnalysisService(ShopSalesDataStore dataStore, ShopValueMapper shopValueMapper) {
         this.dataStore = dataStore;
+        this.shopValueMapper = shopValueMapper;
     }
 
     public int importAllShopSalesData() {
@@ -33,56 +39,111 @@ public class ShopAnalysisService {
         return dataStore.getLastUploadDebug();
     }
 
-    public Map<String, Double> getChannelSalesContribution() {
+    public String getMaxSalesDate() {
+        String databaseMaxDate = getDatabaseMaxSalesDate();
+        if (databaseMaxDate != null && !databaseMaxDate.isEmpty()) {
+            return databaseMaxDate;
+        }
+        LocalDate maxDate = dataStore.getMaxSalesDate();
+        return maxDate == null ? LocalDate.now().toString() : maxDate.toString();
+    }
+
+    public Map<String, Double> getChannelSalesContribution(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectPlatformOrderCount(startDate, endDate));
+            if (!databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> contributionData = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             contributionData.merge(record.getPlatformName(), 1.0, Double::sum);
         }
         return contributionData.isEmpty() ? null : contributionData;
     }
 
-    public Map<String, Double> getChannelRoiValue() {
+    public Map<String, Double> getChannelRoiValue(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectPlatformSalesAmount(startDate, endDate));
+            if (!databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> resultData = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             resultData.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
         }
         return resultData.isEmpty() ? null : resultData;
     }
 
-    public List<Map<String, Object>> getUnitContribution() {
+    public List<Map<String, Object>> getUnitContribution(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            List<Map<String, Object>> databaseData = shopValueMapper.selectUnitContribution(startDate, endDate);
+            if (databaseData != null && !databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> volumeByUnit = new HashMap<>();
         Map<String, Double> amountByUnit = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByUnit.merge(record.getBusinessUnitName(), record.getQuantity(), Double::sum);
             amountByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByUnit, amountByUnit);
     }
 
-    public List<Map<String, Object>> getChannelTotalContribution() {
+    public List<Map<String, Object>> getChannelTotalContribution(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            List<Map<String, Object>> databaseData = shopValueMapper.selectChannelContribution(startDate, endDate);
+            if (databaseData != null && !databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> volumeByChannel = new HashMap<>();
         Map<String, Double> amountByChannel = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByChannel.merge(record.getChannelName(), record.getQuantity(), Double::sum);
             amountByChannel.merge(record.getChannelName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByChannel, amountByChannel);
     }
 
-    public List<Map<String, Object>> getPlatformTotalContribution() {
+    public List<Map<String, Object>> getPlatformTotalContribution(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            List<Map<String, Object>> databaseData = shopValueMapper.selectPlatformContribution(startDate, endDate);
+            if (databaseData != null && !databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> volumeByPlatform = new HashMap<>();
         Map<String, Double> amountByPlatform = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByPlatform.merge(record.getPlatformName(), record.getQuantity(), Double::sum);
             amountByPlatform.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByPlatform, amountByPlatform);
     }
 
-    public Map<String, Object> getTopProductContribution() {
+    public Map<String, Object> getTopProductContribution(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            List<Map<String, Object>> topProducts = shopValueMapper.selectTopProducts(startDate, endDate);
+            Double totalSales = shopValueMapper.selectTotalSales(startDate, endDate);
+            if (topProducts != null && !topProducts.isEmpty()) {
+                double top5SalesSum = topProducts.stream()
+                        .mapToDouble(row -> toDouble(getMapValue(row, "salesAmount")))
+                        .sum();
+                double total = totalSales == null ? 0.0 : totalSales;
+                Map<String, Object> finalResult = new HashMap<>();
+                finalResult.put("top5Products", topProducts);
+                finalResult.put("top5TotalSales", top5SalesSum);
+                finalResult.put("totalSales", total);
+                finalResult.put("contributionRatio", total > 0 ? top5SalesSum / total : 0.0);
+                return finalResult;
+            }
+        }
         double totalSales = 0.0;
         Map<String, Double> productSales = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             totalSales += record.getSalesAmount();
             productSales.merge(record.getProductCode(), record.getSalesAmount(), Double::sum);
         }
@@ -102,7 +163,7 @@ public class ShopAnalysisService {
             topProducts.add(productMap);
         }
 
-        double contributionRatio = (totalSales > 0) ? (top5SalesSum / totalSales) : 0.0;
+        double contributionRatio = totalSales > 0 ? (top5SalesSum / totalSales) : 0.0;
         Map<String, Object> finalResult = new HashMap<>();
         finalResult.put("top5Products", topProducts);
         finalResult.put("top5TotalSales", top5SalesSum);
@@ -111,9 +172,15 @@ public class ShopAnalysisService {
         return finalResult;
     }
 
-    public List<Map<String, Object>> getCrossSellingProducts() {
+    public List<Map<String, Object>> getCrossSellingProducts(String startDate, String endDate, String skuKeyword) {
+        if (hasDatabaseRows()) {
+            List<Map<String, Object>> databaseData = shopValueMapper.selectCrossSellingProducts(startDate, endDate, normalize(skuKeyword));
+            if (databaseData != null && !databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Set<String>> platformByProduct = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, skuKeyword)) {
             platformByProduct
                     .computeIfAbsent(record.getProductCode(), k -> new HashSet<>())
                     .add(record.getPlatformName());
@@ -129,13 +196,20 @@ public class ShopAnalysisService {
                 crossSellingData.add(productMap);
             }
         }
+        crossSellingData.sort((a, b) -> Long.compare((Long) b.get("platformCount"), (Long) a.get("platformCount")));
         return crossSellingData.isEmpty() ? null : crossSellingData;
     }
 
-    public Map<String, Double> getDepartmentOperationalEfficiency() {
+    public Map<String, Double> getDepartmentOperationalEfficiency(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectDepartmentEfficiency(startDate, endDate));
+            if (!databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Double> sumByUnit = new HashMap<>();
         Map<String, Long> countByUnit = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             sumByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
             countByUnit.merge(record.getBusinessUnitName(), 1L, Long::sum);
         }
@@ -149,9 +223,15 @@ public class ShopAnalysisService {
         return efficiencyData.isEmpty() ? null : efficiencyData;
     }
 
-    public Map<String, Long> getChannelProductDiversity() {
+    public Map<String, Long> getChannelProductDiversity(String startDate, String endDate) {
+        if (hasDatabaseRows()) {
+            Map<String, Long> databaseData = toLongMap(shopValueMapper.selectChannelDiversity(startDate, endDate));
+            if (!databaseData.isEmpty()) {
+                return databaseData;
+            }
+        }
         Map<String, Set<String>> productsByChannel = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             productsByChannel
                     .computeIfAbsent(record.getChannelName(), k -> new HashSet<>())
                     .add(record.getProductCode());
@@ -163,10 +243,25 @@ public class ShopAnalysisService {
         return diversityData.isEmpty() ? null : diversityData;
     }
 
-    private List<Map<String, Object>> toContributionList(
-            Map<String, Double> volume,
-            Map<String, Double> amount
-    ) {
+    private List<ShopSalesRecord> filterRecords(String startDate, String endDate, String skuKeyword) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        String normalizedKeyword = normalize(skuKeyword).toLowerCase();
+        boolean hasKeyword = !normalizedKeyword.isEmpty();
+
+        List<ShopSalesRecord> filtered = new ArrayList<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            if (!range.contains(record.getSalesDate())) {
+                continue;
+            }
+            if (hasKeyword && !normalize(record.getProductCode()).toLowerCase().contains(normalizedKeyword)) {
+                continue;
+            }
+            filtered.add(record);
+        }
+        return filtered;
+    }
+
+    private List<Map<String, Object>> toContributionList(Map<String, Double> volume, Map<String, Double> amount) {
         List<Map<String, Object>> result = new ArrayList<>();
         for (String name : volume.keySet()) {
             Map<String, Object> map = new HashMap<>();
@@ -175,9 +270,153 @@ public class ShopAnalysisService {
             map.put("totalAmount", amount.getOrDefault(name, 0.0));
             result.add(map);
         }
+        result.sort((a, b) -> Double.compare((Double) b.get("totalAmount"), (Double) a.get("totalAmount")));
         return result.isEmpty() ? null : result;
     }
-}
 
+    private LocalDateRange parseRange(String startDate, String endDate) {
+        LocalDate start = parseDate(startDate);
+        LocalDate end = parseDate(endDate);
+        if (start == null && end == null) {
+            return new LocalDateRange(null, null);
+        }
+        if (start == null) {
+            start = end;
+        }
+        if (end == null) {
+            end = start;
+        }
+        return new LocalDateRange(start, end);
+    }
 
+    private LocalDate parseDate(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        String value = raw.trim();
+        if (value.isEmpty()) {
+            return null;
+        }
+        if (value.length() > 10) {
+            value = value.substring(0, 10);
+        }
+        value = value.replace('/', '-');
+        try {
+            return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-M-d"));
+        } catch (DateTimeParseException e) {
+            return null;
+        }
+    }
+
+    private String normalize(String value) {
+        return value == null ? "" : value.trim();
+    }
+
+    private boolean hasDatabaseRows() {
+        try {
+            Long count = shopValueMapper.countRows();
+            return count != null && count > 0;
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
+    private String getDatabaseMaxSalesDate() {
+        if (!hasDatabaseRows()) {
+            return null;
+        }
+        return shopValueMapper.selectMaxStatDate();
+    }
+
+    private Map<String, Double> toDoubleMap(List<Map<String, Object>> rows) {
+        Map<String, Double> result = new HashMap<>();
+        if (rows == null) {
+            return result;
+        }
+        for (Map<String, Object> row : rows) {
+            String name = toStringValue(getMapValue(row, "name"));
+            if (!name.isEmpty()) {
+                result.put(name, toDouble(getMapValue(row, "value")));
+            }
+        }
+        return result;
+    }
+
+    private Map<String, Long> toLongMap(List<Map<String, Object>> rows) {
+        Map<String, Long> result = new HashMap<>();
+        if (rows == null) {
+            return result;
+        }
+        for (Map<String, Object> row : rows) {
+            String name = toStringValue(getMapValue(row, "name"));
+            if (!name.isEmpty()) {
+                result.put(name, Math.round(toDouble(getMapValue(row, "value"))));
+            }
+        }
+        return result;
+    }
+
+    private String toStringValue(Object value) {
+        return value == null ? "" : String.valueOf(value).trim();
+    }
 
+    private Object getMapValue(Map<String, Object> row, String key) {
+        if (row == null || key == null) {
+            return null;
+        }
+        if (row.containsKey(key)) {
+            return row.get(key);
+        }
+        String lowerKey = key.toLowerCase();
+        String upperKey = key.toUpperCase();
+        if (row.containsKey(lowerKey)) {
+            return row.get(lowerKey);
+        }
+        if (row.containsKey(upperKey)) {
+            return row.get(upperKey);
+        }
+        for (Map.Entry<String, Object> entry : row.entrySet()) {
+            if (key.equalsIgnoreCase(entry.getKey())) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private double toDouble(Object value) {
+        if (value instanceof Number) {
+            return ((Number) value).doubleValue();
+        }
+        if (value == null) {
+            return 0.0;
+        }
+        try {
+            return Double.parseDouble(String.valueOf(value));
+        } catch (NumberFormatException e) {
+            return 0.0;
+        }
+    }
+
+    private static class LocalDateRange {
+        private final LocalDate start;
+        private final LocalDate end;
+
+        private LocalDateRange(LocalDate start, LocalDate end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public boolean contains(LocalDate date) {
+            if (date == null) {
+                return false;
+            }
+            if (start != null && date.isBefore(start)) {
+                return false;
+            }
+            if (end != null && date.isAfter(end)) {
+                return false;
+            }
+            return true;
+        }
+    }
+}

+ 71 - 70
dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesDataStore.java

@@ -15,6 +15,7 @@ import java.io.Reader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -28,6 +29,7 @@ public class ShopSalesDataStore {
     private volatile List<ShopSalesRecord> salesRecords = Collections.emptyList();
     private volatile long lastLoadCount = 0;
     private volatile String lastUploadDebug = "";
+    private volatile LocalDate maxSalesDate = null;
 
     @PostConstruct
     public void loadOnStartup() {
@@ -36,11 +38,13 @@ public class ShopSalesDataStore {
 
     public int reload() {
         List<ShopSalesRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
         File folder = new File(shopDataFolderPath);
         File[] files = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(".csv"));
         if (files == null || files.length == 0) {
             salesRecords = Collections.emptyList();
             lastLoadCount = 0;
+            maxSalesDate = null;
             return 0;
         }
 
@@ -54,36 +58,14 @@ public class ShopSalesDataStore {
                 }
 
                 while (csvIterator.hasNext()) {
-                    CSVRecord csvRecord = csvIterator.next();
-                    if (csvRecord.size() < 10) {
+                    ShopSalesRecord record = parseRecord(csvIterator.next());
+                    if (record == null) {
                         continue;
                     }
-                    try {
-                        int year = Integer.parseInt(csvRecord.get(0));
-                        int month = Integer.parseInt(csvRecord.get(1));
-                        int day = Integer.parseInt(csvRecord.get(2));
-                        String businessUnitName = csvRecord.get(3);
-                        String channelName = csvRecord.get(4);
-                        String platformName = csvRecord.get(5);
-                        String productCode = csvRecord.get(6);
-                        double quantity = Double.parseDouble(csvRecord.get(7));
-                        double unitPrice = Double.parseDouble(csvRecord.get(8));
-                        double salesAmount = Double.parseDouble(csvRecord.get(9));
-
-                        loaded.add(new ShopSalesRecord(
-                                year,
-                                month,
-                                day,
-                                businessUnitName,
-                                channelName,
-                                platformName,
-                                productCode,
-                                quantity,
-                                unitPrice,
-                                salesAmount
-                        ));
-                    } catch (Exception ignored) {
-                        // Skip bad rows.
+                    loaded.add(record);
+                    LocalDate recordDate = record.getSalesDate();
+                    if (maxDate == null || recordDate.isAfter(maxDate)) {
+                        maxDate = recordDate;
                     }
                 }
             } catch (Exception ignored) {
@@ -93,16 +75,19 @@ public class ShopSalesDataStore {
 
         salesRecords = Collections.unmodifiableList(loaded);
         lastLoadCount = loaded.size();
+        maxSalesDate = maxDate;
         return loaded.size();
     }
 
     public int reloadFromUploads(MultipartFile[] files) {
         List<ShopSalesRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
         StringBuilder debug = new StringBuilder();
         if (files == null || files.length == 0) {
             salesRecords = Collections.emptyList();
             lastLoadCount = 0;
-            lastUploadDebug = "未收到上传文件。";
+            lastUploadDebug = "No uploaded files received.";
+            maxSalesDate = null;
             return 0;
         }
 
@@ -115,27 +100,35 @@ public class ShopSalesDataStore {
             received++;
             String filename = file.getOriginalFilename();
             if (filename == null || !filename.toLowerCase().endsWith(".csv")) {
-                debug.append("跳过非CSV文件: ").append(filename).append("\n");
+                debug.append("Skip non-CSV file: ").append(filename).append("\n");
                 continue;
             }
             csvCount++;
-            debug.append("文件: ").append(filename).append(", 大小=").append(file.getSize()).append("\n");
+            debug.append("File: ").append(filename).append(", size=").append(file.getSize()).append("\n");
             try {
                 byte[] bytes = file.getBytes();
                 int before = loaded.size();
                 int added = parseCsvBytes(bytes, loaded, debug);
-                debug.append("导入行数: ").append(added).append("\n");
+                debug.append("Imported rows: ").append(added).append("\n");
                 if (loaded.size() == before) {
-                    debug.append("提示: 该文件未解析到有效数据,可能是编码/分隔符/列格式不匹配。\n");
+                    debug.append("Hint: no valid rows parsed. Check encoding, delimiter, or columns.\n");
+                    continue;
+                }
+                for (int i = before; i < loaded.size(); i++) {
+                    LocalDate recordDate = loaded.get(i).getSalesDate();
+                    if (maxDate == null || recordDate.isAfter(maxDate)) {
+                        maxDate = recordDate;
+                    }
                 }
             } catch (Exception e) {
-                debug.append("读取文件异常: ").append(e.getMessage()).append("\n");
+                debug.append("Read file error: ").append(e.getMessage()).append("\n");
             }
         }
 
         salesRecords = Collections.unmodifiableList(loaded);
         lastLoadCount = loaded.size();
-        lastUploadDebug = "收到文件数=" + received + ", CSV文件数=" + csvCount + ", 总导入行数=" + loaded.size() + "\n" + debug;
+        maxSalesDate = maxDate;
+        lastUploadDebug = "Received files=" + received + ", CSV files=" + csvCount + ", imported rows=" + loaded.size() + "\n" + debug;
         return loaded.size();
     }
 
@@ -151,6 +144,10 @@ public class ShopSalesDataStore {
         return lastUploadDebug;
     }
 
+    public LocalDate getMaxSalesDate() {
+        return maxSalesDate;
+    }
+
     private int parseCsvBytes(byte[] bytes, List<ShopSalesRecord> loaded, StringBuilder debug) {
         int added = parseCsvBytesWithCharset(bytes, loaded, StandardCharsets.UTF_8, debug);
         if (added > 0) {
@@ -163,10 +160,10 @@ public class ShopSalesDataStore {
         try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), charset);
              CSVParser csvParser = new CSVParser(reader, CSVFormat.EXCEL.builder().setTrim(true).build())) {
             int added = appendCsvRecords(csvParser, loaded);
-            debug.append("编码 ").append(charset.displayName()).append(" 解析行数: ").append(added).append("\n");
+            debug.append("Charset ").append(charset.displayName()).append(" parsed rows: ").append(added).append("\n");
             return added;
         } catch (Exception e) {
-            debug.append("编码 ").append(charset.displayName()).append(" 解析异常: ").append(e.getMessage()).append("\n");
+            debug.append("Charset ").append(charset.displayName()).append(" parse error: ").append(e.getMessage()).append("\n");
             return 0;
         }
     }
@@ -178,42 +175,46 @@ public class ShopSalesDataStore {
         }
         int added = 0;
         while (csvIterator.hasNext()) {
-            CSVRecord csvRecord = csvIterator.next();
-            if (csvRecord.size() < 10) {
+            ShopSalesRecord record = parseRecord(csvIterator.next());
+            if (record == null) {
                 continue;
             }
-            try {
-                int year = Integer.parseInt(csvRecord.get(0));
-                int month = Integer.parseInt(csvRecord.get(1));
-                int day = Integer.parseInt(csvRecord.get(2));
-                String businessUnitName = csvRecord.get(3);
-                String channelName = csvRecord.get(4);
-                String platformName = csvRecord.get(5);
-                String productCode = csvRecord.get(6);
-                double quantity = Double.parseDouble(csvRecord.get(7));
-                double unitPrice = Double.parseDouble(csvRecord.get(8));
-                double salesAmount = Double.parseDouble(csvRecord.get(9));
-
-                loaded.add(new ShopSalesRecord(
-                        year,
-                        month,
-                        day,
-                        businessUnitName,
-                        channelName,
-                        platformName,
-                        productCode,
-                        quantity,
-                        unitPrice,
-                        salesAmount
-                ));
-                added++;
-            } catch (Exception ignored) {
-                // Skip bad rows.
-            }
+            loaded.add(record);
+            added++;
         }
         return added;
     }
-}
-
-
 
+    private ShopSalesRecord parseRecord(CSVRecord csvRecord) {
+        if (csvRecord == null || csvRecord.size() < 10) {
+            return null;
+        }
+        try {
+            int year = Integer.parseInt(csvRecord.get(0));
+            int month = Integer.parseInt(csvRecord.get(1));
+            int day = Integer.parseInt(csvRecord.get(2));
+            String businessUnitName = csvRecord.get(3);
+            String channelName = csvRecord.get(4);
+            String platformName = csvRecord.get(5);
+            String productCode = csvRecord.get(6);
+            double quantity = Double.parseDouble(csvRecord.get(7));
+            double unitPrice = Double.parseDouble(csvRecord.get(8));
+            double salesAmount = Double.parseDouble(csvRecord.get(9));
+
+            return new ShopSalesRecord(
+                    year,
+                    month,
+                    day,
+                    businessUnitName,
+                    channelName,
+                    platformName,
+                    productCode,
+                    quantity,
+                    unitPrice,
+                    salesAmount
+            );
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+}

+ 3 - 0
dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesRecord.java

@@ -1,5 +1,7 @@
 package com.dtm.order.shop.service;
 
+import java.time.LocalDate;
+
 public class ShopSalesRecord {
     private final int year;
     private final int month;
@@ -46,6 +48,7 @@ public class ShopSalesRecord {
     public double getQuantity() { return quantity; }
     public double getUnitPrice() { return unitPrice; }
     public double getSalesAmount() { return salesAmount; }
+    public LocalDate getSalesDate() { return LocalDate.of(year, month, day); }
 }
 
 

+ 251 - 0
dtm-system/src/main/resources/mapper/order/OrderAnalyticsMapper.xml

@@ -0,0 +1,251 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.dtm.order.mapper.OrderAnalyticsMapper">
+
+    <resultMap id="OrderAnalyticsRecordResult" type="com.dtm.order.domain.OrderAnalyticsRecord">
+        <result property="purchaseId" column="purchase_id"/>
+        <result property="orderId" column="order_id"/>
+        <result property="productId" column="product_id"/>
+        <result property="productTitle" column="product_title"/>
+        <result property="productProperties" column="product_properties"/>
+        <result property="productMerchantCode" column="product_merchant_code"/>
+        <result property="orderPrice" column="order_price"/>
+        <result property="orderQuantity" column="order_quantity"/>
+        <result property="orderStatus" column="order_status"/>
+        <result property="orderPayable" column="order_payable"/>
+        <result property="orderActualPayment" column="order_actual_payment"/>
+        <result property="orderRefundStatus" column="order_refund_status"/>
+        <result property="orderRefundAmount" column="order_refund_amount"/>
+        <result property="createdTime" column="created_time"/>
+        <result property="paidTime" column="paid_time"/>
+        <result property="purchasePaymentId" column="purchase_payment_id"/>
+    </resultMap>
+
+    <resultMap id="ProductResult" type="com.dtm.order.dto.ProductDTO">
+        <result property="sku" column="sku"/>
+        <result property="name" column="name"/>
+        <result property="totalSales" column="total_sales"/>
+    </resultMap>
+
+    <resultMap id="CoPurchaseResult" type="com.dtm.order.dto.CoPurchaseDTO">
+        <result property="productA" column="product_a"/>
+        <result property="productAId" column="product_a_id"/>
+        <result property="productB" column="product_b"/>
+        <result property="productBId" column="product_b_id"/>
+        <result property="coPurchaseCount" column="co_purchase_count"/>
+    </resultMap>
+
+    <resultMap id="OrderLeakageSummaryResult" type="com.dtm.order.domain.OrderLeakageSummary">
+        <result property="totalRefundAmount" column="total_refund_amount"/>
+        <result property="totalSuccessAmount" column="total_success_amount"/>
+    </resultMap>
+
+    <resultMap id="OrderFunnelSummaryResult" type="com.dtm.order.domain.OrderFunnelSummary">
+        <result property="paidWithin5Mins" column="paid_within_5_mins"/>
+        <result property="paidBetween5And30Mins" column="paid_between_5_and_30_mins"/>
+        <result property="paidAfter30Mins" column="paid_after_30_mins"/>
+        <result property="unpaidOrders" column="unpaid_orders"/>
+    </resultMap>
+
+    <sql id="dateRangeConditions">
+        <if test="startDate != null and startDate != ''">
+            AND create_time &gt;= STR_TO_DATE(CONCAT(#{startDate}, ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND create_time &lt; DATE_ADD(
+                STR_TO_DATE(CONCAT(#{endDate}, ' 00:00:00'), '%Y-%m-%d %H:%i:%s'),
+                INTERVAL 1 DAY
+            )
+        </if>
+    </sql>
+
+    <sql id="selectOrderAnalyticsColumns">
+        SELECT
+            order_id AS purchase_id,
+            order_id AS order_id,
+            sku AS product_id,
+            title AS product_title,
+            attrs AS product_properties,
+            sku AS product_merchant_code,
+            IFNULL(price, 0) AS order_price,
+            IFNULL(quantity, 0) AS order_quantity,
+            order_status AS order_status,
+            IFNULL(pay_amount, 0) AS order_payable,
+            IFNULL(paid_amount, 0) AS order_actual_payment,
+            refund_status AS order_refund_status,
+            CASE
+                WHEN refund_amount REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(refund_amount AS DECIMAL(12,2))
+                ELSE 0
+            END AS order_refund_amount,
+            create_time AS created_time,
+            pay_time AS paid_time,
+            pay_number AS purchase_payment_id
+        FROM dtm_order_main
+    </sql>
+
+    <select id="countOrders" resultType="java.lang.Long">
+        SELECT COUNT(1) FROM dtm_order_main
+    </select>
+
+    <select id="selectMaxOrderDate" resultType="java.lang.String">
+        SELECT DATE_FORMAT(MAX(create_time), '%Y-%m-%d')
+        FROM dtm_order_main
+        WHERE create_time IS NOT NULL
+    </select>
+
+    <select id="selectOrders" resultMap="OrderAnalyticsRecordResult">
+        <include refid="selectOrderAnalyticsColumns"/>
+        <where>
+            <include refid="dateRangeConditions"/>
+        </where>
+    </select>
+
+    <select id="selectTotalGmv" resultType="java.lang.Double">
+        SELECT IFNULL(SUM(IFNULL(paid_amount, 0)), 0)
+        FROM dtm_order_main
+        <where>
+            <include refid="dateRangeConditions"/>
+        </where>
+    </select>
+
+    <select id="selectTop5Products" resultMap="ProductResult">
+        SELECT
+            sku AS sku,
+            COALESCE(MAX(NULLIF(title, '')), sku) AS name,
+            IFNULL(SUM(IFNULL(paid_amount, 0)), 0) AS total_sales
+        FROM dtm_order_main
+        <where>
+            sku IS NOT NULL
+            AND sku != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY sku
+        ORDER BY total_sales DESC
+        LIMIT 5
+    </select>
+
+    <select id="selectTop5SalesTotal" resultType="java.lang.Double">
+        SELECT IFNULL(SUM(top_product.total_sales), 0)
+        FROM (
+            SELECT IFNULL(SUM(IFNULL(paid_amount, 0)), 0) AS total_sales
+            FROM dtm_order_main
+            <where>
+                sku IS NOT NULL
+                AND sku != ''
+                <include refid="dateRangeConditions"/>
+            </where>
+            GROUP BY sku
+            ORDER BY total_sales DESC
+            LIMIT 5
+        ) top_product
+    </select>
+
+    <select id="selectPaymentAmounts" resultType="java.lang.Double">
+        SELECT payment_amount
+        FROM (
+            SELECT
+                CASE
+                    WHEN IFNULL(paid_amount, 0) &lt;= 0 AND IFNULL(pay_amount, 0) &gt; 0 THEN IFNULL(pay_amount, 0)
+                    ELSE IFNULL(paid_amount, 0)
+                END AS payment_amount
+            FROM dtm_order_main
+            <where>
+                <include refid="dateRangeConditions"/>
+            </where>
+        ) payment_data
+        WHERE payment_amount &gt; 0
+    </select>
+
+    <select id="selectLeakageSummary" resultMap="OrderLeakageSummaryResult">
+        SELECT
+            IFNULL(SUM(refund_amount_value), 0) AS total_refund_amount,
+            IFNULL(SUM(CASE WHEN payment_amount &gt; 0 THEN payment_amount ELSE 0 END), 0) AS total_success_amount
+        FROM (
+            SELECT
+                CASE
+                    WHEN IFNULL(paid_amount, 0) &lt;= 0 AND IFNULL(pay_amount, 0) &gt; 0 THEN IFNULL(pay_amount, 0)
+                    ELSE IFNULL(paid_amount, 0)
+                END AS payment_amount,
+                CASE
+                    WHEN refund_amount REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(refund_amount AS DECIMAL(12,2))
+                    ELSE 0
+                END AS refund_amount_value
+            FROM dtm_order_main
+            <where>
+                <include refid="dateRangeConditions"/>
+            </where>
+        ) leakage_data
+    </select>
+
+    <select id="selectAveragePaymentSeconds" resultType="java.lang.Double">
+        SELECT IFNULL(AVG(TIMESTAMPDIFF(SECOND, create_time, pay_time)), 0)
+        FROM dtm_order_main
+        <where>
+            create_time IS NOT NULL
+            AND pay_time IS NOT NULL
+            AND pay_time &gt;= create_time
+            <include refid="dateRangeConditions"/>
+        </where>
+    </select>
+
+    <select id="selectPaymentFunnel" resultMap="OrderFunnelSummaryResult">
+        SELECT
+            IFNULL(SUM(CASE WHEN pay_time IS NOT NULL AND pay_time &gt;= create_time AND TIMESTAMPDIFF(SECOND, create_time, pay_time) &lt;= 300 THEN 1 ELSE 0 END), 0) AS paid_within_5_mins,
+            IFNULL(SUM(CASE WHEN pay_time IS NOT NULL AND pay_time &gt;= create_time AND TIMESTAMPDIFF(SECOND, create_time, pay_time) &gt; 300 AND TIMESTAMPDIFF(SECOND, create_time, pay_time) &lt;= 1800 THEN 1 ELSE 0 END), 0) AS paid_between_5_and_30_mins,
+            IFNULL(SUM(CASE WHEN pay_time IS NOT NULL AND pay_time &gt;= create_time AND TIMESTAMPDIFF(SECOND, create_time, pay_time) &gt; 1800 THEN 1 ELSE 0 END), 0) AS paid_after_30_mins,
+            IFNULL(SUM(CASE WHEN pay_time IS NULL THEN 1 ELSE 0 END), 0) AS unpaid_orders
+        FROM dtm_order_main
+        <where>
+            create_time IS NOT NULL
+            <include refid="dateRangeConditions"/>
+        </where>
+    </select>
+
+    <select id="selectCoPurchaseRules" resultMap="CoPurchaseResult">
+        SELECT
+            COALESCE(MAX(a.title), a.sku) AS product_a,
+            a.sku AS product_a_id,
+            COALESCE(MAX(b.title), b.sku) AS product_b,
+            b.sku AS product_b_id,
+            COUNT(1) AS co_purchase_count
+        FROM (
+            SELECT
+                pay_number AS purchase_key,
+                sku,
+                MAX(NULLIF(title, '')) AS title
+            FROM dtm_order_main
+            WHERE pay_number IS NOT NULL
+              AND pay_number != ''
+              AND sku IS NOT NULL
+              AND sku != ''
+            GROUP BY pay_number, sku
+        ) a
+        INNER JOIN (
+            SELECT
+                pay_number AS purchase_key,
+                sku,
+                MAX(NULLIF(title, '')) AS title
+            FROM dtm_order_main
+            WHERE pay_number IS NOT NULL
+              AND pay_number != ''
+              AND sku IS NOT NULL
+              AND sku != ''
+            GROUP BY pay_number, sku
+        ) b
+            ON a.purchase_key = b.purchase_key
+           AND a.sku &lt; b.sku
+        <where>
+            <if test="skuKeyword != null and skuKeyword != ''">
+                AND (
+                    a.sku LIKE CONCAT('%', #{skuKeyword}, '%')
+                    OR b.sku LIKE CONCAT('%', #{skuKeyword}, '%')
+                )
+            </if>
+        </where>
+        GROUP BY a.sku, b.sku
+        ORDER BY co_purchase_count DESC
+        LIMIT 50
+    </select>
+</mapper>

+ 164 - 0
dtm-system/src/main/resources/mapper/order/shop/ShopValueMapper.xml

@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.dtm.order.shop.mapper.ShopValueMapper">
+
+    <sql id="statDateExpression">
+        COALESCE(stat_date, STR_TO_DATE(CONCAT(stat_year, '-', stat_month, '-', stat_day), '%Y-%c-%e'))
+    </sql>
+
+    <sql id="dateRangeConditions">
+        <if test="startDate != null and startDate != ''">
+            AND <include refid="statDateExpression"/> &gt;= STR_TO_DATE(#{startDate}, '%Y-%m-%d')
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND <include refid="statDateExpression"/> &lt; DATE_ADD(STR_TO_DATE(#{endDate}, '%Y-%m-%d'), INTERVAL 1 DAY)
+        </if>
+    </sql>
+
+    <select id="countRows" resultType="java.lang.Long">
+        SELECT COUNT(1) FROM dtm_shop_value_main
+    </select>
+
+    <select id="selectMaxStatDate" resultType="java.lang.String">
+        SELECT DATE_FORMAT(MAX(<include refid="statDateExpression"/>), '%Y-%m-%d')
+        FROM dtm_shop_value_main
+        WHERE <include refid="statDateExpression"/> IS NOT NULL
+    </select>
+
+    <select id="selectPlatformOrderCount" resultType="java.util.HashMap">
+        SELECT platform_name AS name, COUNT(1) AS value
+        FROM dtm_shop_value_main
+        <where>
+            platform_name IS NOT NULL
+            AND platform_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY platform_name
+        ORDER BY value DESC
+    </select>
+
+    <select id="selectPlatformSalesAmount" resultType="java.util.HashMap">
+        SELECT platform_name AS name, IFNULL(SUM(IFNULL(sales_amount, 0)), 0) AS value
+        FROM dtm_shop_value_main
+        <where>
+            platform_name IS NOT NULL
+            AND platform_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY platform_name
+        ORDER BY value DESC
+    </select>
+
+    <select id="selectUnitContribution" resultType="java.util.HashMap">
+        SELECT
+            business_unit_name AS name,
+            IFNULL(SUM(IFNULL(quantity, 0)), 0) AS totalVolume,
+            IFNULL(SUM(IFNULL(sales_amount, 0)), 0) AS totalAmount
+        FROM dtm_shop_value_main
+        <where>
+            business_unit_name IS NOT NULL
+            AND business_unit_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY business_unit_name
+        ORDER BY totalAmount DESC
+    </select>
+
+    <select id="selectChannelContribution" resultType="java.util.HashMap">
+        SELECT
+            channel_name AS name,
+            IFNULL(SUM(IFNULL(quantity, 0)), 0) AS totalVolume,
+            IFNULL(SUM(IFNULL(sales_amount, 0)), 0) AS totalAmount
+        FROM dtm_shop_value_main
+        <where>
+            channel_name IS NOT NULL
+            AND channel_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY channel_name
+        ORDER BY totalAmount DESC
+    </select>
+
+    <select id="selectPlatformContribution" resultType="java.util.HashMap">
+        SELECT
+            platform_name AS name,
+            IFNULL(SUM(IFNULL(quantity, 0)), 0) AS totalVolume,
+            IFNULL(SUM(IFNULL(sales_amount, 0)), 0) AS totalAmount
+        FROM dtm_shop_value_main
+        <where>
+            platform_name IS NOT NULL
+            AND platform_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY platform_name
+        ORDER BY totalAmount DESC
+    </select>
+
+    <select id="selectTopProducts" resultType="java.util.HashMap">
+        SELECT product_code AS productCode, IFNULL(SUM(IFNULL(sales_amount, 0)), 0) AS salesAmount
+        FROM dtm_shop_value_main
+        <where>
+            product_code IS NOT NULL
+            AND product_code != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY product_code
+        ORDER BY salesAmount DESC
+        LIMIT 5
+    </select>
+
+    <select id="selectTotalSales" resultType="java.lang.Double">
+        SELECT IFNULL(SUM(IFNULL(sales_amount, 0)), 0)
+        FROM dtm_shop_value_main
+        <where>
+            <include refid="dateRangeConditions"/>
+        </where>
+    </select>
+
+    <select id="selectCrossSellingProducts" resultType="java.util.HashMap">
+        SELECT product_code AS productCode, COUNT(DISTINCT platform_name) AS platformCount
+        FROM dtm_shop_value_main
+        <where>
+            product_code IS NOT NULL
+            AND product_code != ''
+            AND platform_name IS NOT NULL
+            AND platform_name != ''
+            <include refid="dateRangeConditions"/>
+            <if test="skuKeyword != null and skuKeyword != ''">
+                AND product_code LIKE CONCAT('%', #{skuKeyword}, '%')
+            </if>
+        </where>
+        GROUP BY product_code
+        HAVING COUNT(DISTINCT platform_name) &gt;= 2
+        ORDER BY platformCount DESC
+        LIMIT 1000
+    </select>
+
+    <select id="selectDepartmentEfficiency" resultType="java.util.HashMap">
+        SELECT business_unit_name AS name, IFNULL(AVG(IFNULL(sales_amount, 0)), 0) AS value
+        FROM dtm_shop_value_main
+        <where>
+            business_unit_name IS NOT NULL
+            AND business_unit_name != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY business_unit_name
+        ORDER BY value DESC
+    </select>
+
+    <select id="selectChannelDiversity" resultType="java.util.HashMap">
+        SELECT channel_name AS name, COUNT(DISTINCT product_code) AS value
+        FROM dtm_shop_value_main
+        <where>
+            channel_name IS NOT NULL
+            AND channel_name != ''
+            AND product_code IS NOT NULL
+            AND product_code != ''
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY channel_name
+        ORDER BY value DESC
+    </select>
+</mapper>