소스 검색

feat:用户端-获取技师服务时间段

刘学玺 4 달 전
부모
커밋
43cdaa64dc
3개의 변경된 파일274개의 추가작업 그리고 1개의 파일을 삭제
  1. 50 0
      app/Http/Controllers/Client/CoachController.php
  2. 218 0
      app/Services/Client/CoachService.php
  3. 6 1
      routes/api.php

+ 50 - 0
app/Http/Controllers/Client/CoachController.php

@@ -82,4 +82,54 @@ class CoachController extends Controller
 
         return $this->service->getCoachDetail($id, $latitude, $longitude);
     }
+
+    /**
+     * [技师]获取可预约时间段
+     *
+     * @description 获取指定技师的可预约时间段列表,包含日期、星期、时间段等信息
+     *
+     * @queryParam coach_id int required 技师ID Example: 6
+     * @queryParam date string 日期(格式:Y-m-d) Example: 2024-03-22
+     *
+     * @response {
+     *   "data": {
+     *     "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"
+     *   }
+     * }
+     * @response 404 {
+     *   "message": "技师不存在"
+     * }
+     * @response 400 {
+     *   "message": "技师状态异常"
+     * }
+     * @response 400 {
+     *   "message": "不能查询过去的日期"
+     * }
+     * @response 400 {
+     *   "message": "只能查询未来30天内的时间段"
+     * }
+     */
+    public function getSchedule(Request $request)
+    {
+        // 验证参数
+        $validated = $request->validate([
+            'coach_id' => 'required|integer|exists:coach_users,id',
+            'date' => 'nullable|date_format:Y-m-d',
+        ]);
+
+        // 调用service获取技师可预约时间段
+        return $this->service->getSchedule($validated['coach_id'], $validated['date'] ?? null);
+    }
 }

+ 218 - 0
app/Services/Client/CoachService.php

@@ -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;
+    }
 }

+ 6 - 1
routes/api.php

@@ -77,6 +77,9 @@ Route::prefix('client')->group(function () {
                 Route::post('/', [CoachLocationController::class, 'store']); // 创建新的技师定位
                 Route::delete('/{id}', [CoachLocationController::class, 'destroy']);    // 删除技师定位
             });
+            // 获取可预约时间段
+            Route::get('schedule', [CoachController::class, 'getSchedule']);
+
             Route::get('/', [CoachController::class, 'list']); // 获取技师列表
             Route::get('/{id}', [CoachController::class, 'detail']); // 获取技师详情
 
@@ -131,7 +134,7 @@ Route::prefix('client')->group(function () {
 });
 
 // 技师端路由组
-Route::middleware(['auth:sanctum', 'verified'])->prefix('coach')->group(function () {
+Route::middleware(['auth:sanctum'])->prefix('coach')->group(function () {
     // 账户相关路由组
     Route::prefix('account')->group(function () {
         Route::post('base-info', [App\Http\Controllers\Coach\AccountController::class, 'submitBaseInfo'])
@@ -146,6 +149,8 @@ Route::middleware(['auth:sanctum', 'verified'])->prefix('coach')->group(function
             ->name('coach.account.location');
         // 获取技师位置信息
         Route::get('location', [\App\Http\Controllers\Coach\AccountController::class, 'getLocation']);
+        // 排班管理
+        Route::post('schedule', [\App\Http\Controllers\Coach\AccountController::class, 'setSchedule']);
     });
 
     // 订单相关路由