Browse Source

统计:增加商品统计定时任务

owen 1 year ago
parent
commit
0b7d42482f
10 changed files with 206 additions and 168 deletions
  1. 2 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java
  2. 2 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/history/ProductBrowseHistoryServiceImpl.java
  3. 0 74
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductSpuStatisticsDO.java
  4. 0 70
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsDO.java
  5. 20 0
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsMapper.java
  6. 48 0
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java
  7. 7 8
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsService.java
  8. 62 11
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsServiceImpl.java
  9. 1 1
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeStatisticsServiceImpl.java
  10. 64 0
      yudao-module-mall/yudao-module-statistics-biz/src/main/resources/mapper/product/ProductStatisticsMapper.xml

+ 2 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java

@@ -42,8 +42,8 @@ public interface ProductBrowseHistoryMapper extends BaseMapperX<ProductBrowseHis
                 .eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
     }
 
-    default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId) {
-        Page<ProductBrowseHistoryDO> page = Page.of(0, 1);
+    default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId, Integer pageNo, Integer pageSize) {
+        Page<ProductBrowseHistoryDO> page = Page.of(pageNo, pageSize);
         return selectPage(page, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
                 .eqIfPresent(ProductBrowseHistoryDO::getUserId, userId)
                 .orderByAsc(ProductBrowseHistoryDO::getCreateTime));

+ 2 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/history/ProductBrowseHistoryServiceImpl.java

@@ -32,8 +32,8 @@ public class ProductBrowseHistoryServiceImpl implements ProductBrowseHistoryServ
         if (historyDO != null) {
             browseHistoryMapper.deleteById(historyDO);
         } else {
-            // 情况二:限制每个用户的浏览记录的条数
-            Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId);
+            // 情况二:限制每个用户的浏览记录的条数(只查一条最早地记录、记录总数)
+            Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId, 1, 1);
             if (pageResult.getTotal() >= USER_STORE_MAXIMUM) {
                 // 删除最早的一条
                 browseHistoryMapper.deleteById(CollUtil.getFirst(pageResult.getRecords()));

+ 0 - 74
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductSpuStatisticsDO.java

@@ -1,74 +0,0 @@
-package cn.iocoder.yudao.module.statistics.dal.mysql.product;
-
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.*;
-
-import java.time.LocalDateTime;
-
-/**
- * 商品 SPU 统计 DO
- *
- * 以天为维度,统计商品 SPU 的数据
- *
- * @author 芋道源码
- */
-@TableName("product_spu_statistics")
-@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ProductSpuStatisticsDO extends BaseDO {
-
-    /**
-     * 编号,主键自增
-     */
-    @TableId
-    private Long id;
-
-    /**
-     * 商品 SPU 编号
-     *
-     * 关联 ProductSpuDO 的 id 字段
-     */
-    private Long spuId;
-    /**
-     * 统计日期
-     */
-    private LocalDateTime time;
-
-    /**
-     * 浏览量
-     */
-    private Integer browseCount;
-    /**
-     * 收藏量
-     */
-    private Integer favoriteCount;
-
-    /**
-     * 添加购物车次数
-     *
-     * 以商品被添加到购物车的 createTime 计算,后续多次添加,不会增加该值。
-     * 直到该次被下单、或者被删除,后续再次被添加到购物车。
-     */
-    private Integer addCartCount;
-    /**
-     * 创建订单商品数
-     */
-    private Integer createOrderCount;
-    /**
-     * 支付订单商品数
-     */
-    private Integer payOrderCount;
-    /**
-     * 总支付金额,单位:分
-     */
-    private Integer payPrice;
-
-}

+ 0 - 70
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsDO.java

@@ -1,70 +0,0 @@
-package cn.iocoder.yudao.module.statistics.dal.mysql.product;
-
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.*;
-
-import java.time.LocalDateTime;
-
-/**
- * 商品统计 DO
- *
- * 以天为维度,统计全部的数据
- *
- * 和 {@link ProductSpuStatisticsDO} 的差异是,它是全局的统计
- *
- * @author 芋道源码
- */
-@TableName("product_spu_statistics")
-@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ProductStatisticsDO extends BaseDO {
-
-    /**
-     * 编号,主键自增
-     */
-    @TableId
-    private Long id;
-
-    /**
-     * 统计日期
-     */
-    private LocalDateTime time;
-
-    /**
-     * 浏览量
-     */
-    private Integer browseCount;
-    /**
-     * 收藏量
-     */
-    private Integer favoriteCount;
-
-    /**
-     * 添加购物车次数
-     *
-     * 以商品被添加到购物车的 createTime 计算,后续多次添加,不会增加该值。
-     * 直到该次被下单、或者被删除,后续再次被添加到购物车。
-     */
-    private Integer addCartCount;
-    /**
-     * 创建订单商品数
-     */
-    private Integer createOrderCount;
-    /**
-     * 支付订单商品数
-     */
-    private Integer payOrderCount;
-    /**
-     * 总支付金额,单位:分
-     */
-    private Integer payPrice;
-
-}

+ 20 - 0
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsMapper.java

@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.statistics.dal.mysql.product;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
 import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -57,4 +61,20 @@ public interface ProductStatisticsMapper extends BaseMapperX<ProductStatisticsDO
                 .selectAvg(ProductStatisticsDO::getBrowseConvertPercent);
     }
 
+    /**
+     * 根据时间范围统计商品信息
+     *
+     * @param page      分页参数
+     * @param beginTime 起始时间
+     * @param endTime   截止时间
+     * @return 统计
+     */
+    IPage<ProductStatisticsDO> selectStatisticsResultPageByTimeBetween(IPage<ProductStatisticsDO> page,
+                                                                       @Param("beginTime") LocalDateTime beginTime,
+                                                                       @Param("endTime") LocalDateTime endTime);
+
+    default Long selectCountByTimeBetween(LocalDateTime beginTime, LocalDateTime endTime) {
+        return selectCount(new LambdaQueryWrapperX<ProductStatisticsDO>().between(ProductStatisticsDO::getTime, beginTime, endTime));
+    }
+
 }

+ 48 - 0
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.statistics.job.product;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
+
+/**
+ * 商品统计 Job
+ *
+ * @author owen
+ */
+@Component
+public class ProductStatisticsJob implements JobHandler {
+
+    @Resource
+    private ProductStatisticsService productStatisticsService;
+
+    /**
+     * 执行商品统计任务
+     *
+     * @param param 要统计的天数,只能是正整数,1 代表昨日数据
+     * @return 统计结果
+     */
+    @Override
+    @TenantJob
+    public String execute(String param) {
+        // 默认昨日
+        param = ObjUtil.defaultIfBlank(param, "1");
+        // 校验参数的合理性
+        if (!NumberUtil.isInteger(param)) {
+            throw new RuntimeException("商品统计任务的参数只能为是正整数");
+        }
+        Integer days = Convert.toInt(param, 0);
+        if (days < 1) {
+            throw new RuntimeException("商品统计任务的参数只能为是正整数");
+        }
+        String result = productStatisticsService.statisticsProduct(days);
+        return StrUtil.format("商品统计:\n{}", result);
+    }
+}

+ 7 - 8
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsService.java

@@ -16,14 +16,6 @@ import java.util.List;
  */
 public interface ProductStatisticsService {
 
-    /**
-     * 创建商品统计
-     *
-     * @param entity 创建信息
-     * @return 编号
-     */
-    Long createProductStatistics(ProductStatisticsDO entity);
-
     /**
      * 获得商品统计排行榜分页
      *
@@ -49,4 +41,11 @@ public interface ProductStatisticsService {
      */
     List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO);
 
+    /**
+     * 统计指定天数的商品数据
+     *
+     * @return 统计结果
+     */
+    String statisticsProduct(Integer days);
+
 }

+ 62 - 11
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsServiceImpl.java

@@ -1,5 +1,8 @@
 package cn.iocoder.yudao.module.statistics.service.product;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -10,13 +13,18 @@ import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductSta
 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
 import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
 import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StopWatch;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 
 /**
@@ -31,17 +39,6 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
     @Resource
     private ProductStatisticsMapper productStatisticsMapper;
 
-    @Override
-    public Long createProductStatistics(ProductStatisticsDO entity) {
-        // 计算 访客支付转化率(百分比)
-        if (entity.getBrowseUserCount() != null && ObjUtil.notEqual(entity.getBrowseUserCount(), 0)) {
-            entity.setBrowseConvertPercent(100 * entity.getOrderPayCount() / entity.getBrowseUserCount());
-        }
-        // 插入
-        productStatisticsMapper.insert(entity);
-        // 返回
-        return entity.getId();
-    }
 
     @Override
     public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
@@ -69,4 +66,58 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
         return productStatisticsMapper.selectListByTimeBetween(reqVO);
     }
 
+    @Override
+    public String statisticsProduct(Integer days) {
+        LocalDateTime today = LocalDateTime.now();
+        return IntStream.rangeClosed(1, days)
+                .mapToObj(day -> statisticsProduct(today.minusDays(day)))
+                .sorted()
+                .collect(Collectors.joining("\n"));
+    }
+
+    /**
+     * 统计商品数据
+     *
+     * @param date 需要统计的日期
+     * @return 统计结果
+     */
+    private String statisticsProduct(LocalDateTime date) {
+        // 1. 处理统计时间范围
+        LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date);
+        LocalDateTime endTime = LocalDateTimeUtil.endOfDay(date);
+        String dateStr = DatePattern.NORM_DATE_FORMATTER.format(date);
+        // 2. 检查该日是否已经统计过
+        Long count = productStatisticsMapper.selectCountByTimeBetween(beginTime, endTime);
+        if (count != null && count > 0) {
+            return dateStr + " 数据已存在,如果需要重新统计,请先删除对应的数据";
+        }
+
+        // 3. 统计数据
+        StopWatch stopWatch = new StopWatch(dateStr);
+        stopWatch.start();
+
+        // 分页统计,避免商品表数据较多时,出现超时问题
+        final int pageSize = 100;
+        for (int pageNo = 1; ; pageNo ++) {
+            IPage<ProductStatisticsDO> page = productStatisticsMapper.selectStatisticsResultPageByTimeBetween(
+                    Page.of(pageNo, pageSize, false), beginTime, endTime);
+            if (CollUtil.isEmpty(page.getRecords())) {
+                break;
+            }
+
+            for (ProductStatisticsDO record : page.getRecords()) {
+                record.setTime(date.toLocalDate());
+                // 计算 访客支付转化率(百分比)
+                if (record.getBrowseUserCount() != null && ObjUtil.notEqual(record.getBrowseUserCount(), 0)) {
+                    record.setBrowseConvertPercent(100 * record.getOrderPayCount() / record.getBrowseUserCount());
+                }
+            }
+
+            // 4. 插入数据
+            productStatisticsMapper.insertBatch(page.getRecords());
+        }
+
+        return stopWatch.prettyPrint();
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeStatisticsServiceImpl.java

@@ -99,7 +99,7 @@ public class TradeStatisticsServiceImpl implements TradeStatisticsService {
         // 1. 处理统计时间范围
         LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date);
         LocalDateTime endTime = LocalDateTimeUtil.endOfDay(date);
-        String dateStr = DatePattern.NORM_DATE_FORMAT.format(date);
+        String dateStr = DatePattern.NORM_DATE_FORMATTER.format(date);
         // 2. 检查该日是否已经统计过
         TradeStatisticsDO entity = tradeStatisticsMapper.selectByTimeBetween(beginTime, endTime);
         if (entity != null) {

+ 64 - 0
yudao-module-mall/yudao-module-statistics-biz/src/main/resources/mapper/product/ProductStatisticsMapper.xml

@@ -0,0 +1,64 @@
+<?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="cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper">
+
+    <select id="selectStatisticsResultPageByTimeBetween"
+            resultType="cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO">
+        SELECT spu.id                                                       AS spuId
+             -- 浏览量:一个用户可以有多次
+             , (SELECT COUNT(1)
+                FROM product_browse_history
+                WHERE spu_id = spu.id
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS browse_count
+             -- 访客量:按用户去重计数
+             , (SELECT COUNT(DISTINCT user_id)
+                FROM product_browse_history
+                WHERE spu_id = spu.id
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS browse_user_count
+             -- 收藏数量:按用户去重计数
+             , (SELECT COUNT(DISTINCT user_id)
+                FROM product_favorite
+                WHERE spu_id = spu.id
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS favorite_count
+             -- 加购数量:按用户去重计数
+             , (SELECT COUNT(DISTINCT user_id)
+                FROM trade_cart
+                WHERE spu_id = spu.id
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS cart_count
+             -- 下单件数
+             , (SELECT IFNULL(SUM(count), 0)
+                FROM trade_order_item
+                WHERE spu_id = spu.id
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS order_count
+             -- 支付件数
+             , (SELECT IFNULL(SUM(item.count), 0)
+                FROM trade_order_item item
+                         JOIN trade_order o ON item.order_id = o.id
+                WHERE spu_id = spu.id
+                  AND o.pay_status = TRUE
+                  AND item.create_time BETWEEN #{beginTime} AND #{endTime}) AS order_pay_count
+             -- 支付金额
+             , (SELECT IFNULL(SUM(item.pay_price), 0)
+                FROM trade_order_item item
+                         JOIN trade_order o ON item.order_id = o.id
+                WHERE spu_id = spu.id
+                  AND o.pay_status = TRUE
+                  AND item.create_time BETWEEN #{beginTime} AND #{endTime}) AS order_pay_price
+             -- 退款件数
+             , (SELECT IFNULL(SUM(count), 0)
+                FROM trade_after_sale
+                WHERE spu_id = spu.id
+                  AND refund_time IS NOT NULL
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS after_sale_count
+             -- 退款金额
+             , (SELECT IFNULL(SUM(refund_price), 0)
+                FROM trade_after_sale
+                WHERE spu_id = spu.id
+                  AND refund_time IS NOT NULL
+                  AND create_time BETWEEN #{beginTime} AND #{endTime})      AS after_sale_refund_price
+        FROM product_spu spu
+        WHERE spu.deleted = FALSE
+        ORDER BY spu.id
+    </select>
+
+</mapper>