|
@@ -2,12 +2,17 @@
|
|
|
|
|
|
namespace App\Services\Client;
|
|
|
|
|
|
+use App\Enums\OrderStatus;
|
|
|
use App\Enums\TechnicianAuthStatus;
|
|
|
use App\Enums\TechnicianLocationType;
|
|
|
use App\Enums\TechnicianStatus;
|
|
|
use App\Enums\UserStatus;
|
|
|
+use App\Models\CoachSchedule;
|
|
|
use App\Models\CoachUser;
|
|
|
+use App\Models\Order;
|
|
|
+use Illuminate\Support\Carbon;
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
+use Illuminate\Support\Facades\Cache;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Redis;
|
|
|
|
|
@@ -221,4 +226,217 @@ class CoachService
|
|
|
|
|
|
return $result;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取技师可预约时间段列表
|
|
|
+ *
|
|
|
+ * @param int $coachId 技师ID
|
|
|
+ * @param string|null $date 日期,默认当天
|
|
|
+ * @return array 返回格式:[
|
|
|
+ * 'date' => '2024-03-22',
|
|
|
+ * 'day_of_week' => '星期五',
|
|
|
+ * 'is_today' => false,
|
|
|
+ * 'time_slots' => [
|
|
|
+ * [
|
|
|
+ * 'start_time' => '09:00',
|
|
|
+ * 'end_time' => '09:30',
|
|
|
+ * 'is_available' => true,
|
|
|
+ * 'duration' => 30
|
|
|
+ * ]
|
|
|
+ * ],
|
|
|
+ * 'total_slots' => 1,
|
|
|
+ * 'updated_at' => '2024-03-22 10:00:00'
|
|
|
+ * ]
|
|
|
+ *
|
|
|
+ * @throws \Exception
|
|
|
+ */
|
|
|
+ public function getSchedule(int $coachId, ?string $date = null)
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 默认使用当天日期
|
|
|
+ $date = $date ?: now()->toDateString();
|
|
|
+ $targetDate = Carbon::parse($date);
|
|
|
+
|
|
|
+ // 验证技师信息
|
|
|
+ $coach = CoachUser::with('info')->find($coachId);
|
|
|
+ abort_if(! $coach, 404, '技师不存在');
|
|
|
+ abort_if($coach->info->state != TechnicianStatus::ACTIVE->value,
|
|
|
+ 400, '技师状态异常');
|
|
|
+
|
|
|
+ // 验证日期
|
|
|
+ abort_if($targetDate->startOfDay()->lt(now()->startOfDay()),
|
|
|
+ 400, '不能查询过去的日期');
|
|
|
+ abort_if($targetDate->diffInDays(now()) > 30,
|
|
|
+ 400, '只能查询未来30天内的时间段');
|
|
|
+
|
|
|
+ // 使用缓存减少数据库查询
|
|
|
+ return Cache::remember(
|
|
|
+ "coach:timeslots:{$coachId}:{$date}",
|
|
|
+ now()->addMinutes(15), // 缓存15分钟
|
|
|
+ function () use ($coachId, $date, $targetDate) {
|
|
|
+ // 获取技师排班信息
|
|
|
+ $schedule = CoachSchedule::where('coach_id', $coachId)
|
|
|
+ ->where('state', 1)
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (! $schedule || empty($schedule->time_ranges)) {
|
|
|
+ return $this->formatResponse($date, []);
|
|
|
+ }
|
|
|
+
|
|
|
+ $timeRanges = json_decode($schedule->time_ranges, true);
|
|
|
+
|
|
|
+ // 获取当天所有订单
|
|
|
+ $dayOrders = $this->getDayOrders($coachId, $date);
|
|
|
+
|
|
|
+ // 生成时间段列表
|
|
|
+ $timeSlots = $this->generateAvailableTimeSlots(
|
|
|
+ $date,
|
|
|
+ $timeRanges,
|
|
|
+ $dayOrders,
|
|
|
+ $targetDate->isToday()
|
|
|
+ );
|
|
|
+
|
|
|
+ return $this->formatResponse($date, $timeSlots);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('获取可预约时间段失败', [
|
|
|
+ 'coach_id' => $coachId,
|
|
|
+ 'date' => $date,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString(),
|
|
|
+ ]);
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当天所有订单
|
|
|
+ *
|
|
|
+ * @param int $coachId 技师ID
|
|
|
+ * @param string $date 日期
|
|
|
+ * @return array 订单列表
|
|
|
+ */
|
|
|
+ private function getDayOrders(int $coachId, string $date): array
|
|
|
+ {
|
|
|
+ return Order::where('coach_id', $coachId)
|
|
|
+ ->where('service_time', $date)
|
|
|
+ ->whereIn('state', [
|
|
|
+ OrderStatus::ACCEPTED->value,
|
|
|
+ OrderStatus::DEPARTED->value,
|
|
|
+ OrderStatus::ARRIVED->value,
|
|
|
+ OrderStatus::SERVING->value,
|
|
|
+ ])
|
|
|
+ ->select(['id', 'service_start_time', 'service_end_time', 'state'])
|
|
|
+ ->get()
|
|
|
+ ->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成可用时间段列表
|
|
|
+ *
|
|
|
+ * @param string $date 日期
|
|
|
+ * @param array $timeRanges 排班时间段
|
|
|
+ * @param array $dayOrders 当天订单
|
|
|
+ * @param bool $isToday 是否是当天
|
|
|
+ * @return array 可用时间段列表
|
|
|
+ */
|
|
|
+ private function generateAvailableTimeSlots(
|
|
|
+ string $date,
|
|
|
+ array $timeRanges,
|
|
|
+ array $dayOrders,
|
|
|
+ bool $isToday
|
|
|
+ ): array {
|
|
|
+ $timeSlots = [];
|
|
|
+ $currentTime = now();
|
|
|
+
|
|
|
+ foreach ($timeRanges as $range) {
|
|
|
+ $start = Carbon::parse($date.' '.$range['start_time']);
|
|
|
+ $end = Carbon::parse($date.' '.$range['end_time']);
|
|
|
+
|
|
|
+ // 如果是当天且开始时间已过,从下一个30分钟时间点开始
|
|
|
+ if ($isToday && $start->lt($currentTime)) {
|
|
|
+ $start = $currentTime->copy()->addMinutes(30)->floorMinutes(30);
|
|
|
+ // 如果调整后的开始时间已超过结束时间,跳过此时间段
|
|
|
+ if ($start->gt($end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成30分钟间隔的时间段
|
|
|
+ while ($start->lt($end)) {
|
|
|
+ $slotStart = $start->format('H:i');
|
|
|
+ $slotEnd = $start->copy()->addMinutes(30)->format('H:i');
|
|
|
+
|
|
|
+ // 检查时间段是否被订单占用
|
|
|
+ if (! $this->hasConflictingOrder($date, $slotStart, $slotEnd, $dayOrders)) {
|
|
|
+ $timeSlots[] = [
|
|
|
+ 'start_time' => $slotStart,
|
|
|
+ 'end_time' => $slotEnd,
|
|
|
+ 'is_available' => true,
|
|
|
+ 'duration' => 30,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $start->addMinutes(30);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $timeSlots;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化返回数据
|
|
|
+ *
|
|
|
+ * @param string $date 日期
|
|
|
+ * @param array $timeSlots 时间段列表
|
|
|
+ * @return array 格式化后的数据
|
|
|
+ */
|
|
|
+ private function formatResponse(string $date, array $timeSlots): array
|
|
|
+ {
|
|
|
+ $targetDate = Carbon::parse($date);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'date' => $date,
|
|
|
+ 'day_of_week' => $targetDate->isoFormat('dddd'), // 星期几
|
|
|
+ 'is_today' => $targetDate->isToday(),
|
|
|
+ 'time_slots' => $timeSlots,
|
|
|
+ 'total_slots' => count($timeSlots),
|
|
|
+ 'updated_at' => now()->toDateTimeString(),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否与已有订单冲突
|
|
|
+ *
|
|
|
+ * @param string $date 日期
|
|
|
+ * @param string $startTime 开始时间
|
|
|
+ * @param string $endTime 结束时间
|
|
|
+ * @param array $dayOrders 当天订单
|
|
|
+ * @return bool 是否冲突
|
|
|
+ */
|
|
|
+ private function hasConflictingOrder(
|
|
|
+ string $date,
|
|
|
+ string $startTime,
|
|
|
+ string $endTime,
|
|
|
+ array $dayOrders
|
|
|
+ ): bool {
|
|
|
+ $slotStart = Carbon::parse("$date $startTime");
|
|
|
+ $slotEnd = Carbon::parse("$date $endTime");
|
|
|
+
|
|
|
+ foreach ($dayOrders as $order) {
|
|
|
+ $orderStart = Carbon::parse($order['service_start_time']);
|
|
|
+ $orderEnd = Carbon::parse($order['service_end_time']);
|
|
|
+
|
|
|
+ // 检查时间段是否重叠
|
|
|
+ if (($slotStart >= $orderStart && $slotStart < $orderEnd) ||
|
|
|
+ ($slotEnd > $orderStart && $slotEnd <= $orderEnd) ||
|
|
|
+ ($slotStart <= $orderStart && $slotEnd >= $orderEnd)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|