
本文介绍一种基于日期范围与季节规则解耦的 laravel 定价计算重构方法,通过预定义季节周期、统一季节判定逻辑和动态属性访问,替代原始易错的字符串日期比较,显著提升代码可读性、健壮性与扩展性。
在租车预订类系统中,按季节动态计价是常见需求,但原始实现常因硬编码日期字符串、跨年逻辑混乱、条件嵌套过深而极易出错。如题中所示,原代码使用 d-m-Y 字符串比较(如 "1/11/2024")、未处理跨年低季节(11月–3月)、重复判断逻辑冗余,且无法应对闰年、时区或未来年份动态适配。
我们推荐采用 「季节规则中心化 + 日期归一化判定」 的重构策略,核心思想是:
- ✅ 将季节定义为结构化配置(起始月/日 + 持续天数),而非散落的字符串边界;
- ✅ 使用 Carbon 统一处理日期,避免字符串解析歧义;
- ✅ 将季节判定与价格累加职责分离,提升可测试性与复用性;
- ✅ 利用 PHP 动态属性访问($group->{$season}SeasonPrice)消除重复 if-else 分支。
✅ 推荐重构实现(Laravel + Carbon)
use Carbon\Carbon;
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%,逻辑清晰可追溯,新增季节仅需修改配置数组,彻底告别“改一处、崩三处”的维护噩梦。定价引擎从此真正具备业务可演进性。










