Commit 4a007f0c986a5e511483f347e8d5c5f67ad8240d

Authored by owen
1 parent 6de17c38

统计:增加商品统计

Showing 16 changed files with 574 additions and 36 deletions
sql/mysql/optinal/product_statistics.sql 0 → 100644
  1 +CREATE TABLE product_statistics
  2 +(
  3 + id bigint AUTO_INCREMENT COMMENT '编号,主键自增' PRIMARY KEY,
  4 + time date NOT NULL COMMENT '统计日期',
  5 + spu_id bigint NOT NULL COMMENT '商品SPU编号',
  6 + browse_count int DEFAULT 0 NOT NULL COMMENT '浏览量',
  7 + browse_user_count int DEFAULT 0 NOT NULL COMMENT '访客量',
  8 + favorite_count int DEFAULT 0 NOT NULL COMMENT '收藏数量',
  9 + cart_count int DEFAULT 0 NOT NULL COMMENT '加购数量',
  10 + order_count int DEFAULT 0 NOT NULL COMMENT '下单件数',
  11 + order_pay_count int DEFAULT 0 NOT NULL COMMENT '支付件数',
  12 + order_pay_price int DEFAULT 0 NOT NULL COMMENT '支付金额,单位:分',
  13 + after_sale_count int DEFAULT 0 NOT NULL COMMENT '退款件数',
  14 + after_sale_refund_price int DEFAULT 0 NOT NULL COMMENT '退款金额,单位:分',
  15 + browse_convert_percent int DEFAULT 0 NOT NULL COMMENT '访客支付转化率(百分比)',
  16 + creator varchar(64) DEFAULT '' NULL COMMENT '创建者',
  17 + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
  18 + updater varchar(64) DEFAULT '' NULL COMMENT '更新者',
  19 + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  20 + deleted bit DEFAULT b'0' NOT NULL COMMENT '是否删除',
  21 + tenant_id bigint DEFAULT 0 NOT NULL COMMENT '租户编号'
  22 +)
  23 + COMMENT '商品统计表';
  24 +
  25 +CREATE INDEX idx_time
  26 + ON product_statistics (time);
  27 +
  28 +CREATE INDEX idx_spu_id
  29 + ON product_statistics (spu_id);
  30 +
  31 +INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES ('商品统计', '', 2, 6, 2358, 'product', 'fa:product-hunt', 'statistics/product/index', 'ProductStatistics', 0, true, true, true, '', '2023-12-15 18:54:28', '', '2023-12-15 18:54:33', false);
  32 +SELECT @parentId1 := LAST_INSERT_ID();
  33 +INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES ('商品统计查询', 'statistics:product:query', 3, 1, @parentId, '', '', '', null, 0, true, true, true, '', '2023-09-30 03:22:40', '', '2023-09-30 03:22:40', false);
  34 +INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES ('商品统计导出', 'statistics:product:export', 3, 2, @parentId, '', '', '', null, 0, true, true, true, '', '2023-09-30 03:22:40', '', '2023-09-30 03:22:40', false);
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.common.pojo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +import lombok.ToString;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Schema(description = "可排序的分页参数")
  11 +@Data
  12 +@EqualsAndHashCode(callSuper = true)
  13 +@ToString(callSuper = true)
  14 +public class SortablePageParam extends PageParam {
  15 +
  16 + @Schema(description = "排序字段")
  17 + private List<SortingField> sortingFields;
  18 +
  19 +}
0 \ No newline at end of file 20 \ No newline at end of file
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 6
7 import java.util.List; 7 import java.util.List;
  8 +import java.util.function.Consumer;
8 9
9 /** 10 /**
10 * Bean 工具类 11 * Bean 工具类
@@ -27,11 +28,19 @@ public class BeanUtils { @@ -27,11 +28,19 @@ public class BeanUtils {
27 return CollectionUtils.convertList(source, s -> toBean(s, targetType)); 28 return CollectionUtils.convertList(source, s -> toBean(s, targetType));
28 } 29 }
29 30
30 - public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) { 31 + public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
  32 + return toBean(source, targetType, null);
  33 + }
  34 +
  35 + public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
31 if (source == null) { 36 if (source == null) {
32 return null; 37 return null;
33 } 38 }
34 - return new PageResult<>(toBean(source.getList(), targetType), source.getTotal()); 39 + List<T> list = toBean(source.getList(), targetType);
  40 + if (peek != null) {
  41 + list.forEach(peek);
  42 + }
  43 + return new PageResult<>(list, source.getTotal());
35 } 44 }
36 45
37 } 46 }
38 \ No newline at end of file 47 \ No newline at end of file
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageParam; 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  7 +import cn.iocoder.yudao.framework.common.pojo.SortingField;
6 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; 8 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
7 import com.baomidou.mybatisplus.core.conditions.Wrapper; 9 import com.baomidou.mybatisplus.core.conditions.Wrapper;
8 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 10 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -27,7 +29,15 @@ import java.util.List; @@ -27,7 +29,15 @@ import java.util.List;
27 */ 29 */
28 public interface BaseMapperX<T> extends MPJBaseMapper<T> { 30 public interface BaseMapperX<T> extends MPJBaseMapper<T> {
29 31
  32 + default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
  33 + return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
  34 + }
  35 +
30 default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { 36 default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
  37 + return selectPage(pageParam, null, queryWrapper);
  38 + }
  39 +
  40 + default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
31 // 特殊:不分页,直接查询全部 41 // 特殊:不分页,直接查询全部
32 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { 42 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
33 List<T> list = selectList(queryWrapper); 43 List<T> list = selectList(queryWrapper);
@@ -35,7 +45,7 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -35,7 +45,7 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
35 } 45 }
36 46
37 // MyBatis Plus 查询 47 // MyBatis Plus 查询
38 - IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); 48 + IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
39 selectPage(mpPage, queryWrapper); 49 selectPage(mpPage, queryWrapper);
40 // 转换返回 50 // 转换返回
41 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); 51 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
1 package cn.iocoder.yudao.framework.mybatis.core.util; 1 package cn.iocoder.yudao.framework.mybatis.core.util;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.collection.CollectionUtil; 4 import cn.hutool.core.collection.CollectionUtil;
  5 +import cn.hutool.core.lang.func.Func1;
  6 +import cn.hutool.core.lang.func.LambdaUtil;
  7 +import cn.hutool.core.util.ArrayUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageParam; 8 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  9 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
5 import cn.iocoder.yudao.framework.common.pojo.SortingField; 10 import cn.iocoder.yudao.framework.common.pojo.SortingField;
6 import com.baomidou.mybatisplus.core.metadata.OrderItem; 11 import com.baomidou.mybatisplus.core.metadata.OrderItem;
7 import com.baomidou.mybatisplus.core.toolkit.StringPool; 12 import com.baomidou.mybatisplus.core.toolkit.StringPool;
@@ -11,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -11,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
11 import net.sf.jsqlparser.expression.Alias; 16 import net.sf.jsqlparser.expression.Alias;
12 import net.sf.jsqlparser.schema.Column; 17 import net.sf.jsqlparser.schema.Column;
13 import net.sf.jsqlparser.schema.Table; 18 import net.sf.jsqlparser.schema.Table;
  19 +import org.springframework.util.Assert;
14 20
15 import java.util.ArrayList; 21 import java.util.ArrayList;
16 import java.util.Collection; 22 import java.util.Collection;
@@ -45,8 +51,8 @@ public class MyBatisUtils { @@ -45,8 +51,8 @@ public class MyBatisUtils {
45 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 51 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
46 * 52 *
47 * @param interceptor 链 53 * @param interceptor 链
48 - * @param inner 拦截器  
49 - * @param index 位置 54 + * @param inner 拦截器
  55 + * @param index 位置
50 */ 56 */
51 public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { 57 public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
52 List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors()); 58 List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
@@ -73,9 +79,9 @@ public class MyBatisUtils { @@ -73,9 +79,9 @@ public class MyBatisUtils {
73 /** 79 /**
74 * 构建 Column 对象 80 * 构建 Column 对象
75 * 81 *
76 - * @param tableName 表名 82 + * @param tableName 表名
77 * @param tableAlias 别名 83 * @param tableAlias 别名
78 - * @param column 字段名 84 + * @param column 字段名
79 * @return Column 对象 85 * @return Column 对象
80 */ 86 */
81 public static Column buildColumn(String tableName, Alias tableAlias, String column) { 87 public static Column buildColumn(String tableName, Alias tableAlias, String column) {
@@ -85,4 +91,46 @@ public class MyBatisUtils { @@ -85,4 +91,46 @@ public class MyBatisUtils {
85 return new Column(tableName + StringPool.DOT + column); 91 return new Column(tableName + StringPool.DOT + column);
86 } 92 }
87 93
  94 +
  95 + /**
  96 + * 构建排序字段(默认倒序)
  97 + *
  98 + * @param func 排序字段的 Lambda 表达式
  99 + * @param <T> 排序字段所属的类型
  100 + * @return 排序字段
  101 + */
  102 + public static <T> SortingField buildSortingField(Func1<T, ?> func) {
  103 + return buildSortingField(func, SortingField.ORDER_DESC);
  104 + }
  105 +
  106 + /**
  107 + * 构建排序字段
  108 + *
  109 + * @param func 排序字段的 Lambda 表达式
  110 + * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
  111 + * @param <T> 排序字段所属的类型
  112 + * @return 排序字段
  113 + */
  114 + public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
  115 + Object[] orderTypes = {SortingField.ORDER_ASC, SortingField.ORDER_DESC};
  116 + Assert.isTrue(ArrayUtil.contains(orderTypes, order), String.format("字段的排序类型只能是%s/%s", orderTypes));
  117 +
  118 + String fieldName = LambdaUtil.getFieldName(func);
  119 + return new SortingField(fieldName, order);
  120 + }
  121 +
  122 + /**
  123 + * 构建默认的排序字段
  124 + * 如果排序字段为空,则设置排序字段;否则忽略
  125 + *
  126 + * @param sortablePageParam 排序分页查询参数
  127 + * @param func 排序字段的 Lambda 表达式
  128 + * @param <T> 排序字段所属的类型
  129 + */
  130 + public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
  131 + if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
  132 + sortablePageParam.setSortingFields(List.of(buildSortingField(func)));
  133 + }
  134 + }
  135 +
88 } 136 }
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/ProductStatisticsController.java
@@ -2,40 +2,90 @@ package cn.iocoder.yudao.module.statistics.controller.admin.product; @@ -2,40 +2,90 @@ package cn.iocoder.yudao.module.statistics.controller.admin.product;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 -import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductSpuStatisticsDO;  
6 -import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsDO; 5 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  8 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  9 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
  10 +import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
  11 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  12 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  13 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  14 +import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
  15 +import io.swagger.v3.oas.annotations.Operation;
7 import io.swagger.v3.oas.annotations.tags.Tag; 16 import io.swagger.v3.oas.annotations.tags.Tag;
8 -import lombok.extern.slf4j.Slf4j; 17 +import jakarta.annotation.Resource;
  18 +import jakarta.servlet.http.HttpServletResponse;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
9 import org.springframework.validation.annotation.Validated; 21 import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.GetMapping;
10 import org.springframework.web.bind.annotation.RequestMapping; 23 import org.springframework.web.bind.annotation.RequestMapping;
11 import org.springframework.web.bind.annotation.RestController; 24 import org.springframework.web.bind.annotation.RestController;
12 25
13 -import java.time.LocalDateTime; 26 +import java.io.IOException;
14 import java.util.List; 27 import java.util.List;
  28 +import java.util.Map;
  29 +import java.util.Optional;
  30 +import java.util.Set;
  31 +
  32 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
15 35
16 @Tag(name = "管理后台 - 商品统计") 36 @Tag(name = "管理后台 - 商品统计")
17 @RestController 37 @RestController
18 @RequestMapping("/statistics/product") 38 @RequestMapping("/statistics/product")
19 @Validated 39 @Validated
20 -@Slf4j  
21 public class ProductStatisticsController { 40 public class ProductStatisticsController {
22 41
23 - // TODO @麦子:返回 ProductStatisticsComparisonResp, 里面有两个字段,一个是选择的时间范围的合计结果,一个是对比的时间范围的合计结果;  
24 - // 例如说,选择时间范围是 2023-10-01 ~ 2023-10-02,那么对比就是 2023-09-30,再倒推 2 天;  
25 - public CommonResult<Object> getProductStatisticsComparison() {  
26 - return null; 42 + @Resource
  43 + private ProductStatisticsService productStatisticsService;
  44 +
  45 + @Resource
  46 + private ProductSpuApi productSpuApi;
  47 +
  48 + @GetMapping("/analyse")
  49 + @Operation(summary = "获得商品统计分析")
  50 + @PreAuthorize("@ss.hasPermission('statistics:product:query')")
  51 + public CommonResult<DataComparisonRespVO<ProductStatisticsRespVO>> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO) {
  52 + return success(productStatisticsService.getProductStatisticsAnalyse(reqVO));
  53 + }
  54 +
  55 + @GetMapping("/list")
  56 + @Operation(summary = "获得商品统计明细(日期维度)")
  57 + @PreAuthorize("@ss.hasPermission('statistics:product:query')")
  58 + public CommonResult<List<ProductStatisticsRespVO>> getProductStatisticsList(ProductStatisticsReqVO reqVO) {
  59 + List<ProductStatisticsDO> list = productStatisticsService.getProductStatisticsList(reqVO);
  60 + return success(BeanUtils.toBean(list, ProductStatisticsRespVO.class));
27 } 61 }
28 62
29 - // TODO @麦子:查询指定时间范围内的商品统计数据;DO 到时需要改成 VO 哈  
30 - public CommonResult<List<ProductStatisticsDO>> getProductStatisticsList(  
31 - LocalDateTime[] times) {  
32 - return null; 63 + @GetMapping("/export-excel")
  64 + @Operation(summary = "导出获得商品统计明细 Excel(日期维度)")
  65 + @PreAuthorize("@ss.hasPermission('statistics:product:export')")
  66 + public void exportProductStatisticsExcel(ProductStatisticsReqVO reqVO, HttpServletResponse response) throws IOException {
  67 + List<ProductStatisticsDO> list = productStatisticsService.getProductStatisticsList(reqVO);
  68 + // 导出 Excel
  69 + List<ProductStatisticsRespVO> voList = BeanUtils.toBean(list, ProductStatisticsRespVO.class);
  70 + ExcelUtils.write(response, "商品状况.xls", "数据", ProductStatisticsRespVO.class, voList);
33 } 71 }
34 72
35 - // TODO @麦子:查询指定时间范围内的商品 SPU 统计数据;DO 到时需要改成 VO 哈  
36 - // 入参是分页参数 + 时间范围 + 排序字段  
37 - public CommonResult<PageResult<ProductSpuStatisticsDO>> getProductSpuStatisticsPage() {  
38 - return null; 73 + @GetMapping("/rank-page")
  74 + @Operation(summary = "获得商品统计排行榜分页(商品维度)")
  75 + @PreAuthorize("@ss.hasPermission('statistics:product:query')")
  76 + public CommonResult<PageResult<ProductStatisticsRespVO>> getProductStatisticsRankPage(@Valid ProductStatisticsReqVO reqVO,
  77 + @Valid SortablePageParam pageParam) {
  78 + PageResult<ProductStatisticsDO> pageResult = productStatisticsService.getProductStatisticsRankPage(reqVO, pageParam);
  79 + // 处理商品信息
  80 + Set<Long> spuIds = convertSet(pageResult.getList(), ProductStatisticsDO::getSpuId);
  81 + Map<Long, ProductSpuRespDTO> spuMap = convertMap(productSpuApi.getSpuList(spuIds), ProductSpuRespDTO::getId);
  82 + // 拼接返回
  83 + return success(BeanUtils.toBean(pageResult, ProductStatisticsRespVO.class,
  84 + // 拼接商品信息
  85 + item -> Optional.ofNullable(spuMap.get(item.getSpuId())).ifPresent(spu -> {
  86 + item.setName(spu.getName());
  87 + item.setPicUrl(spu.getPicUrl());
  88 + })));
39 } 89 }
40 90
41 -} 91 -}
  92 +}
42 \ No newline at end of file 93 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +import lombok.ToString;
  8 +import org.springframework.format.annotation.DateTimeFormat;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  13 +
  14 +@Schema(description = "管理后台 - 商品统计分析 Request VO")
  15 +@Data
  16 +@ToString(callSuper = true)
  17 +@AllArgsConstructor
  18 +@NoArgsConstructor
  19 +public class ProductStatisticsReqVO {
  20 +
  21 + @Schema(description = "统计时间范围", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
  22 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  23 + private LocalDateTime[] times;
  24 +
  25 +}
0 \ No newline at end of file 26 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
  2 +
  3 +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
  4 +import com.alibaba.excel.annotation.ExcelProperty;
  5 +import com.fasterxml.jackson.annotation.JsonFormat;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import lombok.Data;
  8 +
  9 +import java.time.LocalDate;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
  12 +
  13 +@Schema(description = "管理后台 - 商品统计 Response VO")
  14 +@Data
  15 +@ExcelIgnoreUnannotated
  16 +public class ProductStatisticsRespVO {
  17 +
  18 + @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "12393")
  19 + private Long id;
  20 +
  21 + @Schema(description = "统计日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-12-16")
  22 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
  23 + @ExcelProperty("统计日期")
  24 + private LocalDate time;
  25 +
  26 + @Schema(description = "商品SPU编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15114")
  27 + @ExcelProperty("商品SPU编号")
  28 + private Long spuId;
  29 +
  30 + //region 商品信息
  31 +
  32 + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商品名称")
  33 + @ExcelProperty("商品名称")
  34 + private String name;
  35 +
  36 + @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "15114")
  37 + @ExcelProperty("商品封面图")
  38 + private String picUrl;
  39 +
  40 + //endregion
  41 +
  42 + @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "17505")
  43 + @ExcelProperty("浏览量")
  44 + private Integer browseCount;
  45 +
  46 + @Schema(description = "访客量", requiredMode = Schema.RequiredMode.REQUIRED, example = "11814")
  47 + @ExcelProperty("访客量")
  48 + private Integer browseUserCount;
  49 +
  50 + @Schema(description = "收藏数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20950")
  51 + @ExcelProperty("收藏数量")
  52 + private Integer favoriteCount;
  53 +
  54 + @Schema(description = "加购数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "28493")
  55 + @ExcelProperty("加购数量")
  56 + private Integer cartCount;
  57 +
  58 + @Schema(description = "下单件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18966")
  59 + @ExcelProperty("下单件数")
  60 + private Integer orderCount;
  61 +
  62 + @Schema(description = "支付件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "15142")
  63 + @ExcelProperty("支付件数")
  64 + private Integer orderPayCount;
  65 +
  66 + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "11595")
  67 + @ExcelProperty("支付金额,单位:分")
  68 + private Integer orderPayPrice;
  69 +
  70 + @Schema(description = "退款件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2591")
  71 + @ExcelProperty("退款件数")
  72 + private Integer afterSaleCount;
  73 +
  74 + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "21709")
  75 + @ExcelProperty("退款金额,单位:分")
  76 + private Integer afterSaleRefundPrice;
  77 +
  78 + @Schema(description = "访客支付转化率(百分比)", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
  79 + private Integer browseConvertPercent;
  80 +
  81 +}
0 \ No newline at end of file 82 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/TradeStatisticsController.java
@@ -18,6 +18,9 @@ import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; @@ -18,6 +18,9 @@ import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
18 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; 18 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
19 import io.swagger.v3.oas.annotations.Operation; 19 import io.swagger.v3.oas.annotations.Operation;
20 import io.swagger.v3.oas.annotations.tags.Tag; 20 import io.swagger.v3.oas.annotations.tags.Tag;
  21 +import jakarta.annotation.Resource;
  22 +import jakarta.servlet.http.HttpServletResponse;
  23 +import jakarta.validation.Valid;
21 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
22 import org.springframework.security.access.prepost.PreAuthorize; 25 import org.springframework.security.access.prepost.PreAuthorize;
23 import org.springframework.validation.annotation.Validated; 26 import org.springframework.validation.annotation.Validated;
@@ -25,9 +28,6 @@ import org.springframework.web.bind.annotation.GetMapping; @@ -25,9 +28,6 @@ import org.springframework.web.bind.annotation.GetMapping;
25 import org.springframework.web.bind.annotation.RequestMapping; 28 import org.springframework.web.bind.annotation.RequestMapping;
26 import org.springframework.web.bind.annotation.RestController; 29 import org.springframework.web.bind.annotation.RestController;
27 30
28 -import jakarta.annotation.Resource;  
29 -import jakarta.servlet.http.HttpServletResponse;  
30 -import jakarta.validation.Valid;  
31 import java.io.IOException; 31 import java.io.IOException;
32 import java.util.List; 32 import java.util.List;
33 33
@@ -67,13 +67,11 @@ public class TradeStatisticsController { @@ -67,13 +67,11 @@ public class TradeStatisticsController {
67 return success(TradeStatisticsConvert.INSTANCE.convert(yesterdayData, beforeYesterdayData, monthData, lastMonthData)); 67 return success(TradeStatisticsConvert.INSTANCE.convert(yesterdayData, beforeYesterdayData, monthData, lastMonthData));
68 } 68 }
69 69
70 - // TODO @疯狂:【晚点再改和讨论;等首页的接口出来】这个要不还是叫 analyse,对比选中的时间段,和上一个时间段;类似 MemberStatisticsController 的 getMemberAnalyse  
71 - @GetMapping("/trend/summary") 70 + @GetMapping("/analyse")
72 @Operation(summary = "获得交易状况统计") 71 @Operation(summary = "获得交易状况统计")
73 @PreAuthorize("@ss.hasPermission('statistics:trade:query')") 72 @PreAuthorize("@ss.hasPermission('statistics:trade:query')")
74 - public CommonResult<DataComparisonRespVO<TradeTrendSummaryRespVO>> getTradeTrendSummaryComparison(  
75 - TradeTrendReqVO reqVO) {  
76 - return success(tradeStatisticsService.getTradeTrendSummaryComparison(ArrayUtil.get(reqVO.getTimes(), 0), 73 + public CommonResult<DataComparisonRespVO<TradeTrendSummaryRespVO>> getTradeStatisticsAnalyse(TradeTrendReqVO reqVO) {
  74 + return success(tradeStatisticsService.getTradeStatisticsAnalyse(ArrayUtil.get(reqVO.getTimes(), 0),
77 ArrayUtil.get(reqVO.getTimes(), 1))); 75 ArrayUtil.get(reqVO.getTimes(), 1)));
78 } 76 }
79 77
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryRespVO.java
@@ -12,7 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @@ -12,7 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
12 @Data 12 @Data
13 public class TradeTrendSummaryRespVO { 13 public class TradeTrendSummaryRespVO {
14 14
15 - @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") 15 + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-12-16")
16 @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) 16 @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
17 private LocalDate date; 17 private LocalDate date;
18 18
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/dataobject/product/ProductStatisticsDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.dal.dataobject.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.LocalDate;
  10 +
  11 +/**
  12 + * 商品统计 DO
  13 + *
  14 + * @author owen
  15 + */
  16 +@TableName("product_statistics")
  17 +@KeySequence("product_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +@Builder
  22 +@NoArgsConstructor
  23 +@AllArgsConstructor
  24 +public class ProductStatisticsDO extends BaseDO {
  25 +
  26 + /**
  27 + * 编号,主键自增
  28 + */
  29 + @TableId
  30 + private Long id;
  31 + /**
  32 + * 统计日期
  33 + */
  34 + private LocalDate time;
  35 + /**
  36 + * 商品SPU编号
  37 + */
  38 + private Long spuId;
  39 + /**
  40 + * 浏览量
  41 + */
  42 + private Integer browseCount;
  43 + /**
  44 + * 访客量
  45 + */
  46 + private Integer browseUserCount;
  47 + /**
  48 + * 收藏数量
  49 + */
  50 + private Integer favoriteCount;
  51 + /**
  52 + * 加购数量
  53 + */
  54 + private Integer cartCount;
  55 + /**
  56 + * 下单件数
  57 + */
  58 + private Integer orderCount;
  59 + /**
  60 + * 支付件数
  61 + */
  62 + private Integer orderPayCount;
  63 + /**
  64 + * 支付金额,单位:分
  65 + */
  66 + private Integer orderPayPrice;
  67 + /**
  68 + * 退款件数
  69 + */
  70 + private Integer afterSaleCount;
  71 + /**
  72 + * 退款金额,单位:分
  73 + */
  74 + private Integer afterSaleRefundPrice;
  75 + /**
  76 + * 访客支付转化率(百分比)
  77 + */
  78 + private Integer browseConvertPercent;
  79 +
  80 +}
0 \ No newline at end of file 81 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.dal.mysql.product;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  5 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
  7 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  8 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  9 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 商品统计 Mapper
  16 + *
  17 + * @author owen
  18 + */
  19 +@Mapper
  20 +public interface ProductStatisticsMapper extends BaseMapperX<ProductStatisticsDO> {
  21 +
  22 + default PageResult<ProductStatisticsDO> selectPageGroupBySpuId(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
  23 + return selectPage(pageParam, buildWrapper(reqVO)
  24 + .groupBy(ProductStatisticsDO::getSpuId)
  25 + .select(ProductStatisticsDO::getSpuId)
  26 + );
  27 + }
  28 +
  29 + default List<ProductStatisticsDO> selectListByTimeBetween(ProductStatisticsReqVO reqVO) {
  30 + return selectList(buildWrapper(reqVO)
  31 + .groupBy(ProductStatisticsDO::getTime)
  32 + .select(ProductStatisticsDO::getTime));
  33 + }
  34 +
  35 + default ProductStatisticsRespVO selectVoByTimeBetween(ProductStatisticsReqVO reqVO) {
  36 + return selectJoinOne(ProductStatisticsRespVO.class, buildWrapper(reqVO));
  37 + }
  38 +
  39 + /**
  40 + * 构建 LambdaWrapper
  41 + *
  42 + * @param reqVO 查询参数
  43 + * @return LambdaWrapper
  44 + */
  45 + private static MPJLambdaWrapperX<ProductStatisticsDO> buildWrapper(ProductStatisticsReqVO reqVO) {
  46 + return new MPJLambdaWrapperX<ProductStatisticsDO>()
  47 + .betweenIfPresent(ProductStatisticsDO::getTime, reqVO.getTimes())
  48 + .selectSum(ProductStatisticsDO::getBrowseCount)
  49 + .selectSum(ProductStatisticsDO::getBrowseUserCount)
  50 + .selectSum(ProductStatisticsDO::getFavoriteCount)
  51 + .selectSum(ProductStatisticsDO::getCartCount)
  52 + .selectSum(ProductStatisticsDO::getOrderCount)
  53 + .selectSum(ProductStatisticsDO::getOrderPayCount)
  54 + .selectSum(ProductStatisticsDO::getOrderPayPrice)
  55 + .selectSum(ProductStatisticsDO::getAfterSaleCount)
  56 + .selectSum(ProductStatisticsDO::getAfterSaleRefundPrice)
  57 + .selectAvg(ProductStatisticsDO::getBrowseConvertPercent);
  58 + }
  59 +
  60 +}
0 \ No newline at end of file 61 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/product/ProductStatisticsService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.service.product;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  5 +import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
  6 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  7 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  8 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * 商品统计 Service 接口
  14 + *
  15 + * @author owen
  16 + */
  17 +public interface ProductStatisticsService {
  18 +
  19 + /**
  20 + * 创建商品统计
  21 + *
  22 + * @param entity 创建信息
  23 + * @return 编号
  24 + */
  25 + Long createProductStatistics(ProductStatisticsDO entity);
  26 +
  27 + /**
  28 + * 获得商品统计排行榜分页
  29 + *
  30 + * @param reqVO 查询条件
  31 + * @param pageParam 分页排序查询
  32 + * @return 商品统计分页
  33 + */
  34 + PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam);
  35 +
  36 + /**
  37 + * 获得商品状况统计分析
  38 + *
  39 + * @param reqVO 查询条件
  40 + * @return 统计数据对照
  41 + */
  42 + DataComparisonRespVO<ProductStatisticsRespVO> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO);
  43 +
  44 + /**
  45 + * 获得商品状况明细
  46 + *
  47 + * @param reqVO 查询条件
  48 + * @return 统计数据对照
  49 + */
  50 + List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO);
  51 +
  52 +}
0 \ No newline at end of file 53 \ 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 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.service.product;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
  8 +import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
  9 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  10 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  11 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  12 +import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsMapper;
  13 +import jakarta.annotation.Resource;
  14 +import org.springframework.stereotype.Service;
  15 +import org.springframework.validation.annotation.Validated;
  16 +
  17 +import java.time.Duration;
  18 +import java.time.LocalDateTime;
  19 +import java.util.List;
  20 +
  21 +
  22 +/**
  23 + * 商品统计 Service 实现类
  24 + *
  25 + * @author owen
  26 + */
  27 +@Service
  28 +@Validated
  29 +public class ProductStatisticsServiceImpl implements ProductStatisticsService {
  30 +
  31 + @Resource
  32 + private ProductStatisticsMapper productStatisticsMapper;
  33 +
  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 +
  46 + @Override
  47 + public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
  48 + // 默认浏览量倒序
  49 + MyBatisUtils.buildDefaultSortingField(pageParam, ProductStatisticsDO::getBrowseCount);
  50 + return productStatisticsMapper.selectPageGroupBySpuId(reqVO, pageParam);
  51 + }
  52 +
  53 + @Override
  54 + public DataComparisonRespVO<ProductStatisticsRespVO> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO) {
  55 + LocalDateTime beginTime = ArrayUtil.get(reqVO.getTimes(), 0);
  56 + LocalDateTime endTime = ArrayUtil.get(reqVO.getTimes(), 1);
  57 +
  58 + // 统计数据
  59 + ProductStatisticsRespVO value = productStatisticsMapper.selectVoByTimeBetween(reqVO);
  60 + // 对照数据
  61 + LocalDateTime referenceBeginTime = beginTime.minus(Duration.between(beginTime, endTime));
  62 + ProductStatisticsReqVO referenceReqVO = new ProductStatisticsReqVO(new LocalDateTime[]{referenceBeginTime, beginTime});
  63 + ProductStatisticsRespVO reference = productStatisticsMapper.selectVoByTimeBetween(referenceReqVO);
  64 + return new DataComparisonRespVO<>(value, reference);
  65 + }
  66 +
  67 + @Override
  68 + public List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO) {
  69 + return productStatisticsMapper.selectListByTimeBetween(reqVO);
  70 + }
  71 +
  72 +}
0 \ No newline at end of file 73 \ No newline at end of file
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeStatisticsService.java
@@ -20,7 +20,7 @@ public interface TradeStatisticsService { @@ -20,7 +20,7 @@ public interface TradeStatisticsService {
20 * 20 *
21 * @return 统计数据对照 21 * @return 统计数据对照
22 */ 22 */
23 - DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeTrendSummaryComparison( 23 + DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeStatisticsAnalyse(
24 LocalDateTime beginTime, LocalDateTime endTime); 24 LocalDateTime beginTime, LocalDateTime endTime);
25 25
26 /** 26 /**
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeStatisticsServiceImpl.java
@@ -60,7 +60,7 @@ public class TradeStatisticsServiceImpl implements TradeStatisticsService { @@ -60,7 +60,7 @@ public class TradeStatisticsServiceImpl implements TradeStatisticsService {
60 } 60 }
61 61
62 @Override 62 @Override
63 - public DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeTrendSummaryComparison(LocalDateTime beginTime, 63 + public DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeStatisticsAnalyse(LocalDateTime beginTime,
64 LocalDateTime endTime) { 64 LocalDateTime endTime) {
65 // 统计数据 65 // 统计数据
66 TradeTrendSummaryRespVO value = tradeStatisticsMapper.selectVoByTimeBetween(beginTime, endTime); 66 TradeTrendSummaryRespVO value = tradeStatisticsMapper.selectVoByTimeBetween(beginTime, endTime);