first commit

This commit is contained in:
dev
2026-02-22 12:12:02 +08:00
commit b236048a24
1467 changed files with 160403 additions and 0 deletions
@@ -0,0 +1,109 @@
# 统计中心模块 (ruoyi-mall-statistics)
## 模块简介
`ruoyi-mall-statistics` 是商城系统的统计中心模块,提供交易分析、会员分析等数据统计功能。
## 功能特性
### 1. 交易分析 ✅
- ✅ 订单统计(总订单数、已支付订单数)
- ✅ 销售额统计(总销售额、已支付金额、退款金额)
- ✅ 今日统计(今日订单数、今日销售额)
- ✅ 商品销售排行(按销售金额排序)
- ✅ 交易趋势分析(按日期统计)
### 2. 会员分析 ⏳
- ⏳ 会员增长统计(需会员模块支持)
- ⏳ 会员消费统计(需会员模块支持)
- ⏳ 会员活跃度分析(需会员模块支持)
### 3. 商品分析 ⏳
- ⏳ 商品销售统计(可通过交易统计的商品排行实现)
- ⏳ 商品浏览统计(需商品模块支持浏览统计)
- ⏳ 商品转化率分析(需商品模块支持)
### 4. 营销分析 ⏳
- ⏳ 活动效果统计(需营销模块支持)
- ⏳ 优惠券使用统计(需优惠券模块支持)
- ⏳ 营销ROI分析(需营销模块支持)
## 数据库表
统计模块主要基于现有业务表进行数据分析和统计,可根据需要创建统计汇总表。
## 模块结构
```
org.dromara.mall.statistics
├── controller
│ ├── admin // 管理端API
│ └── app // 用户端API(可选)
├── domain
│ ├── bo // 业务对象
│ └── vo // 视图对象
├── mapper // MyBatis Mapper
├── service // 业务逻辑层
│ └── impl
├── enums // 枚举类
└── config // 配置类
```
## 依赖模块
- `ruoyi-mall-product` - 商品中心模块
- `ruoyi-mall-trade` - 交易中心模块
- `ruoyi-common-*` - 通用工具模块
## API接口
### 管理端接口
#### 统计数据 (`/mall/statistics`)
- `GET /mall/statistics/transaction` - 获取交易统计数据
- 支持按日期范围查询(startDate, endDate
- 支持设置商品销售排行数量限制(limit,默认10)
- 返回数据包括:总订单数、总销售额、已支付订单数、已支付金额、退款金额、今日统计、趋势数据、商品销售排行
- `GET /mall/statistics/member` - 获取会员统计数据
- 支持按日期范围查询(startDate, endDate
- 返回数据包括:总会员数、今日新增会员数、活跃会员数、增长趋势、消费统计
- ⚠️ 注意:会员统计功能需要会员模块支持,当前返回基础结构
## 待实现功能
- [x] 交易统计分析 ✅
- [x] 订单统计 ✅
- [x] 销售额统计 ✅
- [x] 商品销售排行 ✅
- [x] 交易趋势分析 ✅
- [ ] 会员统计分析(需会员模块支持)
- [ ] 商品统计分析(部分功能可通过交易统计实现)
- [ ] 营销效果分析(需营销模块支持)
- [ ] 数据报表导出
## 使用说明
### 交易统计
交易统计基于 `mall_order``mall_order_item` 表进行数据聚合分析:
1. **订单统计**:统计指定时间范围内的订单总数和总金额
2. **已支付统计**:统计已支付(pay_status=1)的订单数和金额
3. **退款统计**:统计已完成的退款金额(基于 `mall_after_sale` 表,status=5
4. **今日统计**:当未指定日期范围时,自动计算今日的订单和销售额
5. **趋势数据**:按日期分组统计订单数和销售额
6. **商品销售排行**:按销售金额排序,支持限制返回数量
### 查询参数
- `startDate`: 开始日期(格式:yyyy-MM-dd),可选
- `endDate`: 结束日期(格式:yyyy-MM-dd),可选
- `limit`: 商品销售排行数量限制,默认10,可选
**注意**:如果不指定日期范围,统计的是全部数据;如果指定了日期范围,则只统计该范围内的数据。
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mall</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-mall-statistics</artifactId>
<description>
ruoyi-mall-statistics 统计中心模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- 商品中心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mall-product</artifactId>
<version>${revision}</version>
</dependency>
<!-- 交易中心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mall-trade</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,23 @@
package org.dromara.mall.statistics.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 统计中心自动配置类
* 主要用于扫描统计中心模块的组件
*
* @author pengles
*/
@Slf4j
@Configuration
@ComponentScan(basePackages = "org.dromara.mall.statistics")
@ConditionalOnProperty(prefix = "ruoyi.mall.statistics", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MallStatisticsAutoConfiguration {
public MallStatisticsAutoConfiguration() {
log.info("统计中心模块已启用");
}
}
@@ -0,0 +1,82 @@
package org.dromara.mall.statistics.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mall.statistics.domain.bo.StatisticsQueryBo;
import org.dromara.mall.statistics.domain.vo.MemberStatisticsVo;
import org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo;
import org.dromara.mall.statistics.service.MallStatisticsService;
import org.springframework.format.annotation.DateTimeFormat;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* 统计管理控制器
*
* @author pengles
*/
@Tag(name = "管理端-统计数据", description = "统计数据查询接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/mall/statistics")
public class AdminMallStatisticsController extends BaseController {
private final MallStatisticsService statisticsService;
/**
* 获取交易统计
*/
@Operation(summary = "获取交易统计", description = "获取交易统计数据,包括订单统计、销售额统计、商品销售排行等")
@SaCheckPermission("mall:statistics:query")
@GetMapping("/transaction")
public R<TransactionStatisticsVo> getTransactionStatistics(
@Parameter(description = "开始日期")
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@Parameter(description = "结束日期")
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@Parameter(description = "商品销售排行限制数量")
@RequestParam(required = false, defaultValue = "10") Integer limit) {
StatisticsQueryBo queryBo = new StatisticsQueryBo();
queryBo.setStartDate(startDate);
queryBo.setEndDate(endDate);
queryBo.setLimit(limit);
TransactionStatisticsVo result = statisticsService.getTransactionStatistics(queryBo);
return R.ok(result);
}
/**
* 获取会员统计
*/
@Operation(summary = "获取会员统计", description = "获取会员统计数据,包括会员增长、消费统计等")
@SaCheckPermission("mall:statistics:query")
@GetMapping("/member")
public R<MemberStatisticsVo> getMemberStatistics(
@Parameter(description = "开始日期")
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@Parameter(description = "结束日期")
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
StatisticsQueryBo queryBo = new StatisticsQueryBo();
queryBo.setStartDate(startDate);
queryBo.setEndDate(endDate);
MemberStatisticsVo result = statisticsService.getMemberStatistics(queryBo);
return R.ok(result);
}
}
@@ -0,0 +1,50 @@
package org.dromara.mall.statistics.domain.bo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 统计查询业务对象
*
* @author pengles
*/
@Data
public class StatisticsQueryBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 开始日期
*/
private Date startDate;
/**
* 结束日期
*/
private Date endDate;
/**
* 商品ID(可选)
*/
private Long productId;
/**
* SKU ID(可选)
*/
private Long skuId;
/**
* 用户ID(可选)
*/
private Long userId;
/**
* 限制数量(用于排行)
*/
private Integer limit;
}
@@ -0,0 +1,88 @@
package org.dromara.mall.statistics.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 会员统计视图对象
*
* @author pengles
*/
@Data
public class MemberStatisticsVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 总会员数
*/
private Long totalMemberCount;
/**
* 今日新增会员数
*/
private Long todayNewMemberCount;
/**
* 活跃会员数(有订单的会员)
*/
private Long activeMemberCount;
/**
* 会员增长趋势
*/
private List<DailyMemberGrowthVo> growthTrend;
/**
* 会员消费统计
*/
private List<MemberConsumptionVo> consumptionStats;
/**
* 每日会员增长
*/
@Data
public static class DailyMemberGrowthVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日期
*/
private String date;
/**
* 新增会员数
*/
private Long newMemberCount;
}
/**
* 会员消费统计
*/
@Data
public static class MemberConsumptionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 订单数
*/
private Integer orderCount;
/**
* 消费金额(分)
*/
private Long consumptionAmount;
}
}
@@ -0,0 +1,129 @@
package org.dromara.mall.statistics.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 交易统计视图对象
*
* @author pengles
*/
@Data
public class TransactionStatisticsVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 总订单数
*/
private Long totalOrderCount;
/**
* 总销售额(分)
*/
private Long totalSalesAmount;
/**
* 已支付订单数
*/
private Long paidOrderCount;
/**
* 已支付金额(分)
*/
private Long paidAmount;
/**
* 退款金额(分)
*/
private Long refundAmount;
/**
* 今日订单数
*/
private Long todayOrderCount;
/**
* 今日销售额(分)
*/
private Long todaySalesAmount;
/**
* 趋势数据(按日期)
*/
private List<DailyStatisticsVo> trendData;
/**
* 商品销售排行
*/
private List<ProductSalesRankVo> productSalesRank;
/**
* 每日统计
*/
@Data
public static class DailyStatisticsVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日期
*/
private String date;
/**
* 订单数
*/
private Long orderCount;
/**
* 销售额(分)
*/
private Long salesAmount;
}
/**
* 商品销售排行
*/
@Data
public static class ProductSalesRankVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 商品ID
*/
private Long productId;
/**
* 商品名称
*/
private String productName;
/**
* SKU ID
*/
private Long skuId;
/**
* SKU名称
*/
private String skuName;
/**
* 销售数量
*/
private Integer salesQuantity;
/**
* 销售金额(分)
*/
private Long salesAmount;
}
}
@@ -0,0 +1,50 @@
package org.dromara.mall.statistics.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 统计Mapper接口
*
* @author pengles
*/
@Mapper
public interface MallStatisticsMapper {
/**
* 统计订单总数和总金额
*/
Map<String, Object> countOrderStatistics(@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 统计已支付订单数和金额
*/
Map<String, Object> countPaidOrderStatistics(@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 统计退款金额
*/
Long sumRefundAmount(@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 统计每日订单数据
*/
List<TransactionStatisticsVo.DailyStatisticsVo> countDailyOrderStatistics(@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 统计商品销售排行
*/
List<TransactionStatisticsVo.ProductSalesRankVo> queryProductSalesRank(@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("limit") Integer limit);
}
@@ -0,0 +1,30 @@
package org.dromara.mall.statistics.service;
import org.dromara.mall.statistics.domain.bo.StatisticsQueryBo;
import org.dromara.mall.statistics.domain.vo.MemberStatisticsVo;
import org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo;
/**
* 统计服务接口
*
* @author pengles
*/
public interface MallStatisticsService {
/**
* 获取交易统计
*
* @param queryBo 查询条件
* @return 交易统计结果
*/
TransactionStatisticsVo getTransactionStatistics(StatisticsQueryBo queryBo);
/**
* 获取会员统计
*
* @param queryBo 查询条件
* @return 会员统计结果
*/
MemberStatisticsVo getMemberStatistics(StatisticsQueryBo queryBo);
}
@@ -0,0 +1,128 @@
package org.dromara.mall.statistics.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.mall.statistics.domain.bo.StatisticsQueryBo;
import org.dromara.mall.statistics.domain.vo.MemberStatisticsVo;
import org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo;
import org.dromara.mall.statistics.mapper.MallStatisticsMapper;
import org.dromara.mall.statistics.service.MallStatisticsService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 统计服务实现类
*
* @author pengles
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class MallStatisticsServiceImpl implements MallStatisticsService {
private final MallStatisticsMapper statisticsMapper;
/**
* 获取交易统计
*/
@Override
public TransactionStatisticsVo getTransactionStatistics(StatisticsQueryBo queryBo) {
Date startDate = queryBo != null ? queryBo.getStartDate() : null;
Date endDate = queryBo != null ? queryBo.getEndDate() : null;
Integer limit = queryBo != null && queryBo.getLimit() != null ? queryBo.getLimit() : 10;
TransactionStatisticsVo vo = new TransactionStatisticsVo();
// 统计总订单数和总金额
Map<String, Object> orderStats = statisticsMapper.countOrderStatistics(startDate, endDate);
if (orderStats != null) {
vo.setTotalOrderCount(getLongValue(orderStats, "totalOrderCount"));
vo.setTotalSalesAmount(getLongValue(orderStats, "totalSalesAmount"));
}
// 统计已支付订单数和金额
Map<String, Object> paidStats = statisticsMapper.countPaidOrderStatistics(startDate, endDate);
if (paidStats != null) {
vo.setPaidOrderCount(getLongValue(paidStats, "paidOrderCount"));
vo.setPaidAmount(getLongValue(paidStats, "paidAmount"));
}
// 统计退款金额
Long refundAmount = statisticsMapper.sumRefundAmount(startDate, endDate);
vo.setRefundAmount(refundAmount != null ? refundAmount : 0L);
// 今日统计(如果没有指定日期范围,计算今日)
if (startDate == null && endDate == null) {
Date todayStart = DateUtil.beginOfDay(new Date());
Date todayEnd = DateUtil.endOfDay(new Date());
Map<String, Object> todayOrderStats = statisticsMapper.countOrderStatistics(todayStart, todayEnd);
if (todayOrderStats != null) {
vo.setTodayOrderCount(getLongValue(todayOrderStats, "totalOrderCount"));
vo.setTodaySalesAmount(getLongValue(todayOrderStats, "totalSalesAmount"));
} else {
vo.setTodayOrderCount(0L);
vo.setTodaySalesAmount(0L);
}
} else {
// 如果指定了日期范围,今日统计为0或null
vo.setTodayOrderCount(0L);
vo.setTodaySalesAmount(0L);
}
// 趋势数据
List<TransactionStatisticsVo.DailyStatisticsVo> trendData = statisticsMapper.countDailyOrderStatistics(startDate, endDate);
vo.setTrendData(trendData != null ? trendData : CollUtil.newArrayList());
// 商品销售排行
List<TransactionStatisticsVo.ProductSalesRankVo> productRank = statisticsMapper.queryProductSalesRank(startDate, endDate, limit);
vo.setProductSalesRank(productRank != null ? productRank : CollUtil.newArrayList());
return vo;
}
/**
* 获取会员统计
*/
@Override
public MemberStatisticsVo getMemberStatistics(StatisticsQueryBo queryBo) {
// TODO: 实现会员统计功能
// 由于会员模块可能不在mall模块中,这里先返回基础结构
MemberStatisticsVo vo = new MemberStatisticsVo();
vo.setTotalMemberCount(0L);
vo.setTodayNewMemberCount(0L);
vo.setActiveMemberCount(0L);
vo.setGrowthTrend(CollUtil.newArrayList());
vo.setConsumptionStats(CollUtil.newArrayList());
return vo;
}
/**
* 从Map中获取Long值
*/
private Long getLongValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return 0L;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (Exception e) {
log.warn("无法转换值 {} 为Long类型: {}", value, e.getMessage());
return 0L;
}
}
}
@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dromara.mall.statistics.config.MallStatisticsAutoConfiguration
@@ -0,0 +1,101 @@
<?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="org.dromara.mall.statistics.mapper.MallStatisticsMapper">
<!-- 统计订单总数和总金额 -->
<select id="countOrderStatistics" resultType="java.util.HashMap">
SELECT
COUNT(*) as totalOrderCount,
IFNULL(SUM(total_amount), 0) as totalSalesAmount
FROM mall_order
<where>
<if test="startDate != null">
AND create_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND create_time &lt;= #{endDate}
</if>
</where>
</select>
<!-- 统计已支付订单数和金额 -->
<select id="countPaidOrderStatistics" resultType="java.util.HashMap">
SELECT
COUNT(*) as paidOrderCount,
IFNULL(SUM(pay_amount), 0) as paidAmount
FROM mall_order
<where>
pay_status = 1
<if test="startDate != null">
AND create_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND create_time &lt;= #{endDate}
</if>
</where>
</select>
<!-- 统计退款金额 -->
<select id="sumRefundAmount" resultType="java.lang.Long">
SELECT IFNULL(SUM(refund_amount), 0)
FROM mall_after_sale
<where>
status = 5
<if test="startDate != null">
AND refund_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND refund_time &lt;= #{endDate}
</if>
</where>
</select>
<!-- 统计每日订单数据 -->
<select id="countDailyOrderStatistics" resultType="org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo$DailyStatisticsVo">
SELECT
DATE_FORMAT(create_time, '%Y-%m-%d') as date,
COUNT(*) as orderCount,
IFNULL(SUM(total_amount), 0) as salesAmount
FROM mall_order
<where>
<if test="startDate != null">
AND create_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND create_time &lt;= #{endDate}
</if>
</where>
GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
ORDER BY date ASC
</select>
<!-- 统计商品销售排行 -->
<select id="queryProductSalesRank" resultType="org.dromara.mall.statistics.domain.vo.TransactionStatisticsVo$ProductSalesRankVo">
SELECT
oi.product_id as productId,
oi.product_name as productName,
oi.sku_id as skuId,
oi.sku_name as skuName,
SUM(oi.quantity) as salesQuantity,
IFNULL(SUM(oi.total_amount), 0) as salesAmount
FROM mall_order_item oi
INNER JOIN mall_order o ON oi.order_id = o.id
<where>
o.pay_status = 1
<if test="startDate != null">
AND o.create_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND o.create_time &lt;= #{endDate}
</if>
</where>
GROUP BY oi.product_id, oi.product_name, oi.sku_id, oi.sku_name
ORDER BY salesAmount DESC
<if test="limit != null and limit > 0">
LIMIT #{limit}
</if>
</select>
</mapper>