
本文介绍一种基于日期范围与季节规则解耦的 laravel 定价计算重构方法,通过预定义季节周期、统一季节判定逻辑和动态属性访问,替代原始易错的字符串日期比较,显著提升代码可读性、健壮性与扩展性。
在租车预订类系统中,按季节动态计价是常见需求,但原始实现常因硬编码日期字符串、跨年逻辑混乱、条件嵌套过深而极易出错。如题中所示,原代码使用 d-m-Y 字符串比较(如 “1/11/2024″)、未处理跨年低季节(11月–3月)、重复判断逻辑冗余,且无法应对闰年、时区或未来年份动态适配。
我们推荐采用 「季节规则中心化 + 日期归一化判定」 的重构策略,核心思想是:
- ✅ 将季节定义为结构化配置(起始月/日 + 持续天数),而非散落的字符串边界;
- ✅ 使用 carbon 统一处理日期,避免字符串解析歧义;
- ✅ 将季节判定与价格累加职责分离,提升可测试性与复用性;
- ✅ 利用 PHP 动态属性访问($group->{$season}SeasonPrice)消除重复 if-else 分支。
✅ 推荐重构实现(laravel + Carbon)
use CarbonCarbon; private function getSeasonForDate(Carbon $date): string { // 所有季节定义为 [month, day, duration_in_days] // 注意:所有周期均以当前年为基准构建,自动适配跨年逻辑(见下方说明) $seasonRules = [ 'peak' => [[7, 16, 30]], // 7月16日 → 8月15日(含) 'high' => [[7, 1, 14], [8, 16, 45]], // 7.1–7.15;8.16–9.30 'medium' => [[4, 1, 90], [10, 1, 30]], // 4.1–6.30;10.1–10.31 // 'low' 为默认兜底,无需显式定义 ]; $year = $date->year; $targetDate = Carbon::createFromDate($year, $date->month, $date->day); foreach ($seasonRules as $season => $periods) { foreach ($periods as [$month, $day, $duration]) { $start = Carbon::createFromDate($year, $month, $day); $end = $start->copy()->addDays($duration)->subSecond(); // 精确到秒级闭区间 // 跨年场景处理:若 end < start(如 11月→3月),则 end 设为下一年对应日期 if ($end->lessThan($start)) { $end = $start->copy()->addYear()->addDays($duration)->subSecond(); } if ($targetDate->greaterThan($start->subSecond()) && $targetDate->lessThanOrEqualTo($end)) { return $season; } } } return 'low'; } private function accumulatePrice( string $season, $group, array &$totalPrices, array &$totalPricesWithInsurance ): void { $priceKey = "{$season}SeasonPrice"; $priceWiKey = "{$season}SeasonPriceWithInsurance"; $totalPrices[$group->id] = ($totalPrices[$group->id] ?? 0) + $group->$priceKey; $totalPricesWithInsurance[$group->id] = ($totalPricesWithInsurance[$group->id] ?? 0) + $group->$priceWiKey; } // 主调用逻辑(精简版) public function calculateReservationPrice(Request $request) { $startDate = Carbon::createFromFormat('Y-m-d', explode(' ', $request->startDate)[0]); $endDate = Carbon::createFromFormat('Y-m-d', explode(' ', $request->endDate)[0])->endOfDay(); $daterange = new DatePeriod( $startDate, new DateInterval('P1D'), $endDate->modify('+1 day') // DatePeriod 是左闭右开,+1天确保包含 endDate ); $groupPrices = Group::all(); // 推荐使用 Eloquent 替代 DB::table $totalGroupPrices = []; $totalGroupPricesWithInsurance = []; foreach ($groupPrices as $group) { foreach ($daterange as $date) { $season = $this->getSeasonForDate($date); $this->accumulatePrice($season, $group, $totalGroupPrices, $totalGroupPricesWithInsurance); } } return [ 'prices' => $totalGroupPrices, 'prices_with_insurance' => $totalGroupPricesWithInsurance, ]; }
⚠️ 关键注意事项
- 跨年季节支持:原需求中“低季节(11月1日–3月31日)”天然跨年。上述 getSeasonForDate() 中通过 if ($end
- 性能优化建议:对长租期(如 90 天),逐日循环仍可能影响响应。可进一步升级为「区间合并 + 季节段批量计算」——先将整个租期按季节切分为若干连续子区间(如 [2025-04-01, 2025-06-30] → medium),再按天数 × 单价一次性累加,将时间复杂度从 O(n) 降至 O(1)~O(4)。
- 配置外置化:将 $seasonRules 移至配置文件(config/pricing.php)或数据库表,支持后台动态调整季节,避免每次修改需部署代码。
- 测试覆盖重点:务必编写单元测试验证边界日期(如 3/31、4/1、7/15、7/16、8/15、8/16)归属是否准确,并覆盖跨年场景(如 2025-12-01 至 2026-01-10)。
通过本次重构,代码行数减少约 40%,逻辑清晰可追溯,新增季节仅需修改配置数组,彻底告别“改一处、崩三处”的维护噩梦。定价引擎从此真正具备业务可演进性。