|
@@ -10,32 +10,89 @@ use App\Models\CoachUser;
|
|
|
use App\Models\Order;
|
|
|
use Carbon\Carbon;
|
|
|
|
|
|
+/**
|
|
|
+ * 服务时间验证 Trait
|
|
|
+ *
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 技师状态验证:
|
|
|
+ * - 技师必须存在且状态为激活
|
|
|
+ * - 技师必须通过认证
|
|
|
+ *
|
|
|
+ * 2. 服务时间验证:
|
|
|
+ * - 服务时间不能早于当前时间
|
|
|
+ * - 最多只能提前7天预约
|
|
|
+ * - 必须在技师的工作时间内
|
|
|
+ * - 不能与其他订单时间冲突
|
|
|
+ *
|
|
|
+ * 3. 工作时间规则:
|
|
|
+ * - 默认工作时间为 09:00-21:00
|
|
|
+ * - 可以通过排班设置自定义工作时间
|
|
|
+ * - 支持设置特殊休息日
|
|
|
+ *
|
|
|
+ * 4. 订单冲突规则:
|
|
|
+ * - 检查已支付、已接单、服务中的订单
|
|
|
+ * - 考虑服务时长(默认2小时)
|
|
|
+ * - 开始时间不能在其他订单的服务时间段内
|
|
|
+ * - 结束时间不能在其他订单的服务时间段内
|
|
|
+ */
|
|
|
trait ValidatesServiceTime
|
|
|
{
|
|
|
+ /**
|
|
|
+ * 验证服务时间参数
|
|
|
+ *
|
|
|
+ * @param int $coachId 技师ID
|
|
|
+ * @param string $serviceTime 服务时间(格式:Y-m-d H:i:s)
|
|
|
+ *
|
|
|
+ * @throws \Exception 验证失败时抛出异常
|
|
|
+ */
|
|
|
private function validateServiceTimeParams(int $coachId, string $serviceTime): void
|
|
|
{
|
|
|
// 验证技师状态
|
|
|
- $coach = CoachUser::where('id', $coachId)
|
|
|
+ $coach = CoachUser::query()
|
|
|
+ ->with(['info'])
|
|
|
+ ->where('id', $coachId)
|
|
|
->where('state', TechnicianStatus::ACTIVE->value)
|
|
|
- ->where('auth_state', TechnicianAuthStatus::PASSED->value)
|
|
|
- ->firstOrFail();
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ abort_if(! $coach, 400, '技师不存在或未激活');
|
|
|
+
|
|
|
+ // 验证技师认证状态
|
|
|
+ abort_if(! $coach->info || $coach->info->state !== TechnicianAuthStatus::PASSED->value,
|
|
|
+ 400, '技师未通过认证');
|
|
|
+
|
|
|
+ // 验证服务时间基本参数
|
|
|
+ $this->validateBasicServiceTime($serviceTime);
|
|
|
+
|
|
|
+ // 验证技师工作时间
|
|
|
+ $workSchedule = $this->getCoachWorkSchedule($coachId);
|
|
|
+ $this->validateWorkingHours($serviceTime, $workSchedule);
|
|
|
|
|
|
- // 验证时间格式
|
|
|
+ // 验证时间冲突
|
|
|
+ $this->checkTimeConflicts($coachId, $serviceTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证服务时间基本参数
|
|
|
+ *
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 服务时间不能早于当前时间
|
|
|
+ * 2. 最多只能提前7天预约
|
|
|
+ *
|
|
|
+ * @param string $serviceTime 服务时间
|
|
|
+ *
|
|
|
+ * @throws \Exception 验证失败时抛出异常
|
|
|
+ */
|
|
|
+ private function validateBasicServiceTime(string $serviceTime): void
|
|
|
+ {
|
|
|
$serviceDateTime = Carbon::parse($serviceTime);
|
|
|
+
|
|
|
+ // 验证是否过期
|
|
|
abort_if(
|
|
|
$serviceDateTime->isPast(),
|
|
|
400,
|
|
|
'服务时间不能早于当前时间'
|
|
|
);
|
|
|
|
|
|
- // 验证预约提前时间(至少提前2小时)
|
|
|
- $minAdvanceHours = config('business.min_advance_hours', 2);
|
|
|
- abort_if(
|
|
|
- $serviceDateTime->diffInHours(now()) < $minAdvanceHours,
|
|
|
- 400,
|
|
|
- "需要至少提前{$minAdvanceHours}小时预约"
|
|
|
- );
|
|
|
-
|
|
|
// 验证预约时间范围(最多提前7天)
|
|
|
$maxAdvanceDays = config('business.max_advance_days', 7);
|
|
|
abort_if(
|
|
@@ -45,13 +102,23 @@ trait ValidatesServiceTime
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取技师工作时间安排
|
|
|
+ *
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 优先使用技师的自定义排班
|
|
|
+ * 2. 如果没有排班,使用默认时间(09:00-21:00)
|
|
|
+ * 3. 默认每天都工作
|
|
|
+ *
|
|
|
+ * @param int $coachId 技师ID
|
|
|
+ * @return array 工作时间安排
|
|
|
+ */
|
|
|
private function getCoachWorkSchedule(int $coachId): array
|
|
|
{
|
|
|
$schedule = CoachSchedule::where('coach_id', $coachId)
|
|
|
->where('state', 1)
|
|
|
->first();
|
|
|
-
|
|
|
- if (! $schedule) {
|
|
|
+ if (! $schedule || empty($schedule->time_ranges)) {
|
|
|
return [
|
|
|
'work_days' => range(1, 7),
|
|
|
'work_hours' => [
|
|
@@ -62,40 +129,62 @@ trait ValidatesServiceTime
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ $timeRanges = json_decode($schedule->time_ranges, true);
|
|
|
+
|
|
|
return [
|
|
|
- 'work_days' => json_decode($schedule->work_days, true),
|
|
|
+ 'work_days' => range(1, 7), // 默认每天都工作
|
|
|
'work_hours' => [
|
|
|
- 'start' => $schedule->work_start_time,
|
|
|
- 'end' => $schedule->work_end_time,
|
|
|
+ 'start' => $timeRanges[0]['start_time'],
|
|
|
+ 'end' => end($timeRanges)['end_time'],
|
|
|
],
|
|
|
- 'rest_dates' => json_decode($schedule->rest_dates, true) ?? [],
|
|
|
+ 'rest_dates' => [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 验证工作时间
|
|
|
+ *
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 检查是否为工作日
|
|
|
+ * 2. 检查是否为特殊休息日
|
|
|
+ * 3. 检查是否在工作时间范围内
|
|
|
+ *
|
|
|
+ * @param string $serviceTime 服务时间
|
|
|
+ * @param array $workSchedule 工作时间安排
|
|
|
+ *
|
|
|
+ * @throws \Exception 验证失败时抛出异常
|
|
|
+ */
|
|
|
private function validateWorkingHours(string $serviceTime, array $workSchedule): void
|
|
|
{
|
|
|
$serviceDateTime = Carbon::parse($serviceTime);
|
|
|
|
|
|
// 检查工作日
|
|
|
$dayOfWeek = $serviceDateTime->dayOfWeek;
|
|
|
+ $workDays = $workSchedule['work_days'] ?? range(1, 7); // 如果未设置,默认每天都工作
|
|
|
abort_if(
|
|
|
- ! in_array($dayOfWeek, $workSchedule['work_days']),
|
|
|
+ ! in_array($dayOfWeek, $workDays),
|
|
|
400,
|
|
|
'该时间不在技师工作日内'
|
|
|
);
|
|
|
|
|
|
// 检查特殊休息日
|
|
|
$dateStr = $serviceDateTime->format('Y-m-d');
|
|
|
+ $restDates = $workSchedule['rest_dates'] ?? []; // 如果未设置,默认没有休息日
|
|
|
abort_if(
|
|
|
- in_array($dateStr, $workSchedule['rest_dates']),
|
|
|
+ in_array($dateStr, $restDates),
|
|
|
400,
|
|
|
'技师该日期休息'
|
|
|
);
|
|
|
|
|
|
// 检查工作时间
|
|
|
$timeStr = $serviceDateTime->format('H:i');
|
|
|
- $startTime = Carbon::parse($workSchedule['work_hours']['start']);
|
|
|
- $endTime = Carbon::parse($workSchedule['work_hours']['end']);
|
|
|
+ $workHours = $workSchedule['work_hours'] ?? [
|
|
|
+ 'start' => '09:00',
|
|
|
+ 'end' => '21:00',
|
|
|
+ ]; // 如果未设置,使用默认工作时间
|
|
|
+
|
|
|
+ $startTime = Carbon::parse($workHours['start']);
|
|
|
+ $endTime = Carbon::parse($workHours['end']);
|
|
|
|
|
|
abort_if(
|
|
|
$timeStr < $startTime->format('H:i') || $timeStr > $endTime->format('H:i'),
|
|
@@ -108,40 +197,41 @@ trait ValidatesServiceTime
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 检查时间冲突
|
|
|
+ *
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 检查已支付、已接单、服务中的订单
|
|
|
+ * 2. 考虑服务时长(默认2小时)
|
|
|
+ * 3. 新订单的服务时间段不能与其他订单重叠
|
|
|
+ *
|
|
|
+ * @param int $coachId 技师ID
|
|
|
+ * @param string $serviceTime 服务时间
|
|
|
+ *
|
|
|
+ * @throws \Exception 验证失败时抛出异常
|
|
|
+ */
|
|
|
private function checkTimeConflicts(int $coachId, string $serviceTime): void
|
|
|
{
|
|
|
$serviceDateTime = Carbon::parse($serviceTime);
|
|
|
|
|
|
- // 获取服务时长(默认2小时)
|
|
|
+ // 获取服务时长配置(默认2小时)
|
|
|
$serviceDuration = config('business.default_service_duration', 120);
|
|
|
|
|
|
- // 计算服务结束时间
|
|
|
+ // 计算本次服务的结束时间
|
|
|
$serviceEndTime = $serviceDateTime->copy()->addMinutes($serviceDuration);
|
|
|
|
|
|
- // 检查是否有其他订单在这个时间段
|
|
|
- $conflictOrder = Order::where('coach_id', $coachId)
|
|
|
- ->whereIn('state', [
|
|
|
+ // 检查是否与其他订单时间冲突
|
|
|
+ $conflictingOrder = Order::query()
|
|
|
+ ->where('coach_id', $coachId)
|
|
|
+ ->where('service_time', '<=', $serviceEndTime)
|
|
|
+ ->where('service_end_time', '>=', $serviceDateTime)
|
|
|
+ ->whereIn('status', [
|
|
|
OrderStatus::PAID->value,
|
|
|
OrderStatus::ACCEPTED->value,
|
|
|
OrderStatus::SERVING->value,
|
|
|
])
|
|
|
- ->where(function ($query) use ($serviceDateTime, $serviceEndTime) {
|
|
|
- $query->where(function ($q) use ($serviceDateTime) {
|
|
|
- // 新订单开始时间在其他订单的服务时间段内
|
|
|
- $q->where('service_time', '<=', $serviceDateTime)
|
|
|
- ->where('service_end_time', '>', $serviceDateTime);
|
|
|
- })->orWhere(function ($q) use ($serviceEndTime) {
|
|
|
- // 新订单结束时间在其他订单的服务时间段内
|
|
|
- $q->where('service_time', '<', $serviceEndTime)
|
|
|
- ->where('service_end_time', '>=', $serviceEndTime);
|
|
|
- });
|
|
|
- })
|
|
|
->first();
|
|
|
|
|
|
- abort_if(
|
|
|
- $conflictOrder,
|
|
|
- 400,
|
|
|
- '该时间段技师已有其他订单'
|
|
|
- );
|
|
|
+ abort_if($conflictingOrder, 400, '该时间段已被预约');
|
|
|
}
|
|
|
}
|