CRMEB/backend/app/services/statistic/OrderStatisticServices.php
吴昊天 dba17dd449 fix(统计): 修正订单统计退款口径与来源类型数据
- 订单量/销售额包含后续退款订单,退款订单数与金额单独统计
- 退款金额/退款订单数按 refund_time 归属,兼容 pay_status=2/3
- 订单来源、订单类型与趋势统一支付口径
2026-10-23 14:14:59 +08:00

293 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\statistic;
use crmeb\basic\BaseService;
use app\model\order\OrderCenter;
use app\model\user\User;
use app\dao\order\OrderCenterDao;
use crmeb\exceptions\AdminException;
class OrderStatisticServices extends BaseService
{
/**
* 订单统计基础
* @param $where
* @return array
*/
public function getBasic($where)
{
$time = explode('-', $where['time']);
if (count($time) != 2) throw new AdminException('请选择时间');
/** @var OrderCenterDao $orderDao */
$orderDao = app()->make(OrderCenterDao::class);
// 支付时间区间(timeKey 绑定 eb_order_center.pay_time)
$timeKey = $this->buildTimeKey($time);
$paidStatuses = [
OrderCenter::PAY_PAID,
OrderCenter::PAY_PARTIAL_REFUND,
OrderCenter::PAY_FULL_REFUND,
];
// 已支付订单:支付金额/订单量(含后续退款订单,退款在退款卡片单独统计)
$wherePaySum = ['is_del' => 0, 'timeKey' => $timeKey];
$wherePaySum[] = ['pay_status', 'in', $paidStatuses];
$data['pay_price'] = $orderDao->getWhereSumField($wherePaySum, 'pay_price');
$data['pay_count'] = $orderDao->getDb()->where('is_del', 0)->whereIn('pay_status', $paidStatuses)
->whereBetweenTime('pay_time', $timeKey['start_time'], $timeKey['end_time'])->count();
// 已退款订单:退款金额/退款订单数(refund_status=已退款 累计 refund_price,按退款时间归属)
$whereRefund = ['refund_status' => OrderCenter::REFUND_SUCCESS, 'is_del' => 0];
$data['refund_price'] = $orderDao->getWhereSumField(
$whereRefund + ['timeKey' => $timeKey],
'refund_price',
'refund_time'
);
$data['refund_count'] = $orderDao->getDb()->where($whereRefund)
->whereBetweenTime('refund_time', $timeKey['start_time'], $timeKey['end_time'])->count();
return $data;
}
/**
* 订单趋势
* @param $where
* @return array
*/
public function getTrend($where)
{
$time = explode('-', $where['time']);
if (count($time) != 2) throw new AdminException('请选择时间');
$dayCount = bcadd(bcdiv(bcsub(strtotime($time[1]), strtotime($time[0])), '86400'), '1');
$data = [];
if ($dayCount == 1) {
$data = $this->trend($time, 0);
} elseif ($dayCount > 1 && $dayCount <= 31) {
$data = $this->trend($time, 1);
} elseif ($dayCount > 31 && $dayCount <= 92) {
$data = $this->trend($time, 3);
} elseif ($dayCount > 92) {
$data = $this->trend($time, 30);
}
return $data;
}
/**
* 订单趋势
* @param $time
* @param $num
* @param false $excel
* @return array
*/
public function trend($time, $num)
{
/** @var OrderCenterDao $storeOrder */
$storeOrder = app()->make(OrderCenterDao::class);
if ($num == 0) {
$xAxis = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
$timeType = '%H';
} elseif ($num != 0) {
$dt_start = strtotime($time[0]);
$dt_end = strtotime($time[1]);
while ($dt_start <= $dt_end) {
if ($num == 30) {
$xAxis[] = date('Y-m', $dt_start);
$dt_start = strtotime("+1 month", $dt_start);
$timeType = '%Y-%m';
} else {
$xAxis[] = date('m-d', $dt_start);
$dt_start = strtotime("+$num day", $dt_start);
$timeType = '%m-%d';
}
}
}
// 支付金额/订单量趋势(按 pay_time 分组,含后续退款订单)
$paidStatuses = [
OrderCenter::PAY_PAID,
OrderCenter::PAY_PARTIAL_REFUND,
OrderCenter::PAY_FULL_REFUND,
];
$wherePay = ['is_del' => 0];
$pay_price = $this->groupTrend($storeOrder, $wherePay, 'SUM(pay_price)', $time, $timeType, 'pay_time', $paidStatuses);
$pay_count = $this->groupTrend($storeOrder, $wherePay, 'COUNT(*)', $time, $timeType, 'pay_time', $paidStatuses);
// 退款金额/退款单数趋势(按 refund_time 分组)
$whereRefund = ['refund_status' => OrderCenter::REFUND_SUCCESS, 'is_del' => 0];
$refund_price = $this->groupTrend($storeOrder, $whereRefund, 'SUM(refund_price)', $time, $timeType, 'refund_time');
$refund_count = $this->groupTrend($storeOrder, $whereRefund, 'COUNT(*)', $time, $timeType, 'refund_time');
$data = $series = [];
foreach ($xAxis as $item) {
$data['订单金额'][] = isset($pay_price[$item]) ? floatval($pay_price[$item]) : 0;
$data['订单量'][] = isset($pay_count[$item]) ? floatval($pay_count[$item]) : 0;
$data['退款金额'][] = isset($refund_price[$item]) ? floatval($refund_price[$item]) : 0;
$data['退款订单量'][] = isset($refund_count[$item]) ? floatval($refund_count[$item]) : 0;
}
foreach ($data as $key => $item) {
$series[] = [
'name' => $key,
'data' => $item,
'type' => 'line',
];
}
return compact('xAxis', 'series');
}
/**
* 订单来源
* @param $where
* @return array
*/
public function getChannel($where)
{
$time = explode('-', $where['time']);
if (count($time) != 2) throw new AdminException('请选择时间');
/** @var OrderCenterDao $orderService */
$orderService = app()->make(OrderCenterDao::class);
// 渠道统计:中台订单无渠道字段,按下单用户的注册渠道(eb_user.user_type)统计已支付订单分布
$channelMap = ['wechat', 'routine', 'h5', 'app'];
$bing_xdata = ['公众号', '小程序', 'H5', 'APP'];
$color = ['#64a1f4', '#3edeb5', '#70869f', '#fc7d6a'];
$userTable = app()->make(User::class)->getName();
$bing_data = [];
foreach ($bing_xdata as $key => $item) {
$bing_data[] = [
'name' => $item,
'value' => $orderService->getDb()->alias('o')
->join($userTable . ' u', 'o.uid = u.uid')
->whereIn('o.pay_status', [
OrderCenter::PAY_PAID,
OrderCenter::PAY_PARTIAL_REFUND,
OrderCenter::PAY_FULL_REFUND,
])
->where('o.is_del', 0)
// 结束日补至当天 23:59:59(whereBetweenTime 对纯日期字符串不扩展当天,join 别名场景用 whereBetween 时间戳更稳)
->whereBetween('o.pay_time', [strtotime($time[0]), strtotime($time[1] . ' 23:59:59')])
->where('u.user_type', $channelMap[$key])
->count(),
'itemStyle' => ['color' => $color[$key]]
];
}
$list = [];
$count = array_sum(array_column($bing_data, 'value'));
foreach ($bing_data as $key => $item) {
$list[] = [
'name' => $item['name'],
'value' => $item['value'],
'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0,
];
}
array_multisort(array_column($list, 'value'), SORT_DESC, $list);
return compact('bing_xdata', 'bing_data', 'list');
}
/**
* 订单类型
* @param $where
* @return array
*/
public function getType($where)
{
$time = explode('-', $where['time']);
if (count($time) != 2) throw new AdminException('请选择时间');
/** @var OrderCenterDao $orderService */
$orderService = app()->make(OrderCenterDao::class);
// 活动类型映射(中台 activity_type 为字符串,普通订单为空或 normal,与旧数字枚举不兼容)
// integral(积分兑换)为 trade 活动类型,独立成片统计;无独立开关,始终展示(无数据时显示 0,与空活动表现一致)
$activityMap = [['', 'normal'], 'seckill', 'bargain', 'combination', 'advance', 'integral'];
$bing_xdata = ['普通订单', '秒杀订单', '砍价订单', '拼团订单', '预售订单', '积分订单'];
$model_checkbox = sys_config('model_checkbox', ['seckill', 'bargain', 'combination']);
$color = ['#64a1f4', '#3edeb5', '#70869f', '#ffc653', '#fc7d6a', '#9b59b6'];
$timeKey = $this->buildTimeKey($time);
$bing_data = [];
foreach ($bing_xdata as $key => $item) {
if (!in_array('seckill', $model_checkbox) && $key == 1) continue;
if (!in_array('bargain', $model_checkbox) && $key == 2) continue;
if (!in_array('combination', $model_checkbox) && $key == 3) continue;
$whereType = ['is_del' => 0, 'timeKey' => $timeKey];
$whereType[] = ['pay_status', 'in', [
OrderCenter::PAY_PAID,
OrderCenter::PAY_PARTIAL_REFUND,
OrderCenter::PAY_FULL_REFUND,
]];
// 普通订单匹配空或 normal,活动订单按活动类型字符串精确匹配
$whereType[] = $key == 0
? ['activity_type', 'IN', $activityMap[$key]]
: ['activity_type', '=', $activityMap[$key]];
$bing_data[] = [
'name' => $item,
'value' => $orderService->getWhereSumField($whereType, 'pay_price'),
'itemStyle' => ['color' => $color[$key]]
];
}
$list = [];
$count = array_sum(array_column($bing_data, 'value'));
foreach ($bing_data as $key => $item) {
$list[] = [
'name' => $item['name'],
'value' => $item['value'],
'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0,
];
}
array_multisort(array_column($list, 'value'), SORT_DESC, $list);
return compact('bing_xdata', 'bing_data', 'list');
}
/**
* 构建统计时间区间(与订单中台 timeKey 约定一致,绑定 pay_time)
* @param array $time 起止时间数组 [开始, 结束]
* @return array
*/
private function buildTimeKey(array $time): array
{
$days = bcadd(bcdiv(bcsub(strtotime($time[1]), strtotime($time[0])), '86400'), '1');
return [
'start_time' => $time[0],
// 结束日补至当天 23:59:59:whereBetweenTime 对纯日期字符串不扩展当天,否则丢失结束日数据
'end_time' => date('Y-m-d 23:59:59', strtotime($time[1])),
'days' => (int)$days,
];
}
/**
* 按支付时间分组统计趋势数据(对应旧 getProductTrend 语义,基于 eb_order_center)
* @param OrderCenterDao $orderDao
* @param array $where 查询条件
* @param string $field 聚合表达式(SUM(x)/COUNT(*))
* @param array $time 起止时间数组
* @param string $timeType 分组时间格式(%H/%m-%d/%Y-%m)
* @param string $timeField 时间过滤/分组字段(支付用 pay_time,退款用 refund_time)
* @param array $payStatuses 额外支付状态过滤
* @return array days=>num 映射
*/
private function groupTrend(
OrderCenterDao $orderDao,
array $where,
string $field,
array $time,
string $timeType,
string $timeField = 'pay_time',
array $payStatuses = []
): array {
$query = $orderDao->getDb()->where($where);
if ($payStatuses) {
$query->whereIn('pay_status', $payStatuses);
}
// 结束日补至当天 23:59:59,保证结束日数据纳入统计
return array_column($query
->whereBetweenTime($timeField, $time[0], $time[1] . ' 23:59:59')
->field("FROM_UNIXTIME($timeField,'$timeType') as days,$field as num")
->group('days')->select()->toArray(), 'num', 'days');
}
}