Commit 0b7d42482f449112e33b74a9b511b0c31524bcb3

Authored by owen
1 parent f374e778

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

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
42 42 .eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
43 43 }
44 44  
45   - default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId) {
46   - Page<ProductBrowseHistoryDO> page = Page.of(0, 1);
  45 + default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId, Integer pageNo, Integer pageSize) {
  46 + Page<ProductBrowseHistoryDO> page = Page.of(pageNo, pageSize);
47 47 return selectPage(page, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
48 48 .eqIfPresent(ProductBrowseHistoryDO::getUserId, userId)
49 49 .orderByAsc(ProductBrowseHistoryDO::getCreateTime));
... ...
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
32 32 if (historyDO != null) {
33 33 browseHistoryMapper.deleteById(historyDO);
34 34 } else {
35   - // 情况二:限制每个用户的浏览记录的条数
36   - Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId);
  35 + // 情况二:限制每个用户的浏览记录的条数(只查一条最早地记录、记录总数)
  36 + Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId, 1, 1);
37 37 if (pageResult.getTotal() >= USER_STORE_MAXIMUM) {
38 38 // 删除最早的一条
39 39 browseHistoryMapper.deleteById(CollUtil.getFirst(pageResult.getRecords()));
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductSpuStatisticsDO.java deleted
1   -package cn.iocoder.yudao.module.statistics.dal.mysql.product;
2   -
3   -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4   -import com.baomidou.mybatisplus.annotation.KeySequence;
5   -import com.baomidou.mybatisplus.annotation.TableId;
6   -import com.baomidou.mybatisplus.annotation.TableName;
7   -import lombok.*;
8   -
9   -import java.time.LocalDateTime;
10   -
11   -/**
12   - * 商品 SPU 统计 DO
13   - *
14   - * 以天为维度,统计商品 SPU 的数据
15   - *
16   - * @author 芋道源码
17   - */
18   -@TableName("product_spu_statistics")
19   -@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
20   -@Data
21   -@EqualsAndHashCode(callSuper = true)
22   -@ToString(callSuper = true)
23   -@Builder
24   -@NoArgsConstructor
25   -@AllArgsConstructor
26   -public class ProductSpuStatisticsDO extends BaseDO {
27   -
28   - /**
29   - * 编号,主键自增
30   - */
31   - @TableId
32   - private Long id;
33   -
34   - /**
35   - * 商品 SPU 编号
36   - *
37   - * 关联 ProductSpuDO 的 id 字段
38   - */
39   - private Long spuId;
40   - /**
41   - * 统计日期
42   - */
43   - private LocalDateTime time;
44   -
45   - /**
46   - * 浏览量
47   - */
48   - private Integer browseCount;
49   - /**
50   - * 收藏量
51   - */
52   - private Integer favoriteCount;
53   -
54   - /**
55   - * 添加购物车次数
56   - *
57   - * 以商品被添加到购物车的 createTime 计算,后续多次添加,不会增加该值。
58   - * 直到该次被下单、或者被删除,后续再次被添加到购物车。
59   - */
60   - private Integer addCartCount;
61   - /**
62   - * 创建订单商品数
63   - */
64   - private Integer createOrderCount;
65   - /**
66   - * 支付订单商品数
67   - */
68   - private Integer payOrderCount;
69   - /**
70   - * 总支付金额,单位:分
71   - */
72   - private Integer payPrice;
73   -
74   -}
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsDO.java deleted
1   -package cn.iocoder.yudao.module.statistics.dal.mysql.product;
2   -
3   -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4   -import com.baomidou.mybatisplus.annotation.KeySequence;
5   -import com.baomidou.mybatisplus.annotation.TableId;
6   -import com.baomidou.mybatisplus.annotation.TableName;
7   -import lombok.*;
8   -
9   -import java.time.LocalDateTime;
10   -
11   -/**
12   - * 商品统计 DO
13   - *
14   - * 以天为维度,统计全部的数据
15   - *
16   - * 和 {@link ProductSpuStatisticsDO} 的差异是,它是全局的统计
17   - *
18   - * @author 芋道源码
19   - */
20   -@TableName("product_spu_statistics")
21   -@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
22   -@Data
23   -@EqualsAndHashCode(callSuper = true)
24   -@ToString(callSuper = true)
25   -@Builder
26   -@NoArgsConstructor
27   -@AllArgsConstructor
28   -public class ProductStatisticsDO extends BaseDO {
29   -
30   - /**
31   - * 编号,主键自增
32   - */
33   - @TableId
34   - private Long id;
35   -
36   - /**
37   - * 统计日期
38   - */
39   - private LocalDateTime time;
40   -
41   - /**
42   - * 浏览量
43   - */
44   - private Integer browseCount;
45   - /**
46   - * 收藏量
47   - */
48   - private Integer favoriteCount;
49   -
50   - /**
51   - * 添加购物车次数
52   - *
53   - * 以商品被添加到购物车的 createTime 计算,后续多次添加,不会增加该值。
54   - * 直到该次被下单、或者被删除,后续再次被添加到购物车。
55   - */
56   - private Integer addCartCount;
57   - /**
58   - * 创建订单商品数
59   - */
60   - private Integer createOrderCount;
61   - /**
62   - * 支付订单商品数
63   - */
64   - private Integer payOrderCount;
65   - /**
66   - * 总支付金额,单位:分
67   - */
68   - private Integer payPrice;
69   -
70   -}
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;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
5 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 7 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
7 8 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
8 9 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
9 10 import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  11 +import com.baomidou.mybatisplus.core.metadata.IPage;
10 12 import org.apache.ibatis.annotations.Mapper;
  13 +import org.apache.ibatis.annotations.Param;
11 14  
  15 +import java.time.LocalDateTime;
12 16 import java.util.List;
13 17  
14 18 /**
... ... @@ -57,4 +61,20 @@ public interface ProductStatisticsMapper extends BaseMapperX&lt;ProductStatisticsDO
57 61 .selectAvg(ProductStatisticsDO::getBrowseConvertPercent);
58 62 }
59 63  
  64 + /**
  65 + * 根据时间范围统计商品信息
  66 + *
  67 + * @param page 分页参数
  68 + * @param beginTime 起始时间
  69 + * @param endTime 截止时间
  70 + * @return 统计
  71 + */
  72 + IPage<ProductStatisticsDO> selectStatisticsResultPageByTimeBetween(IPage<ProductStatisticsDO> page,
  73 + @Param("beginTime") LocalDateTime beginTime,
  74 + @Param("endTime") LocalDateTime endTime);
  75 +
  76 + default Long selectCountByTimeBetween(LocalDateTime beginTime, LocalDateTime endTime) {
  77 + return selectCount(new LambdaQueryWrapperX<ProductStatisticsDO>().between(ProductStatisticsDO::getTime, beginTime, endTime));
  78 + }
  79 +
60 80 }
61 81 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.job.product;
  2 +
  3 +import cn.hutool.core.convert.Convert;
  4 +import cn.hutool.core.util.NumberUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  8 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
  9 +import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
  10 +import jakarta.annotation.Resource;
  11 +import org.springframework.stereotype.Component;
  12 +
  13 +// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
  14 +
  15 +/**
  16 + * 商品统计 Job
  17 + *
  18 + * @author owen
  19 + */
  20 +@Component
  21 +public class ProductStatisticsJob implements JobHandler {
  22 +
  23 + @Resource
  24 + private ProductStatisticsService productStatisticsService;
  25 +
  26 + /**
  27 + * 执行商品统计任务
  28 + *
  29 + * @param param 要统计的天数,只能是正整数,1 代表昨日数据
  30 + * @return 统计结果
  31 + */
  32 + @Override
  33 + @TenantJob
  34 + public String execute(String param) {
  35 + // 默认昨日
  36 + param = ObjUtil.defaultIfBlank(param, "1");
  37 + // 校验参数的合理性
  38 + if (!NumberUtil.isInteger(param)) {
  39 + throw new RuntimeException("商品统计任务的参数只能为是正整数");
  40 + }
  41 + Integer days = Convert.toInt(param, 0);
  42 + if (days < 1) {
  43 + throw new RuntimeException("商品统计任务的参数只能为是正整数");
  44 + }
  45 + String result = productStatisticsService.statisticsProduct(days);
  46 + return StrUtil.format("商品统计:\n{}", result);
  47 + }
  48 +}
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsService.java
... ... @@ -17,14 +17,6 @@ import java.util.List;
17 17 public interface ProductStatisticsService {
18 18  
19 19 /**
20   - * 创建商品统计
21   - *
22   - * @param entity 创建信息
23   - * @return 编号
24   - */
25   - Long createProductStatistics(ProductStatisticsDO entity);
26   -
27   - /**
28 20 * 获得商品统计排行榜分页
29 21 *
30 22 * @param reqVO 查询条件
... ... @@ -49,4 +41,11 @@ public interface ProductStatisticsService {
49 41 */
50 42 List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO);
51 43  
  44 + /**
  45 + * 统计指定天数的商品数据
  46 + *
  47 + * @return 统计结果
  48 + */
  49 + String statisticsProduct(Integer days);
  50 +
52 51 }
53 52 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsServiceImpl.java
1 1 package cn.iocoder.yudao.module.statistics.service.product;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.DatePattern;
  5 +import cn.hutool.core.date.LocalDateTimeUtil;
3 6 import cn.hutool.core.util.ArrayUtil;
4 7 import cn.hutool.core.util.ObjUtil;
5 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
... ... @@ -10,13 +13,18 @@ import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductSta
10 13 import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
11 14 import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
12 15 import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper;
  16 +import com.baomidou.mybatisplus.core.metadata.IPage;
  17 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
13 18 import jakarta.annotation.Resource;
14 19 import org.springframework.stereotype.Service;
  20 +import org.springframework.util.StopWatch;
15 21 import org.springframework.validation.annotation.Validated;
16 22  
17 23 import java.time.Duration;
18 24 import java.time.LocalDateTime;
19 25 import java.util.List;
  26 +import java.util.stream.Collectors;
  27 +import java.util.stream.IntStream;
20 28  
21 29  
22 30 /**
... ... @@ -31,17 +39,6 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
31 39 @Resource
32 40 private ProductStatisticsMapper productStatisticsMapper;
33 41  
34   - @Override
35   - public Long createProductStatistics(ProductStatisticsDO entity) {
36   - // 计算 访客支付转化率(百分比)
37   - if (entity.getBrowseUserCount() != null && ObjUtil.notEqual(entity.getBrowseUserCount(), 0)) {
38   - entity.setBrowseConvertPercent(100 * entity.getOrderPayCount() / entity.getBrowseUserCount());
39   - }
40   - // 插入
41   - productStatisticsMapper.insert(entity);
42   - // 返回
43   - return entity.getId();
44   - }
45 42  
46 43 @Override
47 44 public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
... ... @@ -69,4 +66,58 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
69 66 return productStatisticsMapper.selectListByTimeBetween(reqVO);
70 67 }
71 68  
  69 + @Override
  70 + public String statisticsProduct(Integer days) {
  71 + LocalDateTime today = LocalDateTime.now();
  72 + return IntStream.rangeClosed(1, days)
  73 + .mapToObj(day -> statisticsProduct(today.minusDays(day)))
  74 + .sorted()
  75 + .collect(Collectors.joining("\n"));
  76 + }
  77 +
  78 + /**
  79 + * 统计商品数据
  80 + *
  81 + * @param date 需要统计的日期
  82 + * @return 统计结果
  83 + */
  84 + private String statisticsProduct(LocalDateTime date) {
  85 + // 1. 处理统计时间范围
  86 + LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date);
  87 + LocalDateTime endTime = LocalDateTimeUtil.endOfDay(date);
  88 + String dateStr = DatePattern.NORM_DATE_FORMATTER.format(date);
  89 + // 2. 检查该日是否已经统计过
  90 + Long count = productStatisticsMapper.selectCountByTimeBetween(beginTime, endTime);
  91 + if (count != null && count > 0) {
  92 + return dateStr + " 数据已存在,如果需要重新统计,请先删除对应的数据";
  93 + }
  94 +
  95 + // 3. 统计数据
  96 + StopWatch stopWatch = new StopWatch(dateStr);
  97 + stopWatch.start();
  98 +
  99 + // 分页统计,避免商品表数据较多时,出现超时问题
  100 + final int pageSize = 100;
  101 + for (int pageNo = 1; ; pageNo ++) {
  102 + IPage<ProductStatisticsDO> page = productStatisticsMapper.selectStatisticsResultPageByTimeBetween(
  103 + Page.of(pageNo, pageSize, false), beginTime, endTime);
  104 + if (CollUtil.isEmpty(page.getRecords())) {
  105 + break;
  106 + }
  107 +
  108 + for (ProductStatisticsDO record : page.getRecords()) {
  109 + record.setTime(date.toLocalDate());
  110 + // 计算 访客支付转化率(百分比)
  111 + if (record.getBrowseUserCount() != null && ObjUtil.notEqual(record.getBrowseUserCount(), 0)) {
  112 + record.setBrowseConvertPercent(100 * record.getOrderPayCount() / record.getBrowseUserCount());
  113 + }
  114 + }
  115 +
  116 + // 4. 插入数据
  117 + productStatisticsMapper.insertBatch(page.getRecords());
  118 + }
  119 +
  120 + return stopWatch.prettyPrint();
  121 + }
  122 +
72 123 }
73 124 \ No newline at end of file
... ...
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 {
99 99 // 1. 处理统计时间范围
100 100 LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date);
101 101 LocalDateTime endTime = LocalDateTimeUtil.endOfDay(date);
102   - String dateStr = DatePattern.NORM_DATE_FORMAT.format(date);
  102 + String dateStr = DatePattern.NORM_DATE_FORMATTER.format(date);
103 103 // 2. 检查该日是否已经统计过
104 104 TradeStatisticsDO entity = tradeStatisticsMapper.selectByTimeBetween(beginTime, endTime);
105 105 if (entity != null) {
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/resources/mapper/product/ProductStatisticsMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper">
  4 +
  5 + <select id="selectStatisticsResultPageByTimeBetween"
  6 + resultType="cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO">
  7 + SELECT spu.id AS spuId
  8 + -- 浏览量:一个用户可以有多次
  9 + , (SELECT COUNT(1)
  10 + FROM product_browse_history
  11 + WHERE spu_id = spu.id
  12 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS browse_count
  13 + -- 访客量:按用户去重计数
  14 + , (SELECT COUNT(DISTINCT user_id)
  15 + FROM product_browse_history
  16 + WHERE spu_id = spu.id
  17 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS browse_user_count
  18 + -- 收藏数量:按用户去重计数
  19 + , (SELECT COUNT(DISTINCT user_id)
  20 + FROM product_favorite
  21 + WHERE spu_id = spu.id
  22 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS favorite_count
  23 + -- 加购数量:按用户去重计数
  24 + , (SELECT COUNT(DISTINCT user_id)
  25 + FROM trade_cart
  26 + WHERE spu_id = spu.id
  27 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS cart_count
  28 + -- 下单件数
  29 + , (SELECT IFNULL(SUM(count), 0)
  30 + FROM trade_order_item
  31 + WHERE spu_id = spu.id
  32 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS order_count
  33 + -- 支付件数
  34 + , (SELECT IFNULL(SUM(item.count), 0)
  35 + FROM trade_order_item item
  36 + JOIN trade_order o ON item.order_id = o.id
  37 + WHERE spu_id = spu.id
  38 + AND o.pay_status = TRUE
  39 + AND item.create_time BETWEEN #{beginTime} AND #{endTime}) AS order_pay_count
  40 + -- 支付金额
  41 + , (SELECT IFNULL(SUM(item.pay_price), 0)
  42 + FROM trade_order_item item
  43 + JOIN trade_order o ON item.order_id = o.id
  44 + WHERE spu_id = spu.id
  45 + AND o.pay_status = TRUE
  46 + AND item.create_time BETWEEN #{beginTime} AND #{endTime}) AS order_pay_price
  47 + -- 退款件数
  48 + , (SELECT IFNULL(SUM(count), 0)
  49 + FROM trade_after_sale
  50 + WHERE spu_id = spu.id
  51 + AND refund_time IS NOT NULL
  52 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS after_sale_count
  53 + -- 退款金额
  54 + , (SELECT IFNULL(SUM(refund_price), 0)
  55 + FROM trade_after_sale
  56 + WHERE spu_id = spu.id
  57 + AND refund_time IS NOT NULL
  58 + AND create_time BETWEEN #{beginTime} AND #{endTime}) AS after_sale_refund_price
  59 + FROM product_spu spu
  60 + WHERE spu.deleted = FALSE
  61 + ORDER BY spu.id
  62 + </select>
  63 +
  64 +</mapper>
... ...