Selaa lähdekoodia

订单价值后端

Gogs 3 kuukautta sitten
vanhempi
sitoutus
c2466ee787

+ 9 - 4
dtm-admin/pom.xml

@@ -21,7 +21,7 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-devtools</artifactId>
-            <optional>true</optional> <!-- 銵函內靘肽�銝滢�隡𣳇�-->
+            <optional>true</optional> <!-- 銵函內靘肽�銝滢�隡𣳇�?-->
         </dependency>
 
         <!-- swagger3-->
@@ -30,14 +30,14 @@
             <artifactId>springfox-boot-starter</artifactId>
         </dependency>
 
-        <!-- �脫迫餈𥕦�swagger憿菟𢒰�亦掩�贝蓮�a�霂荔��㘾膄3.0.0銝剔�撘閧鍂嚗峕��典���1.6.2��𧋦 -->
+        <!-- �脫迫餈𥕦�swagger憿菟𢒰�亦掩�贝蓮�a�霂荔��㘾膄3.0.0銝剔�撘閧鍂嚗峕��典��?.6.2��𧋦 -->
         <dependency>
             <groupId>io.swagger</groupId>
             <artifactId>swagger-models</artifactId>
             <version>1.6.2</version>
         </dependency>
 
-         <!-- Mysql撽勗𢆡�-->
+         <!-- Mysql撽勗𢆡�?-->
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
@@ -50,6 +50,11 @@
         </dependency>
 
         <!-- 摰𡁏𧒄隞餃𦛚-->
+        <dependency>
+            <groupId>com.dtm</groupId>
+            <artifactId>dtm-order</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>com.dtm</groupId>
             <artifactId>dtm-quartz</artifactId>
@@ -98,4 +103,4 @@
         <finalName>${project.artifactId}</finalName>
     </build>
 
-</project>
+</project>

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

@@ -139,4 +139,13 @@ xss:
 flask:
   api:
     # Flask服务基础URL
-    base-url: http://localhost:8085
+    base-url: http://localhost:8085
+
+# 订单监测数据目录配置
+data:
+  folder:
+    path: C:/Users/akiby/Documents/dingdandata
+shop:
+  data:
+    folder:
+      path: C:/Users/akiby/Documents/shopdata

+ 53 - 0
dtm-order/pom.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>dtm_java</artifactId>
+        <groupId>com.dtm</groupId>
+        <version>3.9.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>dtm-order</artifactId>
+
+    <description>
+        订单监测模块
+    </description>
+
+    <dependencies>
+        <!-- 通用工具与注解 -->
+        <dependency>
+            <groupId>com.dtm</groupId>
+            <artifactId>dtm-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-csv</artifactId>
+            <version>1.10.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 54 - 0
dtm-order/src/main/java/com/dtm/order/DataImportController.java

@@ -0,0 +1,54 @@
+package com.dtm.order;
+
+import com.dtm.order.service.OrderDataStore;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.File;
+
+@RestController
+@RequestMapping("/api/import")
+public class DataImportController {
+
+    @Value("${data.folder.path}")
+    private String dataFolderPath;
+
+    private final OrderDataStore orderDataStore;
+
+    @Autowired
+    public DataImportController(OrderDataStore orderDataStore) {
+        this.orderDataStore = orderDataStore;
+    }
+
+    @GetMapping("/test-connection")
+    public String testConnection() {
+        long count = orderDataStore.getLastLoadCount();
+        return "Local data store ready. Loaded rows: " + count;
+    }
+
+    @GetMapping("/import-data")
+    public String importData() {
+        StringBuilder response = new StringBuilder("<html><head><title>Data Reload</title></head><body>");
+        response.append("<h1>Local CSV reload</h1>");
+
+        File folder = new File(dataFolderPath);
+        File[] files = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(".csv"));
+        if (files == null) {
+            response.append("<p style='color:red;'>Data folder not found: ").append(dataFolderPath).append("</p>");
+            return response.append("</body></html>").toString();
+        }
+
+        response.append("<p>Found ").append(files.length).append(" CSV files.</p>");
+        int loadedCount = orderDataStore.reload();
+        response.append("<p>Loaded rows: ").append(loadedCount).append("</p>");
+        response.append("<pre>").append(orderDataStore.getLastDebug()).append("</pre>");
+
+        return response.append("</body></html>").toString();
+    }
+}
+
+
+

+ 169 - 0
dtm-order/src/main/java/com/dtm/order/controller/AnalysisController.java

@@ -0,0 +1,169 @@
+package com.dtm.order.controller;
+
+import com.dtm.common.annotation.Anonymous;
+import com.dtm.order.dto.CoPurchaseDTO;
+import com.dtm.order.dto.ProductDTO;
+import com.dtm.order.service.AnalysisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Anonymous
+@RestController
+@RequestMapping("/api/analysis")
+public class AnalysisController {
+
+    @Autowired
+    private AnalysisService analysisService;
+
+    @GetMapping("/gmv")
+    public Double getTotalGMV(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            endDate = maxDate;
+            startDate = maxDate;
+        }
+
+        return analysisService.calculateTotalGMV(startDate, endDate);
+    }
+
+    @RequestMapping(value = "/top5-products", method = RequestMethod.GET)
+    public List<ProductDTO> getTop5Products(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            endDate = maxDate;
+            startDate = maxDate;
+        }
+        return analysisService.getTop5Products(startDate, endDate);
+    }
+
+    @GetMapping("/r-big")
+    public Map<String, Double> getP80AndRBig(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            endDate = maxDate;
+            startDate = maxDate;
+        }
+        return analysisService.calculateP80AndRBig(startDate, endDate);
+    }
+
+    @GetMapping("/leakage-rate")
+    public Map<String, Double> getLeakageRate(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        if (startDate == null && endDate == null) {
+            return analysisService.calculateLeakageRate(null, null);
+        }
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            if (startDate == null) {
+                startDate = endDate != null ? endDate : maxDate;
+            }
+            if (endDate == null) {
+                endDate = startDate != null ? startDate : maxDate;
+            }
+        }
+        return analysisService.calculateLeakageRate(startDate, endDate);
+    }
+
+    @GetMapping("/leakage-debug")
+    public Map<String, Object> getLeakageDebug() {
+        return analysisService.getLeakageDebug();
+    }
+
+    @RequestMapping(value = "/co-purchase", method = RequestMethod.GET)
+    public List<CoPurchaseDTO> getCoPurchaseRules() {
+        return analysisService.findCoPurchaseRules();
+    }
+
+    @GetMapping("/average-payment-time")
+    public ResponseEntity<Map<String, Object>> getAveragePaymentTime(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            endDate = maxDate;
+            startDate = maxDate;
+        }
+
+        Double averageTime = analysisService.calculateAveragePaymentTime(startDate, endDate);
+        Map<String, Object> response = new HashMap<>();
+        if (averageTime != null) {
+            response.put("success", true);
+            response.put("averagePaymentSeconds", averageTime);
+            return ResponseEntity.ok(response);
+        } else {
+            response.put("success", false);
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+        }
+    }
+
+    @GetMapping("/payment-decision-funnel")
+    public ResponseEntity<Map<String, Object>> getPaymentDecisionFunnel(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        Map<String, Long> funnelData = analysisService.analyzePaymentDecisionFunnel(startDate, endDate);
+        Map<String, Object> response = new HashMap<>();
+
+        if (funnelData != null) {
+            response.put("success", true);
+            response.put("data", funnelData);
+            response.put("message", "Payment decision funnel analysis completed.");
+            return ResponseEntity.ok(response);
+        } else {
+            response.put("success", false);
+            response.put("message", "No order data found for funnel analysis.");
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
+        }
+    }
+
+    @GetMapping("/max-date")
+    public String getMaxOrderDate() {
+        return analysisService.getMaxOrderDate();
+    }
+
+    @GetMapping("/top5-percentage")
+    public ResponseEntity<Map<String, Object>> getTop5Percentage(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (startDate == null || endDate == null) {
+            String maxDate = analysisService.getMaxOrderDate();
+            endDate = maxDate;
+            startDate = maxDate;
+        }
+
+        Double percentage = analysisService.calculateTop5Percentage(startDate, endDate);
+
+        Map<String, Object> response = new HashMap<>();
+        if (percentage != null && percentage >= 0) {
+            response.put("success", true);
+            Map<String, Object> dataMap = new HashMap<>();
+            dataMap.put("top5Percentage", percentage);
+            response.put("data", dataMap);
+            return ResponseEntity.ok(response);
+        } else {
+            response.put("success", false);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+}

+ 39 - 0
dtm-order/src/main/java/com/dtm/order/dto/CoPurchaseDTO.java

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

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

@@ -0,0 +1,43 @@
+package com.dtm.order.dto;
+
+public class ProductDTO {
+    private String name;
+    private String sku;
+    private Double totalSales;
+
+    // 鏋勯€犲嚱鏁?
+     public ProductDTO(String sku, String name, Double totalSales) {
+        this.sku = sku;
+        this.name = name;
+        this.totalSales = totalSales;
+    }
+    public ProductDTO(String name, Double totalSales) {
+        this.name = name;
+        this.totalSales = totalSales;
+    }
+
+    // Getter 鍜?Setter锛孞ava鐨勫皬涔犳儻
+     public String getSku() {
+        return sku;
+    }
+    public void setSku(String sku) {
+        this.sku = sku;
+    }
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Double getTotalSales() {
+        return totalSales;
+    }
+
+    public void setTotalSales(Double totalSales) {
+        this.totalSales = totalSales;
+    }
+}
+
+

+ 430 - 0
dtm-order/src/main/java/com/dtm/order/service/AnalysisService.java

@@ -0,0 +1,430 @@
+package com.dtm.order.service;
+
+import com.dtm.order.dto.CoPurchaseDTO;
+import com.dtm.order.dto.ProductDTO;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.InputStream;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class AnalysisService {
+
+
+    private final OrderDataStore orderDataStore;
+
+    @Autowired
+    public AnalysisService(OrderDataStore orderDataStore) {
+        this.orderDataStore = orderDataStore;
+    }
+
+    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();
+            }
+        }
+        return sum;
+    }
+
+    public List<ProductDTO> getTop5Products(String startDate, String endDate) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        Map<String, Double> salesBySku = new HashMap<>();
+        Map<String, String> nameBySku = new HashMap<>();
+
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            if (!range.contains(order.getCreatedDate())) {
+                continue;
+            }
+            String sku = order.getProductMerchantCode();
+            if (sku == null || sku.isEmpty()) {
+                sku = order.getProductId();
+            }
+            if (sku == null || sku.isEmpty()) {
+                continue;
+            }
+            salesBySku.merge(sku, order.getOrderActualPayment(), Double::sum);
+            if (!nameBySku.containsKey(sku) && order.getProductTitle() != null) {
+                nameBySku.put(sku, order.getProductTitle());
+            }
+        }
+
+        List<ProductDTO> result = new ArrayList<>();
+        salesBySku.entrySet().stream()
+                .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
+                .limit(5)
+                .forEach(entry -> {
+                    String sku = entry.getKey();
+                    String name = nameBySku.getOrDefault(sku, sku);
+                    result.add(new ProductDTO(sku, name, entry.getValue()));
+                });
+        return result;
+    }
+
+    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();
+            }
+            if (payment > 0) {
+                payments.add(payment);
+            }
+        }
+
+        if (payments.isEmpty()) {
+            Map<String, Double> empty = new HashMap<>();
+            empty.put("p80Threshold", 0.0);
+            empty.put("rBigRatio", 0.0);
+            return empty;
+        }
+
+        Collections.sort(payments);
+        int p80Index = (int) (payments.size() * 0.8);
+        if (p80Index >= payments.size()) p80Index = payments.size() - 1;
+        double p80Threshold = payments.get(p80Index);
+
+        double totalSales = 0.0;
+        double rBigSales = 0.0;
+        for (Double payment : payments) {
+            totalSales += payment;
+            if (payment >= p80Threshold) {
+                rBigSales += payment;
+            }
+        }
+
+        double rBigRatio = (totalSales > 0) ? (rBigSales / totalSales) * 100 : 0.0;
+        Map<String, Double> result = new HashMap<>();
+        result.put("p80Threshold", p80Threshold);
+        result.put("rBigRatio", rBigRatio);
+        return result;
+    }
+
+    public Map<String, Double> calculateLeakageRate(String startDate, String endDate) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        double totalRefundAmount = 0.0;
+        double totalSuccessAmount = 0.0;
+
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            if (!range.contains(order.getCreatedDate())) {
+                continue;
+            }
+            double payment = order.getOrderActualPayment();
+            if (payment <= 0 && order.getOrderPayable() > 0) {
+                payment = order.getOrderPayable();
+            }
+            if (payment > 0) {
+                totalSuccessAmount += payment;
+            }
+
+            String refundStatus = normalize(order.getOrderRefundStatus());
+            double refundAmount = order.getOrderRefundAmount();
+            if (refundAmount > 0) {
+                totalRefundAmount += refundAmount;
+            }
+        }
+
+        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;
+    }
+
+    public Map<String, Object> getLeakageDebug() {
+        Map<String, Long> statusCounts = new HashMap<>();
+        double refundAmountSum = 0.0;
+        double successAmountSum = 0.0;
+        long refundAmountCount = 0;
+        long refundStatusCount = 0;
+
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            String refundStatus = normalize(order.getOrderRefundStatus());
+            if (!refundStatus.isEmpty()) {
+                statusCounts.merge(refundStatus, 1L, Long::sum);
+                refundStatusCount++;
+            }
+
+            double refundAmount = order.getOrderRefundAmount();
+            if (refundAmount > 0) {
+                refundAmountSum += refundAmount;
+                refundAmountCount++;
+            }
+
+            double payment = order.getOrderActualPayment();
+            if (payment <= 0 && order.getOrderPayable() > 0) {
+                payment = order.getOrderPayable();
+            }
+            if (payment > 0) {
+                successAmountSum += payment;
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("orders", orderDataStore.getOrders().size());
+        result.put("refundAmountSum", refundAmountSum);
+        result.put("refundAmountCount", refundAmountCount);
+        result.put("refundStatusCount", refundStatusCount);
+        result.put("successAmountSum", successAmountSum);
+        result.put("refundStatusTop", statusCounts.entrySet().stream()
+                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
+                .limit(10)
+                .collect(Collectors.toList()));
+        return result;
+    }
+
+    public List<CoPurchaseDTO> findCoPurchaseRules() {
+        Map<String, Set<String>> purchaseProducts = new HashMap<>();
+        Map<String, String> productTitle = new HashMap<>();
+
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            String purchaseKey = order.getPurchaseId();
+            if (purchaseKey == null || purchaseKey.isEmpty()) {
+                purchaseKey = order.getPurchasePaymentId();
+            }
+            if (purchaseKey == null || purchaseKey.isEmpty()) {
+                purchaseKey = order.getOrderId();
+            }
+            if (purchaseKey == null || purchaseKey.isEmpty()) {
+                continue;
+            }
+
+            String productId = order.getProductId();
+            if (productId == null || productId.isEmpty()) {
+                productId = order.getProductMerchantCode();
+            }
+            if (productId == null || productId.isEmpty()) {
+                continue;
+            }
+
+            purchaseProducts
+                    .computeIfAbsent(purchaseKey, k -> new HashSet<>())
+                    .add(productId);
+            if (order.getProductTitle() != null && !order.getProductTitle().isEmpty()) {
+                productTitle.putIfAbsent(productId, order.getProductTitle());
+            }
+        }
+
+        Map<String, Long> pairCounts = new HashMap<>();
+        for (Set<String> products : purchaseProducts.values()) {
+            if (products.size() < 2) {
+                continue;
+            }
+            List<String> list = new ArrayList<>(products);
+            Collections.sort(list);
+            for (int i = 0; i < list.size(); i++) {
+                for (int j = i + 1; j < list.size(); j++) {
+                    String a = list.get(i);
+                    String b = list.get(j);
+                    String key = a + "||" + b;
+                    pairCounts.merge(key, 1L, Long::sum);
+                }
+            }
+        }
+
+        List<Map.Entry<String, Long>> sortedPairs = new ArrayList<>(pairCounts.entrySet());
+        sortedPairs.sort(Map.Entry.<String, Long>comparingByValue().reversed());
+
+        List<CoPurchaseDTO> result = new ArrayList<>();
+        int limit = Math.min(50, sortedPairs.size());
+        for (int i = 0; i < limit; i++) {
+            String[] ids = sortedPairs.get(i).getKey().split("\\|\\|", 2);
+            String aId = ids[0];
+            String bId = ids.length > 1 ? ids[1] : "";
+            result.add(new CoPurchaseDTO(
+                    productTitle.getOrDefault(aId, aId),
+                    aId,
+                    productTitle.getOrDefault(bId, bId),
+                    bId,
+                    sortedPairs.get(i).getValue()
+            ));
+        }
+        return result;
+    }
+
+    public Double calculateAveragePaymentTime(String startDate, String endDate) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        double totalSeconds = 0.0;
+        long count = 0;
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            if (!range.contains(order.getCreatedDate())) {
+                continue;
+            }
+            if (order.getCreatedTime() == null || order.getPaidTime() == null) {
+                continue;
+            }
+            long seconds = Duration.between(order.getCreatedTime(), order.getPaidTime()).getSeconds();
+            if (seconds >= 0) {
+                totalSeconds += seconds;
+                count++;
+            }
+        }
+        return count == 0 ? 0.0 : totalSeconds / count;
+    }
+
+    public Map<String, Long> analyzePaymentDecisionFunnel(String startDate, String endDate) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        long paidWithin5Mins = 0;
+        long paidBetween5And30Mins = 0;
+        long paidAfter30Mins = 0;
+        long unpaidOrders = 0;
+
+        for (OrderRecord order : orderDataStore.getOrders()) {
+            if (!range.contains(order.getCreatedDate())) {
+                continue;
+            }
+            if (order.getCreatedTime() == null) {
+                continue;
+            }
+            if (order.getPaidTime() == null) {
+                unpaidOrders++;
+                continue;
+            }
+            long seconds = Duration.between(order.getCreatedTime(), order.getPaidTime()).getSeconds();
+            if (seconds <= 300) {
+                paidWithin5Mins++;
+            } else if (seconds <= 1800) {
+                paidBetween5And30Mins++;
+            } else {
+                paidAfter30Mins++;
+            }
+        }
+
+        Map<String, Long> funnelData = new HashMap<>();
+        funnelData.put("paidWithin5Mins", paidWithin5Mins);
+        funnelData.put("paidBetween5And30Mins", paidBetween5And30Mins);
+        funnelData.put("paidAfter30Mins", paidAfter30Mins);
+        funnelData.put("unpaidOrders", unpaidOrders);
+        return funnelData;
+    }
+
+    public int importNewSalesData(InputStream inputStream) {
+        int successCount = 0;
+        try (XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            int rowIndex = 0;
+            for (Row row : sheet) {
+                if (rowIndex++ == 0) {
+                    continue;
+                }
+                successCount++;
+            }
+        } catch (Exception ignored) {
+            return 0;
+        }
+        return successCount;
+    }
+
+    public Double calculateTop5Percentage(String startDate, String endDate) {
+        List<ProductDTO> top5List = getTop5Products(startDate, endDate);
+        if (top5List == null || top5List.isEmpty()) {
+            return 0.0;
+        }
+        double top5Sum = top5List.stream()
+                .mapToDouble(ProductDTO::getTotalSales)
+                .sum();
+        Double totalGmv = calculateTotalGMV(startDate, endDate);
+        if (totalGmv == null || totalGmv <= 0) {
+            return 0.0;
+        }
+        return (top5Sum / totalGmv) * 100;
+    }
+
+    public String getMaxOrderDate() {
+        LocalDate maxDate = orderDataStore.getMaxOrderDate();
+        if (maxDate != null) {
+            return maxDate.toString();
+        }
+        return LocalDate.now().toString();
+    }
+
+    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 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;
+        }
+    }
+}
+
+
+
+
+

+ 305 - 0
dtm-order/src/main/java/com/dtm/order/service/OrderDataStore.java

@@ -0,0 +1,305 @@
+package com.dtm.order.service;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.io.FileReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class OrderDataStore {
+    @Value("${data.folder.path}")
+    private String dataFolderPath;
+
+    private volatile List<OrderRecord> orders = Collections.emptyList();
+    private volatile LocalDate maxOrderDate = null;
+    private volatile long lastLoadCount = 0;
+    private volatile String lastDebug = "";
+
+    @PostConstruct
+    public void loadOnStartup() {
+        reload();
+    }
+
+    public int reload() {
+        List<OrderRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
+        StringBuilder debug = new StringBuilder();
+
+        File folder = new File(dataFolderPath);
+        File[] files = folder.listFiles();
+        if (files == null) {
+            orders = Collections.emptyList();
+            maxOrderDate = null;
+            lastLoadCount = 0;
+            lastDebug = "No files found in folder: " + dataFolderPath;
+            return 0;
+        }
+
+        for (File file : files) {
+            if (!file.isFile() || !file.getName().toLowerCase().endsWith(".csv")) {
+                continue;
+            }
+            debug.append("FILE=").append(file.getName()).append(" size=").append(file.length()).append("\n");
+            int added = loadFromFile(file, CSVFormat.DEFAULT.withHeader(), Charset.forName("GBK"), loaded, debug);
+            if (added == 0) {
+                added = loadFromFile(file, CSVFormat.DEFAULT.withDelimiter(';').withHeader(), Charset.forName("GBK"), loaded, debug);
+            }
+            if (added == 0) {
+                added = loadFromFile(file, CSVFormat.TDF.withHeader(), Charset.forName("GBK"), loaded, debug);
+            }
+            if (added == 0) {
+                added = loadFromFile(file, CSVFormat.DEFAULT.withHeader(), StandardCharsets.UTF_8, loaded, debug);
+            }
+            if (added == 0) {
+                added = loadFromFile(file, CSVFormat.DEFAULT.withDelimiter(';').withHeader(), StandardCharsets.UTF_8, loaded, debug);
+            }
+            if (added == 0) {
+                added = loadFromFile(file, CSVFormat.TDF.withHeader(), StandardCharsets.UTF_8, loaded, debug);
+            }
+            debug.append("ADDED=").append(added).append("\n");
+
+            if (added > 0) {
+                LocalDate fileMaxDate = findMaxDate(loaded);
+                if (fileMaxDate != null && (maxDate == null || fileMaxDate.isAfter(maxDate))) {
+                    maxDate = fileMaxDate;
+                }
+            }
+        }
+
+        orders = Collections.unmodifiableList(loaded);
+        maxOrderDate = maxDate;
+        lastLoadCount = loaded.size();
+        lastDebug = debug.toString();
+        return loaded.size();
+    }
+
+    public List<OrderRecord> getOrders() {
+        return orders;
+    }
+
+    public LocalDate getMaxOrderDate() {
+        return maxOrderDate;
+    }
+
+    public long getLastLoadCount() {
+        return lastLoadCount;
+    }
+
+    public String getLastDebug() {
+        return lastDebug;
+    }
+
+    private OrderRecord toOrderRecord(CSVRecord record) {
+        if (record == null || record.size() < 20) {
+            return null;
+        }
+
+        String orderId = safeGetString(record, 0);
+        String purchaseId = safeGetString(record, 1);
+        String productTitle = safeGetString(record, 2);
+        double orderPrice = safeGetDouble(record, 3);
+        int orderQuantity = safeGetInt(record, 4);
+        String productId = safeGetString(record, 5);
+        String productProperties = safeGetString(record, 6);
+        String orderStatus = safeGetString(record, 11);
+        String productMerchantCode = safeGetString(record, 12);
+        String purchasePaymentId = safeGetString(record, 13);
+        double orderPayable = safeGetDouble(record, 14);
+        double orderActualPayment = safeGetDouble(record, 15);
+        String orderRefundStatus = safeGetString(record, 16);
+        double orderRefundAmount = safeGetDouble(record, 17);
+        String createdTimeRaw = safeGetString(record, 18);
+        String paidTimeRaw = safeGetString(record, 19);
+
+        LocalDateTime createdTime = parseDateTime(createdTimeRaw);
+        LocalDateTime paidTime = parseDateTime(paidTimeRaw);
+
+        if (purchaseId.isEmpty()
+                && orderId.isEmpty()
+                && productId.isEmpty()
+                && productMerchantCode.isEmpty()) {
+            return null;
+        }
+
+        return new OrderRecord(
+                purchaseId,
+                orderId,
+                productId,
+                productTitle,
+                productProperties,
+                productMerchantCode,
+                orderPrice,
+                orderQuantity,
+                orderStatus,
+                orderPayable,
+                orderActualPayment,
+                orderRefundStatus,
+                orderRefundAmount,
+                createdTime,
+                paidTime,
+                purchasePaymentId
+        );
+    }
+
+    private int loadFromFile(File file, CSVFormat format, Charset charset, List<OrderRecord> target, StringBuilder debug) {
+        List<OrderRecord> buffer = new ArrayList<>();
+        int added = 0;
+        int garbled = 0;
+        try (Reader reader = new FileReader(file, charset);
+             CSVParser csvParser = new CSVParser(reader, format)) {
+            for (CSVRecord record : csvParser) {
+                OrderRecord order = toOrderRecord(record);
+                if (order != null) {
+                    buffer.add(order);
+                    added++;
+                    if (isGarbled(order.getProductTitle())) {
+                        garbled++;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            debug.append("ERROR=").append(e.getMessage()).append("\n");
+            return 0;
+        }
+
+        if (added > 0) {
+            double ratio = (double) garbled / (double) added;
+            if (ratio > 0.2) {
+                debug.append("GARBLED_RATIO=").append(ratio).append("\n");
+                return 0;
+            }
+            target.addAll(buffer);
+        }
+        return added;
+    }
+
+    private LocalDate findMaxDate(List<OrderRecord> records) {
+        LocalDate max = null;
+        for (OrderRecord record : records) {
+            LocalDate date = record.getCreatedDate();
+            if (date != null && (max == null || date.isAfter(max))) {
+                max = date;
+            }
+        }
+        return max;
+    }
+
+    private static String safeGetString(CSVRecord record, String header) {
+        return record.isSet(header) ? record.get(header).trim() : "";
+    }
+
+    private static String safeGetString(CSVRecord record, int index) {
+        if (index < 0 || index >= record.size()) {
+            return "";
+        }
+        String value = record.get(index);
+        return value == null ? "" : value.trim();
+    }
+
+    private static double safeGetDouble(CSVRecord record, String header) {
+        if (!record.isSet(header) || record.get(header).isEmpty()) {
+            return 0.0;
+        }
+        try {
+            return Double.parseDouble(record.get(header));
+        } catch (NumberFormatException e) {
+            return 0.0;
+        }
+    }
+
+    private static double safeGetDouble(CSVRecord record, int index) {
+        String value = safeGetString(record, index);
+        if (value.isEmpty()) {
+            return 0.0;
+        }
+        try {
+            return Double.parseDouble(value);
+        } catch (NumberFormatException e) {
+            return 0.0;
+        }
+    }
+
+    private static int safeGetInt(CSVRecord record, String header) {
+        if (!record.isSet(header) || record.get(header).isEmpty()) {
+            return 0;
+        }
+        try {
+            return Integer.parseInt(record.get(header));
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    private static int safeGetInt(CSVRecord record, int index) {
+        String value = safeGetString(record, index);
+        if (value.isEmpty()) {
+            return 0;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    private static LocalDateTime parseDateTime(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        String value = raw.trim();
+        if (value.isEmpty()) {
+            return null;
+        }
+        value = value.replace('/', '-');
+        if (value.contains(" ")) {
+            value = value.replace(" ", "T");
+        }
+
+        LocalDateTime parsed = tryParseDateTime(value, "yyyy-M-d'T'HH:mm:ss");
+        if (parsed != null) return parsed;
+        parsed = tryParseDateTime(value, "yyyy-M-d'T'HH:mm");
+        if (parsed != null) return parsed;
+        parsed = tryParseDateTime(value, "yyyy-MM-dd'T'HH:mm:ss");
+        if (parsed != null) return parsed;
+        parsed = tryParseDateTime(value, "yyyy-MM-dd'T'HH:mm");
+        if (parsed != null) return parsed;
+
+        try {
+            LocalDate date = LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-M-d"));
+            return date.atStartOfDay();
+        } catch (DateTimeParseException e) {
+            return null;
+        }
+    }
+
+    private static LocalDateTime tryParseDateTime(String value, String pattern) {
+        try {
+            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
+        } catch (DateTimeParseException e) {
+            return null;
+        }
+    }
+
+    private boolean isGarbled(String value) {
+        return value != null && value.contains("\uFFFD");
+    }
+}
+
+
+
+

+ 83 - 0
dtm-order/src/main/java/com/dtm/order/service/OrderRecord.java

@@ -0,0 +1,83 @@
+package com.dtm.order.service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+public class OrderRecord {
+    private final String purchaseId;
+    private final String orderId;
+    private final String productId;
+    private final String productTitle;
+    private final String productProperties;
+    private final String productMerchantCode;
+    private final double orderPrice;
+    private final int orderQuantity;
+    private final String orderStatus;
+    private final double orderPayable;
+    private final double orderActualPayment;
+    private final String orderRefundStatus;
+    private final double orderRefundAmount;
+    private final LocalDateTime createdTime;
+    private final LocalDateTime paidTime;
+    private final String purchasePaymentId;
+
+    public OrderRecord(
+            String purchaseId,
+            String orderId,
+            String productId,
+            String productTitle,
+            String productProperties,
+            String productMerchantCode,
+            double orderPrice,
+            int orderQuantity,
+            String orderStatus,
+            double orderPayable,
+            double orderActualPayment,
+            String orderRefundStatus,
+            double orderRefundAmount,
+            LocalDateTime createdTime,
+            LocalDateTime paidTime,
+            String purchasePaymentId
+    ) {
+        this.purchaseId = purchaseId;
+        this.orderId = orderId;
+        this.productId = productId;
+        this.productTitle = productTitle;
+        this.productProperties = productProperties;
+        this.productMerchantCode = productMerchantCode;
+        this.orderPrice = orderPrice;
+        this.orderQuantity = orderQuantity;
+        this.orderStatus = orderStatus;
+        this.orderPayable = orderPayable;
+        this.orderActualPayment = orderActualPayment;
+        this.orderRefundStatus = orderRefundStatus;
+        this.orderRefundAmount = orderRefundAmount;
+        this.createdTime = createdTime;
+        this.paidTime = paidTime;
+        this.purchasePaymentId = purchasePaymentId;
+    }
+
+    public String getPurchaseId() { return purchaseId; }
+    public String getOrderId() { return orderId; }
+    public String getProductId() { return productId; }
+    public String getProductTitle() { return productTitle; }
+    public String getProductProperties() { return productProperties; }
+    public String getProductMerchantCode() { return productMerchantCode; }
+    public double getOrderPrice() { return orderPrice; }
+    public int getOrderQuantity() { return orderQuantity; }
+    public String getOrderStatus() { return orderStatus; }
+    public double getOrderPayable() { return orderPayable; }
+    public double getOrderActualPayment() { return orderActualPayment; }
+    public String getOrderRefundStatus() { return orderRefundStatus; }
+    public double getOrderRefundAmount() { return orderRefundAmount; }
+    public LocalDateTime getCreatedTime() { return createdTime; }
+    public LocalDateTime getPaidTime() { return paidTime; }
+    public String getPurchasePaymentId() { return purchasePaymentId; }
+
+    public LocalDate getCreatedDate() {
+        return createdTime == null ? null : createdTime.toLocalDate();
+    }
+}
+
+
+

+ 249 - 0
dtm-order/src/main/java/com/dtm/order/shop/Controller/ShopDataImportController.java

@@ -0,0 +1,249 @@
+package com.dtm.order.shop.Controller;
+
+import com.dtm.order.shop.service.ShopAnalysisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/shop/import")
+public class ShopDataImportController {
+
+    private final ShopAnalysisService shopAnalysisService;
+    private final String shopDataFolderPath;
+
+    @Autowired
+    public ShopDataImportController(ShopAnalysisService shopAnalysisService,
+                                    @Value("${shop.data.folder.path}") String shopDataFolderPath) {
+        this.shopAnalysisService = shopAnalysisService;
+        this.shopDataFolderPath = shopDataFolderPath;
+    }
+
+    @GetMapping("/test-connection")
+    public String testConnection() {
+        return "Shop data store ready. Path: " + shopDataFolderPath;
+    }
+
+    @GetMapping("/import-sales-data")
+    public ResponseEntity<Map<String, Object>> importSalesData() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            int importedCount = shopAnalysisService.importAllShopSalesData();
+
+            if (importedCount < 0) {
+                response.put("success", false);
+                response.put("message", "Import failed. Check folder path or CSV files.");
+                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
+            }
+
+            response.put("success", true);
+            response.put("message", "Shop sales data loaded.");
+            response.put("importedRows", importedCount);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Import failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/channel-contribution")
+    public ResponseEntity<Map<String, Object>> getChannelContribution() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Channel contribution ready.");
+            response.put("data", contributionData);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/channel-roi-value")
+    public ResponseEntity<Map<String, Object>> getChannelRoiValue() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Channel ROI ready.");
+            response.put("data", roiData);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/unit-contribution")
+    public ResponseEntity<Map<String, Object>> getUnitContribution() {
+        Map<String, Object> response = new HashMap<>();
+        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);
+        }
+
+        response.put("success", true);
+        response.put("message", "Unit contribution ready.");
+        response.put("data", data);
+        return ResponseEntity.ok(response);
+    }
+
+    @GetMapping("/channel-total-contribution")
+    public ResponseEntity<Map<String, Object>> getChannelTotalContribution() {
+        Map<String, Object> response = new HashMap<>();
+        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);
+        }
+
+        response.put("success", true);
+        response.put("message", "Channel total contribution ready.");
+        response.put("data", data);
+        return ResponseEntity.ok(response);
+    }
+
+    @GetMapping("/platform-total-contribution")
+    public ResponseEntity<Map<String, Object>> getPlatformTotalContribution() {
+        Map<String, Object> response = new HashMap<>();
+        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);
+        }
+
+        response.put("success", true);
+        response.put("message", "Platform total contribution ready.");
+        response.put("data", data);
+        return ResponseEntity.ok(response);
+    }
+
+    @GetMapping("/top-product-contribution")
+    public ResponseEntity<Map<String, Object>> getTopProductContribution() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Top product contribution ready.");
+            response.put("data", result);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/cross-selling-products")
+    public ResponseEntity<Map<String, Object>> getCrossSellingProducts() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Cross-selling products ready.");
+            response.put("data", data);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/department-efficiency")
+    public ResponseEntity<Map<String, Object>> getDepartmentOperationalEfficiency() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Department efficiency ready.");
+            response.put("data", data);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
+    @GetMapping("/channel-diversity")
+    public ResponseEntity<Map<String, Object>> getChannelProductDiversity() {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            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);
+            }
+
+            response.put("success", true);
+            response.put("message", "Channel diversity ready.");
+            response.put("data", data);
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "Analysis failed: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+}
+
+
+

+ 34 - 0
dtm-order/src/main/java/com/dtm/order/shop/dto/ShopSalesDTO.java

@@ -0,0 +1,34 @@
+package com.dtm.order.shop.dto;
+
+// DTO涓嶉渶瑕佸お澶氬唴瀹癸紝鍙渶瑕佷竴涓畝鍗曠殑缁撴瀯
+public class ShopSalesDTO {
+    // 鏍规嵁闇€瑕侊紝鍙互鐢ㄦ潵瑁呰浇娓犻亾鍚嶇О鍜岄攢鍞
+    private String channelName;
+    private double totalSales;
+    
+    // 鏋勯€犲嚱鏁般€丟etter鍜孲etter...
+    
+    public ShopSalesDTO(String channelName, double totalSales) {
+        this.channelName = channelName;
+        this.totalSales = totalSales;
+    }
+
+    public String getChannelName() {
+        return channelName;
+    }
+
+    public void setChannelName(String channelName) {
+        this.channelName = channelName;
+    }
+
+    public double getTotalSales() {
+        return totalSales;
+    }
+
+    public void setTotalSales(double totalSales) {
+        this.totalSales = totalSales;
+    }
+}
+
+
+

+ 174 - 0
dtm-order/src/main/java/com/dtm/order/shop/service/ShopAnalysisService.java

@@ -0,0 +1,174 @@
+package com.dtm.order.shop.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class ShopAnalysisService {
+
+    private final ShopSalesDataStore dataStore;
+
+    @Autowired
+    public ShopAnalysisService(ShopSalesDataStore dataStore) {
+        this.dataStore = dataStore;
+    }
+
+    public int importAllShopSalesData() {
+        return dataStore.reload();
+    }
+
+    public Map<String, Double> getChannelSalesContribution() {
+        Map<String, Double> contributionData = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            contributionData.merge(record.getPlatformName(), 1.0, Double::sum);
+        }
+        return contributionData.isEmpty() ? null : contributionData;
+    }
+
+    public Map<String, Double> getChannelRoiValue() {
+        Map<String, Double> resultData = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            resultData.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
+        }
+        return resultData.isEmpty() ? null : resultData;
+    }
+
+    public List<Map<String, Object>> getUnitContribution() {
+        Map<String, Double> volumeByUnit = new HashMap<>();
+        Map<String, Double> amountByUnit = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            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() {
+        Map<String, Double> volumeByChannel = new HashMap<>();
+        Map<String, Double> amountByChannel = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            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() {
+        Map<String, Double> volumeByPlatform = new HashMap<>();
+        Map<String, Double> amountByPlatform = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            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() {
+        double totalSales = 0.0;
+        Map<String, Double> productSales = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            totalSales += record.getSalesAmount();
+            productSales.merge(record.getProductCode(), record.getSalesAmount(), Double::sum);
+        }
+
+        List<Map.Entry<String, Double>> sorted = new ArrayList<>(productSales.entrySet());
+        sorted.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
+
+        double top5SalesSum = 0.0;
+        List<Map<String, Object>> topProducts = new ArrayList<>();
+        int limit = Math.min(5, sorted.size());
+        for (int i = 0; i < limit; i++) {
+            Map.Entry<String, Double> entry = sorted.get(i);
+            top5SalesSum += entry.getValue();
+            Map<String, Object> productMap = new HashMap<>();
+            productMap.put("productCode", entry.getKey());
+            productMap.put("salesAmount", entry.getValue());
+            topProducts.add(productMap);
+        }
+
+        double contributionRatio = (totalSales > 0) ? (top5SalesSum / totalSales) : 0.0;
+        Map<String, Object> finalResult = new HashMap<>();
+        finalResult.put("top5Products", topProducts);
+        finalResult.put("top5TotalSales", top5SalesSum);
+        finalResult.put("totalSales", totalSales);
+        finalResult.put("contributionRatio", contributionRatio);
+        return finalResult;
+    }
+
+    public List<Map<String, Object>> getCrossSellingProducts() {
+        Map<String, Set<String>> platformByProduct = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            platformByProduct
+                    .computeIfAbsent(record.getProductCode(), k -> new HashSet<>())
+                    .add(record.getPlatformName());
+        }
+
+        List<Map<String, Object>> crossSellingData = new ArrayList<>();
+        for (Map.Entry<String, Set<String>> entry : platformByProduct.entrySet()) {
+            int platformCount = entry.getValue().size();
+            if (platformCount >= 2) {
+                Map<String, Object> productMap = new HashMap<>();
+                productMap.put("productCode", entry.getKey());
+                productMap.put("platformCount", (long) platformCount);
+                crossSellingData.add(productMap);
+            }
+        }
+        return crossSellingData.isEmpty() ? null : crossSellingData;
+    }
+
+    public Map<String, Double> getDepartmentOperationalEfficiency() {
+        Map<String, Double> sumByUnit = new HashMap<>();
+        Map<String, Long> countByUnit = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            sumByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
+            countByUnit.merge(record.getBusinessUnitName(), 1L, Long::sum);
+        }
+        Map<String, Double> efficiencyData = new HashMap<>();
+        for (Map.Entry<String, Double> entry : sumByUnit.entrySet()) {
+            long count = countByUnit.getOrDefault(entry.getKey(), 0L);
+            if (count > 0) {
+                efficiencyData.put(entry.getKey(), entry.getValue() / count);
+            }
+        }
+        return efficiencyData.isEmpty() ? null : efficiencyData;
+    }
+
+    public Map<String, Long> getChannelProductDiversity() {
+        Map<String, Set<String>> productsByChannel = new HashMap<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            productsByChannel
+                    .computeIfAbsent(record.getChannelName(), k -> new HashSet<>())
+                    .add(record.getProductCode());
+        }
+        Map<String, Long> diversityData = new HashMap<>();
+        for (Map.Entry<String, Set<String>> entry : productsByChannel.entrySet()) {
+            diversityData.put(entry.getKey(), (long) entry.getValue().size());
+        }
+        return diversityData.isEmpty() ? null : diversityData;
+    }
+
+    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<>();
+            map.put("name", name);
+            map.put("totalVolume", volume.getOrDefault(name, 0.0));
+            map.put("totalAmount", amount.getOrDefault(name, 0.0));
+            result.add(map);
+        }
+        return result.isEmpty() ? null : result;
+    }
+}
+
+
+

+ 104 - 0
dtm-order/src/main/java/com/dtm/order/shop/service/ShopSalesDataStore.java

@@ -0,0 +1,104 @@
+package com.dtm.order.shop.service;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.io.FileReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+@Service
+public class ShopSalesDataStore {
+    @Value("${shop.data.folder.path}")
+    private String shopDataFolderPath;
+
+    private volatile List<ShopSalesRecord> salesRecords = Collections.emptyList();
+    private volatile long lastLoadCount = 0;
+
+    @PostConstruct
+    public void loadOnStartup() {
+        reload();
+    }
+
+    public int reload() {
+        List<ShopSalesRecord> loaded = new ArrayList<>();
+        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;
+            return 0;
+        }
+
+        for (File file : files) {
+            try (Reader reader = new FileReader(file, StandardCharsets.UTF_8);
+                 CSVParser csvParser = new CSVParser(reader, CSVFormat.EXCEL.builder().setTrim(true).build())) {
+
+                Iterator<CSVRecord> csvIterator = csvParser.iterator();
+                if (csvIterator.hasNext()) {
+                    csvIterator.next();
+                }
+
+                while (csvIterator.hasNext()) {
+                    CSVRecord csvRecord = csvIterator.next();
+                    if (csvRecord.size() < 10) {
+                        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.
+                    }
+                }
+            } catch (Exception ignored) {
+                // Skip bad files.
+            }
+        }
+
+        salesRecords = Collections.unmodifiableList(loaded);
+        lastLoadCount = loaded.size();
+        return loaded.size();
+    }
+
+    public List<ShopSalesRecord> getSalesRecords() {
+        return salesRecords;
+    }
+
+    public long getLastLoadCount() {
+        return lastLoadCount;
+    }
+}
+
+
+

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

@@ -0,0 +1,52 @@
+package com.dtm.order.shop.service;
+
+public class ShopSalesRecord {
+    private final int year;
+    private final int month;
+    private final int day;
+    private final String businessUnitName;
+    private final String channelName;
+    private final String platformName;
+    private final String productCode;
+    private final double quantity;
+    private final double unitPrice;
+    private final double salesAmount;
+
+    public ShopSalesRecord(
+            int year,
+            int month,
+            int day,
+            String businessUnitName,
+            String channelName,
+            String platformName,
+            String productCode,
+            double quantity,
+            double unitPrice,
+            double salesAmount
+    ) {
+        this.year = year;
+        this.month = month;
+        this.day = day;
+        this.businessUnitName = businessUnitName;
+        this.channelName = channelName;
+        this.platformName = platformName;
+        this.productCode = productCode;
+        this.quantity = quantity;
+        this.unitPrice = unitPrice;
+        this.salesAmount = salesAmount;
+    }
+
+    public int getYear() { return year; }
+    public int getMonth() { return month; }
+    public int getDay() { return day; }
+    public String getBusinessUnitName() { return businessUnitName; }
+    public String getChannelName() { return channelName; }
+    public String getPlatformName() { return platformName; }
+    public String getProductCode() { return productCode; }
+    public double getQuantity() { return quantity; }
+    public double getUnitPrice() { return unitPrice; }
+    public double getSalesAmount() { return salesAmount; }
+}
+
+
+

+ 5 - 0
mvn-settings.xml

@@ -0,0 +1,5 @@
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
+  <localRepository>C:\Users\akiby\Desktop\houduan\.m2\repository</localRepository>
+</settings>

+ 29 - 19
pom.xml

@@ -41,7 +41,7 @@
     <dependencyManagement>
         <dependencies>
 
-            <!-- 覆盖SpringFramework的�赖�置-->
+            <!-- 覆盖SpringFrameworkçš„ä¾�èµ–é…�ç½-->
             <dependency>
                 <groupId>org.springframework</groupId>
                 <artifactId>spring-framework-bom</artifactId>
@@ -50,7 +50,7 @@
                 <scope>import</scope>
             </dependency>
 
-            <!-- 覆盖SpringSecurity的�赖�置-->
+            <!-- 覆盖SpringSecurityçš„ä¾�èµ–é…�ç½-->
             <dependency>
                 <groupId>org.springframework.security</groupId>
                 <artifactId>spring-security-bom</artifactId>
@@ -59,7 +59,7 @@
                 <scope>import</scope>
             </dependency>
 
-            <!-- SpringBoot的�赖�置-->
+            <!-- SpringBootçš„ä¾�èµ–é…�ç½-->
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-dependencies</artifactId>
@@ -68,7 +68,7 @@
                 <scope>import</scope>
             </dependency>
 
-            <!-- 覆盖logback的�赖�置-->
+            <!-- 覆盖logbackçš„ä¾�èµ–é…�ç½-->
             <dependency>
                 <groupId>ch.qos.logback</groupId>
                 <artifactId>logback-core</artifactId>
@@ -81,7 +81,7 @@
                 <version>${logback.version}</version>
             </dependency>
 
-            <!-- 覆盖tomcat的�赖�置-->
+            <!-- 覆盖tomcatçš„ä¾�èµ–é…�ç½-->
             <dependency>
                 <groupId>org.apache.tomcat.embed</groupId>
                 <artifactId>tomcat-embed-core</artifactId>
@@ -107,7 +107,7 @@
                 <version>${druid.version}</version>
             </dependency>
 
-            <!-- è§£æž�客户端æ“�作系统ã€�æµ�览器ç­-->
+            <!-- 解�客户端�作系统��览器�-->
             <dependency>
                 <groupId>eu.bitwalker</groupId>
                 <artifactId>UserAgentUtils</artifactId>
@@ -141,7 +141,7 @@
                 </exclusions>
             </dependency>
 
-            <!-- io常用工具类 -->
+            <!-- io常用工具�-->
             <dependency>
                 <groupId>commons-io</groupId>
                 <artifactId>commons-io</artifactId>
@@ -162,21 +162,21 @@
                 <version>${velocity.version}</version>
             </dependency>
 
-            <!-- 阿里JSON解�器 -->
+            <!-- 阿里JSON解��-->
             <dependency>
                 <groupId>com.alibaba.fastjson2</groupId>
                 <artifactId>fastjson2</artifactId>
                 <version>${fastjson.version}</version>
             </dependency>
 
-            <!-- Token生æˆ�与解æž-->
+            <!-- Token生æˆ�与解æž-->
             <dependency>
                 <groupId>io.jsonwebtoken</groupId>
                 <artifactId>jjwt</artifactId>
                 <version>${jwt.version}</version>
             </dependency>
 
-            <!-- 验è¯�ç -->
+            <!-- 验��-->
             <dependency>
                 <groupId>pro.fessional</groupId>
                 <artifactId>kaptcha</artifactId>
@@ -212,6 +212,12 @@
             </dependency>
 
             <!-- 通用工具-->
+            <dependency>
+                <groupId>com.dtm</groupId>
+                <artifactId>dtm-order</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>com.dtm</groupId>
                 <artifactId>dtm-common</artifactId>
@@ -221,14 +227,15 @@
         </dependencies>
     </dependencyManagement>
 
-    <modules>
-        <module>dtm-admin</module>
-        <module>dtm-framework</module>
-        <module>dtm-system</module>
-        <module>dtm-quartz</module>
-        <module>dtm-generator</module>
-        <module>dtm-common</module>
-    </modules>
+    <modules>
+        <module>dtm-admin</module>
+        <module>dtm-framework</module>
+        <module>dtm-system</module>
+        <module>dtm-order</module>
+        <module>dtm-quartz</module>
+        <module>dtm-generator</module>
+        <module>dtm-common</module>
+    </modules>
     <packaging>pom</packaging>
 
     <build>
@@ -276,4 +283,7 @@
         </pluginRepository>
     </pluginRepositories>
 
-</project>
+</project>
+
+
+