Jelajahi Sumber

生命周期模块生命周期分析、爆品分析改写为连接数据库

Zhu Jiaqi 3 minggu lalu
induk
melakukan
18de8961c6
16 mengubah file dengan 1988 tambahan dan 74 penghapusan
  1. 0 11
      dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/HotProductionAnalysisController.java
  2. 32 0
      dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/HotProductionController.java
  3. 11 63
      dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/LifecycleAnalysisController.java
  4. 28 0
      dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/LifecycleDatabaseOverviewController.java
  5. 139 0
      dtm-system/src/main/java/com/dtm/lifecycle/domain/HotProductAggregate.java
  6. 70 0
      dtm-system/src/main/java/com/dtm/lifecycle/domain/LifecycleSkuDailySales.java
  7. 13 0
      dtm-system/src/main/java/com/dtm/lifecycle/mapper/HotProductDatabaseMapper.java
  8. 19 0
      dtm-system/src/main/java/com/dtm/lifecycle/mapper/LifecycleDatabaseOverviewMapper.java
  9. 10 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/IHotProductDatabaseService.java
  10. 10 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/ILifecycleDatabaseAnalysisService.java
  11. 8 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/ILifecycleDatabaseOverviewService.java
  12. 344 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/impl/HotProductDatabaseServiceImpl.java
  13. 522 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleDatabaseAnalysisServiceImpl.java
  14. 514 0
      dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleDatabaseOverviewServiceImpl.java
  15. 192 0
      dtm-system/src/main/resources/mapper/lifecycle/HotProductDatabaseMapper.xml
  16. 76 0
      dtm-system/src/main/resources/mapper/lifecycle/LifecycleDatabaseOverviewMapper.xml

+ 0 - 11
dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/HotProductionAnalysisController.java

@@ -1,11 +0,0 @@
-package com.dtm.web.controller.lifecycle;
-
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/lifecycle/hotproduct")
-public class HotProductionAnalysisController
-{
-    
-}

+ 32 - 0
dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/HotProductionController.java

@@ -0,0 +1,32 @@
+package com.dtm.web.controller.lifecycle;
+
+import com.dtm.common.core.controller.BaseController;
+import com.dtm.common.core.domain.AjaxResult;
+import com.dtm.lifecycle.service.IHotProductDatabaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/lifecycle/hotproduct")
+public class HotProductionController extends BaseController
+{
+    @Autowired
+    private IHotProductDatabaseService hotProductDatabaseService;
+
+    @GetMapping("/database/sku-results")
+    public AjaxResult getDatabaseSkuResults(@RequestParam Map<String, Object> params)
+    {
+        return AjaxResult.success(hotProductDatabaseService.getSkuResults(params));
+    }
+
+    @GetMapping("/database/spu-results")
+    public AjaxResult getDatabaseSpuResults(@RequestParam Map<String, Object> params)
+    {
+        return AjaxResult.success(hotProductDatabaseService.getSpuResults(params));
+    }
+}

+ 11 - 63
dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/LifecycleAnalysisController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 import com.dtm.common.core.domain.AjaxResult;
 import com.dtm.lifecycle.service.ILifecycleAnalysisService;
+import com.dtm.lifecycle.service.ILifecycleDatabaseAnalysisService;
 
 /**
  * 生命周期统计分析控制器
@@ -21,76 +22,23 @@ import com.dtm.lifecycle.service.ILifecycleAnalysisService;
 public class LifecycleAnalysisController
 {
     @Autowired
-    private ILifecycleAnalysisService lifecycleStatisticsService;
+    private ILifecycleDatabaseAnalysisService lifecycleDatabaseAnalysisService;
 
     /**
-     * 上传文件并分析
+     * 获取数据库版SKU生命周期分析结果
      */
-    @PostMapping("/upload")
-    public AjaxResult uploadAndAnalyze(MultipartFile file)
+    @GetMapping("/database/sku-results")
+    public AjaxResult getDatabaseSkuResults(@RequestParam Map<String, Object> params)
     {
-        if (file == null || file.isEmpty())
-        {
-            return AjaxResult.error("请选择要上传的文件");
-        }
-        
-        Map<String, Object> result = lifecycleStatisticsService.uploadAndAnalyze(file);
-        
-        Integer code = (Integer) result.get("code");
-        String msg = (String) result.get("msg");
-        Object data = result.get("data");
-        
-        if (code == 200)
-        {
-            return AjaxResult.success(msg, data);
-        }
-        else
-        {
-            return AjaxResult.error(msg);
-        }
+        return AjaxResult.success(lifecycleDatabaseAnalysisService.getSkuResults(params));
     }
-    
-    /**
-     * 获取生命周期分析总览
-     */
-    @GetMapping("/overview")
-    public AjaxResult getOverview(@RequestParam Map<String, Object> params)
-    {
-        Map<String, Object> result = lifecycleStatisticsService.getOverview(params);
-        
-        Integer code = (Integer) result.get("code");
-        String msg = (String) result.get("msg");
-        Object data = result.get("data");
-        
-        if (code == 200)
-        {
-            return AjaxResult.success(msg, data);
-        }
-        else
-        {
-            return AjaxResult.error(msg);
-        }
-    }
-    
+
     /**
-     * 获取分析结果
+     * 获取数据库版SPU生命周期分析结果
      */
-    @GetMapping("/results")
-    public AjaxResult getResults()
+    @GetMapping("/database/spu-results")
+    public AjaxResult getDatabaseSpuResults(@RequestParam Map<String, Object> params)
     {
-        Map<String, Object> result = lifecycleStatisticsService.getResults();
-        
-        Integer code = (Integer) result.get("code");
-        String msg = (String) result.get("msg");
-        Object data = result.get("data");
-        
-        if (code == 200)
-        {
-            return AjaxResult.success(msg, data);
-        }
-        else
-        {
-            return AjaxResult.error(msg);
-        }
+        return AjaxResult.success(lifecycleDatabaseAnalysisService.getSpuResults(params));
     }
 }

+ 28 - 0
dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/LifecycleDatabaseOverviewController.java

@@ -0,0 +1,28 @@
+package com.dtm.web.controller.lifecycle;
+
+import com.dtm.common.core.domain.AjaxResult;
+import com.dtm.lifecycle.service.ILifecycleDatabaseOverviewService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * Database-backed lifecycle overview controller.
+ */
+@RestController
+@RequestMapping("/lifecycle/database")
+public class LifecycleDatabaseOverviewController
+{
+    @Autowired
+    private ILifecycleDatabaseOverviewService lifecycleDatabaseOverviewService;
+
+    @GetMapping("/overview")
+    public AjaxResult getOverview(@RequestParam Map<String, Object> params)
+    {
+        return AjaxResult.success(lifecycleDatabaseOverviewService.getOverview(params));
+    }
+}

+ 139 - 0
dtm-system/src/main/java/com/dtm/lifecycle/domain/HotProductAggregate.java

@@ -0,0 +1,139 @@
+package com.dtm.lifecycle.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+public class HotProductAggregate
+{
+    private String entityId;
+
+    private String productTitle;
+
+    private Long totalQuantity;
+
+    private BigDecimal totalRevenue;
+
+    private BigDecimal avgPrice;
+
+    private Date firstOrderTime;
+
+    private Date lastOrderTime;
+
+    private Long uniqueBuyers;
+
+    private Long repurchaseBuyers;
+
+    private BigDecimal refundRate;
+
+    private BigDecimal nightOrderRatio;
+
+    public String getEntityId()
+    {
+        return entityId;
+    }
+
+    public void setEntityId(String entityId)
+    {
+        this.entityId = entityId;
+    }
+
+    public String getProductTitle()
+    {
+        return productTitle;
+    }
+
+    public void setProductTitle(String productTitle)
+    {
+        this.productTitle = productTitle;
+    }
+
+    public Long getTotalQuantity()
+    {
+        return totalQuantity;
+    }
+
+    public void setTotalQuantity(Long totalQuantity)
+    {
+        this.totalQuantity = totalQuantity;
+    }
+
+    public BigDecimal getTotalRevenue()
+    {
+        return totalRevenue;
+    }
+
+    public void setTotalRevenue(BigDecimal totalRevenue)
+    {
+        this.totalRevenue = totalRevenue;
+    }
+
+    public BigDecimal getAvgPrice()
+    {
+        return avgPrice;
+    }
+
+    public void setAvgPrice(BigDecimal avgPrice)
+    {
+        this.avgPrice = avgPrice;
+    }
+
+    public Date getFirstOrderTime()
+    {
+        return firstOrderTime;
+    }
+
+    public void setFirstOrderTime(Date firstOrderTime)
+    {
+        this.firstOrderTime = firstOrderTime;
+    }
+
+    public Date getLastOrderTime()
+    {
+        return lastOrderTime;
+    }
+
+    public void setLastOrderTime(Date lastOrderTime)
+    {
+        this.lastOrderTime = lastOrderTime;
+    }
+
+    public Long getUniqueBuyers()
+    {
+        return uniqueBuyers;
+    }
+
+    public void setUniqueBuyers(Long uniqueBuyers)
+    {
+        this.uniqueBuyers = uniqueBuyers;
+    }
+
+    public Long getRepurchaseBuyers()
+    {
+        return repurchaseBuyers;
+    }
+
+    public void setRepurchaseBuyers(Long repurchaseBuyers)
+    {
+        this.repurchaseBuyers = repurchaseBuyers;
+    }
+
+    public BigDecimal getRefundRate()
+    {
+        return refundRate;
+    }
+
+    public void setRefundRate(BigDecimal refundRate)
+    {
+        this.refundRate = refundRate;
+    }
+
+    public BigDecimal getNightOrderRatio()
+    {
+        return nightOrderRatio;
+    }
+
+    public void setNightOrderRatio(BigDecimal nightOrderRatio)
+    {
+        this.nightOrderRatio = nightOrderRatio;
+    }
+}

+ 70 - 0
dtm-system/src/main/java/com/dtm/lifecycle/domain/LifecycleSkuDailySales.java

@@ -0,0 +1,70 @@
+package com.dtm.lifecycle.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * SKU daily sales aggregate for lifecycle overview.
+ */
+public class LifecycleSkuDailySales
+{
+    private String sku;
+
+    private String productName;
+
+    private Date saleDate;
+
+    private Long totalQuantity;
+
+    private BigDecimal totalRevenue;
+
+    public String getSku()
+    {
+        return sku;
+    }
+
+    public void setSku(String sku)
+    {
+        this.sku = sku;
+    }
+
+    public String getProductName()
+    {
+        return productName;
+    }
+
+    public void setProductName(String productName)
+    {
+        this.productName = productName;
+    }
+
+    public Date getSaleDate()
+    {
+        return saleDate;
+    }
+
+    public void setSaleDate(Date saleDate)
+    {
+        this.saleDate = saleDate;
+    }
+
+    public Long getTotalQuantity()
+    {
+        return totalQuantity;
+    }
+
+    public void setTotalQuantity(Long totalQuantity)
+    {
+        this.totalQuantity = totalQuantity;
+    }
+
+    public BigDecimal getTotalRevenue()
+    {
+        return totalRevenue;
+    }
+
+    public void setTotalRevenue(BigDecimal totalRevenue)
+    {
+        this.totalRevenue = totalRevenue;
+    }
+}

+ 13 - 0
dtm-system/src/main/java/com/dtm/lifecycle/mapper/HotProductDatabaseMapper.java

@@ -0,0 +1,13 @@
+package com.dtm.lifecycle.mapper;
+
+import com.dtm.lifecycle.domain.HotProductAggregate;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface HotProductDatabaseMapper
+{
+    List<HotProductAggregate> selectSkuAggregates(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    List<HotProductAggregate> selectSpuAggregates(@Param("startDate") String startDate, @Param("endDate") String endDate);
+}

+ 19 - 0
dtm-system/src/main/java/com/dtm/lifecycle/mapper/LifecycleDatabaseOverviewMapper.java

@@ -0,0 +1,19 @@
+package com.dtm.lifecycle.mapper;
+
+import com.dtm.lifecycle.domain.LifecycleSkuDailySales;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface LifecycleDatabaseOverviewMapper
+{
+    List<LifecycleSkuDailySales> selectSkuDailySales(
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate
+    );
+
+    List<LifecycleSkuDailySales> selectSpuDailySales(
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate
+    );
+}

+ 10 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/IHotProductDatabaseService.java

@@ -0,0 +1,10 @@
+package com.dtm.lifecycle.service;
+
+import java.util.Map;
+
+public interface IHotProductDatabaseService
+{
+    Map<String, Object> getSkuResults(Map<String, Object> params);
+
+    Map<String, Object> getSpuResults(Map<String, Object> params);
+}

+ 10 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/ILifecycleDatabaseAnalysisService.java

@@ -0,0 +1,10 @@
+package com.dtm.lifecycle.service;
+
+import java.util.Map;
+
+public interface ILifecycleDatabaseAnalysisService
+{
+    Map<String, Object> getSkuResults(Map<String, Object> params);
+
+    Map<String, Object> getSpuResults(Map<String, Object> params);
+}

+ 8 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/ILifecycleDatabaseOverviewService.java

@@ -0,0 +1,8 @@
+package com.dtm.lifecycle.service;
+
+import java.util.Map;
+
+public interface ILifecycleDatabaseOverviewService
+{
+    Map<String, Object> getOverview(Map<String, Object> params);
+}

+ 344 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/impl/HotProductDatabaseServiceImpl.java

@@ -0,0 +1,344 @@
+package com.dtm.lifecycle.service.impl;
+
+import com.dtm.lifecycle.domain.HotProductAggregate;
+import com.dtm.lifecycle.mapper.HotProductDatabaseMapper;
+import com.dtm.lifecycle.service.IHotProductDatabaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class HotProductDatabaseServiceImpl implements IHotProductDatabaseService
+{
+    private static final String LEVEL_SUPER = "超级爆款";
+
+    private static final String LEVEL_POTENTIAL = "潜力爆款";
+
+    private static final String LEVEL_REGULAR = "常规款";
+
+    private static final String LEVEL_CLEARANCE = "清货款";
+
+    @Autowired
+    private HotProductDatabaseMapper hotProductDatabaseMapper;
+
+    @Override
+    public Map<String, Object> getSkuResults(Map<String, Object> params)
+    {
+        return buildResults(
+            hotProductDatabaseMapper.selectSkuAggregates(getParam(params, "startDate"), getParam(params, "endDate")),
+            "sku_id",
+            "sku_results",
+            "total_sku_count",
+            "top_5_skus"
+        );
+    }
+
+    @Override
+    public Map<String, Object> getSpuResults(Map<String, Object> params)
+    {
+        return buildResults(
+            hotProductDatabaseMapper.selectSpuAggregates(getParam(params, "startDate"), getParam(params, "endDate")),
+            "spu_id",
+            "spu_results",
+            "total_spu_count",
+            "top_5_spus"
+        );
+    }
+
+    private Map<String, Object> buildResults(List<HotProductAggregate> aggregates, String idField, String resultKey, String countKey, String topKey)
+    {
+        List<EntityMetrics> entities = new ArrayList<>();
+        if (aggregates != null)
+        {
+            for (HotProductAggregate aggregate : aggregates)
+            {
+                if (aggregate == null || isBlank(aggregate.getEntityId()))
+                {
+                    continue;
+                }
+                entities.add(EntityMetrics.from(aggregate));
+            }
+        }
+
+        normalizeMetric(entities, "sales_heat");
+        normalizeMetric(entities, "price_acceptance");
+        normalizeMetric(entities, "refund_stability");
+        normalizeMetric(entities, "repurchase_rate");
+        normalizeMetric(entities, "night_burst");
+
+        Map<String, Object> resultMap = new LinkedHashMap<>();
+        Map<String, Integer> levelDistribution = new LinkedHashMap<>();
+        levelDistribution.put(LEVEL_SUPER, 0);
+        levelDistribution.put(LEVEL_POTENTIAL, 0);
+        levelDistribution.put(LEVEL_REGULAR, 0);
+        levelDistribution.put(LEVEL_CLEARANCE, 0);
+
+        double totalScore = 0D;
+        double maxScore = 0D;
+        double minScore = entities.isEmpty() ? 0D : Double.MAX_VALUE;
+
+        for (EntityMetrics entity : entities)
+        {
+            entity.finalizeMetrics();
+            levelDistribution.put(entity.level, levelDistribution.get(entity.level) + 1);
+            totalScore += entity.score;
+            maxScore = Math.max(maxScore, entity.score);
+            minScore = Math.min(minScore, entity.score);
+
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put(idField, entity.id);
+            item.put("product_title", entity.title);
+            item.put("hotproduct_score", round(entity.score, 4));
+            item.put("hotproduct_level", entity.level);
+
+            Map<String, Object> metrics = new LinkedHashMap<>();
+            metrics.put("sales_heat", round(entity.metrics.get("sales_heat"), 4));
+            metrics.put("price_acceptance", round(entity.metrics.get("price_acceptance"), 4));
+            metrics.put("refund_stability", round(entity.metrics.get("refund_stability"), 4));
+            metrics.put("repurchase_rate", round(entity.metrics.get("repurchase_rate"), 4));
+            metrics.put("night_burst", round(entity.metrics.get("night_burst"), 4));
+            item.put("metrics", metrics);
+
+            Map<String, Object> rawData = new LinkedHashMap<>();
+            rawData.put("total_quantity", entity.totalQuantity);
+            rawData.put("total_revenue", round(entity.totalRevenue.doubleValue(), 2));
+            rawData.put("avg_price", round(entity.avgPrice.doubleValue(), 2));
+            rawData.put("days_span", entity.daysSpan);
+            rawData.put("daily_sales", round(entity.salesHeatRaw, 2));
+            rawData.put("unique_buyers", entity.uniqueBuyers);
+            rawData.put("repurchase_buyers", entity.repurchaseBuyers);
+            rawData.put("refund_rate", round(entity.refundRateRaw, 4));
+            item.put("raw_data", rawData);
+
+            resultMap.put(entity.id, item);
+        }
+
+        List<EntityMetrics> ranked = new ArrayList<>(entities);
+        ranked.sort((a, b) -> Double.compare(b.score, a.score));
+        List<String> topIds = new ArrayList<>();
+        for (int i = 0; i < Math.min(5, ranked.size()); i++)
+        {
+            topIds.add(ranked.get(i).id);
+        }
+
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put(countKey, entities.size());
+        summary.put("level_distribution", levelDistribution);
+        summary.put("avg_score", round(entities.isEmpty() ? 0D : totalScore / entities.size(), 4));
+        summary.put("max_score", round(maxScore, 4));
+        summary.put("min_score", round(entities.isEmpty() ? 0D : minScore, 4));
+        summary.put(topKey, topIds);
+
+        Map<String, Object> weights = new LinkedHashMap<>();
+        weights.put("sales_heat", 0.4);
+        weights.put("price_acceptance", 0.3);
+        weights.put("refund_stability", 0.1);
+        weights.put("repurchase_rate", 0.1);
+        weights.put("night_burst", 0.1);
+
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put(resultKey, resultMap);
+        payload.put("summary", summary);
+        payload.put("weights", weights);
+        return payload;
+    }
+
+    private void normalizeMetric(List<EntityMetrics> entities, String key)
+    {
+        if (entities.isEmpty())
+        {
+            return;
+        }
+
+        double min = Double.MAX_VALUE;
+        double max = -Double.MAX_VALUE;
+        for (EntityMetrics entity : entities)
+        {
+            double value = entity.rawMetrics.get(key);
+            min = Math.min(min, value);
+            max = Math.max(max, value);
+        }
+
+        if (Double.compare(max, min) == 0)
+        {
+            for (EntityMetrics entity : entities)
+            {
+                entity.metrics.put(key, 0D);
+            }
+            return;
+        }
+
+        for (EntityMetrics entity : entities)
+        {
+            double normalized = (entity.rawMetrics.get(key) - min) / (max - min);
+            entity.metrics.put(key, normalized);
+        }
+    }
+
+    private String getParam(Map<String, Object> params, String key)
+    {
+        if (params == null || params.get(key) == null)
+        {
+            return null;
+        }
+        return String.valueOf(params.get(key));
+    }
+
+    private boolean isBlank(String value)
+    {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private double round(double value, int scale)
+    {
+        return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    private static class EntityMetrics
+    {
+        private final String id;
+
+        private final String title;
+
+        private final long totalQuantity;
+
+        private final BigDecimal totalRevenue;
+
+        private final BigDecimal avgPrice;
+
+        private final long uniqueBuyers;
+
+        private final long repurchaseBuyers;
+
+        private final int daysSpan;
+
+        private final double salesHeatRaw;
+
+        private final double refundRateRaw;
+
+        private final Map<String, Double> rawMetrics = new HashMap<>();
+
+        private final Map<String, Double> metrics = new HashMap<>();
+
+        private double score;
+
+        private String level = LEVEL_CLEARANCE;
+
+        private EntityMetrics(String id, String title, long totalQuantity, BigDecimal totalRevenue, BigDecimal avgPrice,
+                              long uniqueBuyers, long repurchaseBuyers, int daysSpan, double salesHeatRaw, double refundRateRaw)
+        {
+            this.id = id;
+            this.title = title;
+            this.totalQuantity = totalQuantity;
+            this.totalRevenue = totalRevenue;
+            this.avgPrice = avgPrice;
+            this.uniqueBuyers = uniqueBuyers;
+            this.repurchaseBuyers = repurchaseBuyers;
+            this.daysSpan = daysSpan;
+            this.salesHeatRaw = salesHeatRaw;
+            this.refundRateRaw = refundRateRaw;
+        }
+
+        private static EntityMetrics from(HotProductAggregate aggregate)
+        {
+            long totalQuantity = defaultLong(aggregate.getTotalQuantity());
+            BigDecimal totalRevenue = defaultDecimal(aggregate.getTotalRevenue());
+            BigDecimal avgPrice = defaultDecimal(aggregate.getAvgPrice());
+            long uniqueBuyers = defaultLong(aggregate.getUniqueBuyers());
+            long repurchaseBuyers = defaultLong(aggregate.getRepurchaseBuyers());
+            int daysSpan = calculateDaysSpan(aggregate.getFirstOrderTime(), aggregate.getLastOrderTime());
+
+            EntityMetrics entity = new EntityMetrics(
+                aggregate.getEntityId(),
+                isBlankStatic(aggregate.getProductTitle()) ? aggregate.getEntityId() : aggregate.getProductTitle(),
+                totalQuantity,
+                totalRevenue,
+                avgPrice,
+                uniqueBuyers,
+                repurchaseBuyers,
+                daysSpan,
+                daysSpan <= 0 ? 0D : totalQuantity * 1D / daysSpan,
+                clip(defaultDecimal(aggregate.getRefundRate()).doubleValue(), 0D, 1D)
+            );
+
+            double denominator = avgPrice.doubleValue() * totalQuantity;
+            double priceAcceptance = denominator > 0 ? totalRevenue.doubleValue() / denominator : 0D;
+            double refundStability = 1D - entity.refundRateRaw;
+            double repurchaseRate = uniqueBuyers > 0 ? repurchaseBuyers * 1D / uniqueBuyers : 0D;
+            double nightBurst = clip(defaultDecimal(aggregate.getNightOrderRatio()).doubleValue(), 0D, 1D);
+
+            entity.rawMetrics.put("sales_heat", Math.max(entity.salesHeatRaw, 0D));
+            entity.rawMetrics.put("price_acceptance", clip(priceAcceptance, 0D, 1D));
+            entity.rawMetrics.put("refund_stability", clip(refundStability, 0D, 1D));
+            entity.rawMetrics.put("repurchase_rate", clip(repurchaseRate, 0D, 1D));
+            entity.rawMetrics.put("night_burst", nightBurst);
+            return entity;
+        }
+
+        private void finalizeMetrics()
+        {
+            score = metrics.get("sales_heat") * 0.4
+                + metrics.get("price_acceptance") * 0.3
+                + metrics.get("refund_stability") * 0.1
+                + metrics.get("repurchase_rate") * 0.1
+                + metrics.get("night_burst") * 0.1;
+
+            if (score >= 0.8)
+            {
+                level = LEVEL_SUPER;
+            }
+            else if (score >= 0.6)
+            {
+                level = LEVEL_POTENTIAL;
+            }
+            else if (score >= 0.4)
+            {
+                level = LEVEL_REGULAR;
+            }
+            else
+            {
+                level = LEVEL_CLEARANCE;
+            }
+        }
+
+        private static long defaultLong(Long value)
+        {
+            return value == null ? 0L : value;
+        }
+
+        private static BigDecimal defaultDecimal(BigDecimal value)
+        {
+            return value == null ? BigDecimal.ZERO : value;
+        }
+
+        private static boolean isBlankStatic(String value)
+        {
+            return value == null || value.trim().isEmpty();
+        }
+
+        private static int calculateDaysSpan(Date first, Date last)
+        {
+            if (first == null || last == null)
+            {
+                return 1;
+            }
+            long diff = Math.max(0L, last.getTime() - first.getTime());
+            return (int) TimeUnit.MILLISECONDS.toDays(diff) + 1;
+        }
+
+        private static double clip(double value, double min, double max)
+        {
+            return Math.max(min, Math.min(max, value));
+        }
+    }
+}

+ 522 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleDatabaseAnalysisServiceImpl.java

@@ -0,0 +1,522 @@
+package com.dtm.lifecycle.service.impl;
+
+import com.dtm.lifecycle.domain.LifecycleSkuDailySales;
+import com.dtm.lifecycle.mapper.LifecycleDatabaseOverviewMapper;
+import com.dtm.lifecycle.service.ILifecycleDatabaseAnalysisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseAnalysisService
+{
+    private static final List<String> STAGES = Arrays.asList("引入期", "成长期", "成熟期", "衰退期");
+
+    private static final int COMPLETE_DAYS = 120;
+
+    private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+
+    @Autowired
+    private LifecycleDatabaseOverviewMapper lifecycleDatabaseOverviewMapper;
+
+    @Override
+    public Map<String, Object> getSkuResults(Map<String, Object> params)
+    {
+        return buildResults(lifecycleDatabaseOverviewMapper.selectSkuDailySales(getParam(params, "startDate"), getParam(params, "endDate")));
+    }
+
+    @Override
+    public Map<String, Object> getSpuResults(Map<String, Object> params)
+    {
+        return buildResults(lifecycleDatabaseOverviewMapper.selectSpuDailySales(getParam(params, "startDate"), getParam(params, "endDate")));
+    }
+
+    private Map<String, Object> buildResults(List<LifecycleSkuDailySales> rows)
+    {
+        Map<String, EntitySeries> grouped = groupRows(rows);
+        Map<String, Object> results = new LinkedHashMap<>();
+        int completeCount = 0;
+        int insufficientCount = 0;
+
+        for (Map.Entry<String, EntitySeries> entry : grouped.entrySet())
+        {
+            Map<String, Object> detail = entry.getValue().toDetail();
+            results.put(entry.getKey(), detail);
+            if (Boolean.TRUE.equals(detail.get("is_complete")))
+            {
+                completeCount++;
+            }
+            if (Boolean.TRUE.equals(detail.get("insufficient")))
+            {
+                insufficientCount++;
+            }
+        }
+
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("total_count", grouped.size());
+        summary.put("complete_count", completeCount);
+        summary.put("insufficient_count", insufficientCount);
+        summary.put("generated_at", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+        results.put("_analysis_summary_", summary);
+        return results;
+    }
+
+    private Map<String, EntitySeries> groupRows(List<LifecycleSkuDailySales> rows)
+    {
+        Map<String, EntitySeries> grouped = new LinkedHashMap<>();
+        if (rows == null)
+        {
+            return grouped;
+        }
+
+        for (LifecycleSkuDailySales row : rows)
+        {
+            if (row == null || isBlank(row.getSku()) || row.getSaleDate() == null)
+            {
+                continue;
+            }
+
+            EntitySeries series = grouped.get(row.getSku());
+            if (series == null)
+            {
+                series = new EntitySeries(row.getSku(), row.getProductName());
+                grouped.put(row.getSku(), series);
+            }
+            series.add(row);
+        }
+
+        return grouped;
+    }
+
+    private String getParam(Map<String, Object> params, String key)
+    {
+        if (params == null || params.get(key) == null)
+        {
+            return null;
+        }
+        return String.valueOf(params.get(key));
+    }
+
+    private boolean isBlank(String value)
+    {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private double round(double value, int scale)
+    {
+        return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    private class EntitySeries
+    {
+        private final String entityId;
+
+        private final String name;
+
+        private final Map<Long, DailyValue> dailyValues = new LinkedHashMap<>();
+
+        private EntitySeries(String entityId, String name)
+        {
+            this.entityId = entityId;
+            this.name = isBlank(name) ? entityId : name;
+        }
+
+        private void add(LifecycleSkuDailySales row)
+        {
+            long day = clearTime(row.getSaleDate()).getTime();
+            DailyValue value = dailyValues.get(day);
+            if (value == null)
+            {
+                value = new DailyValue(day);
+                dailyValues.put(day, value);
+            }
+            value.quantity += row.getTotalQuantity() == null ? 0L : row.getTotalQuantity();
+            value.revenue = value.revenue.add(row.getTotalRevenue() == null ? BigDecimal.ZERO : row.getTotalRevenue());
+        }
+
+        private Map<String, Object> toDetail()
+        {
+            List<DailyValue> values = new ArrayList<>(dailyValues.values());
+            Map<String, Object> detail = new HashMap<>();
+            List<String> dates = new ArrayList<>();
+            List<BigDecimal> revenueSeries = new ArrayList<>();
+            List<Long> quantitySeries = new ArrayList<>();
+            List<String> stagesMap = new ArrayList<>();
+            BigDecimal totalRevenue = BigDecimal.ZERO;
+            long totalQuantity = 0L;
+            BigDecimal peakRevenue = BigDecimal.ZERO;
+            long peakQuantity = 0L;
+            int peakRevenueIdx = 0;
+            int peakQuantityIdx = 0;
+
+            for (int i = 0; i < values.size(); i++)
+            {
+                DailyValue value = values.get(i);
+                dates.add(dateFormat.format(new Date(value.day)));
+                revenueSeries.add(value.revenue.setScale(2, RoundingMode.HALF_UP));
+                quantitySeries.add(value.quantity);
+                totalRevenue = totalRevenue.add(value.revenue);
+                totalQuantity += value.quantity;
+                if (value.revenue.compareTo(peakRevenue) > 0)
+                {
+                    peakRevenue = value.revenue;
+                    peakRevenueIdx = i;
+                }
+                if (value.quantity > peakQuantity)
+                {
+                    peakQuantity = value.quantity;
+                    peakQuantityIdx = i;
+                }
+            }
+
+            long lifecycleDays = values.isEmpty() ? 0L : TimeUnit.MILLISECONDS.toDays(values.get(values.size() - 1).day - values.get(0).day) + 1L;
+            Map<String, StageStats> stageStats = buildStageStats(values, lifecycleDays);
+            Map<String, Object> stageStatistics = toStageStatistics(stageStats, totalRevenue, totalQuantity);
+            stagesMap.addAll(buildStagesMap(values, lifecycleDays));
+
+            Map<String, CompletionHit> completion = buildCompletion(values, stageStats, lifecycleDays, peakRevenueIdx, revenueSeries, quantitySeries);
+            int score = 0;
+            for (CompletionHit hit : completion.values())
+            {
+                if (hit.hit)
+                {
+                    score += hit.weight;
+                }
+            }
+            boolean complete = score >= 80;
+            String currentStage = complete ? latestStageWithSales(stageStats) : "数据不足";
+
+            detail.put("details", name);
+            detail.put("current_stage", currentStage);
+            detail.put("is_complete", complete);
+            detail.put("insufficient", lifecycleDays < COMPLETE_DAYS || values.size() < 3);
+            detail.put("completeness_score", score);
+            detail.put("completion_details", completionToMap(completion));
+            detail.put("date_series", dates);
+            detail.put("revenue_series", revenueSeries);
+            detail.put("quantity_series", quantitySeries);
+            detail.put("smoothed_revenue", revenueSeries);
+            detail.put("smoothed_quantity", quantitySeries);
+            detail.put("stage_statistics", stageStatistics);
+            detail.put("stage_boundaries", buildStageBoundaries(values, lifecycleDays));
+            detail.put("stages_map", stagesMap);
+            detail.put("total_revenue", totalRevenue.setScale(2, RoundingMode.HALF_UP));
+            detail.put("total_quantity", totalQuantity);
+            detail.put("peak_revenue", peakRevenue.setScale(2, RoundingMode.HALF_UP));
+            detail.put("peak_revenue_date", dates.isEmpty() ? null : dates.get(peakRevenueIdx));
+            detail.put("peak_quantity", peakQuantity);
+            detail.put("peak_quantity_date", dates.isEmpty() ? null : dates.get(peakQuantityIdx));
+            detail.put("revenue_peak_idx", peakRevenueIdx);
+            detail.put("quantity_peak_idx", peakQuantityIdx);
+            detail.put("next_stage_prediction", complete ? null : predictNextStage(currentStage));
+            detail.put("entity_id", entityId);
+            return detail;
+        }
+
+        private Map<String, StageStats> buildStageStats(List<DailyValue> values, long lifecycleDays)
+        {
+            Map<String, StageStats> map = new LinkedHashMap<>();
+            for (String stage : STAGES)
+            {
+                map.put(stage, new StageStats(stage));
+            }
+            if (values.isEmpty())
+            {
+                return map;
+            }
+
+            long first = values.get(0).day;
+            long totalDays = Math.max(1L, lifecycleDays);
+            long base = totalDays / 4;
+            long remainder = totalDays % 4;
+            long startOffset = 0L;
+
+            for (int i = 0; i < STAGES.size(); i++)
+            {
+                String stage = STAGES.get(i);
+                StageStats stats = map.get(stage);
+                long duration = base + (i < remainder ? 1 : 0);
+                long endOffset = startOffset + Math.max(1L, duration) - 1L;
+                stats.durationDays = (int) Math.max(1L, duration);
+                stats.startDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(startOffset)));
+                stats.endDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(Math.min(totalDays - 1L, endOffset))));
+                startOffset = endOffset + 1L;
+            }
+
+            for (DailyValue value : values)
+            {
+                long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
+                int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
+                map.get(STAGES.get(index)).add(value);
+            }
+            return map;
+        }
+
+        private List<String> buildStagesMap(List<DailyValue> values, long lifecycleDays)
+        {
+            List<String> result = new ArrayList<>();
+            if (values.isEmpty())
+            {
+                return result;
+            }
+            long first = values.get(0).day;
+            long totalDays = Math.max(1L, lifecycleDays);
+            for (DailyValue value : values)
+            {
+                long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
+                int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
+                result.add(STAGES.get(index));
+            }
+            return result;
+        }
+
+        private Map<String, Object> toStageStatistics(Map<String, StageStats> stageStats, BigDecimal totalRevenue, long totalQuantity)
+        {
+            Map<String, Object> result = new LinkedHashMap<>();
+            for (String stage : STAGES)
+            {
+                StageStats stats = stageStats.get(stage);
+                Map<String, Object> map = new HashMap<>();
+                map.put("totalRevenue", stats.totalRevenue.setScale(2, RoundingMode.HALF_UP));
+                map.put("revenuePercentage", totalRevenue.compareTo(BigDecimal.ZERO) == 0 ? 0 : round(stats.totalRevenue.multiply(BigDecimal.valueOf(100)).divide(totalRevenue, 6, RoundingMode.HALF_UP).doubleValue(), 1));
+                map.put("totalQuantity", stats.totalQuantity);
+                map.put("quantityPercentage", totalQuantity == 0 ? 0 : round(stats.totalQuantity * 100.0 / totalQuantity, 1));
+                map.put("avgDailyRevenue", stats.durationDays == 0 ? 0 : stats.totalRevenue.divide(BigDecimal.valueOf(stats.durationDays), 2, RoundingMode.HALF_UP));
+                map.put("avgDailyQuantity", stats.durationDays == 0 ? 0 : round(stats.totalQuantity * 1.0 / stats.durationDays, 1));
+                map.put("durationDays", stats.durationDays);
+                map.put("startDate", stats.startDate);
+                map.put("endDate", stats.endDate);
+                result.put(stage, map);
+            }
+            return result;
+        }
+
+        private List<Map<String, Object>> buildStageBoundaries(List<DailyValue> values, long lifecycleDays)
+        {
+            List<Map<String, Object>> result = new ArrayList<>();
+            if (values.size() < 4)
+            {
+                return result;
+            }
+            long first = values.get(0).day;
+            long totalDays = Math.max(1L, lifecycleDays);
+            for (int i = 1; i < STAGES.size(); i++)
+            {
+                long boundaryOffset = totalDays * i / 4;
+                int index = findNearestIndex(values, first + TimeUnit.DAYS.toMillis(boundaryOffset));
+                Map<String, Object> item = new HashMap<>();
+                item.put("type", STAGES.get(i) + "开始");
+                item.put("date", dateFormat.format(new Date(values.get(index).day)));
+                item.put("index", index);
+                result.add(item);
+            }
+            return result;
+        }
+
+        private int findNearestIndex(List<DailyValue> values, long targetDay)
+        {
+            int index = 0;
+            long diff = Long.MAX_VALUE;
+            for (int i = 0; i < values.size(); i++)
+            {
+                long currentDiff = Math.abs(values.get(i).day - targetDay);
+                if (currentDiff < diff)
+                {
+                    diff = currentDiff;
+                    index = i;
+                }
+            }
+            return index;
+        }
+
+        private Map<String, CompletionHit> buildCompletion(List<DailyValue> values, Map<String, StageStats> stats, long lifecycleDays, int peakRevenueIdx, List<BigDecimal> revenue, List<Long> qty)
+        {
+            Map<String, CompletionHit> map = new LinkedHashMap<>();
+            int length = values.size();
+            boolean hasGrowth = stats.get("成长期").totalQuantity > stats.get("引入期").totalQuantity || stats.get("成长期").totalRevenue.compareTo(stats.get("引入期").totalRevenue) > 0;
+            boolean hasDecline = stats.get("衰退期").totalQuantity < stats.get("成熟期").totalQuantity || stats.get("衰退期").totalRevenue.compareTo(stats.get("成熟期").totalRevenue) < 0;
+            double revenueCv = coefficientOfVariation(revenue);
+            double qtyCv = coefficientOfVariation(qty);
+            double avgRevenue = averageBigDecimal(revenue);
+            boolean peakSignificant = avgRevenue <= 0 ? false : revenue.get(peakRevenueIdx).doubleValue() >= avgRevenue * 1.8;
+
+            map.put("sufficient_time", new CompletionHit(lifecycleDays >= COMPLETE_DAYS, 15));
+            map.put("reasonable_peak_position", new CompletionHit(length > 0 && peakRevenueIdx >= length * 0.25 && peakRevenueIdx <= length * 0.75, 10));
+            map.put("significant_growth", new CompletionHit(hasGrowth, 15));
+            map.put("noticeable_decline", new CompletionHit(hasDecline, 15));
+            map.put("has_lifecycle_shape", new CompletionHit(revenueCv >= 0.3 && qtyCv >= 0.25, 10));
+            map.put("peak_significance", new CompletionHit(peakSignificant, 10));
+            map.put("data_quality_check", new CompletionHit(length >= 3 && lifecycleDays >= COMPLETE_DAYS && !revenue.isEmpty(), 10));
+            map.put("cycle_completeness", new CompletionHit(hasStageSales(stats, "引入期") && hasStageSales(stats, "成长期") && hasStageSales(stats, "成熟期") && hasStageSales(stats, "衰退期"), 10));
+            map.put("trend_consistency", new CompletionHit(hasGrowth || hasDecline, 5));
+            return map;
+        }
+
+        private boolean hasStageSales(Map<String, StageStats> stats, String stage)
+        {
+            StageStats value = stats.get(stage);
+            return value != null && (value.totalQuantity > 0 || value.totalRevenue.compareTo(BigDecimal.ZERO) > 0);
+        }
+
+        private Map<String, Object> completionToMap(Map<String, CompletionHit> completion)
+        {
+            Map<String, Object> result = new LinkedHashMap<>();
+            for (Map.Entry<String, CompletionHit> entry : completion.entrySet())
+            {
+                Map<String, Object> item = new HashMap<>();
+                item.put("hit", entry.getValue().hit);
+                item.put("weight", entry.getValue().weight);
+                result.put(entry.getKey(), item);
+            }
+            return result;
+        }
+
+        private String latestStageWithSales(Map<String, StageStats> stats)
+        {
+            for (int i = STAGES.size() - 1; i >= 0; i--)
+            {
+                String stage = STAGES.get(i);
+                if (hasStageSales(stats, stage))
+                {
+                    return stage;
+                }
+            }
+            return "数据不足";
+        }
+
+        private String predictNextStage(String currentStage)
+        {
+            if ("数据不足".equals(currentStage))
+            {
+                return "继续积累销售数据";
+            }
+            int index = STAGES.indexOf(currentStage);
+            if (index >= 0 && index < STAGES.size() - 1)
+            {
+                return STAGES.get(index + 1);
+            }
+            return null;
+        }
+
+        private double coefficientOfVariation(List<?> values)
+        {
+            if (values == null || values.isEmpty())
+            {
+                return 0.0;
+            }
+            double avg = 0.0;
+            for (Object value : values)
+            {
+                avg += numberValue(value);
+            }
+            avg = avg / values.size();
+            if (avg == 0.0)
+            {
+                return 0.0;
+            }
+            double variance = 0.0;
+            for (Object value : values)
+            {
+                double diff = numberValue(value) - avg;
+                variance += diff * diff;
+            }
+            return Math.sqrt(variance / values.size()) / avg;
+        }
+
+        private double averageBigDecimal(List<BigDecimal> values)
+        {
+            if (values == null || values.isEmpty())
+            {
+                return 0.0;
+            }
+            BigDecimal total = BigDecimal.ZERO;
+            for (BigDecimal value : values)
+            {
+                total = total.add(value == null ? BigDecimal.ZERO : value);
+            }
+            return total.divide(BigDecimal.valueOf(values.size()), 6, RoundingMode.HALF_UP).doubleValue();
+        }
+
+        private double numberValue(Object value)
+        {
+            if (value instanceof BigDecimal)
+            {
+                return ((BigDecimal) value).doubleValue();
+            }
+            if (value instanceof Number)
+            {
+                return ((Number) value).doubleValue();
+            }
+            return 0.0;
+        }
+
+        private Date clearTime(Date date)
+        {
+            return new Date(date.getTime() / 86400000L * 86400000L);
+        }
+    }
+
+    private static class DailyValue
+    {
+        private final long day;
+
+        private long quantity = 0L;
+
+        private BigDecimal revenue = BigDecimal.ZERO;
+
+        private DailyValue(long day)
+        {
+            this.day = day;
+        }
+    }
+
+    private static class StageStats
+    {
+        private final String stage;
+
+        private BigDecimal totalRevenue = BigDecimal.ZERO;
+
+        private long totalQuantity = 0L;
+
+        private int durationDays = 0;
+
+        private String startDate;
+
+        private String endDate;
+
+        private StageStats(String stage)
+        {
+            this.stage = stage;
+        }
+
+        private void add(DailyValue value)
+        {
+            totalQuantity += value.quantity;
+            totalRevenue = totalRevenue.add(value.revenue);
+        }
+    }
+
+    private static class CompletionHit
+    {
+        private final boolean hit;
+
+        private final int weight;
+
+        private CompletionHit(boolean hit, int weight)
+        {
+            this.hit = hit;
+            this.weight = weight;
+        }
+    }
+}

+ 514 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleDatabaseOverviewServiceImpl.java

@@ -0,0 +1,514 @@
+package com.dtm.lifecycle.service.impl;
+
+import com.dtm.lifecycle.domain.LifecycleSkuDailySales;
+import com.dtm.lifecycle.mapper.LifecycleDatabaseOverviewMapper;
+import com.dtm.lifecycle.service.ILifecycleDatabaseOverviewService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class LifecycleDatabaseOverviewServiceImpl implements ILifecycleDatabaseOverviewService
+{
+    private static final List<String> STAGES = Arrays.asList("引入期", "成长期", "成熟期", "衰退期");
+
+    private static final int COMPLETE_LIFECYCLE_DAYS = 120;
+
+    @Autowired
+    private LifecycleDatabaseOverviewMapper lifecycleDatabaseOverviewMapper;
+
+    @Override
+    public Map<String, Object> getOverview(Map<String, Object> params)
+    {
+        String startDate = params == null ? null : toStringValue(params.get("startDate"));
+        String endDate = params == null ? null : toStringValue(params.get("endDate"));
+        List<LifecycleSkuDailySales> rows = lifecycleDatabaseOverviewMapper.selectSkuDailySales(startDate, endDate);
+        Map<String, SkuAggregate> skuMap = buildSkuAggregates(rows);
+
+        return buildOverview(skuMap);
+    }
+
+    private Map<String, SkuAggregate> buildSkuAggregates(List<LifecycleSkuDailySales> rows)
+    {
+        Map<String, SkuAggregate> skuMap = new LinkedHashMap<>();
+        if (rows == null)
+        {
+            return skuMap;
+        }
+
+        for (LifecycleSkuDailySales row : rows)
+        {
+            if (row == null || isBlank(row.getSku()) || row.getSaleDate() == null)
+            {
+                continue;
+            }
+
+            SkuAggregate aggregate = skuMap.get(row.getSku());
+            if (aggregate == null)
+            {
+                aggregate = new SkuAggregate(row.getSku(), row.getProductName());
+                skuMap.put(row.getSku(), aggregate);
+            }
+            aggregate.add(row);
+        }
+
+        return skuMap;
+    }
+
+    private Map<String, Object> buildOverview(Map<String, SkuAggregate> skuMap)
+    {
+        long orderCount = 0L;
+        int completeCount = 0;
+        int insufficientCount = 0;
+        long completeLifecycleDays = 0L;
+        Set<String> productNames = new LinkedHashSet<>();
+        Map<String, Long> stageDistribution = initLongStageMap();
+        Map<String, StageMetric> stageMetrics = initMetricStageMap();
+        Map<String, List<Long>> stageDurations = initDurationStageMap();
+        long[] funnelCounts = new long[] { 0L, 0L, 0L, 0L };
+        List<Map<String, Object>> skuList = new ArrayList<>();
+
+        for (SkuAggregate sku : skuMap.values())
+        {
+            sku.finish();
+            orderCount += sku.totalQuantity;
+            if (!isBlank(sku.productName))
+            {
+                productNames.add(sku.productName);
+            }
+
+            if (sku.insufficient)
+            {
+                insufficientCount++;
+            }
+            else
+            {
+                stageDistribution.put(sku.currentStage, stageDistribution.get(sku.currentStage) + 1);
+            }
+
+            if (sku.complete)
+            {
+                completeCount++;
+                completeLifecycleDays += sku.lifecycleDays;
+            }
+
+            for (String stage : STAGES)
+            {
+                StageMetric metric = stageMetrics.get(stage);
+                StageBucket bucket = sku.stageBuckets.get(stage);
+                if (bucket != null)
+                {
+                    metric.totalRevenue = metric.totalRevenue.add(bucket.totalRevenue);
+                    metric.totalQuantity += bucket.totalQuantity;
+                    if (bucket.totalQuantity > 0 || bucket.totalRevenue.compareTo(BigDecimal.ZERO) > 0)
+                    {
+                        stageDurations.get(stage).add((long) bucket.durationDays);
+                    }
+                }
+            }
+
+            updateFunnelCounts(funnelCounts, sku);
+            skuList.add(sku.toMap());
+        }
+
+        int skuCount = skuMap.size();
+        Map<String, Object> data = new HashMap<>();
+        data.put("summary", buildSummary(orderCount, skuCount, productNames.size(), completeCount, completeLifecycleDays, insufficientCount));
+        data.put("stageDistribution", buildStageDistribution(stageDistribution, insufficientCount));
+        data.put("stageMetrics", buildStageMetrics(stageMetrics));
+        data.put("avgDuration", buildAvgDuration(stageDurations));
+        data.put("funnel", buildFunnel(funnelCounts));
+        data.put("skuList", skuList);
+        data.put("generatedAt", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+        return data;
+    }
+
+    private Map<String, Object> buildSummary(long orderCount, int skuCount, int productCount, int completeCount, long completeLifecycleDays, int insufficientCount)
+    {
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("orderCount", orderCount);
+        summary.put("skuCount", skuCount);
+        summary.put("productCount", productCount);
+        summary.put("completeCount", completeCount);
+        summary.put("completeRatio", skuCount == 0 ? 0 : round(completeCount * 100.0 / skuCount, 1));
+        summary.put("avgLifecycleDays", completeCount == 0 ? 0 : Math.round(completeLifecycleDays * 1.0 / completeCount));
+        summary.put("insufficientCount", insufficientCount);
+        return summary;
+    }
+
+    private List<Map<String, Object>> buildStageDistribution(Map<String, Long> stageDistribution, int insufficientCount)
+    {
+        List<Map<String, Object>> data = new ArrayList<>();
+        for (String stage : STAGES)
+        {
+            data.add(namedValue(stage, stageDistribution.get(stage)));
+        }
+        data.add(namedValue("数据不足", (long) insufficientCount));
+        return data;
+    }
+
+    private Map<String, Object> buildStageMetrics(Map<String, StageMetric> stageMetrics)
+    {
+        List<String> labels = new ArrayList<>(STAGES);
+        List<BigDecimal> revenue = new ArrayList<>();
+        List<Long> quantity = new ArrayList<>();
+        for (String stage : STAGES)
+        {
+            StageMetric metric = stageMetrics.get(stage);
+            revenue.add(metric.totalRevenue.setScale(2, RoundingMode.HALF_UP));
+            quantity.add(metric.totalQuantity);
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("labels", labels);
+        data.put("revenue", revenue);
+        data.put("quantity", quantity);
+        return data;
+    }
+
+    private Map<String, Object> buildAvgDuration(Map<String, List<Long>> stageDurations)
+    {
+        List<String> labels = new ArrayList<>(STAGES);
+        List<Long> values = new ArrayList<>();
+        for (String stage : STAGES)
+        {
+            List<Long> durations = stageDurations.get(stage);
+            if (durations == null || durations.isEmpty())
+            {
+                values.add(0L);
+            }
+            else
+            {
+                long total = 0L;
+                for (Long duration : durations)
+                {
+                    total += duration == null ? 0L : duration;
+                }
+                values.add(Math.round(total * 1.0 / durations.size()));
+            }
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("labels", labels);
+        data.put("values", values);
+        return data;
+    }
+
+    private Map<String, Object> buildFunnel(long[] counts)
+    {
+        List<String> labels = new ArrayList<>(STAGES);
+        List<Double> values = new ArrayList<>();
+        for (int i = 0; i < counts.length; i++)
+        {
+            if (i == 0)
+            {
+                values.add(counts[0] > 0 ? 100.0 : 0.0);
+            }
+            else
+            {
+                long prev = counts[i - 1];
+                values.add(prev == 0 ? 0.0 : round(counts[i] * 100.0 / prev, 1));
+            }
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("labels", labels);
+        data.put("values", values);
+        data.put("counts", counts);
+        return data;
+    }
+
+    private void updateFunnelCounts(long[] counts, SkuAggregate sku)
+    {
+        boolean intro = hasStageSales(sku, "引入期");
+        boolean growth = hasStageSales(sku, "成长期");
+        boolean maturity = hasStageSales(sku, "成熟期");
+        boolean decline = hasStageSales(sku, "衰退期");
+        if (intro)
+        {
+            counts[0]++;
+        }
+        if (intro && growth)
+        {
+            counts[1]++;
+        }
+        if (intro && growth && maturity)
+        {
+            counts[2]++;
+        }
+        if (intro && growth && maturity && decline)
+        {
+            counts[3]++;
+        }
+    }
+
+    private boolean hasStageSales(SkuAggregate sku, String stage)
+    {
+        StageBucket bucket = sku.stageBuckets.get(stage);
+        return bucket != null && (bucket.totalQuantity > 0 || bucket.totalRevenue.compareTo(BigDecimal.ZERO) > 0);
+    }
+
+    private Map<String, Long> initLongStageMap()
+    {
+        Map<String, Long> map = new LinkedHashMap<>();
+        for (String stage : STAGES)
+        {
+            map.put(stage, 0L);
+        }
+        return map;
+    }
+
+    private Map<String, StageMetric> initMetricStageMap()
+    {
+        Map<String, StageMetric> map = new LinkedHashMap<>();
+        for (String stage : STAGES)
+        {
+            map.put(stage, new StageMetric());
+        }
+        return map;
+    }
+
+    private Map<String, List<Long>> initDurationStageMap()
+    {
+        Map<String, List<Long>> map = new LinkedHashMap<>();
+        for (String stage : STAGES)
+        {
+            map.put(stage, new ArrayList<Long>());
+        }
+        return map;
+    }
+
+    private Map<String, Object> namedValue(String name, Long value)
+    {
+        Map<String, Object> map = new HashMap<>();
+        map.put("name", name);
+        map.put("value", value == null ? 0L : value);
+        return map;
+    }
+
+    private String toStringValue(Object value)
+    {
+        return value == null ? null : String.valueOf(value);
+    }
+
+    private boolean isBlank(String value)
+    {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private double round(double value, int scale)
+    {
+        return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    private static class StageMetric
+    {
+        private BigDecimal totalRevenue = BigDecimal.ZERO;
+
+        private long totalQuantity = 0L;
+    }
+
+    private class SkuAggregate
+    {
+        private final String sku;
+
+        private final String productName;
+
+        private final Map<Long, DailyValue> dailyValues = new LinkedHashMap<>();
+
+        private final Map<String, StageBucket> stageBuckets = new LinkedHashMap<>();
+
+        private Date firstDate;
+
+        private Date lastDate;
+
+        private long totalQuantity = 0L;
+
+        private BigDecimal totalRevenue = BigDecimal.ZERO;
+
+        private long lifecycleDays = 0L;
+
+        private boolean insufficient = true;
+
+        private boolean complete = false;
+
+        private String currentStage = "数据不足";
+
+        private SkuAggregate(String sku, String productName)
+        {
+            this.sku = sku;
+            this.productName = isBlank(productName) ? sku : productName;
+            for (String stage : STAGES)
+            {
+                stageBuckets.put(stage, new StageBucket(stage));
+            }
+        }
+
+        private void add(LifecycleSkuDailySales row)
+        {
+            Date date = row.getSaleDate();
+            if (firstDate == null || date.before(firstDate))
+            {
+                firstDate = date;
+            }
+            if (lastDate == null || date.after(lastDate))
+            {
+                lastDate = date;
+            }
+
+            long quantity = row.getTotalQuantity() == null ? 0L : row.getTotalQuantity();
+            BigDecimal revenue = row.getTotalRevenue() == null ? BigDecimal.ZERO : row.getTotalRevenue();
+            totalQuantity += quantity;
+            totalRevenue = totalRevenue.add(revenue);
+
+            long dayKey = clearTime(date).getTime();
+            DailyValue dailyValue = dailyValues.get(dayKey);
+            if (dailyValue == null)
+            {
+                dailyValue = new DailyValue();
+                dailyValues.put(dayKey, dailyValue);
+            }
+            dailyValue.quantity += quantity;
+            dailyValue.revenue = dailyValue.revenue.add(revenue);
+        }
+
+        private void finish()
+        {
+            lifecycleDays = calculateLifecycleDays(firstDate, lastDate);
+            insufficient = lifecycleDays < COMPLETE_LIFECYCLE_DAYS || dailyValues.size() < 3 || totalQuantity <= 0;
+            buildStageBuckets();
+            currentStage = insufficient ? "数据不足" : classifyStage();
+            complete = !insufficient && hasStageSales(this, "引入期") && hasStageSales(this, "成长期")
+                    && hasStageSales(this, "成熟期") && hasStageSales(this, "衰退期");
+        }
+
+        private void buildStageBuckets()
+        {
+            if (firstDate == null || lastDate == null)
+            {
+                return;
+            }
+
+            long first = clearTime(firstDate).getTime();
+            long totalDays = Math.max(1L, lifecycleDays);
+
+            for (Map.Entry<Long, DailyValue> entry : dailyValues.entrySet())
+            {
+                long offset = TimeUnit.MILLISECONDS.toDays(entry.getKey() - first);
+                int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
+                String stage = STAGES.get(index);
+                stageBuckets.get(stage).add(entry.getValue());
+            }
+
+            long base = totalDays / 4;
+            long remainder = totalDays % 4;
+            for (int i = 0; i < STAGES.size(); i++)
+            {
+                StageBucket bucket = stageBuckets.get(STAGES.get(i));
+                bucket.durationDays = (int) (base + (i < remainder ? 1 : 0));
+            }
+        }
+
+        private String classifyStage()
+        {
+            double intro = stageBuckets.get("引入期").avgDailyQuantity();
+            double growth = stageBuckets.get("成长期").avgDailyQuantity();
+            double maturity = stageBuckets.get("成熟期").avgDailyQuantity();
+            double decline = stageBuckets.get("衰退期").avgDailyQuantity();
+
+            if (decline >= maturity * 1.1 && decline >= growth)
+            {
+                return "成长期";
+            }
+            if (maturity >= growth * 0.8 && decline >= maturity * 0.8)
+            {
+                return "成熟期";
+            }
+            if (growth >= intro * 1.2 && maturity >= decline * 1.2)
+            {
+                return "衰退期";
+            }
+            if (decline < maturity * 0.7)
+            {
+                return "衰退期";
+            }
+            return "成熟期";
+        }
+
+        private Map<String, Object> toMap()
+        {
+            Map<String, Object> map = new HashMap<>();
+            map.put("sku", sku);
+            map.put("productName", productName);
+            map.put("currentStage", currentStage);
+            map.put("totalQuantity", totalQuantity);
+            map.put("totalRevenue", totalRevenue.setScale(2, RoundingMode.HALF_UP));
+            map.put("lifecycleDays", lifecycleDays);
+            map.put("complete", complete);
+            map.put("insufficient", insufficient);
+            return map;
+        }
+
+        private long calculateLifecycleDays(Date start, Date end)
+        {
+            if (start == null || end == null)
+            {
+                return 0L;
+            }
+            long diff = clearTime(end).getTime() - clearTime(start).getTime();
+            return TimeUnit.MILLISECONDS.toDays(diff) + 1L;
+        }
+
+        private Date clearTime(Date date)
+        {
+            return new Date(date.getTime() / 86400000L * 86400000L);
+        }
+    }
+
+    private static class DailyValue
+    {
+        private long quantity = 0L;
+
+        private BigDecimal revenue = BigDecimal.ZERO;
+    }
+
+    private static class StageBucket
+    {
+        private final String stage;
+
+        private long totalQuantity = 0L;
+
+        private BigDecimal totalRevenue = BigDecimal.ZERO;
+
+        private int durationDays = 0;
+
+        private StageBucket(String stage)
+        {
+            this.stage = stage;
+        }
+
+        private void add(DailyValue value)
+        {
+            totalQuantity += value.quantity;
+            totalRevenue = totalRevenue.add(value.revenue);
+        }
+
+        private double avgDailyQuantity()
+        {
+            return durationDays <= 0 ? 0.0 : totalQuantity * 1.0 / durationDays;
+        }
+    }
+}

+ 192 - 0
dtm-system/src/main/resources/mapper/lifecycle/HotProductDatabaseMapper.xml

@@ -0,0 +1,192 @@
+<?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.lifecycle.mapper.HotProductDatabaseMapper">
+
+    <resultMap id="HotProductAggregateResult" type="com.dtm.lifecycle.domain.HotProductAggregate">
+        <result property="entityId" column="entity_id"/>
+        <result property="productTitle" column="product_title"/>
+        <result property="totalQuantity" column="total_quantity"/>
+        <result property="totalRevenue" column="total_revenue"/>
+        <result property="avgPrice" column="avg_price"/>
+        <result property="firstOrderTime" column="first_order_time"/>
+        <result property="lastOrderTime" column="last_order_time"/>
+        <result property="uniqueBuyers" column="unique_buyers"/>
+        <result property="repurchaseBuyers" column="repurchase_buyers"/>
+        <result property="refundRate" column="refund_rate"/>
+        <result property="nightOrderRatio" column="night_order_ratio"/>
+    </resultMap>
+
+    <sql id="dateRangeConditions">
+        <if test="startDate != null and startDate != ''">
+            AND o.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 o.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="refundFlag">
+        CASE
+            WHEN (
+                NULLIF(o.refund_status, '') IS NOT NULL
+                AND o.refund_status NOT IN ('无退款', '未退款', 'NONE', 'NO_REFUND', '0')
+            ) OR (
+                o.refund_amount REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+                AND CAST(o.refund_amount AS DECIMAL(16, 2)) &gt; 0
+            ) THEN 1
+            ELSE 0
+        END
+    </sql>
+
+    <select id="selectSkuAggregates" resultMap="HotProductAggregateResult">
+        SELECT
+            base.entity_id,
+            base.product_title,
+            base.total_quantity,
+            base.total_revenue,
+            base.avg_price,
+            base.first_order_time,
+            base.last_order_time,
+            base.unique_buyers,
+            IFNULL(rep.repurchase_buyers, 0) AS repurchase_buyers,
+            base.refund_rate,
+            base.night_order_ratio
+        FROM (
+            SELECT
+                o.sku AS entity_id,
+                COALESCE(MAX(NULLIF(p.product_name, '')), MAX(NULLIF(o.title, '')), o.sku) AS product_title,
+                IFNULL(SUM(IFNULL(o.quantity, 0)), 0) AS total_quantity,
+                IFNULL(SUM(
+                    CASE
+                        WHEN IFNULL(o.paid_amount, 0) &gt; 0 THEN IFNULL(o.paid_amount, 0)
+                        WHEN IFNULL(o.pay_amount, 0) &gt; 0 THEN IFNULL(o.pay_amount, 0)
+                        ELSE IFNULL(o.price, 0) * IFNULL(o.quantity, 0)
+                    END
+                ), 0) AS total_revenue,
+                IFNULL(AVG(CASE WHEN IFNULL(o.price, 0) &gt; 0 THEN o.price ELSE p.product_price END), 0) AS avg_price,
+                MIN(o.create_time) AS first_order_time,
+                MAX(o.create_time) AS last_order_time,
+                COUNT(DISTINCT CASE
+                    WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                    ELSE o.order_id
+                END) AS unique_buyers,
+                AVG(<include refid="refundFlag"/>) AS refund_rate,
+                AVG(CASE WHEN HOUR(o.create_time) BETWEEN 0 AND 6 THEN 1 ELSE 0 END) AS night_order_ratio
+            FROM dtm_order_main o
+            LEFT JOIN dtm_product p ON p.sku = o.sku
+            <where>
+                o.sku IS NOT NULL
+                AND o.sku != ''
+                AND o.create_time IS NOT NULL
+                <include refid="dateRangeConditions"/>
+            </where>
+            GROUP BY o.sku
+        ) base
+        LEFT JOIN (
+            SELECT
+                entity_id,
+                COUNT(1) AS repurchase_buyers
+            FROM (
+                SELECT
+                    o.sku AS entity_id,
+                    CASE
+                        WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                        ELSE o.order_id
+                    END AS purchase_key
+                FROM dtm_order_main o
+                <where>
+                    o.sku IS NOT NULL
+                    AND o.sku != ''
+                    AND o.create_time IS NOT NULL
+                    <include refid="dateRangeConditions"/>
+                </where>
+                GROUP BY o.sku, CASE
+                    WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                    ELSE o.order_id
+                END
+                HAVING COUNT(1) &gt; 1
+            ) t
+            GROUP BY entity_id
+        ) rep ON rep.entity_id = base.entity_id
+        ORDER BY base.total_revenue DESC, base.total_quantity DESC, base.entity_id ASC
+    </select>
+
+    <select id="selectSpuAggregates" resultMap="HotProductAggregateResult">
+        SELECT
+            base.entity_id,
+            base.product_title,
+            base.total_quantity,
+            base.total_revenue,
+            base.avg_price,
+            base.first_order_time,
+            base.last_order_time,
+            base.unique_buyers,
+            IFNULL(rep.repurchase_buyers, 0) AS repurchase_buyers,
+            base.refund_rate,
+            base.night_order_ratio
+        FROM (
+            SELECT
+                COALESCE(NULLIF(p.spu, ''), o.sku) AS entity_id,
+                COALESCE(MAX(NULLIF(p.product_name, '')), MAX(NULLIF(o.title, '')), MAX(COALESCE(NULLIF(p.spu, ''), o.sku))) AS product_title,
+                IFNULL(SUM(IFNULL(o.quantity, 0)), 0) AS total_quantity,
+                IFNULL(SUM(
+                    CASE
+                        WHEN IFNULL(o.paid_amount, 0) &gt; 0 THEN IFNULL(o.paid_amount, 0)
+                        WHEN IFNULL(o.pay_amount, 0) &gt; 0 THEN IFNULL(o.pay_amount, 0)
+                        ELSE IFNULL(o.price, 0) * IFNULL(o.quantity, 0)
+                    END
+                ), 0) AS total_revenue,
+                IFNULL(AVG(CASE WHEN IFNULL(o.price, 0) &gt; 0 THEN o.price ELSE p.product_price END), 0) AS avg_price,
+                MIN(o.create_time) AS first_order_time,
+                MAX(o.create_time) AS last_order_time,
+                COUNT(DISTINCT CASE
+                    WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                    ELSE o.order_id
+                END) AS unique_buyers,
+                AVG(<include refid="refundFlag"/>) AS refund_rate,
+                AVG(CASE WHEN HOUR(o.create_time) BETWEEN 0 AND 6 THEN 1 ELSE 0 END) AS night_order_ratio
+            FROM dtm_order_main o
+            LEFT JOIN dtm_product p ON p.sku = o.sku
+            <where>
+                o.sku IS NOT NULL
+                AND o.sku != ''
+                AND o.create_time IS NOT NULL
+                <include refid="dateRangeConditions"/>
+            </where>
+            GROUP BY COALESCE(NULLIF(p.spu, ''), o.sku)
+        ) base
+        LEFT JOIN (
+            SELECT
+                entity_id,
+                COUNT(1) AS repurchase_buyers
+            FROM (
+                SELECT
+                    COALESCE(NULLIF(p.spu, ''), o.sku) AS entity_id,
+                    CASE
+                        WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                        ELSE o.order_id
+                    END AS purchase_key
+                FROM dtm_order_main o
+                LEFT JOIN dtm_product p ON p.sku = o.sku
+                <where>
+                    o.sku IS NOT NULL
+                    AND o.sku != ''
+                    AND o.create_time IS NOT NULL
+                    <include refid="dateRangeConditions"/>
+                </where>
+                GROUP BY COALESCE(NULLIF(p.spu, ''), o.sku), CASE
+                    WHEN NULLIF(o.pay_number, '') IS NOT NULL THEN o.pay_number
+                    ELSE o.order_id
+                END
+                HAVING COUNT(1) &gt; 1
+            ) t
+            GROUP BY entity_id
+        ) rep ON rep.entity_id = base.entity_id
+        ORDER BY base.total_revenue DESC, base.total_quantity DESC, base.entity_id ASC
+    </select>
+</mapper>

+ 76 - 0
dtm-system/src/main/resources/mapper/lifecycle/LifecycleDatabaseOverviewMapper.xml

@@ -0,0 +1,76 @@
+<?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.lifecycle.mapper.LifecycleDatabaseOverviewMapper">
+
+    <resultMap id="LifecycleSkuDailySalesResult" type="com.dtm.lifecycle.domain.LifecycleSkuDailySales">
+        <result property="sku" column="sku"/>
+        <result property="productName" column="product_name"/>
+        <result property="saleDate" column="sale_date"/>
+        <result property="totalQuantity" column="total_quantity"/>
+        <result property="totalRevenue" column="total_revenue"/>
+    </resultMap>
+
+    <sql id="dateRangeConditions">
+        <if test="startDate != null and startDate != ''">
+            AND o.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 o.create_time &lt; DATE_ADD(
+                STR_TO_DATE(CONCAT(#{endDate}, ' 00:00:00'), '%Y-%m-%d %H:%i:%s'),
+                INTERVAL 1 DAY
+            )
+        </if>
+    </sql>
+
+    <select id="selectSkuDailySales" resultMap="LifecycleSkuDailySalesResult">
+        SELECT
+            o.sku AS sku,
+            COALESCE(MAX(NULLIF(p.product_name, '')), MAX(NULLIF(o.title, '')), o.sku) AS product_name,
+            DATE(o.create_time) AS sale_date,
+            IFNULL(SUM(IFNULL(o.quantity, 0)), 0) AS total_quantity,
+            IFNULL(SUM(
+                CASE
+                    WHEN IFNULL(o.paid_amount, 0) &gt; 0 THEN IFNULL(o.paid_amount, 0)
+                    WHEN IFNULL(o.pay_amount, 0) &gt; 0 THEN IFNULL(o.pay_amount, 0)
+                    ELSE IFNULL(o.price, 0) * IFNULL(o.quantity, 0)
+                END
+            ), 0) AS total_revenue
+        FROM dtm_order_main o
+        LEFT JOIN dtm_product p ON p.sku = o.sku
+        <where>
+            o.sku IS NOT NULL
+            AND o.sku != ''
+            AND o.create_time IS NOT NULL
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY o.sku, DATE(o.create_time)
+        ORDER BY o.sku ASC, sale_date ASC
+    </select>
+
+    <select id="selectSpuDailySales" resultMap="LifecycleSkuDailySalesResult">
+        SELECT
+            COALESCE(NULLIF(p.spu, ''), o.sku) AS sku,
+            COALESCE(MAX(NULLIF(p.product_name, '')), MAX(NULLIF(o.title, '')), MAX(COALESCE(NULLIF(p.spu, ''), o.sku))) AS product_name,
+            DATE(o.create_time) AS sale_date,
+            IFNULL(SUM(IFNULL(o.quantity, 0)), 0) AS total_quantity,
+            IFNULL(SUM(
+                CASE
+                    WHEN IFNULL(o.paid_amount, 0) &gt; 0 THEN IFNULL(o.paid_amount, 0)
+                    WHEN IFNULL(o.pay_amount, 0) &gt; 0 THEN IFNULL(o.pay_amount, 0)
+                    ELSE IFNULL(o.price, 0) * IFNULL(o.quantity, 0)
+                END
+            ), 0) AS total_revenue
+        FROM dtm_order_main o
+        LEFT JOIN dtm_product p ON p.sku = o.sku
+        <where>
+            o.sku IS NOT NULL
+            AND o.sku != ''
+            AND o.create_time IS NOT NULL
+            <include refid="dateRangeConditions"/>
+        </where>
+        GROUP BY COALESCE(NULLIF(p.spu, ''), o.sku), DATE(o.create_time)
+        ORDER BY sku ASC, sale_date ASC
+    </select>
+</mapper>