Browse Source

fixed:用户端-获取技师可预约时间

刘学玺 4 months ago
parent
commit
8e17582f0a
2 changed files with 150 additions and 120 deletions
  1. 61 13
      app/Http/Controllers/Client/CoachController.php
  2. 89 107
      app/Services/Client/CoachService.php

+ 61 - 13
app/Http/Controllers/Client/CoachController.php

@@ -38,7 +38,7 @@ class CoachController extends Controller
      * - 技师必须通过基本信息认证
      * - 技师必须通过实名认证
      * - 技师必须通过资质认证
-     * - 如果技师设置了接单距离,���户距离必须在范围内
+     * - 如果技师设置了接单距离,户距离必须在范围内
      * - 未设置接单距离的技师,只要在系统搜索半径内就显示
      *
      * 排序规则:
@@ -133,32 +133,80 @@ class CoachController extends Controller
     /**
      * [技师]获取技师详情
      *
-     * 根据ID获取技师的详细信息
+     * @description 获取技师的详细信息,包括基本资料、认证状态、位置信息和距离计算
      *
-     * @authenticated
+     * 业务流程:
+     * 1. 验证用户登录状态和权限
+     * 2. 验证技师ID的有效性
+     * 3. 获取技师基本信息和认证状态
+     * 4. 如果提供了用户坐标,计算与技师的距离
+     * 5. 返回完整的技师信息
      *
-     * @urlParam id int required 技师ID. Example: 6
+     * 数据验证:
+     * - 技师必须存在且状态正常
+     * - 技师必须通过各项认证
+     * - 坐标格式必须正确(如果提供)
      *
-     * @queryParam latitude float 纬度. Example: 34.0522
-     * @queryParam longitude float 经度. Example: -118.2437
+     * @authenticated 需要用户登录认证
      *
-     * @response {
+     * @urlParam id required int 技师ID. Example: 6
+     * @queryParam latitude float 用户当前纬度坐标(-9090). Example: 39.9042
+     * @queryParam longitude float 用户当前经度坐标(-180180). Example: 116.4074
+     *
+     * @response scenario=success {
      *   "code": 200,
-     *   "message": "获取成功",
+     *   "message": "success",
      *   "data": {
-     *     "id": 1,
-     *     "name": "技师A",
-     *     "latitude": 34.0522,
-     *     "longitude": -118.2437,
-     *     "details": "详细信息"
+     *     "id": 6,
+     *     "user_id": 12,
+     *     "info_record_id": 53,
+     *     "real_auth_record_id": 1,
+     *     "qualification_record_id": 1,
+     *     "shop_id": null,
+     *     "level": 1,
+     *     "virtual_order": 0,
+     *     "score": "5.00",
+     *     "work_status": 2,
+     *     "virtual_status": 1,
+     *     "state": 2,
+     *     "created_at": "2024-11-19 18:25:04",
+     *     "updated_at": "2024-12-13 08:01:02",
+     *     "is_vip": 1,
+     *     "distance": 2.5,  // 用户到技师的距离(公里)
+     *     "state_text": "正常服务",
+     *     "info": {
+     *       "id": 53,
+     *       "nickname": "张三1",
+     *       "avatar": null,
+     *       "gender": "1",
+     *       "state_text": ""
+     *     }
      *   }
      * }
+     *
+     * @response status=401 scenario="未登录" {
+     *   "message": "用户未登录"
+     * }
+     * @response status=400 scenario="状态异常" {
+     *   "message": "用户状态异常"
+     * }
+     * @response status=404 scenario="不存在" {
+     *   "message": "技师不存在"
+     * }
+     * @response status=422 scenario="参数错误" {
+     *   "message": "无效的经纬度坐标"
+     * }
+     * @response status=500 scenario="系统错误" {
+     *   "message": "Redis服务不可用"
+     * }
      */
     public function detail(Request $request, $id)
     {
+        // 获取用户当前位置坐标
         $latitude = $request->input('latitude');
         $longitude = $request->input('longitude');
 
+        // 调用服务层获取技师详情
         return $this->success($this->service->getCoachDetail($id, $latitude, $longitude));
     }
 

+ 89 - 107
app/Services/Client/CoachService.php

@@ -146,6 +146,7 @@ class CoachService
     {
         // 验证用户状态
         $user = MemberUser::findOrFail($userId);
+        abort_if(!$user, 401, '用户未登录');
         abort_if($user->state !== UserStatus::OPEN->value, 400, '用户状态异常');
 
         // 检查Redis连接
@@ -258,7 +259,7 @@ class CoachService
             });
         }
 
-        // 实际距离排序
+        // 实际距离排序
         if (!empty($coachDistances)) {
             $query->orderByRaw('CASE coach_users.id ' .
                 collect($coachDistances)->map(function ($distance, $id) {
@@ -289,21 +290,43 @@ class CoachService
             ->values();
     }
 
+    /**
+     * 验证地理坐标
+     *
+     * @param float|null $latitude 纬度
+     * @param float|null $longitude 经度
+     * @throws \Exception 当坐标无效时
+     */
+    protected function validateCoordinates(?float $latitude, ?float $longitude): void
+    {
+        $isInvalid = !is_numeric($latitude) || !is_numeric($longitude);
+        if ($isInvalid) {
+            Log::error('Invalid coordinates:', ['latitude' => $latitude, 'longitude' => $longitude]);
+        }
+        abort_if($isInvalid, 422, '无效的经纬度坐标');
+    }
+
     /**
      * 获取技师详情
      *
      * 业务流程:
-     * 1. 验证Redis连接状态
-     * 2. 验证用户身份和状态
-     * 3. 获取技师基本信息和认证状态
-     * 4. 获取技师位置信息
-     * 5. 计算用户与技师之间的距离
+     * 1. 验证用户身份和Redis连接状态
+     * 2. 验证技师存在性和认证状态
+     * 3. 获取技师位置信息(常驻地和当前位置)
+     * 4. 计算用户与技师的距离(如果提供了用户坐标)
+     * 5. 返回完整的技师信息
      *
      * 技术实现:
-     * - Redis连接状态检查
+     * - 使用Redis GEO功能计算距离
      * - 多重认证状态验证
-     * - 地理位置距离计算
-     * - 数据关联查询
+     * - 关联查询技师信息
+     * - 临时存储用户位置
+     *
+     * 数据处理:
+     * - 技师位置数据从Redis获取
+     * - 距离计算保留2位小数
+     * - 选择最近的距离(常驻地和当前位置)
+     * - 自动清理临时位置数据
      *
      * 数据验证:
      * - 技师ID有效性
@@ -320,92 +343,71 @@ class CoachService
      * @param int $coachId 技师ID
      * @param float|null $latitude 用户当前纬度
      * @param float|null $longitude 用户当前经度
-     * @return CoachUser 技师详细信息
+     * @return CoachUser 技师详细信息,包含距离计算结果
      * @throws \Exception 当验证失败或查询异常时
      */
     public function getCoachDetail($coachId, $latitude, $longitude)
     {
-        // 验证基本要求
+        // 验证基本要求(用户状态和Redis连接)
         $this->validateBasicRequirements(Auth::id());
 
-        // 检查Redis连接
-        try {
-            $pingResult = Redis::connection()->ping();
-            Log::info('Redis connection test:', ['ping_result' => $pingResult]);
-        } catch (\Exception $e) {
-            Log::error('Redis connection error:', ['error' => $e->getMessage()]);
-            throw new \Exception('Redis连接失败:' . $e->getMessage());
-        }
-
-        // 获取当前用户
-        $user = Auth::user();
-
-        Log::info('Current user and coordinates:', [
-            'user' => $user ? $user->id : null,
-            'latitude' => $latitude,
-            'longitude' => $longitude,
-            'coach_id' => $coachId,
-        ]);
+        // 验证坐标
+        $this->validateCoordinates($latitude, $longitude);
 
-        // 检查用户状态
-        if (! $user) {
-            throw new \Exception('用户未登录');
-        }
-
-        if ($user->state !== UserStatus::OPEN->value) {
-            throw new \Exception('用户状态异常');
-        }
-        // 修改查询以处理可能的 null 值
+        // 获取技师信息(包含认证状态检查)
         $query = CoachUser::where('state', TechnicianStatus::ACTIVE->value);
         $this->addAuthStatusChecks($query);
 
+        // 获取技师详细信息
         $coach = $query->with(['info:id,nickname,avatar,gender'])
             ->find($coachId);
 
-        if (! $coach) {
-            throw new \Exception('技师不存在');
-        }
+        abort_if(!$coach, 404, '技师不存在');
 
-        // 从 Redis 获取技师的 id_home 和 id_work 的经纬度
-        $homeLocation = Redis::geopos('coach_locations', $coachId . '_' . TechnicianLocationType::COMMON->value);
-        $workLocation = Redis::geopos('coach_locations', $coachId . '_' . TechnicianLocationType::CURRENT->value);
+        // 计算并设置距离
+        $coach->distance = $this->calculateDistanceToCoach(Auth::id(), $coachId, $latitude, $longitude);
 
-        // 检输入的经纬度否效
-        if (! is_numeric($latitude) || ! is_numeric($longitude)) {
-            Log::error('Invalid coordinates:', ['latitude' => $latitude, 'longitude' => $longitude]);
-            throw new \Exception('无效的经纬度坐标');
-        }
+        return $coach;
+    }
 
-        // 临时存储用户当前位置用于计算距离
-        $tempKey = 'user_temp_' . $user->id;
+    /**
+     * 计算用户到技师的距离
+     *
+     * @param int $userId 用户ID
+     * @param int $coachId 技师ID
+     * @param float $latitude 用户纬度
+     * @param float $longitude 用户经度
+     * @return float|null 最近距离(公里)
+     */
+    protected function calculateDistanceToCoach(int $userId, int $coachId, float $latitude, float $longitude): ?float
+    {
+        // 临时存储用户位置
+        $tempKey = 'user_temp_' . $userId;
         Redis::geoadd('coach_locations', $longitude, $latitude, $tempKey);
 
-        // 计算距离(单位:km)
-        $distanceHome = null;
-        $distanceWork = null;
-
-        if ($homeLocation && ! empty($homeLocation[0])) {
-            $distanceHome = Redis::geodist('coach_locations', $tempKey, $coachId . '_' . TechnicianLocationType::COMMON->value, 'km');
-            Log::info('Home distance calculation:', [
-                'from' => $tempKey,
-                'to' => $coachId . '_' . TechnicianLocationType::COMMON->value,
-                'distance' => $distanceHome,
-                'home_location' => $homeLocation[0],
-            ]);
-        }
+        try {
+            // 获取技师位置
+            $homeLocation = $this->getCoachLocation($coachId . '_' . TechnicianLocationType::COMMON->value);
+            $workLocation = $this->getCoachLocation($coachId . '_' . TechnicianLocationType::CURRENT->value);
 
-        if ($workLocation && ! empty($workLocation[0])) {
-            $distanceWork = Redis::geodist('coach_locations', $tempKey, $coachId . '_' . TechnicianLocationType::CURRENT->value, 'km');
-        }
+            $distances = [];
 
-        // 删除临位置点
-        Redis::zrem('coach_locations', $tempKey);
+            // 计算到常驻地的距离
+            if ($homeLocation && !empty($homeLocation[0])) {
+                $distances[] = $this->calculateDistance($tempKey, $coachId . '_' . TechnicianLocationType::COMMON->value);
+            }
 
-        // 选择最近的距离
-        $distances = array_filter([$distanceHome, $distanceWork]);
-        $coach->distance = ! empty($distances) ? round(min($distances), 2) : null;
+            // 计算到当前位置的距离
+            if ($workLocation && !empty($workLocation[0])) {
+                $distances[] = $this->calculateDistance($tempKey, $coachId . '_' . TechnicianLocationType::CURRENT->value);
+            }
 
-        return $coach;
+            // 返回最近的距离
+            return !empty($distances) ? round(min(array_filter($distances)), 2) : null;
+        } finally {
+            // 确保清理临时位置数据
+            Redis::zrem('coach_locations', $tempKey);
+        }
     }
 
     /**
@@ -422,7 +424,7 @@ class CoachService
      * - 位置更新验证
      *
      * 数据验证:
-     * - 标范围检查
+     * - 标范围检查
      * - 位置类型验证
      * - 技师ID验证
      *
@@ -440,39 +442,17 @@ class CoachService
      */
     public function setCoachLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value)
     {
-        if (! is_numeric($latitude) || ! is_numeric($longitude)) {
-            Log::error('Invalid coordinates in setCoachLocation:', [
-                'coach_id' => $coachId,
-                'latitude' => $latitude,
-                'longitude' => $longitude,
-            ]);
-            throw new \Exception('无效的经纬度坐标');
-        }
+        // 验证坐标
+        $this->validateCoordinates($latitude, $longitude);
 
-        if (! in_array($type, [TechnicianLocationType::CURRENT->value, TechnicianLocationType::COMMON->value])) {
-            throw new \Exception('无效的位置类型,必须是 current 或 common');
-        }
+        abort_if(
+            !in_array($type, [TechnicianLocationType::CURRENT->value, TechnicianLocationType::COMMON->value]),
+            422,
+            '无效的位置类型,必须是 current 或 common'
+        );
 
         $key = $coachId . '_' . $type;
-        $result = Redis::geoadd('coach_locations', $longitude, $latitude, $key);
-
-        Log::info('Coach location set:', [
-            'coach_id' => $coachId,
-            'type' => $type,
-            'key' => $key,
-            'latitude' => $latitude,
-            'longitude' => $longitude,
-            'result' => $result,
-        ]);
-
-        // 验证数据是否成功写入
-        $location = Redis::geopos('coach_locations', $key);
-        Log::info('Verify location after set:', [
-            'key' => $key,
-            'location' => $location,
-        ]);
-
-        return $result;
+        return Redis::geoadd('coach_locations', $longitude, $latitude, $key);
     }
 
     /**
@@ -515,10 +495,10 @@ class CoachService
             $targetDate = Carbon::parse($date);
 
             // 验证技师信息
-            $coach = CoachUser::with('info')->find($coachId);
+            $coach = CoachUser::find($coachId);
             abort_if(! $coach, 404, '技师不存在');
             abort_if(
-                $coach->info->state != TechnicianStatus::ACTIVE->value,
+                (int)$coach->state != TechnicianStatus::ACTIVE->value,
                 400,
                 '技师状态异常'
             );
@@ -552,7 +532,9 @@ class CoachService
                         return $this->formatResponse($date, []);
                     }
 
-                    $timeRanges = json_decode($schedule->time_ranges, true);
+                    $timeRanges = is_array($schedule->time_ranges)
+                        ? $schedule->time_ranges
+                        : json_decode($schedule->time_ranges, true);
 
                     // 获取当天所有订单
                     $dayOrders = $this->getDayOrders($coachId, $date);