在Laravel项目结合MongoDB的使用场景中,按小时分组并动态聚合各分类价格总和是常见的数据统计需求,比如统计每小时不同商品分类的订单总金额,这类需求可以通过MongoDB的聚合管道配合Laravel的MongoDB查询扩展来实现。

核心实现思路
实现该需求主要分为三个步骤:首先提取数据中的小时维度,然后按照小时和分类字段进行分组,最后动态计算不同分类的价格总和。MongoDB的聚合管道提供了$dateToString操作符用于提取时间中的小时部分,$group操作符用于分组聚合,结合Laravel的MongoDB查询构造器可以很方便地组装这些聚合操作。
基础数据准备
假设我们的MongoDB集合名为orders,每条订单数据包含以下字段:
created_at:订单创建时间,类型为MongoDB的ISODatecategory:订单所属分类,值为字符串,比如电子产品、服装等price:订单价格,类型为数值
Laravel中聚合管道实现
首先需要在Laravel项目中安装MongoDB的扩展包,确保可以正常操作MongoDB数据库。接下来编写聚合查询代码:
<?php
namespace AppServices;
use MongoDBClient;
use CarbonCarbon;
class OrderStatService
{
protected $mongoClient;
public function __construct()
{
// 初始化MongoDB客户端,这里使用默认的本地连接配置
$this->mongoClient = new Client('mongodb://127.0.0.1:27017');
}
/**
* 按小时分组动态聚合各分类价格总和
* @param string $startTime 统计开始时间
* @param string $endTime 统计结束时间
* @param array $categories 需要统计的分类列表,为空则统计所有分类
* @return array
*/
public function hourlyCategoryPriceSum($startTime, $endTime, $categories = [])
{
$database = $this->mongoClient->selectDatabase('test_db');
$collection = $database->selectCollection('orders');
// 组装聚合管道
$pipeline = [];
// 第一步:时间范围过滤
$matchCondition = [
'created_at' => [
'$gte' => new MongoDBBSONUTCDateTime(Carbon::parse($startTime)->timestamp * 1000),
'$lte' => new MongoDBBSONUTCDateTime(Carbon::parse($endTime)->timestamp * 1000)
]
];
// 如果指定了分类,添加分类过滤条件
if (!empty($categories)) {
$matchCondition['category'] = ['$in' => $categories];
}
$pipeline[] = ['$match' => $matchCondition];
// 第二步:提取小时维度,添加小时字段
$pipeline[] = [
'$addFields' => [
'hour' => [
'$dateToString' => [
'format' => '%H',
'date' => '$created_at',
'timezone' => 'Asia/Shanghai' // 设置为本地时区
]
]
]
];
// 第三步:按小时和分类分组,聚合价格总和
$pipeline[] = [
'$group' => [
'_id' => [
'hour' => '$hour',
'category' => '$category'
],
'total_price' => ['$sum' => '$price'],
'order_count' => ['$sum' => 1]
]
];
// 第四步:格式化输出结果
$pipeline[] = [
'$project' => [
'_id' => 0,
'hour' => '$_id.hour',
'category' => '$_id.category',
'total_price' => 1,
'order_count' => 1
]
];
// 执行聚合查询
$result = $collection->aggregate($pipeline)->toArray();
return $result;
}
}
代码解析
上述代码的核心逻辑如下:
$match阶段:先过滤出指定时间范围和分类的订单数据,减少后续聚合的数据量,提升查询效率。$addFields阶段:使用$dateToString操作符从created_at字段中提取小时部分,生成新的hour字段,格式为两位小时数,比如08、14等。$group阶段:按照hour和category两个字段组合分组,使用$sum操作符计算每个分组的价格总和,同时统计每个分组的订单数量。$project阶段:调整输出结果的字段格式,去掉默认的_id字段,将分组条件字段提取为独立字段,方便后续使用。
动态分类处理说明
上述代码中的$categories参数支持动态传入需要统计的分类列表,如果传入空数组则会统计所有存在的分类。如果业务需要动态适配不同的分类字段,比如有时候按category统计,有时候按sub_category统计,只需要将分组字段改为动态参数即可:
// 动态分组字段示例,将分类字段作为参数传入
public function hourlyDynamicGroupPriceSum($startTime, $endTime, $groupField = 'category')
{
$database = $this->mongoClient->selectDatabase('test_db');
$collection = $database->selectCollection('orders');
$pipeline = [];
// 时间过滤
$pipeline[] = [
'$match' => [
'created_at' => [
'$gte' => new MongoDBBSONUTCDateTime(Carbon::parse($startTime)->timestamp * 1000),
'$lte' => new MongoDBBSONUTCDateTime(Carbon::parse($endTime)->timestamp * 1000)
]
]
];
// 提取小时字段
$pipeline[] = [
'$addFields' => [
'hour' => [
'$dateToString' => [
'format' => '%H',
'date' => '$created_at',
'timezone' => 'Asia/Shanghai'
]
]
]
];
// 动态分组,使用传入的字段名作为分组条件
$pipeline[] = [
'$group' => [
'_id' => [
'hour' => '$hour',
'group_field' => '$' . $groupField // 动态引用字段
],
'total_price' => ['$sum' => '$price']
]
];
// 格式化输出
$pipeline[] = [
'$project' => [
'_id' => 0,
'hour' => '$_id.hour',
'group_value' => '$_id.group_field',
'total_price' => 1
]
];
return $collection->aggregate($pipeline)->toArray();
}
结果示例
执行上述查询后,返回的结果格式如下:
[
{
"hour": "08",
"category": "电子产品",
"total_price": 15680.5,
"order_count": 12
},
{
"hour": "08",
"category": "服装",
"total_price": 8920.0,
"order_count": 8
},
{
"hour": "09",
"category": "电子产品",
"total_price": 23450.0,
"order_count": 18
}
]
如果需要将结果按小时维度整理,把同一小时的不同分类数据合并到一个条目中,可以在获取到聚合结果后做二次处理:
// 整理结果为按小时分组的格式
public function formatResultByHour($rawResult)
{
$formatted = [];
foreach ($rawResult as $item) {
$hour = $item['hour'];
if (!isset($formatted[$hour])) {
$formatted[$hour] = [
'hour' => $hour,
'category_sum' => []
];
}
$formatted[$hour]['category_sum'][$item['category']] = [
'total_price' => $item['total_price'],
'order_count' => $item['order_count']
];
}
return array_values($formatted);
}
注意事项
- MongoDB的时间字段如果是字符串类型,需要先使用
$toDate操作符转换为ISODate类型再进行时间提取操作。 - 时区设置需要根据项目实际使用的时区调整,避免小时提取出现偏差。
- 如果数据量较大,建议在
created_at和category字段上创建复合索引,提升聚合查询的性能。 - 动态传入分组字段时,需要做好参数校验,避免传入不存在的字段导致查询报错。