
本文介绍一种结合 carbon 时间处理与集合操作的 php 逻辑方案,用于为指定员工生成日期范围内按 30 分钟粒度划分的可用预约时段,自动排除非工作日、工作时间外及已被占用的时间段。
本文介绍一种结合 carbon 时间处理与集合操作的 php 逻辑方案,用于为指定员工生成日期范围内按 30 分钟粒度划分的可用预约时段,自动排除非工作日、工作时间外及已被占用的时间段。
在预约类系统中,单纯依赖 SQL 查询计算“可用时间段”往往低效且难以维护——尤其当涉及时间重叠判断、跨天预约、动态工作日规则(如周末是否上班)、多员工多时段等场景时,数据库层的时间运算易出错、难调试,且缺乏灵活性。因此,推荐采用“数据库 + 应用层逻辑”协同策略:由数据库精准获取基础数据(员工工作时间、已存在预约),再由 PHP 在内存中完成精细化时间槽生成与冲突剔除。
以下是一个完整、可扩展的 Laravel 控制器方法实现,适用于 Employee A 在 2022-05-22 至 2022-06-01 区间内按 30 分钟粒度生成可用时段:
use Carbon\Carbon;
use Illuminate\Http\Request;
public function getAvailableSlots(Request $request)
{
$employeeId = $request->input('employee_id', 1); // 示例:Employee A ID
$startDate = Carbon::parse($request->input('start_date', '2022-05-22'));
$endDate = Carbon::parse($request->input('end_date', '2022-06-01'));
// 1. 获取员工的工作时间(支持多天、含星期映射)
$workingHours = Employee::with('workingHours')
->findOrFail($employeeId)
->workingHours
->mapWithKeys(function ($wh) {
return [$wh->day => [
'start_time' => $wh->start_time,
'end_time' => $wh->end_time,
]];
})
->toArray();
// 2. 获取该员工在此日期范围内的所有已预约记录
$appointments = Appointment::where('employee_id', $employeeId)
->whereBetween('start_date_time', [$startDate->startOfDay(), $endDate->endOfDay()])
->get()
->mapWithKeys(function ($appt) {
$date = Carbon::parse($appt->start_date_time)->format('Y-m-d');
return [$date => $appt];
})
->toArray();
// 3. 主循环:逐日生成可用时段
$result = [];
$current = $startDate->copy();
while ($current->lte($endDate)) {
$dateStr = $current->format('Y-m-d');
$dayOfWeek = $current->dayOfWeek; // Sunday=0, Monday=1, ..., Saturday=6
// 检查当日是否有定义工作时间
if (!isset($workingHours[$dayOfWeek])) {
$result[$dateStr] = [];
$current->addDay();
continue;
}
$wh = $workingHours[$dayOfWeek];
$times = $this->generateTimeSlots(
$wh['start_time'],
$wh['end_time'],
30 // 分钟粒度,可配置
);
// 若当日存在预约,移除被占用的起始时间点(注意:本例假设预约均为整块30分钟,仅需排除 start_time)
if (isset($appointments[$dateStr])) {
$appt = $appointments[$dateStr];
$bookedStart = Carbon::parse($appt->start_date_time)->format('H:i');
$bookedEnd = Carbon::parse($appt->end_date_time)->format('H:i');
// 移除被完全占用的起始时间(如 09:00–09:30 占用,则 09:00 不可用)
$times = array_values(array_diff($times, [$bookedStart]));
}
$result[$dateStr] = $times;
$current->addDay();
}
return response()->json($result);
}
private function generateTimeSlots(string $start, string $end, int $intervalMinutes = 30): array
{
$startCarbon = Carbon::parse($start);
$endCarbon = Carbon::parse($end)->subMinutes($intervalMinutes); // 确保最后一个 slot 起始时间 ≤ end_time - interval
$slots = [];
while ($startCarbon->lte($endCarbon)) {
$slots[] = $startCarbon->format('H:i');
$startCarbon->addMinutes($intervalMinutes);
}
return $slots;
}
✅ 关键设计说明:
- 避免 SQL 时间运算陷阱:不使用 TIME()、HOUR() 或复杂 NOT EXISTS 子查询,规避 MySQL 时区、精度、索引失效等问题;
- 灵活支持 MorphTo 关系:working_hours 表通过 employeeable_id/type 支持员工/部门等多态关联,代码中已适配;
- 粒度可控:$intervalMinutes 参数可轻松切换为 15/30/60 分钟;
- 边界安全:subMinutes($intervalMinutes) 确保不会生成超出工作结束时间的无效 slot(如 17:00–17:30);
- 预约冲突逻辑清晰:当前实现按「预约起始时间」排除 slot,若需支持更复杂重叠检测(如 09:15–09:45 占用影响 09:00/09:30),可引入 spatie/laravel-time-range 等库进行区间运算。
⚠️ 注意事项:
- 若预约量极大(如单日数百条),建议对 appointments 查询添加复合索引:INDEX(employee_id, start_date_time);
- 生产环境应增加缓存(如 Redis)存储高频查询结果,避免重复计算;
- 前端需明确约定时间格式(H:i)并做校验,防止非法输入导致 Carbon 解析异常。
该方案兼顾可读性、可维护性与性能,在中小型预约系统中表现稳健,是 Laravel 生态下处理“可用时段”问题的推荐实践。











