Commit eb048532f707630b7a0d3cec82d056aa6f0e4b7e

Authored by YunaiV
2 parents a259e032 09445840

Merge branch 'develop' of https://gitee.com/CrazyWorld/ruoyi-vue-pro into develop

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/article/AppArticleController.java
Showing 41 changed files with 1327 additions and 184 deletions
sql/mysql/optinal/product_browse_history.sql 0 → 100644
  1 +CREATE TABLE product_browse_history
  2 +(
  3 + id bigint AUTO_INCREMENT COMMENT '记录编号'
  4 + PRIMARY KEY,
  5 + user_id bigint NOT NULL COMMENT '用户编号',
  6 + spu_id bigint NOT NULL COMMENT '商品 SPU 编号',
  7 + user_deleted bit DEFAULT b'0' NOT NULL COMMENT '用户是否删除',
  8 + creator varchar(64) DEFAULT '' NULL COMMENT '创建者',
  9 + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
  10 + updater varchar(64) DEFAULT '' NULL COMMENT '更新者',
  11 + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  12 + deleted bit DEFAULT b'0' NOT NULL COMMENT '是否删除',
  13 + tenant_id bigint DEFAULT 0 NOT NULL COMMENT '租户编号'
  14 +)
  15 + COMMENT '商品浏览记录表';
  16 +
  17 +CREATE INDEX idx_spuId
  18 + ON product_browse_history (spu_id);
  19 +
  20 +CREATE INDEX idx_userId
  21 + ON product_browse_history (user_id);
  22 +
... ...
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 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 5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 6  
7 7 import java.util.List;
  8 +import java.util.function.Consumer;
8 9  
9 10 /**
10 11 * Bean 工具类
... ... @@ -27,11 +28,19 @@ public class BeanUtils {
27 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 36 if (source == null) {
32 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 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 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 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 8 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
7 9 import com.baomidou.mybatisplus.core.conditions.Wrapper;
8 10 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
... ... @@ -27,7 +29,15 @@ import java.util.List;
27 29 */
28 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 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 42 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
33 43 List<T> list = selectList(queryWrapper);
... ... @@ -35,7 +45,7 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
35 45 }
36 46  
37 47 // MyBatis Plus 查询
38   - IPage<T> mpPage = MyBatisUtils.buildPage(pageParam);
  48 + IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
39 49 selectPage(mpPage, queryWrapper);
40 50 // 转换返回
41 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 1 package cn.iocoder.yudao.framework.mybatis.core.util;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 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 8 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  9 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
5 10 import cn.iocoder.yudao.framework.common.pojo.SortingField;
6 11 import com.baomidou.mybatisplus.core.metadata.OrderItem;
7 12 import com.baomidou.mybatisplus.core.toolkit.StringPool;
... ... @@ -11,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
11 16 import net.sf.jsqlparser.expression.Alias;
12 17 import net.sf.jsqlparser.schema.Column;
13 18 import net.sf.jsqlparser.schema.Table;
  19 +import org.springframework.util.Assert;
14 20  
15 21 import java.util.ArrayList;
16 22 import java.util.Collection;
... ... @@ -45,8 +51,8 @@ public class MyBatisUtils {
45 51 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
46 52 *
47 53 * @param interceptor 链
48   - * @param inner 拦截器
49   - * @param index 位置
  54 + * @param inner 拦截器
  55 + * @param index 位置
50 56 */
51 57 public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
52 58 List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
... ... @@ -73,9 +79,9 @@ public class MyBatisUtils {
73 79 /**
74 80 * 构建 Column 对象
75 81 *
76   - * @param tableName 表名
  82 + * @param tableName 表名
77 83 * @param tableAlias 别名
78   - * @param column 字段名
  84 + * @param column 字段名
79 85 * @return Column 对象
80 86 */
81 87 public static Column buildColumn(String tableName, Alias tableAlias, String column) {
... ... @@ -85,4 +91,46 @@ public class MyBatisUtils {
85 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-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm
... ... @@ -5,7 +5,6 @@ import lombok.*;
5 5 import java.util.*;
6 6 import jakarta.validation.constraints.*;
7 7 ## 处理 BigDecimal 字段的引入
8   -import java.util.*;
9 8 #foreach ($column in $columns)
10 9 #if (${column.javaType} == "BigDecimal")
11 10 import java.math.BigDecimal;
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm
1 1 import request from '@/config/axios'
2 2 #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
3 3  
  4 +// ${table.classComment} VO
4 5 export interface ${simpleClassName}VO {
5 6 #foreach ($column in $columns)
6 7 #if ($column.createOperation || $column.updateOperation)
  8 + // ${column.columnComment}
7 9 #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
8 10 ${column.javaField}: number
9   -#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime")
  11 +#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
10 12 ${column.javaField}: Date
11 13 #else
12 14 ${column.javaField}: ${column.javaType.toLowerCase()}
... ... @@ -15,42 +17,44 @@ export interface ${simpleClassName}VO {
15 17 #end
16 18 }
17 19  
  20 +// ${table.classComment} API
  21 +export const ${simpleClassName}Api = {
18 22 #if ( $table.templateType != 2 )
19   -// 查询${table.classComment}分页
20   -export const get${simpleClassName}Page = async (params) => {
21   - return await request.get({ url: `${baseURL}/page`, params })
22   -}
  23 + // 查询${table.classComment}分页
  24 + get${simpleClassName}Page: async (params: any) => {
  25 + return await request.get({ url: `${baseURL}/page`, params })
  26 + },
23 27 #else
24   -// 查询${table.classComment}列表
25   -export const get${simpleClassName}List = async (params) => {
26   - return await request.get({ url: `${baseURL}/list`, params })
27   -}
  28 + // 查询${table.classComment}列表
  29 + get${simpleClassName}List: async (params) => {
  30 + return await request.get({ url: `${baseURL}/list`, params })
  31 + },
28 32 #end
29 33  
30   -// 查询${table.classComment}详情
31   -export const get${simpleClassName} = async (id: number) => {
32   - return await request.get({ url: `${baseURL}/get?id=` + id })
33   -}
  34 + // 查询${table.classComment}详情
  35 + get${simpleClassName}: async (id: number) => {
  36 + return await request.get({ url: `${baseURL}/get?id=` + id })
  37 + },
34 38  
35   -// 新增${table.classComment}
36   -export const create${simpleClassName} = async (data: ${simpleClassName}VO) => {
37   - return await request.post({ url: `${baseURL}/create`, data })
38   -}
  39 + // 新增${table.classComment}
  40 + create${simpleClassName}: async (data: ${simpleClassName}VO) => {
  41 + return await request.post({ url: `${baseURL}/create`, data })
  42 + },
39 43  
40   -// 修改${table.classComment}
41   -export const update${simpleClassName} = async (data: ${simpleClassName}VO) => {
42   - return await request.put({ url: `${baseURL}/update`, data })
43   -}
  44 + // 修改${table.classComment}
  45 + update${simpleClassName}: async (data: ${simpleClassName}VO) => {
  46 + return await request.put({ url: `${baseURL}/update`, data })
  47 + },
44 48  
45   -// 删除${table.classComment}
46   -export const delete${simpleClassName} = async (id: number) => {
47   - return await request.delete({ url: `${baseURL}/delete?id=` + id })
48   -}
  49 + // 删除${table.classComment}
  50 + delete${simpleClassName}: async (id: number) => {
  51 + return await request.delete({ url: `${baseURL}/delete?id=` + id })
  52 + },
49 53  
50   -// 导出${table.classComment} Excel
51   -export const export${simpleClassName} = async (params) => {
52   - return await request.download({ url: `${baseURL}/export-excel`, params })
53   -}
  54 + // 导出${table.classComment} Excel
  55 + export${simpleClassName}: async (params) => {
  56 + return await request.download({ url: `${baseURL}/export-excel`, params })
  57 + },
54 58 ## 特殊:主子表专属逻辑
55 59 #foreach ($subTable in $subTables)
56 60 #set ($index = $foreach.count - 1)
... ... @@ -66,46 +70,47 @@ export const export${simpleClassName} = async (params) =&gt; {
66 70 ## 情况一:MASTER_ERP 时,需要分查询页子表
67 71 #if ( $table.templateType == 11 )
68 72  
69   -// 获得${subTable.classComment}分页
70   -export const get${subSimpleClassName}Page = async (params) => {
71   - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params })
72   -}
  73 + // 获得${subTable.classComment}分页
  74 + get${subSimpleClassName}Page: async (params) => {
  75 + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params })
  76 + },
73 77 ## 情况二:非 MASTER_ERP 时,需要列表查询子表
74 78 #else
75 79 #if ( $subTable.subJoinMany )
76 80  
77   -// 获得${subTable.classComment}列表
78   -export const get${subSimpleClassName}ListBy${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
79   - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
80   -}
  81 + // 获得${subTable.classComment}列表
  82 + get${subSimpleClassName}ListBy${SubJoinColumnName}: async (${subJoinColumn.javaField}) => {
  83 + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
  84 + },
81 85 #else
82 86  
83   -// 获得${subTable.classComment}
84   -export const get${subSimpleClassName}By${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
85   - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
86   -}
  87 + // 获得${subTable.classComment}
  88 + get${subSimpleClassName}By${SubJoinColumnName}: async (${subJoinColumn.javaField}) => {
  89 + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
  90 + },
87 91 #end
88 92 #end
89 93 ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
90 94 #if ( $table.templateType == 11 )
91   -// 新增${subTable.classComment}
92   -export const create${subSimpleClassName} = async (data) => {
93   - return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data })
94   -}
  95 + // 新增${subTable.classComment}
  96 + create${subSimpleClassName}: async (data) => {
  97 + return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data })
  98 + },
95 99  
96   -// 修改${subTable.classComment}
97   -export const update${subSimpleClassName} = async (data) => {
98   - return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data })
99   -}
  100 + // 修改${subTable.classComment}
  101 + update${subSimpleClassName}: async (data) => {
  102 + return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data })
  103 + },
100 104  
101   -// 删除${subTable.classComment}
102   -export const delete${subSimpleClassName} = async (id: number) => {
103   - return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id })
104   -}
  105 + // 删除${subTable.classComment}
  106 + delete${subSimpleClassName}: async (id: number) => {
  107 + return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id })
  108 + },
105 109  
106   -// 获得${subTable.classComment}
107   -export const get${subSimpleClassName} = async (id: number) => {
108   - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id })
109   -}
  110 + // 获得${subTable.classComment}
  111 + get${subSimpleClassName}: async (id: number) => {
  112 + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id })
  113 + },
110 114 #end
111   -#end
112 115 \ No newline at end of file
  116 +#end
  117 +}
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
... ... @@ -114,7 +114,7 @@
114 114 </template>
115 115 <script setup lang="ts">
116 116 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
117   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
  117 +import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
118 118  
119 119 const { t } = useI18n() // 国际化
120 120 const message = useMessage() // 消息弹窗
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
... ... @@ -265,7 +265,7 @@
265 265 </template>
266 266 <script setup lang="ts">
267 267 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
268   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
  268 +import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
269 269  
270 270 const props = defineProps<{
271 271 ${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm
... ... @@ -85,7 +85,7 @@
85 85 <script setup lang="ts">
86 86 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
87 87 import { dateFormatter } from '@/utils/formatTime'
88   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
  88 +import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
89 89 #if ($table.templateType == 11)
90 90 import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue'
91 91 #end
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
... ... @@ -140,7 +140,7 @@
140 140 </template>
141 141 <script setup lang="ts">
142 142 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
143   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
  143 +import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
144 144 ## 特殊:树表专属逻辑
145 145 #if ( $table.templateType == 2 )
146 146 import { defaultProps, handleTree } from '@/utils/tree'
... ... @@ -152,6 +152,9 @@ import ${subSimpleClassName}Form from &#39;./components/${subSimpleClassName}Form.vu
152 152 #end
153 153 #end
154 154  
  155 +/** ${table.classComment} 表单 */
  156 +defineOptions({ name: '${simpleClassName}Form' })
  157 +
155 158 const { t } = useI18n() // 国际化
156 159 const message = useMessage() // 消息弹窗
157 160  
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
... ... @@ -240,7 +240,7 @@ import { dateFormatter } from &#39;@/utils/formatTime&#39;
240 240 import { handleTree } from '@/utils/tree'
241 241 #end
242 242 import download from '@/utils/download'
243   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
  243 +import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
244 244 import ${simpleClassName}Form from './${simpleClassName}Form.vue'
245 245 ## 特殊:主子表专属逻辑
246 246 #if ( $table.templateType != 10 )
... ... @@ -249,16 +249,22 @@ import ${subSimpleClassName}List from &#39;./components/${subSimpleClassName}List.vu
249 249 #end
250 250 #end
251 251  
  252 +/** ${table.classComment} 列表 */
252 253 defineOptions({ name: '${table.className}' })
253 254  
254   -const message = useMessage() // 消息弹窗
255   -const { t } = useI18n() // 国际化
  255 +// 消息弹窗
  256 +const message = useMessage()
  257 +// 国际化
  258 +const { t } = useI18n()
256 259  
257   -const loading = ref(true) // 列表的加载中
258   -const list = ref([]) // 列表的数据
  260 +// 列表的加载中
  261 +const loading = ref(true)
  262 +// 列表的数据
  263 +const list = ref<${simpleClassName}VO[]>([])
259 264 ## 特殊:树表专属逻辑(树不需要分页接口)
260 265 #if ( $table.templateType != 2 )
261   -const total = ref(0) // 列表的总页数
  266 +// 列表的总页数
  267 +const total = ref(0)
262 268 #end
263 269 const queryParams = reactive({
264 270 ## 特殊:树表专属逻辑(树不需要分页接口)
... ... @@ -277,8 +283,10 @@ const queryParams = reactive({
277 283 #end
278 284 #end
279 285 })
280   -const queryFormRef = ref() // 搜索的表单
281   -const exportLoading = ref(false) // 导出的加载中
  286 +// 搜索的表单
  287 +const queryFormRef = ref()
  288 +// 导出的加载中
  289 +const exportLoading = ref(false)
282 290  
283 291 /** 查询列表 */
284 292 const getList = async () => {
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.history;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
  7 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryRespVO;
  8 +import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
  9 +import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
  10 +import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.GetMapping;
  17 +import org.springframework.web.bind.annotation.RequestMapping;
  18 +import org.springframework.web.bind.annotation.RestController;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  21 +
  22 +@Tag(name = "管理后台 - 商品浏览记录")
  23 +@RestController
  24 +@RequestMapping("/product/browse-history")
  25 +@Validated
  26 +public class ProductBrowseHistoryController {
  27 +
  28 + @Resource
  29 + private ProductBrowseHistoryService browseHistoryService;
  30 +
  31 + @GetMapping("/page")
  32 + @Operation(summary = "获得商品浏览记录分页")
  33 + @PreAuthorize("@ss.hasPermission('product:browse-history:query')")
  34 + public CommonResult<PageResult<ProductBrowseHistoryRespVO>> getBrowseHistoryPage(@Valid ProductBrowseHistoryPageReqVO pageReqVO) {
  35 + PageResult<ProductBrowseHistoryDO> pageResult = browseHistoryService.getBrowseHistoryPage(pageReqVO);
  36 + return success(BeanUtils.toBean(pageResult, ProductBrowseHistoryRespVO.class));
  37 + }
  38 +
  39 +}
0 40 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.history.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  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 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +public class ProductBrowseHistoryPageReqVO extends SortablePageParam {
  19 +
  20 + @Schema(description = "用户编号", example = "4314")
  21 + private Long userId;
  22 +
  23 + @Schema(description = "用户是否删除", example = "false")
  24 + private Boolean userDeleted;
  25 +
  26 + @Schema(description = "商品 SPU 编号", example = "42")
  27 + private Long spuId;
  28 +
  29 + @Schema(description = "创建时间")
  30 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  31 + private LocalDateTime[] createTime;
  32 +
  33 +}
0 34 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.history.vo;
  2 +
  3 +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
  4 +import com.alibaba.excel.annotation.ExcelProperty;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import lombok.Data;
  7 +
  8 +import java.time.LocalDateTime;
  9 +
  10 +@Schema(description = "管理后台 - 商品浏览记录 Response VO")
  11 +@Data
  12 +@ExcelIgnoreUnannotated
  13 +public class ProductBrowseHistoryRespVO {
  14 +
  15 + @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26055")
  16 + @ExcelProperty("记录编号")
  17 + private Long id;
  18 +
  19 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4314")
  20 + @ExcelProperty("用户编号")
  21 + private Long userId;
  22 +
  23 + @Schema(description = "用户是否删除", example = "false")
  24 + private Boolean userDeleted;
  25 +
  26 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "42")
  27 + @ExcelProperty("商品 SPU 编号")
  28 + private Long spuId;
  29 +
  30 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + @ExcelProperty("创建时间")
  32 + private LocalDateTime createTime;
  33 +
  34 +}
0 35 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/AppProductBrowseHistoryController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.app.history;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
  8 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
  9 +import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryDeleteReqVO;
  10 +import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryPageReqVO;
  11 +import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryRespVO;
  12 +import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
  13 +import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
  14 +import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
  15 +import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
  16 +import io.swagger.v3.oas.annotations.Operation;
  17 +import io.swagger.v3.oas.annotations.tags.Tag;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.web.bind.annotation.*;
  21 +
  22 +import java.util.Map;
  23 +import java.util.Optional;
  24 +import java.util.Set;
  25 +
  26 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  29 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  30 +
  31 +@Tag(name = "用户 APP - 商品浏览记录")
  32 +@RestController
  33 +@RequestMapping("/product/browse-history")
  34 +public class AppProductBrowseHistoryController {
  35 +
  36 + @Resource
  37 + private ProductBrowseHistoryService productBrowseHistoryService;
  38 + @Resource
  39 + private ProductSpuService productSpuService;
  40 +
  41 + @DeleteMapping(value = "/delete")
  42 + @Operation(summary = "删除商品浏览记录")
  43 + @PreAuthenticated
  44 + public CommonResult<Boolean> deleteBrowseHistory(@RequestBody @Valid AppProductBrowseHistoryDeleteReqVO reqVO) {
  45 + productBrowseHistoryService.hideUserBrowseHistory(getLoginUserId(), reqVO.getSpuIds());
  46 + return success(Boolean.TRUE);
  47 + }
  48 +
  49 + @DeleteMapping(value = "/clean")
  50 + @Operation(summary = "清空商品浏览记录")
  51 + @PreAuthenticated
  52 + public CommonResult<Boolean> cleanBrowseHistory() {
  53 + productBrowseHistoryService.hideUserBrowseHistory(getLoginUserId(), null);
  54 + return success(Boolean.TRUE);
  55 + }
  56 +
  57 + @GetMapping(value = "/get-count")
  58 + @Operation(summary = "获得商品浏览记录数量")
  59 + @PreAuthenticated
  60 + public CommonResult<Long> getBrowseHistoryCount() {
  61 + return success(productBrowseHistoryService.getBrowseHistoryCount(getLoginUserId(), false));
  62 + }
  63 +
  64 + @GetMapping(value = "/page")
  65 + @Operation(summary = "获得商品浏览记录分页")
  66 + @PreAuthenticated
  67 + public CommonResult<PageResult<AppProductBrowseHistoryRespVO>> getBrowseHistoryPage(AppProductBrowseHistoryPageReqVO reqVO) {
  68 + ProductBrowseHistoryPageReqVO pageReqVO = BeanUtils.toBean(reqVO, ProductBrowseHistoryPageReqVO.class);
  69 + pageReqVO.setUserId(getLoginUserId());
  70 + // 排除用户已删除的(隐藏的)
  71 + pageReqVO.setUserDeleted(false);
  72 + PageResult<ProductBrowseHistoryDO> pageResult = productBrowseHistoryService.getBrowseHistoryPage(pageReqVO);
  73 + if (CollUtil.isEmpty(pageResult.getList())) {
  74 + return success(PageResult.empty());
  75 + }
  76 +
  77 + // 得到商品 spu 信息
  78 + Set<Long> spuIds = convertSet(pageResult.getList(), ProductBrowseHistoryDO::getSpuId);
  79 + Map<Long, ProductSpuDO> spuMap = convertMap(productSpuService.getSpuList(spuIds), ProductSpuDO::getId);
  80 +
  81 + // 转换 VO 结果
  82 + PageResult<AppProductBrowseHistoryRespVO> result = BeanUtils.toBean(pageResult, AppProductBrowseHistoryRespVO.class,
  83 + vo -> Optional.ofNullable(spuMap.get(vo.getSpuId())).ifPresent(spu -> {
  84 + vo.setSpuName(spu.getName());
  85 + vo.setPicUrl(spu.getPicUrl());
  86 + }));
  87 + return success(result);
  88 + }
  89 +
  90 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryDeleteReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.app.history.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
  10 +
  11 +@Schema(description = "用户 APP - 删除商品浏览记录的 Request VO")
  12 +@Data
  13 +public class AppProductBrowseHistoryDeleteReqVO {
  14 +
  15 + @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
  16 + @NotEmpty(message = "商品 SPU 编号数组不能为空")
  17 + private List<Long> spuIds;
  18 +
  19 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.app.history.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  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 = "用户 APP - 商品浏览记录分页 Request VO")
  15 +@Data
  16 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +public class AppProductBrowseHistoryPageReqVO extends PageParam {
  19 + @Schema(description = "创建时间")
  20 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  21 + private LocalDateTime[] createTime;
  22 +
  23 +}
0 24 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.app.history.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
  7 +
  8 +@Schema(description = "用户 App - 商品浏览记录 Response VO")
  9 +@Data
  10 +public class AppProductBrowseHistoryRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
  13 + private Long id;
  14 +
  15 + @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
  16 + private Long spuId;
  17 +
  18 + // ========== 商品相关字段 ==========
  19 +
  20 + @Schema(description = "商品 SPU 名称", example = "赵六")
  21 + private String spuName;
  22 +
  23 + @Schema(description = "商品封面图", example = "https://domain/pic.png")
  24 + private String picUrl;
  25 +
  26 + @Schema(description = "商品单价", example = "100")
  27 + private Integer price;
  28 +
  29 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
... ... @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
14 14 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
15 15 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
16 16 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
  17 +import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
17 18 import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
18 19 import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
19 20 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -48,6 +49,8 @@ public class AppProductSpuController {
48 49 private ProductSpuService productSpuService;
49 50 @Resource
50 51 private ProductSkuService productSkuService;
  52 + @Resource
  53 + private ProductBrowseHistoryService productBrowseHistoryService;
51 54  
52 55 @Resource
53 56 private MemberLevelApi memberLevelApi;
... ... @@ -122,6 +125,11 @@ public class AppProductSpuController {
122 125 throw exception(SPU_NOT_ENABLE);
123 126 }
124 127  
  128 + // 增加浏览量
  129 + productSpuService.updateBrowseCount(id, 1);
  130 + // 保存浏览记录
  131 + productBrowseHistoryService.createBrowseHistory(getLoginUserId(), id);
  132 +
125 133 // 拼接返回
126 134 List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
127 135 AppProductSpuDetailRespVO detailVO = ProductSpuConvert.INSTANCE.convertForGetSpuDetail(spu, skus);
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductStatisticsDO.java renamed to yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/history/ProductBrowseHistoryDO.java
1   -package cn.iocoder.yudao.module.statistics.dal.mysql.product;
  1 +package cn.iocoder.yudao.module.product.dal.dataobject.history;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import com.baomidou.mybatisplus.annotation.KeySequence;
... ... @@ -6,65 +6,37 @@ import com.baomidou.mybatisplus.annotation.TableId;
6 6 import com.baomidou.mybatisplus.annotation.TableName;
7 7 import lombok.*;
8 8  
9   -import java.time.LocalDateTime;
10   -
11 9 /**
12   - * 商品统计 DO
13   - *
14   - * 以天为维度,统计全部的数据
  10 + * 商品浏览记录 DO
15 11 *
16   - * 和 {@link ProductSpuStatisticsDO} 的差异是,它是全局的统计
17   - *
18   - * @author 芋道源码
  12 + * @author owen
19 13 */
20   -@TableName("product_spu_statistics")
21   -@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  14 +@TableName("product_browse_history")
  15 +@KeySequence("product_browse_history_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
22 16 @Data
23 17 @EqualsAndHashCode(callSuper = true)
24 18 @ToString(callSuper = true)
25 19 @Builder
26 20 @NoArgsConstructor
27 21 @AllArgsConstructor
28   -public class ProductStatisticsDO extends BaseDO {
  22 +public class ProductBrowseHistoryDO extends BaseDO {
29 23  
30 24 /**
31   - * 编号,主键自增
  25 + * 记录编号
32 26 */
33 27 @TableId
34 28 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 29 /**
58   - * 创建订单商品数
  30 + * 商品 SPU 编号
59 31 */
60   - private Integer createOrderCount;
  32 + private Long spuId;
61 33 /**
62   - * 支付订单商品数
  34 + * 用户编号
63 35 */
64   - private Integer payOrderCount;
  36 + private Long userId;
65 37 /**
66   - * 总支付金额,单位:分
  38 + * 用户是否删除
67 39 */
68   - private Integer payPrice;
  40 + private Boolean userDeleted;
69 41  
70 42 -}
  43 +}
71 44 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.dal.mysql.history;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
  8 +import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
  9 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  10 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  11 +import org.apache.ibatis.annotations.Mapper;
  12 +
  13 +import java.util.Collection;
  14 +
  15 +/**
  16 + * 商品浏览记录 Mapper
  17 + *
  18 + * @author owen
  19 + */
  20 +@Mapper
  21 +public interface ProductBrowseHistoryMapper extends BaseMapperX<ProductBrowseHistoryDO> {
  22 +
  23 + default PageResult<ProductBrowseHistoryDO> selectPage(ProductBrowseHistoryPageReqVO reqVO) {
  24 + return selectPage(reqVO, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
  25 + .eqIfPresent(ProductBrowseHistoryDO::getUserId, reqVO.getUserId())
  26 + .eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, reqVO.getUserDeleted())
  27 + .eqIfPresent(ProductBrowseHistoryDO::getSpuId, reqVO.getSpuId())
  28 + .betweenIfPresent(ProductBrowseHistoryDO::getCreateTime, reqVO.getCreateTime())
  29 + .orderByDesc(ProductBrowseHistoryDO::getId));
  30 + }
  31 +
  32 + default void updateUserDeletedByUserId(Long userId, Collection<Long> spuIds, Boolean userDeleted) {
  33 + update(new LambdaUpdateWrapper<ProductBrowseHistoryDO>()
  34 + .eq(ProductBrowseHistoryDO::getUserId, userId)
  35 + .in(CollUtil.isNotEmpty(spuIds), ProductBrowseHistoryDO::getSpuId, spuIds)
  36 + .set(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
  37 + }
  38 +
  39 + default Long selectCountByUserIdAndUserDeleted(Long userId, Boolean userDeleted) {
  40 + return selectCount(new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
  41 + .eq(ProductBrowseHistoryDO::getUserId, userId)
  42 + .eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
  43 + }
  44 +
  45 + default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId, Integer pageNo, Integer pageSize) {
  46 + Page<ProductBrowseHistoryDO> page = Page.of(pageNo, pageSize);
  47 + return selectPage(page, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
  48 + .eqIfPresent(ProductBrowseHistoryDO::getUserId, userId)
  49 + .orderByAsc(ProductBrowseHistoryDO::getCreateTime));
  50 + }
  51 +
  52 +}
0 53 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java
... ... @@ -71,7 +71,7 @@ public interface ProductSpuMapper extends BaseMapperX&lt;ProductSpuDO&gt; {
71 71 query.eq(ProductSpuDO::getRecommendBenefit, true);
72 72 } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_BEST)) {
73 73 query.eq(ProductSpuDO::getRecommendBest, true);
74   - } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_NEW)) {
  74 + } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_NEW)) {
75 75 query.eq(ProductSpuDO::getRecommendNew, true);
76 76 } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_GOOD)) {
77 77 query.eq(ProductSpuDO::getRecommendGood, true);
... ... @@ -141,8 +141,8 @@ public interface ProductSpuMapper extends BaseMapperX&lt;ProductSpuDO&gt; {
141 141 /**
142 142 * 添加后台 Tab 选项的查询条件
143 143 *
144   - * @param tabType 标签类型
145   - * @param query 查询条件
  144 + * @param tabType 标签类型
  145 + * @param query 查询条件
146 146 */
147 147 static void appendTabQuery(Integer tabType, LambdaQueryWrapperX<ProductSpuDO> query) {
148 148 // 出售中商品
... ... @@ -169,4 +169,17 @@ public interface ProductSpuMapper extends BaseMapperX&lt;ProductSpuDO&gt; {
169 169 }
170 170 }
171 171  
  172 + /**
  173 + * 更新商品 SPU 浏览量
  174 + *
  175 + * @param id 商品 SPU 编号
  176 + * @param incrCount 增加的数量
  177 + */
  178 + default void updateBrowseCount(Long id, int incrCount) {
  179 + LambdaUpdateWrapper<ProductSpuDO> updateWrapper = new LambdaUpdateWrapper<ProductSpuDO>()
  180 + .setSql(" browse_count = browse_count +" + incrCount)
  181 + .eq(ProductSpuDO::getId, id);
  182 + update(null, updateWrapper);
  183 + }
  184 +
172 185 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/history/ProductBrowseHistoryService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.service.history;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
  5 +import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
  6 +
  7 +import java.util.Collection;
  8 +
  9 +/**
  10 + * 商品浏览记录 Service 接口
  11 + *
  12 + * @author owen
  13 + */
  14 +public interface ProductBrowseHistoryService {
  15 +
  16 + /**
  17 + * 创建商品浏览记录
  18 + *
  19 + * @param userId 用户编号
  20 + * @param spuId SPU 编号
  21 + * @return 编号
  22 + */
  23 + Long createBrowseHistory(Long userId, Long spuId);
  24 +
  25 + /**
  26 + * 隐藏用户商品浏览记录
  27 + *
  28 + * @param userId 用户编号
  29 + * @param spuId SPU 编号
  30 + */
  31 + void hideUserBrowseHistory(Long userId, Collection<Long> spuId);
  32 +
  33 + /**
  34 + * 获得商品浏览记录
  35 + *
  36 + * @param id 编号
  37 + * @return 商品浏览记录
  38 + */
  39 + ProductBrowseHistoryDO getBrowseHistory(Long id);
  40 +
  41 + /**
  42 + * 获取用户记录数量
  43 + *
  44 + * @param userId 用户编号
  45 + * @param userDeleted 用户是否删除
  46 + * @return 数量
  47 + */
  48 + Long getBrowseHistoryCount(Long userId, Boolean userDeleted);
  49 +
  50 + /**
  51 + * 获得商品浏览记录分页
  52 + *
  53 + * @param pageReqVO 分页查询
  54 + * @return 商品浏览记录分页
  55 + */
  56 + PageResult<ProductBrowseHistoryDO> getBrowseHistoryPage(ProductBrowseHistoryPageReqVO pageReqVO);
  57 +
  58 +}
0 59 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/history/ProductBrowseHistoryServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.service.history;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
  6 +import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
  7 +import cn.iocoder.yudao.module.product.dal.mysql.history.ProductBrowseHistoryMapper;
  8 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  9 +import jakarta.annotation.Resource;
  10 +import org.springframework.stereotype.Service;
  11 +import org.springframework.validation.annotation.Validated;
  12 +
  13 +import java.util.Collection;
  14 +
  15 +/**
  16 + * 商品浏览记录 Service 实现类
  17 + *
  18 + * @author owen
  19 + */
  20 +@Service
  21 +@Validated
  22 +public class ProductBrowseHistoryServiceImpl implements ProductBrowseHistoryService {
  23 + private static final int USER_STORE_MAXIMUM = 100;
  24 +
  25 + @Resource
  26 + private ProductBrowseHistoryMapper browseHistoryMapper;
  27 +
  28 + @Override
  29 + public Long createBrowseHistory(Long userId, Long spuId) {
  30 + // 用户未登录时不记录
  31 + if (userId == null) {
  32 + return null;
  33 + }
  34 +
  35 + // 情况一:同一个商品,只保留最新的一条记录
  36 + ProductBrowseHistoryDO historyDO = browseHistoryMapper.selectOne(ProductBrowseHistoryDO::getUserId, userId, ProductBrowseHistoryDO::getSpuId, spuId);
  37 + if (historyDO != null) {
  38 + browseHistoryMapper.deleteById(historyDO);
  39 + } else {
  40 + // 情况二:限制每个用户的浏览记录的条数(只查一条最早地记录、记录总数)
  41 + Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId, 1, 1);
  42 + if (pageResult.getTotal() >= USER_STORE_MAXIMUM) {
  43 + // 删除最早的一条
  44 + browseHistoryMapper.deleteById(CollUtil.getFirst(pageResult.getRecords()));
  45 + }
  46 + }
  47 +
  48 + // 插入
  49 + ProductBrowseHistoryDO browseHistory = new ProductBrowseHistoryDO()
  50 + .setUserId(userId)
  51 + .setSpuId(spuId);
  52 + browseHistoryMapper.insert(browseHistory);
  53 + // 返回
  54 + return browseHistory.getId();
  55 + }
  56 +
  57 + @Override
  58 + public void hideUserBrowseHistory(Long userId, Collection<Long> spuIds) {
  59 + browseHistoryMapper.updateUserDeletedByUserId(userId, spuIds, true);
  60 + }
  61 +
  62 + @Override
  63 + public ProductBrowseHistoryDO getBrowseHistory(Long id) {
  64 + return browseHistoryMapper.selectById(id);
  65 + }
  66 +
  67 + @Override
  68 + public Long getBrowseHistoryCount(Long userId, Boolean userDeleted) {
  69 + return browseHistoryMapper.selectCountByUserIdAndUserDeleted(userId, userDeleted);
  70 + }
  71 +
  72 + @Override
  73 + public PageResult<ProductBrowseHistoryDO> getBrowseHistoryPage(ProductBrowseHistoryPageReqVO pageReqVO) {
  74 + return browseHistoryMapper.selectPage(pageReqVO);
  75 + }
  76 +
  77 +}
0 78 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
... ... @@ -148,4 +148,12 @@ public interface ProductSpuService {
148 148 */
149 149 List<ProductSpuDO> validateSpuList(Collection<Long> ids);
150 150  
  151 + /**
  152 + * 更新商品 SPU 浏览量
  153 + *
  154 + * @param id 商品 SPU 编号
  155 + * @param incrCount 增加的数量
  156 + */
  157 + void updateBrowseCount(Long id, int incrCount);
  158 +
151 159 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
... ... @@ -157,6 +157,11 @@ public class ProductSpuServiceImpl implements ProductSpuService {
157 157 }
158 158  
159 159 @Override
  160 + public void updateBrowseCount(Long id, int incrCount) {
  161 + productSpuMapper.updateBrowseCount(id , incrCount);
  162 + }
  163 +
  164 + @Override
160 165 @Transactional(rollbackFor = Exception.class)
161 166 public void deleteSpu(Long id) {
162 167 // 校验存在
... ...
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 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 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 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 21 import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.GetMapping;
10 23 import org.springframework.web.bind.annotation.RequestMapping;
11 24 import org.springframework.web.bind.annotation.RestController;
12 25  
13   -import java.time.LocalDateTime;
  26 +import java.io.IOException;
14 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 36 @Tag(name = "管理后台 - 商品统计")
17 37 @RestController
18 38 @RequestMapping("/statistics/product")
19 39 @Validated
20   -@Slf4j
21 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 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 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 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 18 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
19 19 import io.swagger.v3.oas.annotations.Operation;
20 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 24 import lombok.extern.slf4j.Slf4j;
22 25 import org.springframework.security.access.prepost.PreAuthorize;
23 26 import org.springframework.validation.annotation.Validated;
... ... @@ -25,9 +28,6 @@ import org.springframework.web.bind.annotation.GetMapping;
25 28 import org.springframework.web.bind.annotation.RequestMapping;
26 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 31 import java.io.IOException;
32 32 import java.util.List;
33 33  
... ... @@ -67,13 +67,11 @@ public class TradeStatisticsController {
67 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 71 @Operation(summary = "获得交易状况统计")
73 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 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 12 @Data
13 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 16 @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
17 17 private LocalDate date;
18 18  
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/product/ProductSpuStatisticsDO.java renamed to yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/dataobject/product/ProductStatisticsDO.java
1   -package cn.iocoder.yudao.module.statistics.dal.mysql.product;
  1 +package cn.iocoder.yudao.module.statistics.dal.dataobject.product;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import com.baomidou.mybatisplus.annotation.KeySequence;
... ... @@ -6,69 +6,75 @@ import com.baomidou.mybatisplus.annotation.TableId;
6 6 import com.baomidou.mybatisplus.annotation.TableName;
7 7 import lombok.*;
8 8  
9   -import java.time.LocalDateTime;
  9 +import java.time.LocalDate;
10 10  
11 11 /**
12   - * 商品 SPU 统计 DO
  12 + * 商品统计 DO
13 13 *
14   - * 以天为维度,统计商品 SPU 的数据
15   - *
16   - * @author 芋道源码
  14 + * @author owen
17 15 */
18   -@TableName("product_spu_statistics")
19   -@KeySequence("product_spu_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  16 +@TableName("product_statistics")
  17 +@KeySequence("product_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
20 18 @Data
21 19 @EqualsAndHashCode(callSuper = true)
22 20 @ToString(callSuper = true)
23 21 @Builder
24 22 @NoArgsConstructor
25 23 @AllArgsConstructor
26   -public class ProductSpuStatisticsDO extends BaseDO {
  24 +public class ProductStatisticsDO extends BaseDO {
27 25  
28 26 /**
29 27 * 编号,主键自增
30 28 */
31 29 @TableId
32 30 private Long id;
33   -
34 31 /**
35   - * 商品 SPU 编号
36   - *
37   - * 关联 ProductSpuDO 的 id 字段
  32 + * 统计日期
38 33 */
39   - private Long spuId;
  34 + private LocalDate time;
40 35 /**
41   - * 统计日期
  36 + * 商品SPU编号
42 37 */
43   - private LocalDateTime time;
44   -
  38 + private Long spuId;
45 39 /**
46 40 * 浏览量
47 41 */
48 42 private Integer browseCount;
49 43 /**
50   - * 收藏量
  44 + * 访客量
  45 + */
  46 + private Integer browseUserCount;
  47 + /**
  48 + * 收藏数量
51 49 */
52 50 private Integer favoriteCount;
53   -
54 51 /**
55   - * 添加购物车次数
56   - *
57   - * 以商品被添加到购物车的 createTime 计算,后续多次添加,不会增加该值。
58   - * 直到该次被下单、或者被删除,后续再次被添加到购物车。
  52 + * 加购数量
  53 + */
  54 + private Integer cartCount;
  55 + /**
  56 + * 下单件数
  57 + */
  58 + private Integer orderCount;
  59 + /**
  60 + * 支付件数
  61 + */
  62 + private Integer orderPayCount;
  63 + /**
  64 + * 支付金额,单位:分
59 65 */
60   - private Integer addCartCount;
  66 + private Integer orderPayPrice;
61 67 /**
62   - * 创建订单商品数
  68 + * 退款件数
63 69 */
64   - private Integer createOrderCount;
  70 + private Integer afterSaleCount;
65 71 /**
66   - * 支付订单商品数
  72 + * 退款金额,单位:分
67 73 */
68   - private Integer payOrderCount;
  74 + private Integer afterSaleRefundPrice;
69 75 /**
70   - * 总支付金额,单位:分
  76 + * 访客支付转化率(百分比)
71 77 */
72   - private Integer payPrice;
  78 + private Integer browseConvertPercent;
73 79  
74 80 -}
  81 +}
75 82 \ 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.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
  8 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  9 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  10 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  11 +import com.baomidou.mybatisplus.core.metadata.IPage;
  12 +import org.apache.ibatis.annotations.Mapper;
  13 +import org.apache.ibatis.annotations.Param;
  14 +
  15 +import java.time.LocalDateTime;
  16 +import java.util.List;
  17 +
  18 +/**
  19 + * 商品统计 Mapper
  20 + *
  21 + * @author owen
  22 + */
  23 +@Mapper
  24 +public interface ProductStatisticsMapper extends BaseMapperX<ProductStatisticsDO> {
  25 +
  26 + default PageResult<ProductStatisticsDO> selectPageGroupBySpuId(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
  27 + return selectPage(pageParam, buildWrapper(reqVO)
  28 + .groupBy(ProductStatisticsDO::getSpuId)
  29 + .select(ProductStatisticsDO::getSpuId)
  30 + );
  31 + }
  32 +
  33 + default List<ProductStatisticsDO> selectListByTimeBetween(ProductStatisticsReqVO reqVO) {
  34 + return selectList(buildWrapper(reqVO)
  35 + .groupBy(ProductStatisticsDO::getTime)
  36 + .select(ProductStatisticsDO::getTime));
  37 + }
  38 +
  39 + default ProductStatisticsRespVO selectVoByTimeBetween(ProductStatisticsReqVO reqVO) {
  40 + return selectJoinOne(ProductStatisticsRespVO.class, buildWrapper(reqVO));
  41 + }
  42 +
  43 + /**
  44 + * 构建 LambdaWrapper
  45 + *
  46 + * @param reqVO 查询参数
  47 + * @return LambdaWrapper
  48 + */
  49 + private static MPJLambdaWrapperX<ProductStatisticsDO> buildWrapper(ProductStatisticsReqVO reqVO) {
  50 + return new MPJLambdaWrapperX<ProductStatisticsDO>()
  51 + .betweenIfPresent(ProductStatisticsDO::getTime, reqVO.getTimes())
  52 + .selectSum(ProductStatisticsDO::getBrowseCount)
  53 + .selectSum(ProductStatisticsDO::getBrowseUserCount)
  54 + .selectSum(ProductStatisticsDO::getFavoriteCount)
  55 + .selectSum(ProductStatisticsDO::getCartCount)
  56 + .selectSum(ProductStatisticsDO::getOrderCount)
  57 + .selectSum(ProductStatisticsDO::getOrderPayCount)
  58 + .selectSum(ProductStatisticsDO::getOrderPayPrice)
  59 + .selectSum(ProductStatisticsDO::getAfterSaleCount)
  60 + .selectSum(ProductStatisticsDO::getAfterSaleRefundPrice)
  61 + .selectAvg(ProductStatisticsDO::getBrowseConvertPercent);
  62 + }
  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 +
  80 +}
0 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 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 reqVO 查询条件
  23 + * @param pageParam 分页排序查询
  24 + * @return 商品统计分页
  25 + */
  26 + PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam);
  27 +
  28 + /**
  29 + * 获得商品状况统计分析
  30 + *
  31 + * @param reqVO 查询条件
  32 + * @return 统计数据对照
  33 + */
  34 + DataComparisonRespVO<ProductStatisticsRespVO> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO);
  35 +
  36 + /**
  37 + * 获得商品状况明细
  38 + *
  39 + * @param reqVO 查询条件
  40 + * @return 统计数据对照
  41 + */
  42 + List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO);
  43 +
  44 + /**
  45 + * 统计指定天数的商品数据
  46 + *
  47 + * @return 统计结果
  48 + */
  49 + String statisticsProduct(Integer days);
  50 +
  51 +}
0 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 0 → 100644
  1 +package cn.iocoder.yudao.module.statistics.service.product;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.DatePattern;
  5 +import cn.hutool.core.date.LocalDateTimeUtil;
  6 +import cn.hutool.core.util.ArrayUtil;
  7 +import cn.hutool.core.util.ObjUtil;
  8 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  9 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  10 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
  11 +import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
  12 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
  13 +import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
  14 +import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
  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;
  18 +import jakarta.annotation.Resource;
  19 +import org.springframework.stereotype.Service;
  20 +import org.springframework.util.StopWatch;
  21 +import org.springframework.validation.annotation.Validated;
  22 +
  23 +import java.time.Duration;
  24 +import java.time.LocalDateTime;
  25 +import java.util.List;
  26 +import java.util.stream.Collectors;
  27 +import java.util.stream.IntStream;
  28 +
  29 +
  30 +/**
  31 + * 商品统计 Service 实现类
  32 + *
  33 + * @author owen
  34 + */
  35 +@Service
  36 +@Validated
  37 +public class ProductStatisticsServiceImpl implements ProductStatisticsService {
  38 +
  39 + @Resource
  40 + private ProductStatisticsMapper productStatisticsMapper;
  41 +
  42 +
  43 + @Override
  44 + public PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
  45 + // 默认浏览量倒序
  46 + MyBatisUtils.buildDefaultSortingField(pageParam, ProductStatisticsDO::getBrowseCount);
  47 + return productStatisticsMapper.selectPageGroupBySpuId(reqVO, pageParam);
  48 + }
  49 +
  50 + @Override
  51 + public DataComparisonRespVO<ProductStatisticsRespVO> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO) {
  52 + LocalDateTime beginTime = ArrayUtil.get(reqVO.getTimes(), 0);
  53 + LocalDateTime endTime = ArrayUtil.get(reqVO.getTimes(), 1);
  54 +
  55 + // 统计数据
  56 + ProductStatisticsRespVO value = productStatisticsMapper.selectVoByTimeBetween(reqVO);
  57 + // 对照数据
  58 + LocalDateTime referenceBeginTime = beginTime.minus(Duration.between(beginTime, endTime));
  59 + ProductStatisticsReqVO referenceReqVO = new ProductStatisticsReqVO(new LocalDateTime[]{referenceBeginTime, beginTime});
  60 + ProductStatisticsRespVO reference = productStatisticsMapper.selectVoByTimeBetween(referenceReqVO);
  61 + return new DataComparisonRespVO<>(value, reference);
  62 + }
  63 +
  64 + @Override
  65 + public List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO) {
  66 + return productStatisticsMapper.selectListByTimeBetween(reqVO);
  67 + }
  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 +
  123 +}
0 124 \ 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 20 *
21 21 * @return 统计数据对照
22 22 */
23   - DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeTrendSummaryComparison(
  23 + DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeStatisticsAnalyse(
24 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 60 }
61 61  
62 62 @Override
63   - public DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeTrendSummaryComparison(LocalDateTime beginTime,
  63 + public DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeStatisticsAnalyse(LocalDateTime beginTime,
64 64 LocalDateTime endTime) {
65 65 // 统计数据
66 66 TradeTrendSummaryRespVO value = tradeStatisticsMapper.selectVoByTimeBetween(beginTime, endTime);
... ... @@ -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>
... ...