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

This commit is contained in:
owen 2023-12-17 19:30:50 +08:00
parent f374e778bb
commit 0b7d42482f
10 changed files with 206 additions and 168 deletions

View File

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

View File

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

View File

@ -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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 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;
}

View File

@ -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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 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;
}

View File

@ -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.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 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.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.ProductStatisticsReqVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO; 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.dataobject.product.ProductStatisticsDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
@ -57,4 +61,20 @@ public interface ProductStatisticsMapper extends BaseMapperX<ProductStatisticsDO
.selectAvg(ProductStatisticsDO::getBrowseConvertPercent); .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));
}
} }

View File

@ -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);
}
}

View File

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

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.statistics.service.product; 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.ArrayUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.controller.admin.product.vo.ProductStatisticsRespVO;
import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO; import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper; 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 jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/** /**
@ -31,17 +39,6 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
@Resource @Resource
private ProductStatisticsMapper productStatisticsMapper; 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 @Override
public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) { public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
@ -69,4 +66,58 @@ public class ProductStatisticsServiceImpl implements ProductStatisticsService {
return productStatisticsMapper.selectListByTimeBetween(reqVO); 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();
}
} }

View File

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

View File

@ -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>