You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP/Laravel分组拆分月度营收性能过慢问题及优化需求

解决Laravel中限时许可证月度收入拆分性能问题及格式修正

问题背景

我们的客户购买限时许可证(如6个月期限),需要将许可证总金额按覆盖的月份拆分计算占比:先算总天数得出每日金额,再按每个月的实际天数计算对应金额。原MariaDB分组查询速度极慢,转而用PHP/Laravel处理,但现有代码因循环遍历每一天导致运行异常缓慢,且输出格式不符合需求。

原输入示例:

Product: product1
Payment date: 2024-01-01
Start date: 2024-01-01
End date: 2024-03-31
Total excluding tax: $29.99

期望拆分结果:

January 2024: 31 * $0.32956 = $10.216
February 2024: 29 * $0.32956 = $9.557
March 2024: 31 * $0.32956 = $10.216

期望输出JSON格式:

{
"product1":
    {
        "January 2024": 2255209.2525,
        "February 2024": 5252525.5336,
        "March 2024": 35363.3636
    },
"product2":
    {
        "December 2023": 309906.3532,
        "January 2024": 3059035.9092
    }
}

性能瓶颈分析

原代码的核心问题在splitRevenueByMonth方法:对每一条付款记录循环遍历每一天,当数据量达数千条且许可证期限较长时,循环次数呈指数级增长(比如一条6个月的记录要循环180次),直接导致性能急剧下降。

优化方案

1. 替换每日循环为月度批量计算

不再遍历每一天,而是直接计算付款周期覆盖的所有月份,统计每个月内的有效天数,再乘以每日金额得到月度金额,每个付款记录最多循环3-4次(跨月最多的情况),大幅减少计算量。

2. 修正输出格式

  • 将分组键从product_id替换为产品名称
  • 移除多余的total字段,匹配期望的JSON结构

优化后完整代码

public function revenueWaterfall(Request $request)
{
    // 获取产品列表并建立ID到名称的映射
    $products = $request->get('product_id') == 'all' 
        ? Product::all() 
        : Product::whereId($request->get('product_id'))->get();
    $productMap = $products->pluck('name', 'id')->toArray();

    $bookedPeriodTo = Carbon::parse($request->get('bookedPeriodTo'));
    $bookedPeriodFrom = Carbon::parse($request->get('bookedPeriodFrom'));

    $productId = $products->count() == 1 ? $products->first()->id : null;

    // 获取所有数据并按product_id分组
    $allData = Invoice::getAllForPeriod($bookedPeriodFrom, $bookedPeriodTo, $productId)
        ->groupBy('product_id');

    // 处理拆分并转换为产品名称作为键
    $splitValues = collect();
    foreach ($allData as $productId => $productPayments) {
        $productName = $productMap[$productId] ?? "product_{$productId}";
        $splitValues->put($productName, $this->splitRevenueByMonth($productPayments));
    }

    return response()->json($splitValues);
}

function splitRevenueByMonth($payments)
{
    $byMonth = [];

    foreach ($payments as $payment) {
        $start = Carbon::parse($payment->period_start)->startOfDay();
        $end = Carbon::parse($payment->period_end)->startOfDay();
        $daysInPeriod = $end->diffInDays($start);

        if ($daysInPeriod < 1) {
            throw new \Exception("Invalid period detected for payment ID: {$payment->id}");
        }

        $amountPerDay = $payment->total_excluding_tax / $daysInPeriod;
        $currentMonth = clone $start;

        // 循环处理每个覆盖的月份
        while ($currentMonth->lessThanOrEqualTo($end)) {
            // 获取当前月份的最后一天
            $monthEnd = $currentMonth->copy()->endOfMonth();
            // 计算当前月份内的有效天数:取当前月最后一天和周期结束日的较小值,减去周期开始日(或当月第一天)
            $periodStartInMonth = $currentMonth->greaterThan($start) ? $currentMonth : $start;
            $periodEndInMonth = $monthEnd->lessThan($end) ? $monthEnd : $end;
            $daysInMonth = $periodEndInMonth->diffInDays($periodStartInMonth) + 1; // 包含首尾两天

            $monthKey = $currentMonth->format('F Y'); // 输出"January 2024"格式
            $byMonth[$monthKey] = ($byMonth[$monthKey] ?? 0) + ($amountPerDay * $daysInMonth);

            // 跳到下一个月
            $currentMonth->addMonth()->startOfMonth();
        }
    }

    // 对月份按时间排序(可选,提升可读性)
    ksort($byMonth);

    return $byMonth;
}

关键改动说明

  1. 产品映射:用pluck建立产品ID到名称的映射,确保输出用产品名称作为键
  2. 月度批量计算:
    • 遍历付款周期覆盖的每个月份,而非每一天
    • 计算每个月内的有效天数(处理跨月开始/结束的情况)
    • 直接计算月度金额,避免大量循环
  3. 格式修正:移除原代码中多余的total字段,严格匹配期望的JSON结构
  4. 错误信息优化:增加付款ID便于定位无效数据
  5. 可选排序:对月份键排序,让输出更直观

性能提升效果

优化后,计算复杂度从O(总天数)降至O(总记录数×平均跨月数),对于数千条记录的场景,性能提升可达数十倍甚至上百倍。

内容的提问来源于stack exchange,提问作者TravelingFox

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 03:10:59