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
This commit is contained in:
YunaiV 2024-01-06 21:52:10 +08:00
commit eb048532f7
43 changed files with 1398 additions and 255 deletions

View File

@ -0,0 +1,22 @@
CREATE TABLE product_browse_history
(
id bigint AUTO_INCREMENT COMMENT '记录编号'
PRIMARY KEY,
user_id bigint NOT NULL COMMENT '用户编号',
spu_id bigint NOT NULL COMMENT '商品 SPU 编号',
user_deleted bit DEFAULT b'0' NOT NULL COMMENT '用户是否删除',
creator varchar(64) DEFAULT '' NULL COMMENT '创建者',
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
updater varchar(64) DEFAULT '' NULL COMMENT '更新者',
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit DEFAULT b'0' NOT NULL COMMENT '是否删除',
tenant_id bigint DEFAULT 0 NOT NULL COMMENT '租户编号'
)
COMMENT '商品浏览记录表';
CREATE INDEX idx_spuId
ON product_browse_history (spu_id);
CREATE INDEX idx_userId
ON product_browse_history (user_id);

View File

@ -0,0 +1,34 @@
CREATE TABLE product_statistics
(
id bigint AUTO_INCREMENT COMMENT '编号,主键自增' PRIMARY KEY,
time date NOT NULL COMMENT '统计日期',
spu_id bigint NOT NULL COMMENT '商品SPU编号',
browse_count int DEFAULT 0 NOT NULL COMMENT '浏览量',
browse_user_count int DEFAULT 0 NOT NULL COMMENT '访客量',
favorite_count int DEFAULT 0 NOT NULL COMMENT '收藏数量',
cart_count int DEFAULT 0 NOT NULL COMMENT '加购数量',
order_count int DEFAULT 0 NOT NULL COMMENT '下单件数',
order_pay_count int DEFAULT 0 NOT NULL COMMENT '支付件数',
order_pay_price int DEFAULT 0 NOT NULL COMMENT '支付金额,单位:分',
after_sale_count int DEFAULT 0 NOT NULL COMMENT '退款件数',
after_sale_refund_price int DEFAULT 0 NOT NULL COMMENT '退款金额,单位:分',
browse_convert_percent int DEFAULT 0 NOT NULL COMMENT '访客支付转化率(百分比)',
creator varchar(64) DEFAULT '' NULL COMMENT '创建者',
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
updater varchar(64) DEFAULT '' NULL COMMENT '更新者',
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit DEFAULT b'0' NOT NULL COMMENT '是否删除',
tenant_id bigint DEFAULT 0 NOT NULL COMMENT '租户编号'
)
COMMENT '商品统计表';
CREATE INDEX idx_time
ON product_statistics (time);
CREATE INDEX idx_spu_id
ON product_statistics (spu_id);
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);
SELECT @parentId1 := LAST_INSERT_ID();
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);
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);

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
@Schema(description = "可排序的分页参数")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SortablePageParam extends PageParam {
@Schema(description = "排序字段")
private List<SortingField> sortingFields;
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import java.util.List;
import java.util.function.Consumer;
/**
* Bean 工具类
@ -28,10 +29,18 @@ public class BeanUtils {
}
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
return toBean(source, targetType, null);
}
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
if (source == null) {
return null;
}
return new PageResult<>(toBean(source.getList(), targetType), source.getTotal());
List<T> list = toBean(source.getList(), targetType);
if (peek != null) {
list.forEach(peek);
}
return new PageResult<>(list, source.getTotal());
}
}

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -27,7 +29,15 @@ import java.util.List;
*/
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, null, queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
// 特殊不分页直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<T> list = selectList(queryWrapper);
@ -35,7 +45,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
}
// MyBatis Plus 查询
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam);
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
selectPage(mpPage, queryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());

View File

@ -1,7 +1,12 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
@ -11,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Collection;
@ -85,4 +91,46 @@ public class MyBatisUtils {
return new Column(tableName + StringPool.DOT + column);
}
/**
* 构建排序字段默认倒序
*
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public static <T> SortingField buildSortingField(Func1<T, ?> func) {
return buildSortingField(func, SortingField.ORDER_DESC);
}
/**
* 构建排序字段
*
* @param func 排序字段的 Lambda 表达式
* @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
Object[] orderTypes = {SortingField.ORDER_ASC, SortingField.ORDER_DESC};
Assert.isTrue(ArrayUtil.contains(orderTypes, order), String.format("字段的排序类型只能是%s/%s", orderTypes));
String fieldName = LambdaUtil.getFieldName(func);
return new SortingField(fieldName, order);
}
/**
* 构建默认的排序字段
* 如果排序字段为空则设置排序字段否则忽略
*
* @param sortablePageParam 排序分页查询参数
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
*/
public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
sortablePageParam.setSortingFields(List.of(buildSortingField(func)));
}
}
}

View File

@ -5,7 +5,6 @@ import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
## 处理 BigDecimal 字段的引入
import java.util.*;
#foreach ($column in $columns)
#if (${column.javaType} == "BigDecimal")
import java.math.BigDecimal;

View File

@ -1,12 +1,14 @@
import request from '@/config/axios'
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
// ${table.classComment} VO
export interface ${simpleClassName}VO {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
// ${column.columnComment}
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}: number
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime")
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}: Date
#else
${column.javaField}: ${column.javaType.toLowerCase()}
@ -15,42 +17,44 @@ export interface ${simpleClassName}VO {
#end
}
// ${table.classComment} API
export const ${simpleClassName}Api = {
#if ( $table.templateType != 2 )
// 查询${table.classComment}分页
export const get${simpleClassName}Page = async (params) => {
get${simpleClassName}Page: async (params: any) => {
return await request.get({ url: `${baseURL}/page`, params })
}
},
#else
// 查询${table.classComment}列表
export const get${simpleClassName}List = async (params) => {
get${simpleClassName}List: async (params) => {
return await request.get({ url: `${baseURL}/list`, params })
}
},
#end
// 查询${table.classComment}详情
export const get${simpleClassName} = async (id: number) => {
get${simpleClassName}: async (id: number) => {
return await request.get({ url: `${baseURL}/get?id=` + id })
}
},
// 新增${table.classComment}
export const create${simpleClassName} = async (data: ${simpleClassName}VO) => {
create${simpleClassName}: async (data: ${simpleClassName}VO) => {
return await request.post({ url: `${baseURL}/create`, data })
}
},
// 修改${table.classComment}
export const update${simpleClassName} = async (data: ${simpleClassName}VO) => {
update${simpleClassName}: async (data: ${simpleClassName}VO) => {
return await request.put({ url: `${baseURL}/update`, data })
}
},
// 删除${table.classComment}
export const delete${simpleClassName} = async (id: number) => {
delete${simpleClassName}: async (id: number) => {
return await request.delete({ url: `${baseURL}/delete?id=` + id })
}
},
// 导出${table.classComment} Excel
export const export${simpleClassName} = async (params) => {
export${simpleClassName}: async (params) => {
return await request.download({ url: `${baseURL}/export-excel`, params })
}
},
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
@ -67,45 +71,46 @@ export const export${simpleClassName} = async (params) => {
#if ( $table.templateType == 11 )
// 获得${subTable.classComment}分页
export const get${subSimpleClassName}Page = async (params) => {
get${subSimpleClassName}Page: async (params) => {
return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params })
}
},
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
// 获得${subTable.classComment}列表
export const get${subSimpleClassName}ListBy${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
get${subSimpleClassName}ListBy${SubJoinColumnName}: async (${subJoinColumn.javaField}) => {
return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
}
},
#else
// 获得${subTable.classComment}
export const get${subSimpleClassName}By${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
get${subSimpleClassName}By${SubJoinColumnName}: async (${subJoinColumn.javaField}) => {
return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
}
},
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
// 新增${subTable.classComment}
export const create${subSimpleClassName} = async (data) => {
create${subSimpleClassName}: async (data) => {
return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data })
}
},
// 修改${subTable.classComment}
export const update${subSimpleClassName} = async (data) => {
update${subSimpleClassName}: async (data) => {
return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data })
}
},
// 删除${subTable.classComment}
export const delete${subSimpleClassName} = async (id: number) => {
delete${subSimpleClassName}: async (id: number) => {
return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id })
}
},
// 获得${subTable.classComment}
export const get${subSimpleClassName} = async (id: number) => {
get${subSimpleClassName}: async (id: number) => {
return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id })
},
#end
#end
}
#end
#end

View File

@ -114,7 +114,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗

View File

@ -265,7 +265,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
const props = defineProps<{
${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)

View File

@ -85,7 +85,7 @@
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
#if ($table.templateType == 11)
import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue'
#end

View File

@ -140,7 +140,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
## 特殊:树表专属逻辑
#if ( $table.templateType == 2 )
import { defaultProps, handleTree } from '@/utils/tree'
@ -152,6 +152,9 @@ import ${subSimpleClassName}Form from './components/${subSimpleClassName}Form.vu
#end
#end
/** ${table.classComment} 表单 */
defineOptions({ name: '${simpleClassName}Form' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗

View File

@ -240,7 +240,7 @@ import { dateFormatter } from '@/utils/formatTime'
import { handleTree } from '@/utils/tree'
#end
import download from '@/utils/download'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
import ${simpleClassName}Form from './${simpleClassName}Form.vue'
## 特殊:主子表专属逻辑
#if ( $table.templateType != 10 )
@ -249,16 +249,22 @@ import ${subSimpleClassName}List from './components/${subSimpleClassName}List.vu
#end
#end
/** ${table.classComment} 列表 */
defineOptions({ name: '${table.className}' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
// 消息弹窗
const message = useMessage()
// 国际化
const { t } = useI18n()
const loading = ref(true) // 列表的加载中
const list = ref([]) // 列表的数据
// 列表的加载中
const loading = ref(true)
// 列表的数据
const list = ref<${simpleClassName}VO[]>([])
## 特殊:树表专属逻辑(树不需要分页接口)
#if ( $table.templateType != 2 )
const total = ref(0) // 列表的总页数
// 列表的总页数
const total = ref(0)
#end
const queryParams = reactive({
## 特殊:树表专属逻辑(树不需要分页接口)
@ -277,8 +283,10 @@ const queryParams = reactive({
#end
#end
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
// 搜索的表单
const queryFormRef = ref()
// 导出的加载中
const exportLoading = ref(false)
/** 查询列表 */
const getList = async () => {

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.product.controller.admin.history;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryRespVO;
import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 商品浏览记录")
@RestController
@RequestMapping("/product/browse-history")
@Validated
public class ProductBrowseHistoryController {
@Resource
private ProductBrowseHistoryService browseHistoryService;
@GetMapping("/page")
@Operation(summary = "获得商品浏览记录分页")
@PreAuthorize("@ss.hasPermission('product:browse-history:query')")
public CommonResult<PageResult<ProductBrowseHistoryRespVO>> getBrowseHistoryPage(@Valid ProductBrowseHistoryPageReqVO pageReqVO) {
PageResult<ProductBrowseHistoryDO> pageResult = browseHistoryService.getBrowseHistoryPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ProductBrowseHistoryRespVO.class));
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.product.controller.admin.history.vo;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 商品浏览记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ProductBrowseHistoryPageReqVO extends SortablePageParam {
@Schema(description = "用户编号", example = "4314")
private Long userId;
@Schema(description = "用户是否删除", example = "false")
private Boolean userDeleted;
@Schema(description = "商品 SPU 编号", example = "42")
private Long spuId;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.product.controller.admin.history.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 商品浏览记录 Response VO")
@Data
@ExcelIgnoreUnannotated
public class ProductBrowseHistoryRespVO {
@Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26055")
@ExcelProperty("记录编号")
private Long id;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4314")
@ExcelProperty("用户编号")
private Long userId;
@Schema(description = "用户是否删除", example = "false")
private Boolean userDeleted;
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "42")
@ExcelProperty("商品 SPU 编号")
private Long spuId;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.product.controller.app.history;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryDeleteReqVO;
import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.controller.app.history.vo.AppProductBrowseHistoryRespVO;
import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 APP - 商品浏览记录")
@RestController
@RequestMapping("/product/browse-history")
public class AppProductBrowseHistoryController {
@Resource
private ProductBrowseHistoryService productBrowseHistoryService;
@Resource
private ProductSpuService productSpuService;
@DeleteMapping(value = "/delete")
@Operation(summary = "删除商品浏览记录")
@PreAuthenticated
public CommonResult<Boolean> deleteBrowseHistory(@RequestBody @Valid AppProductBrowseHistoryDeleteReqVO reqVO) {
productBrowseHistoryService.hideUserBrowseHistory(getLoginUserId(), reqVO.getSpuIds());
return success(Boolean.TRUE);
}
@DeleteMapping(value = "/clean")
@Operation(summary = "清空商品浏览记录")
@PreAuthenticated
public CommonResult<Boolean> cleanBrowseHistory() {
productBrowseHistoryService.hideUserBrowseHistory(getLoginUserId(), null);
return success(Boolean.TRUE);
}
@GetMapping(value = "/get-count")
@Operation(summary = "获得商品浏览记录数量")
@PreAuthenticated
public CommonResult<Long> getBrowseHistoryCount() {
return success(productBrowseHistoryService.getBrowseHistoryCount(getLoginUserId(), false));
}
@GetMapping(value = "/page")
@Operation(summary = "获得商品浏览记录分页")
@PreAuthenticated
public CommonResult<PageResult<AppProductBrowseHistoryRespVO>> getBrowseHistoryPage(AppProductBrowseHistoryPageReqVO reqVO) {
ProductBrowseHistoryPageReqVO pageReqVO = BeanUtils.toBean(reqVO, ProductBrowseHistoryPageReqVO.class);
pageReqVO.setUserId(getLoginUserId());
// 排除用户已删除的隐藏的
pageReqVO.setUserDeleted(false);
PageResult<ProductBrowseHistoryDO> pageResult = productBrowseHistoryService.getBrowseHistoryPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 得到商品 spu 信息
Set<Long> spuIds = convertSet(pageResult.getList(), ProductBrowseHistoryDO::getSpuId);
Map<Long, ProductSpuDO> spuMap = convertMap(productSpuService.getSpuList(spuIds), ProductSpuDO::getId);
// 转换 VO 结果
PageResult<AppProductBrowseHistoryRespVO> result = BeanUtils.toBean(pageResult, AppProductBrowseHistoryRespVO.class,
vo -> Optional.ofNullable(spuMap.get(vo.getSpuId())).ifPresent(spu -> {
vo.setSpuName(spu.getName());
vo.setPicUrl(spu.getPicUrl());
}));
return success(result);
}
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.product.controller.app.history.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 APP - 删除商品浏览记录的 Request VO")
@Data
public class AppProductBrowseHistoryDeleteReqVO {
@Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
@NotEmpty(message = "商品 SPU 编号数组不能为空")
private List<Long> spuIds;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.product.controller.app.history.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "用户 APP - 商品浏览记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppProductBrowseHistoryPageReqVO extends PageParam {
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.product.controller.app.history.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Schema(description = "用户 App - 商品浏览记录 Response VO")
@Data
public class AppProductBrowseHistoryRespVO {
@Schema(description = "编号", requiredMode = REQUIRED, example = "1")
private Long id;
@Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
private Long spuId;
// ========== 商品相关字段 ==========
@Schema(description = "商品 SPU 名称", example = "赵六")
private String spuName;
@Schema(description = "商品封面图", example = "https://domain/pic.png")
private String picUrl;
@Schema(description = "商品单价", example = "100")
private Integer price;
}

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
import io.swagger.v3.oas.annotations.Operation;
@ -48,6 +49,8 @@ public class AppProductSpuController {
private ProductSpuService productSpuService;
@Resource
private ProductSkuService productSkuService;
@Resource
private ProductBrowseHistoryService productBrowseHistoryService;
@Resource
private MemberLevelApi memberLevelApi;
@ -122,6 +125,11 @@ public class AppProductSpuController {
throw exception(SPU_NOT_ENABLE);
}
// 增加浏览量
productSpuService.updateBrowseCount(id, 1);
// 保存浏览记录
productBrowseHistoryService.createBrowseHistory(getLoginUserId(), id);
// 拼接返回
List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
AppProductSpuDetailRespVO detailVO = ProductSpuConvert.INSTANCE.convertForGetSpuDetail(spu, skus);

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.product.dal.dataobject.history;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 商品浏览记录 DO
*
* @author owen
*/
@TableName("product_browse_history")
@KeySequence("product_browse_history_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductBrowseHistoryDO extends BaseDO {
/**
* 记录编号
*/
@TableId
private Long id;
/**
* 商品 SPU 编号
*/
private Long spuId;
/**
* 用户编号
*/
private Long userId;
/**
* 用户是否删除
*/
private Boolean userDeleted;
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.product.dal.mysql.history;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
/**
* 商品浏览记录 Mapper
*
* @author owen
*/
@Mapper
public interface ProductBrowseHistoryMapper extends BaseMapperX<ProductBrowseHistoryDO> {
default PageResult<ProductBrowseHistoryDO> selectPage(ProductBrowseHistoryPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
.eqIfPresent(ProductBrowseHistoryDO::getUserId, reqVO.getUserId())
.eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, reqVO.getUserDeleted())
.eqIfPresent(ProductBrowseHistoryDO::getSpuId, reqVO.getSpuId())
.betweenIfPresent(ProductBrowseHistoryDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ProductBrowseHistoryDO::getId));
}
default void updateUserDeletedByUserId(Long userId, Collection<Long> spuIds, Boolean userDeleted) {
update(new LambdaUpdateWrapper<ProductBrowseHistoryDO>()
.eq(ProductBrowseHistoryDO::getUserId, userId)
.in(CollUtil.isNotEmpty(spuIds), ProductBrowseHistoryDO::getSpuId, spuIds)
.set(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
}
default Long selectCountByUserIdAndUserDeleted(Long userId, Boolean userDeleted) {
return selectCount(new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
.eq(ProductBrowseHistoryDO::getUserId, userId)
.eqIfPresent(ProductBrowseHistoryDO::getUserDeleted, userDeleted));
}
default Page<ProductBrowseHistoryDO> selectPageByUserIdOrderByCreateTimeAsc(Long userId, Integer pageNo, Integer pageSize) {
Page<ProductBrowseHistoryDO> page = Page.of(pageNo, pageSize);
return selectPage(page, new LambdaQueryWrapperX<ProductBrowseHistoryDO>()
.eqIfPresent(ProductBrowseHistoryDO::getUserId, userId)
.orderByAsc(ProductBrowseHistoryDO::getCreateTime));
}
}

View File

@ -169,4 +169,17 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
}
}
/**
* 更新商品 SPU 浏览量
*
* @param id 商品 SPU 编号
* @param incrCount 增加的数量
*/
default void updateBrowseCount(Long id, int incrCount) {
LambdaUpdateWrapper<ProductSpuDO> updateWrapper = new LambdaUpdateWrapper<ProductSpuDO>()
.setSql(" browse_count = browse_count +" + incrCount)
.eq(ProductSpuDO::getId, id);
update(null, updateWrapper);
}
}

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.product.service.history;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
import java.util.Collection;
/**
* 商品浏览记录 Service 接口
*
* @author owen
*/
public interface ProductBrowseHistoryService {
/**
* 创建商品浏览记录
*
* @param userId 用户编号
* @param spuId SPU 编号
* @return 编号
*/
Long createBrowseHistory(Long userId, Long spuId);
/**
* 隐藏用户商品浏览记录
*
* @param userId 用户编号
* @param spuId SPU 编号
*/
void hideUserBrowseHistory(Long userId, Collection<Long> spuId);
/**
* 获得商品浏览记录
*
* @param id 编号
* @return 商品浏览记录
*/
ProductBrowseHistoryDO getBrowseHistory(Long id);
/**
* 获取用户记录数量
*
* @param userId 用户编号
* @param userDeleted 用户是否删除
* @return 数量
*/
Long getBrowseHistoryCount(Long userId, Boolean userDeleted);
/**
* 获得商品浏览记录分页
*
* @param pageReqVO 分页查询
* @return 商品浏览记录分页
*/
PageResult<ProductBrowseHistoryDO> getBrowseHistoryPage(ProductBrowseHistoryPageReqVO pageReqVO);
}

View File

@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.product.service.history;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
import cn.iocoder.yudao.module.product.dal.mysql.history.ProductBrowseHistoryMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
/**
* 商品浏览记录 Service 实现类
*
* @author owen
*/
@Service
@Validated
public class ProductBrowseHistoryServiceImpl implements ProductBrowseHistoryService {
private static final int USER_STORE_MAXIMUM = 100;
@Resource
private ProductBrowseHistoryMapper browseHistoryMapper;
@Override
public Long createBrowseHistory(Long userId, Long spuId) {
// 用户未登录时不记录
if (userId == null) {
return null;
}
// 情况一同一个商品只保留最新的一条记录
ProductBrowseHistoryDO historyDO = browseHistoryMapper.selectOne(ProductBrowseHistoryDO::getUserId, userId, ProductBrowseHistoryDO::getSpuId, spuId);
if (historyDO != null) {
browseHistoryMapper.deleteById(historyDO);
} else {
// 情况二限制每个用户的浏览记录的条数只查一条最早地记录记录总数
Page<ProductBrowseHistoryDO> pageResult = browseHistoryMapper.selectPageByUserIdOrderByCreateTimeAsc(userId, 1, 1);
if (pageResult.getTotal() >= USER_STORE_MAXIMUM) {
// 删除最早的一条
browseHistoryMapper.deleteById(CollUtil.getFirst(pageResult.getRecords()));
}
}
// 插入
ProductBrowseHistoryDO browseHistory = new ProductBrowseHistoryDO()
.setUserId(userId)
.setSpuId(spuId);
browseHistoryMapper.insert(browseHistory);
// 返回
return browseHistory.getId();
}
@Override
public void hideUserBrowseHistory(Long userId, Collection<Long> spuIds) {
browseHistoryMapper.updateUserDeletedByUserId(userId, spuIds, true);
}
@Override
public ProductBrowseHistoryDO getBrowseHistory(Long id) {
return browseHistoryMapper.selectById(id);
}
@Override
public Long getBrowseHistoryCount(Long userId, Boolean userDeleted) {
return browseHistoryMapper.selectCountByUserIdAndUserDeleted(userId, userDeleted);
}
@Override
public PageResult<ProductBrowseHistoryDO> getBrowseHistoryPage(ProductBrowseHistoryPageReqVO pageReqVO) {
return browseHistoryMapper.selectPage(pageReqVO);
}
}

View File

@ -148,4 +148,12 @@ public interface ProductSpuService {
*/
List<ProductSpuDO> validateSpuList(Collection<Long> ids);
/**
* 更新商品 SPU 浏览量
*
* @param id 商品 SPU 编号
* @param incrCount 增加的数量
*/
void updateBrowseCount(Long id, int incrCount);
}

View File

@ -156,6 +156,11 @@ public class ProductSpuServiceImpl implements ProductSpuService {
return list;
}
@Override
public void updateBrowseCount(Long id, int incrCount) {
productSpuMapper.updateBrowseCount(id , incrCount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteSpu(Long id) {

View File

@ -2,40 +2,90 @@ package cn.iocoder.yudao.module.statistics.controller.admin.product;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductSpuStatisticsDO;
import cn.iocoder.yudao.module.statistics.dal.mysql.product.ProductStatisticsDO;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - 商品统计")
@RestController
@RequestMapping("/statistics/product")
@Validated
@Slf4j
public class ProductStatisticsController {
// TODO @麦子返回 ProductStatisticsComparisonResp 里面有两个字段一个是选择的时间范围的合计结果一个是对比的时间范围的合计结果
// 例如说选择时间范围是 2023-10-01 ~ 2023-10-02那么对比就是 2023-09-30再倒推 2
public CommonResult<Object> getProductStatisticsComparison() {
return null;
@Resource
private ProductStatisticsService productStatisticsService;
@Resource
private ProductSpuApi productSpuApi;
@GetMapping("/analyse")
@Operation(summary = "获得商品统计分析")
@PreAuthorize("@ss.hasPermission('statistics:product:query')")
public CommonResult<DataComparisonRespVO<ProductStatisticsRespVO>> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO) {
return success(productStatisticsService.getProductStatisticsAnalyse(reqVO));
}
// TODO @麦子查询指定时间范围内的商品统计数据DO 到时需要改成 VO
public CommonResult<List<ProductStatisticsDO>> getProductStatisticsList(
LocalDateTime[] times) {
return null;
@GetMapping("/list")
@Operation(summary = "获得商品统计明细(日期维度)")
@PreAuthorize("@ss.hasPermission('statistics:product:query')")
public CommonResult<List<ProductStatisticsRespVO>> getProductStatisticsList(ProductStatisticsReqVO reqVO) {
List<ProductStatisticsDO> list = productStatisticsService.getProductStatisticsList(reqVO);
return success(BeanUtils.toBean(list, ProductStatisticsRespVO.class));
}
// TODO @麦子查询指定时间范围内的商品 SPU 统计数据DO 到时需要改成 VO
// 入参是分页参数 + 时间范围 + 排序字段
public CommonResult<PageResult<ProductSpuStatisticsDO>> getProductSpuStatisticsPage() {
return null;
@GetMapping("/export-excel")
@Operation(summary = "导出获得商品统计明细 Excel日期维度")
@PreAuthorize("@ss.hasPermission('statistics:product:export')")
public void exportProductStatisticsExcel(ProductStatisticsReqVO reqVO, HttpServletResponse response) throws IOException {
List<ProductStatisticsDO> list = productStatisticsService.getProductStatisticsList(reqVO);
// 导出 Excel
List<ProductStatisticsRespVO> voList = BeanUtils.toBean(list, ProductStatisticsRespVO.class);
ExcelUtils.write(response, "商品状况.xls", "数据", ProductStatisticsRespVO.class, voList);
}
@GetMapping("/rank-page")
@Operation(summary = "获得商品统计排行榜分页(商品维度)")
@PreAuthorize("@ss.hasPermission('statistics:product:query')")
public CommonResult<PageResult<ProductStatisticsRespVO>> getProductStatisticsRankPage(@Valid ProductStatisticsReqVO reqVO,
@Valid SortablePageParam pageParam) {
PageResult<ProductStatisticsDO> pageResult = productStatisticsService.getProductStatisticsRankPage(reqVO, pageParam);
// 处理商品信息
Set<Long> spuIds = convertSet(pageResult.getList(), ProductStatisticsDO::getSpuId);
Map<Long, ProductSpuRespDTO> spuMap = convertMap(productSpuApi.getSpuList(spuIds), ProductSpuRespDTO::getId);
// 拼接返回
return success(BeanUtils.toBean(pageResult, ProductStatisticsRespVO.class,
// 拼接商品信息
item -> Optional.ofNullable(spuMap.get(item.getSpuId())).ifPresent(spu -> {
item.setName(spu.getName());
item.setPicUrl(spu.getPicUrl());
})));
}
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 商品统计分析 Request VO")
@Data
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public class ProductStatisticsReqVO {
@Schema(description = "统计时间范围", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] times;
}

View File

@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - 商品统计 Response VO")
@Data
@ExcelIgnoreUnannotated
public class ProductStatisticsRespVO {
@Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "12393")
private Long id;
@Schema(description = "统计日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-12-16")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
@ExcelProperty("统计日期")
private LocalDate time;
@Schema(description = "商品SPU编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15114")
@ExcelProperty("商品SPU编号")
private Long spuId;
//region 商品信息
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商品名称")
@ExcelProperty("商品名称")
private String name;
@Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "15114")
@ExcelProperty("商品封面图")
private String picUrl;
//endregion
@Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "17505")
@ExcelProperty("浏览量")
private Integer browseCount;
@Schema(description = "访客量", requiredMode = Schema.RequiredMode.REQUIRED, example = "11814")
@ExcelProperty("访客量")
private Integer browseUserCount;
@Schema(description = "收藏数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20950")
@ExcelProperty("收藏数量")
private Integer favoriteCount;
@Schema(description = "加购数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "28493")
@ExcelProperty("加购数量")
private Integer cartCount;
@Schema(description = "下单件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18966")
@ExcelProperty("下单件数")
private Integer orderCount;
@Schema(description = "支付件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "15142")
@ExcelProperty("支付件数")
private Integer orderPayCount;
@Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "11595")
@ExcelProperty("支付金额,单位:分")
private Integer orderPayPrice;
@Schema(description = "退款件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2591")
@ExcelProperty("退款件数")
private Integer afterSaleCount;
@Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "21709")
@ExcelProperty("退款金额,单位:分")
private Integer afterSaleRefundPrice;
@Schema(description = "访客支付转化率(百分比)", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
private Integer browseConvertPercent;
}

View File

@ -18,6 +18,9 @@ import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -25,9 +28,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.io.IOException;
import java.util.List;
@ -67,13 +67,11 @@ public class TradeStatisticsController {
return success(TradeStatisticsConvert.INSTANCE.convert(yesterdayData, beforeYesterdayData, monthData, lastMonthData));
}
// TODO @疯狂晚点再改和讨论等首页的接口出来这个要不还是叫 analyse对比选中的时间段和上一个时间段类似 MemberStatisticsController getMemberAnalyse
@GetMapping("/trend/summary")
@GetMapping("/analyse")
@Operation(summary = "获得交易状况统计")
@PreAuthorize("@ss.hasPermission('statistics:trade:query')")
public CommonResult<DataComparisonRespVO<TradeTrendSummaryRespVO>> getTradeTrendSummaryComparison(
TradeTrendReqVO reqVO) {
return success(tradeStatisticsService.getTradeTrendSummaryComparison(ArrayUtil.get(reqVO.getTimes(), 0),
public CommonResult<DataComparisonRespVO<TradeTrendSummaryRespVO>> getTradeStatisticsAnalyse(TradeTrendReqVO reqVO) {
return success(tradeStatisticsService.getTradeStatisticsAnalyse(ArrayUtil.get(reqVO.getTimes(), 0),
ArrayUtil.get(reqVO.getTimes(), 1)));
}

View File

@ -12,7 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
@Data
public class TradeTrendSummaryRespVO {
@Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-12-16")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate date;

View File

@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.statistics.dal.dataobject.product;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
* 商品统计 DO
*
* @author owen
*/
@TableName("product_statistics")
@KeySequence("product_statistics_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductStatisticsDO extends BaseDO {
/**
* 编号主键自增
*/
@TableId
private Long id;
/**
* 统计日期
*/
private LocalDate time;
/**
* 商品SPU编号
*/
private Long spuId;
/**
* 浏览量
*/
private Integer browseCount;
/**
* 访客量
*/
private Integer browseUserCount;
/**
* 收藏数量
*/
private Integer favoriteCount;
/**
* 加购数量
*/
private Integer cartCount;
/**
* 下单件数
*/
private Integer orderCount;
/**
* 支付件数
*/
private Integer orderPayCount;
/**
* 支付金额单位
*/
private Integer orderPayPrice;
/**
* 退款件数
*/
private Integer afterSaleCount;
/**
* 退款金额单位
*/
private Integer afterSaleRefundPrice;
/**
* 访客支付转化率百分比
*/
private Integer browseConvertPercent;
}

View File

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

View File

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

View File

@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.statistics.dal.mysql.product;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
/**
* 商品统计 Mapper
*
* @author owen
*/
@Mapper
public interface ProductStatisticsMapper extends BaseMapperX<ProductStatisticsDO> {
default PageResult<ProductStatisticsDO> selectPageGroupBySpuId(ProductStatisticsReqVO reqVO, SortablePageParam pageParam) {
return selectPage(pageParam, buildWrapper(reqVO)
.groupBy(ProductStatisticsDO::getSpuId)
.select(ProductStatisticsDO::getSpuId)
);
}
default List<ProductStatisticsDO> selectListByTimeBetween(ProductStatisticsReqVO reqVO) {
return selectList(buildWrapper(reqVO)
.groupBy(ProductStatisticsDO::getTime)
.select(ProductStatisticsDO::getTime));
}
default ProductStatisticsRespVO selectVoByTimeBetween(ProductStatisticsReqVO reqVO) {
return selectJoinOne(ProductStatisticsRespVO.class, buildWrapper(reqVO));
}
/**
* 构建 LambdaWrapper
*
* @param reqVO 查询参数
* @return LambdaWrapper
*/
private static MPJLambdaWrapperX<ProductStatisticsDO> buildWrapper(ProductStatisticsReqVO reqVO) {
return new MPJLambdaWrapperX<ProductStatisticsDO>()
.betweenIfPresent(ProductStatisticsDO::getTime, reqVO.getTimes())
.selectSum(ProductStatisticsDO::getBrowseCount)
.selectSum(ProductStatisticsDO::getBrowseUserCount)
.selectSum(ProductStatisticsDO::getFavoriteCount)
.selectSum(ProductStatisticsDO::getCartCount)
.selectSum(ProductStatisticsDO::getOrderCount)
.selectSum(ProductStatisticsDO::getOrderPayCount)
.selectSum(ProductStatisticsDO::getOrderPayPrice)
.selectSum(ProductStatisticsDO::getAfterSaleCount)
.selectSum(ProductStatisticsDO::getAfterSaleRefundPrice)
.selectAvg(ProductStatisticsDO::getBrowseConvertPercent);
}
/**
* 根据时间范围统计商品信息
*
* @param page 分页参数
* @param beginTime 起始时间
* @param endTime 截止时间
* @return 统计
*/
IPage<ProductStatisticsDO> selectStatisticsResultPageByTimeBetween(IPage<ProductStatisticsDO> page,
@Param("beginTime") LocalDateTime beginTime,
@Param("endTime") LocalDateTime endTime);
default Long selectCountByTimeBetween(LocalDateTime beginTime, LocalDateTime endTime) {
return selectCount(new LambdaQueryWrapperX<ProductStatisticsDO>().between(ProductStatisticsDO::getTime, beginTime, endTime));
}
}

View File

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

View File

@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.statistics.service.product;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.module.statistics.controller.admin.common.vo.DataComparisonRespVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsReqVO;
import cn.iocoder.yudao.module.statistics.controller.admin.product.vo.ProductStatisticsRespVO;
import cn.iocoder.yudao.module.statistics.dal.dataobject.product.ProductStatisticsDO;
import java.util.List;
/**
* 商品统计 Service 接口
*
* @author owen
*/
public interface ProductStatisticsService {
/**
* 获得商品统计排行榜分页
*
* @param reqVO 查询条件
* @param pageParam 分页排序查询
* @return 商品统计分页
*/
PageResult<ProductStatisticsDO> getProductStatisticsRankPage(ProductStatisticsReqVO reqVO, SortablePageParam pageParam);
/**
* 获得商品状况统计分析
*
* @param reqVO 查询条件
* @return 统计数据对照
*/
DataComparisonRespVO<ProductStatisticsRespVO> getProductStatisticsAnalyse(ProductStatisticsReqVO reqVO);
/**
* 获得商品状况明细
*
* @param reqVO 查询条件
* @return 统计数据对照
*/
List<ProductStatisticsDO> getProductStatisticsList(ProductStatisticsReqVO reqVO);
/**
* 统计指定天数的商品数据
*
* @return 统计结果
*/
String statisticsProduct(Integer days);
}

View File

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

View File

@ -20,7 +20,7 @@ public interface TradeStatisticsService {
*
* @return 统计数据对照
*/
DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeTrendSummaryComparison(
DataComparisonRespVO<TradeTrendSummaryRespVO> getTradeStatisticsAnalyse(
LocalDateTime beginTime, LocalDateTime endTime);
/**

View File

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

View File

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