Browse Source

fixed:用户端-获取附近技师

刘学玺 3 months ago
parent
commit
2f0cee5f2b

+ 98 - 20
app/Http/Controllers/Client/CoachController.php

@@ -2,10 +2,11 @@
 
 namespace App\Http\Controllers\Client;
 
-use App\Http\Controllers\Controller;
-use App\Services\Client\CoachService;
 use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
 use Illuminate\Support\Facades\Auth;
+use App\Services\Client\CoachService;
+use App\Http\Requests\Client\Coach\ListNearbyCoachRequest;
 
 /**
  * @group 用户端
@@ -24,32 +25,109 @@ class CoachController extends Controller
     /**
      * [技师]获取附近技师列表
      *
-     * 根据经纬度获取技师列表
+     * @description 根据用户当前位置获取指定范围内的技师列表,支持分页和距离筛选
      *
-     * @authenticated
+     * 业务流程:
+     * 1. 验证用户位置和分页参数
+     * 2. 获取系统设置的最大搜索半径
+     * 3. 查询指定范围内的技师
+     * 4. 根据技师个人设置的接单距离筛选
+     * 5. 返回分页后的技师列表
      *
-     * @queryParam latitude float required 纬度. Example: 39.9042
-     * @queryParam longitude float required 经度. Example: 116.4074
+     * 筛选规则:
+     * - 技师必须通过基本信息认证
+     * - 技师必须通过实名认证
+     * - 技师必须通过资质认证
+     * - 如果技师设置了接单距离,���户距离必须在范围内
+     * - 未设置接单距离的技师,只要在系统搜索半径内就显示
      *
-     * @response {
+     * 排序规则:
+     * - 按照技师到用户的实际距离升序排序
+     * - 未获取到距离的技师排在最后
+     *
+     * @authenticated 需要用户登录认证
+     *
+     * @queryParam page int 当前页码,默认1. Example: 1
+     * @queryParam per_page int 每页数量,默认15. Example: 15
+     * @queryParam latitude float required 用户当前纬度坐标(-90到90). Example: 39.9042
+     * @queryParam longitude float required 用户当前经度坐标(-180到180). Example: 116.4074
+     *
+     * @response scenario=success {
      *   "code": 200,
-     *   "message": "获取成功",
-     *   "data": [
-     *     {
-     *       "id": 1,
-     *       "name": "技师A",
-     *       "latitude": 34.0522,
-     *       "longitude": -118.2437
-     *     }
-     *   ]
+     *   "message": "success",
+     *   "data": {
+     *     "items": [
+     *       {
+     *         "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",
+     *         "deleted_at": null,
+     *         "is_vip": 1,
+     *         "vip_time": null,
+     *         "invite_code": null,
+     *         "qr_code": null,
+     *         "formal_photo": null,
+     *         "formal_photo_remark": null,
+     *         "formal_photo_updated_at": null,
+     *         "formal_photo_admin_id": null,
+     *         "newcomer_sort": 0,
+     *         "newcomer_sort_updated_at": null,
+     *         "newcomer_sort_admin_id": null,
+     *         "distance": 0,
+     *         "state_text": "正常服务",
+     *         "info": {
+     *           "id": 53,
+     *           "nickname": "张三1",
+     *           "avatar": null,
+     *           "gender": "1",
+     *           "state_text": ""
+     *         },
+     *         "distance": 2.5  // 用户到技师的距离(公里)
+     *       }
+     *     ],
+     *     "total": 1  // 符合条件的技师总数
+     *   }
+     * }
+     *
+     * @response status=401 scenario="未登录" {
+     *   "message": "用户未登录"
+     * }
+     * @response status=400 scenario="状态异常" {
+     *   "message": "用户状态异常"
+     * }
+     * @response status=422 scenario="参数错误" {
+     *   "message": "验证错误",
+     *   "errors": {
+     *     "latitude": ["纬度不能为空"],
+     *     "longitude": ["经度不能为空"]
+     *   }
+     * }
+     * @response status=500 scenario="系统错误" {
+     *   "message": "Redis服务不可用"
      * }
      */
-    public function list(Request $request)
+    public function list(ListNearbyCoachRequest $request)
     {
-        $latitude = $request->input('latitude');
-        $longitude = $request->input('longitude');
+        // 获取验证后的数据
+        $validated = $request->validated();
 
-        return $this->success($this->service->getNearCoachList(Auth::user()->id, $latitude, $longitude));
+        // 调用服务层获取技师列表
+        return $this->success($this->service->getNearCoachList(
+            Auth::user()->id,        // 当前登录用户ID
+            $validated['latitude'],   // 用户当前纬度
+            $validated['longitude']   // 用户当前经度
+        ));
     }
 
     /**

+ 51 - 0
app/Http/Requests/Client/Coach/ListNearbyCoachRequest.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Http\Requests\Client\Coach;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ListNearbyCoachRequest extends FormRequest
+{
+    /**
+     * 确定用户是否有权发起此请求
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * 获取适用于请求的验证规则
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        return [
+            'latitude' => 'required|numeric|between:-90,90',
+            'longitude' => 'required|numeric|between:-180,180',
+            'page' => 'nullable|integer|min:1',
+            'per_page' => 'nullable|integer|min:1|max:50'
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'latitude.required' => '纬度不能为空',
+            'latitude.numeric' => '纬度必须是数字',
+            'latitude.between' => '纬度必须在 -90 到 90 之间',
+            'longitude.required' => '经度不能为空',
+            'longitude.numeric' => '经度必须是数字',
+            'longitude.between' => '经度必须在 -180 到 180 之间',
+            'page.integer' => '页码必须是整数',
+            'page.min' => '页码不能小于1',
+            'per_page.integer' => '每页数量必须是整数',
+            'per_page.min' => '每页数量不能小于1',
+            'per_page.max' => '每页数量不能超过50'
+        ];
+    }
+}

+ 10 - 0
app/Models/CoachUser.php

@@ -22,6 +22,7 @@ use App\Models\ShopCoachService;
 use App\Enums\TechnicianLocationType;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Slowlyo\OwlAdmin\Models\BaseModel as Model;
+use App\Models\SettingValue;
 
 /**
  * 技师用户模型
@@ -325,4 +326,13 @@ class CoachUser extends Model
         return $this->hasOne(CoachStatistic::class, 'coach_id');
     }
 
+    /**
+     * @Author FelixYin
+     *
+     * @description 技师关联设置
+     */
+    public function settings()
+    {
+        return $this->morphMany(SettingValue::class, 'object');
+    }
 }

+ 15 - 3
app/Models/SettingItem.php

@@ -10,7 +10,19 @@ use Slowlyo\OwlAdmin\Models\BaseModel as Model;
  */
 class SettingItem extends Model
 {
-	use SoftDeletes;
+    use SoftDeletes;
 
-	protected $table = 'setting_items';
-}
+    protected $table = 'setting_items';
+
+    // 应该添加与 setting_groups 的关联
+    public function group()
+    {
+        return $this->belongsTo(SettingGroup::class, 'group_id');
+    }
+
+    // 与 setting_values 的关联
+    public function values()
+    {
+        return $this->hasMany(SettingValue::class, 'item_id');
+    }
+}

+ 9 - 0
app/Models/SettingValue.php

@@ -4,6 +4,7 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Slowlyo\OwlAdmin\Models\BaseModel as Model;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
  * 设置值管理
@@ -15,4 +16,12 @@ class SettingValue extends Model
     protected $table = 'setting_values';
 
     protected $guarded = [];
+
+    /**
+     * 关联设置项
+     */
+    public function item()
+    {
+        return $this->belongsTo(SettingItem::class, 'item_id');
+    }
 }

+ 406 - 118
app/Services/Client/CoachService.php

@@ -2,106 +2,341 @@
 
 namespace App\Services\Client;
 
-use App\Enums\OrderStatus;
-use App\Enums\TechnicianAuthStatus;
-use App\Enums\TechnicianLocationType;
-use App\Enums\TechnicianStatus;
+use App\Models\Order;
 use App\Enums\UserStatus;
-use App\Models\CoachSchedule;
 use App\Models\CoachUser;
+use App\Enums\OrderStatus;
 use App\Models\MemberUser;
-use App\Models\Order;
+use App\Models\CoachSchedule;
 use Illuminate\Support\Carbon;
+use App\Enums\TechnicianStatus;
+use App\Enums\TechnicianAuthStatus;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Auth;
+use App\Enums\TechnicianLocationType;
 use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Redis;
-
+use App\Models\SettingItem;
+
+/**
+ * 用户端技师服务类
+ *
+ * 该服务类处理与技师相关的所有业务逻辑,包括:
+ * - 获取附近技师列表
+ * - 获取技师详细信息
+ * - 管理技师位置信息
+ * - 处理技师排班和预约时间段
+ *
+ * 主要功能:
+ * 1. 基于地理位置的技师查询和排序
+ * 2. 技师认证状态验证
+ * 3. 技师位置信息的缓存管理
+ * 4. 技师预约时间段的计算和管理
+ */
 class CoachService
 {
     /**
-     * 获取技师列表
+     * 获取附近技师列表
+     *
+     * 业务流程:
+     * 1. 验证用户身份和状态
+     * 2. 获取系统设置的最大搜索半径
+     * 3. 使用Redis GEO功能查询范围内的技师
+     * 4. 过滤并验证技师认证状态
+     * 5. 根据技师接单距离设置筛选
+     * 6. 计算并排序技师距离
+     * 7. 返回分页后的技师列表
+     *
+     * 技术实现:
+     * - 使用Redis GEORADIUS命令进行地理位置查询
+     * - 使用Eloquent关联加载技师信息
+     * - 实现基于距离的排序
+     * - 支持分页功能
+     *
+     * 数据处理:
+     * - 技师位置数据存储在Redis中
+     * - 技师基本信息从数据库获取
+     * - 认证状态多表关联验证
+     * - 接单距离设置验证
+     * - 距离数据格式化(保留2位小数)
+     *
+     * 筛选条件:
+     * 1. 技师认证状态检查
+     *    - 基本信息认证通过
+     *    - 实名认证通过
+     *    - 资质认证通过
+     * 2. 距离筛选
+     *    - 有设置接单距离:用户距离 <= 设置值
+     *    - 无设置接单距离:在系统搜索半径内即可
+     *
+     * 排序规则:
+     * - 按照技师到用户的实际距离升序排序
+     * - 未获取到距离的技师排在最后
+     *
+     * 异常处理:
+     * - 用户未登录/状态异常
+     * - Redis连接异常
+     * - 无效的地理坐标
+     * - 设置项未找到
+     *
+     * @param int $userId 当前用户ID
+     * @param float $latitude 纬度坐标 (-90 到 90)
+     * @param float $longitude 经度坐标 (-180 到 180)
+     * @return array{
+     *     items: array{
+     *         id: int,
+     *         info: array{
+     *             id: int,
+     *             nickname: string,
+     *             avatar: string,
+     *             gender: int
+     *         },
+     *         distance: float
+     *     }[],
+     *     total: int
+     * } 技师列表和总数
+     * @throws \Exception 当用户验证失败或Redis操作异常时
      */
     public function getNearCoachList(int $userId, float $latitude, float $longitude)
     {
-        $page = request()->get('page', 1);
+        // 初始化分页参数
         $perPage = request()->get('per_page', 15);
-        // 获取当前用户
-        $user = MemberUser::find($userId);
 
-        Log::info('Current user and coordinates:', [
-            'user' => $user ? $user->id : null,
-            'latitude' => $latitude,
-            'longitude' => $longitude,
-        ]);
+        // 基础验证(用户状态和Redis连接)
+        $this->validateBasicRequirements($userId);
 
-        // 检查用户状态
-        if (! $user) {
-            throw new \Exception('用户未登录');
-        }
+        // 获取搜索半径设置
+        $distanceSettingItem = $this->getDistanceSetting();
+        $maxSearchRadius = $distanceSettingItem?->max_value ?? 40; // 默认40公里
 
-        if ($user->state !== UserStatus::OPEN->value) {
-            throw new \Exception('用户状态异常');
+        // 获取附近的技师(使用Redis GEO功能)
+        $nearbyCoaches = $this->getNearbyCoaches($longitude, $latitude, $maxSearchRadius);
+        if (empty($nearbyCoaches)) {
+            return ['items' => [], 'total' => 0];
         }
 
-        // 使用 Redis 的 georadius 命令获取附近的技师 ID
-        $nearbyCoachIds = Redis::georadius('coach_locations', $longitude, $latitude, 40, 'km', ['WITHDIST']);
+        // 处理技师距离数据(提取ID和距离信息)
+        [$coachDistances, $coachIds] = $this->processCoachDistances($nearbyCoaches);
 
-        $coachData = array_map(function ($item) {
-            [$id, $type] = explode('_', $item[0]);
+        // 构建技师查询(包含认证状态和距离筛选)
+        $query = $this->buildCoachQuery($coachIds, $distanceSettingItem, $coachDistances);
 
-            return ['id' => $id, 'type' => $type, 'distance' => $item[1]];
-        }, $nearbyCoachIds);
+        // 获取分页结果(带基本信息)
+        $coaches = $query->with(['info:id,nickname,avatar,gender'])
+            ->paginate($perPage);
 
-        // 提取所有的id
-        $coachIds = array_unique(array_column($coachData, 'id'));
+        // 处理返回结果(添加距离信息)
+        $items = $this->processCoachResults($coaches, $coachDistances);
 
-        // 分页截取 coachIds
-        $paginatedCoachIds = array_slice($coachIds, ($page - 1) * $perPage, $perPage);
+        return [
+            'items' => $items,
+            'total' => $coaches->total()
+        ];
+    }
 
-        // 查询数据库获取技师信息
-        $coaches = CoachUser::query()
-            ->whereIn('id', $paginatedCoachIds)
-            ->whereHas('info', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
-            })
-            ->whereHas('real', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
-            })
-            ->whereHas('qual', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
+    /**
+     * 验证基本要求
+     *
+     * @param int $userId 用户ID
+     * @param bool $checkRedis 是否检查Redis连接
+     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException 当用户不存在时
+     * @throws \Symfony\Component\HttpKernel\Exception\HttpException 当验证失败时
+     */
+    protected function validateBasicRequirements(int $userId, bool $checkRedis = true): void
+    {
+        // 验证用户状态
+        $user = MemberUser::findOrFail($userId);
+        abort_if($user->state !== UserStatus::OPEN->value, 400, '用户状态异常');
+
+        // 检查Redis连接
+        if ($checkRedis) {
+            abort_if(!Redis::ping(), 500, 'Redis服务不可用');
+        }
+    }
+
+    /**
+     * 获取距离设置项
+     *
+     * @return \App\Models\SettingItem|null 距离设置项
+     */
+    protected function getDistanceSetting(): ?SettingItem
+    {
+        return SettingItem::query()
+            ->whereHas('group', function ($query) {
+                $query->where('code', 'order');
             })
-            ->with(['info:id,nickname,avatar,gender'])
-            ->paginate($perPage);
+            ->where('code', 'distance')
+            ->first();
+    }
 
-        // 遍历技师并设置距离
-        foreach ($coaches as $coach) {
-            $coach->distance = round($coachData[array_search($coach->id, array_column($coachData, 'id'))]['distance'] ?? null, 2);
+    /**
+     * 获取附近的技师
+     *
+     * @param float $longitude 经度
+     * @param float $latitude 纬度
+     * @param int $maxSearchRadius 最大搜索半径(km)
+     * @return array 附近技师的位置和距离信息
+     */
+    protected function getNearbyCoaches(float $longitude, float $latitude, int $maxSearchRadius): array
+    {
+        return Redis::georadius(
+            'coach_locations',
+            $longitude,
+            $latitude,
+            $maxSearchRadius,
+            'km',
+            ['WITHDIST']
+        );
+    }
+
+    /**
+     * 处理技师距离数据
+     *
+     * @param array $nearbyCoaches Redis返回的原始数据
+     * @return array{0: array<string, float>, 1: array<int>} [距离映射, 技师ID列表]
+     */
+    protected function processCoachDistances(array $nearbyCoaches): array
+    {
+        $coachDistances = [];
+        $coachIds = [];
+
+        foreach ($nearbyCoaches as $coach) {
+            [$id, $type] = explode('_', $coach[0]);
+            // 只保留最近的距离
+            if (!isset($coachDistances[$id]) || $coach[1] < $coachDistances[$id]) {
+                $coachDistances[$id] = (float)$coach[1];
+                $coachIds[] = $id;
+            }
         }
-        // 按 distance 升序排序
-        $coaches = $coaches->sortBy('distance')->values();
 
-        return $coaches;
+        return [$coachDistances, array_unique($coachIds)];
+    }
+
+    /**
+     * 添加技师认证状态检查条件
+     *
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    protected function addAuthStatusChecks($query)
+    {
+        return $query->whereHas('info', fn($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
+            ->whereHas('real', fn($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
+            ->whereHas('qual', fn($q) => $q->where('state', TechnicianAuthStatus::PASSED->value));
+    }
+
+    /**
+     * 构建技师查询
+     *
+     * @param array $coachIds 技师ID列表
+     * @param SettingItem|null $distanceSettingItem 距离设置项
+     * @param array $coachDistances 技师距离映射
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    protected function buildCoachQuery(array $coachIds, ?SettingItem $distanceSettingItem, array $coachDistances): \Illuminate\Database\Eloquent\Builder
+    {
+        $query = CoachUser::query()
+            ->whereIn('id', $coachIds);
+
+        // 添加认证状态检查
+        $this->addAuthStatusChecks($query);
+
+        // 添加接单距离筛选条件
+        if ($distanceSettingItem) {
+            $query->where(function ($q) use ($distanceSettingItem, $coachDistances) {
+                $q->whereHas('settings', function ($sq) use ($distanceSettingItem, $coachDistances) {
+                    // 检查用户距离是否在技师设置的接单范围内
+                    $sq->where('item_id', $distanceSettingItem->id)
+                        ->whereRaw('? <= CAST(value AS DECIMAL)', [
+                            $coachDistances[$sq->getModel()->coach_id] ?? 0
+                        ]);
+                })
+                    // 未设置接单距离的技师直接显示
+                    ->orWhereDoesntHave('settings', function ($sq) use ($distanceSettingItem) {
+                        $sq->where('item_id', $distanceSettingItem->id);
+                    });
+            });
+        }
+
+        // 按实际距离排序
+        if (!empty($coachDistances)) {
+            $query->orderByRaw('CASE coach_users.id ' .
+                collect($coachDistances)->map(function ($distance, $id) {
+                    return "WHEN $id THEN CAST($distance AS DECIMAL(10,2))";
+                })->implode(' ') .
+                ' ELSE ' . ($distanceSettingItem->default_value ?? PHP_FLOAT_MAX) .
+                ' END ASC');
+        }
+
+        return $query;
+    }
+
+    /**
+     * 处理技师查询结果
+     *
+     * @param \Illuminate\Pagination\LengthAwarePaginator $coaches 分页后的技师数据
+     * @param array $coachDistances 技师距离映射
+     * @return \Illuminate\Support\Collection
+     */
+    protected function processCoachResults($coaches, array $coachDistances): \Illuminate\Support\Collection
+    {
+        return $coaches->getCollection()
+            ->map(function ($coach) use ($coachDistances) {
+                // 添加距离信息(保留2位小数)
+                $coach->distance = round($coachDistances[$coach->id] ?? 0, 2);
+                return $coach;
+            })
+            ->values();
     }
 
     /**
      * 获取技师详情
+     *
+     * 业务流程:
+     * 1. 验证Redis连接状态
+     * 2. 验证用户身份和状态
+     * 3. 获取技师基本信息和认证状态
+     * 4. 获取技师位置信息
+     * 5. 计算用户与技师之间的距离
+     *
+     * 技术实现:
+     * - Redis连接状态检查
+     * - 多重认证状态验证
+     * - 地理位置距离计算
+     * - 数据关联查询
+     *
+     * 数据验证:
+     * - 技师ID有效性
+     * - 用户状态检查
+     * - 地理坐标有效性
+     * - 认证状态验证
+     *
+     * 异常处理:
+     * - Redis连接异常
+     * - 技师不存在
+     * - 无效坐标
+     * - 认证状态异常
+     *
+     * @param int $coachId 技师ID
+     * @param float|null $latitude 用户当前纬度
+     * @param float|null $longitude 用户当前经度
+     * @return CoachUser 技师详细信息
+     * @throws \Exception 当验证失败或查询异常时
      */
     public function getCoachDetail($coachId, $latitude, $longitude)
     {
+        // 验证基本要求
+        $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());
+            throw new \Exception('Redis连接失败:' . $e->getMessage());
         }
 
-        // 检查Redis中的所有位置数据
-        $allLocations = Redis::zrange('coach_locations', 0, -1, 'WITHSCORES');
-        Log::info('All locations in Redis:', ['locations' => $allLocations]);
-
         // 获取当前用户
         $user = Auth::user();
 
@@ -120,18 +355,11 @@ class CoachService
         if ($user->state !== UserStatus::OPEN->value) {
             throw new \Exception('用户状态异常');
         }
-        // 获取技师信息
-        $coach = CoachUser::where('state', TechnicianStatus::ACTIVE->value)
-            ->whereHas('info', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
-            })
-            ->whereHas('real', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
-            })
-            ->whereHas('qual', function ($query) {
-                $query->where('state', TechnicianAuthStatus::PASSED->value);
-            })
-            ->with(['info:id,nickname,avatar,gender'])
+        // 修改查询以处理可能的 null 值
+        $query = CoachUser::where('state', TechnicianStatus::ACTIVE->value);
+        $this->addAuthStatusChecks($query);
+
+        $coach = $query->with(['info:id,nickname,avatar,gender'])
             ->find($coachId);
 
         if (! $coach) {
@@ -139,17 +367,17 @@ class CoachService
         }
 
         // 从 Redis 获取技师的 id_home 和 id_work 的经纬度
-        $homeLocation = Redis::geopos('coach_locations', $coachId.'_'.TechnicianLocationType::COMMON->value);
-        $workLocation = Redis::geopos('coach_locations', $coachId.'_'.TechnicianLocationType::CURRENT->value);
+        $homeLocation = Redis::geopos('coach_locations', $coachId . '_' . TechnicianLocationType::COMMON->value);
+        $workLocation = Redis::geopos('coach_locations', $coachId . '_' . TechnicianLocationType::CURRENT->value);
 
-        // 检输入的经纬度
+        // 检输入的经纬度否效
         if (! is_numeric($latitude) || ! is_numeric($longitude)) {
             Log::error('Invalid coordinates:', ['latitude' => $latitude, 'longitude' => $longitude]);
             throw new \Exception('无效的经纬度坐标');
         }
 
         // 临时存储用户当前位置用于计算距离
-        $tempKey = 'user_temp_'.$user->id;
+        $tempKey = 'user_temp_' . $user->id;
         Redis::geoadd('coach_locations', $longitude, $latitude, $tempKey);
 
         // 计算距离(单位:km)
@@ -157,20 +385,20 @@ class CoachService
         $distanceWork = null;
 
         if ($homeLocation && ! empty($homeLocation[0])) {
-            $distanceHome = Redis::geodist('coach_locations', $tempKey, $coachId.'_'.TechnicianLocationType::COMMON->value, 'km');
+            $distanceHome = Redis::geodist('coach_locations', $tempKey, $coachId . '_' . TechnicianLocationType::COMMON->value, 'km');
             Log::info('Home distance calculation:', [
                 'from' => $tempKey,
-                'to' => $coachId.'_'.TechnicianLocationType::COMMON->value,
+                'to' => $coachId . '_' . TechnicianLocationType::COMMON->value,
                 'distance' => $distanceHome,
                 'home_location' => $homeLocation[0],
             ]);
         }
 
         if ($workLocation && ! empty($workLocation[0])) {
-            $distanceWork = Redis::geodist('coach_locations', $tempKey, $coachId.'_'.TechnicianLocationType::CURRENT->value, 'km');
+            $distanceWork = Redis::geodist('coach_locations', $tempKey, $coachId . '_' . TechnicianLocationType::CURRENT->value, 'km');
         }
 
-        // 删除临位置点
+        // 删除临位置点
         Redis::zrem('coach_locations', $tempKey);
 
         // 选择最近的距离
@@ -183,13 +411,32 @@ class CoachService
     /**
      * 设置技师位置信息
      *
-     * @param  int  $coachId  技师ID
-     * @param  float  $latitude  纬度
-     * @param  float  $longitude  经度
-     * @param  int  $type  位置类型 (current|common)
-     * @return bool
+     * 业务流程:
+     * 1. 验证位置信息的有效性
+     * 2. 根据类型更新技师位置
+     * 3. 验证更新结果
+     *
+     * 技术实现:
+     * - 使用Redis GEOADD命令存储位置
+     * - 支持多种位置类型(常驻/当前)
+     * - 位置更新验证
      *
-     * @throws \Exception
+     * 数据验证:
+     * - 标范围检查
+     * - 位置类型验证
+     * - 技师ID验证
+     *
+     * 日志记录:
+     * - 位置更新操作
+     * - 错误信息
+     * - 验证结果
+     *
+     * @param int $coachId 技师ID
+     * @param float $latitude 纬度坐标
+     * @param float $longitude 经度坐标
+     * @param int $type 位置类型 (TechnicianLocationType 举值)
+     * @return bool 更新是否成功
+     * @throws \Exception 当参数无效或Redis操作失败时
      */
     public function setCoachLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value)
     {
@@ -206,7 +453,7 @@ class CoachService
             throw new \Exception('无效的位置类型,必须是 current 或 common');
         }
 
-        $key = $coachId.'_'.$type;
+        $key = $coachId . '_' . $type;
         $result = Redis::geoadd('coach_locations', $longitude, $latitude, $key);
 
         Log::info('Coach location set:', [
@@ -231,25 +478,34 @@ class CoachService
     /**
      * 获取技师可预约时间段列表
      *
-     * @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
+     * 业务流程:
+     * 1. 验证技师信息和状态
+     * 2. 获取技师排班设置
+     * 3. 获取已有预约订单
+     * 4. 计算用时间段
+     * 5. 缓存处结果
+     *
+     * 技术实现:
+     * - 使用Carbon处理日期时间
+     * - 缓存机制优化查询性能
+     * - 订单冲突检测
+     * - 时间段生成算法
+     *
+     * 数据处理:
+     * - 排班数据解析
+     * - 订单时间过滤
+     * - 时间段计算
+     * - 数据格式化
+     *
+     * 缓存策略:
+     * - 15分钟缓存时间
+     * - 按技师和日期缓存
+     * - 支持缓存更新
+     *
+     * @param int $coachId 技师ID
+     * @param string|null $date 日期,默认当天
+     * @return array 格式化的时间段表
+     * @throws \Exception 当验证失败或理异常时
      */
     public function getSchedule(int $coachId, ?string $date = null)
     {
@@ -261,14 +517,23 @@ class CoachService
             // 验证技师信息
             $coach = CoachUser::with('info')->find($coachId);
             abort_if(! $coach, 404, '技师不存在');
-            abort_if($coach->info->state != TechnicianStatus::ACTIVE->value,
-                400, '技师状态异常');
+            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天内的时间段');
+            abort_if(
+                $targetDate->startOfDay()->lt(now()->startOfDay()),
+                400,
+                '不能询过去的日期'
+            );
+            abort_if(
+                $targetDate->diffInDays(now()) > 30,
+                400,
+                '只能查询未来30天内的时间段'
+            );
 
             $cacheKey = "coach:timeslots:{$coachId}:{$date}";
             Cache::forget($cacheKey);
@@ -303,7 +568,6 @@ class CoachService
                     return $this->formatResponse($date, $timeSlots);
                 }
             );
-
         } catch (\Exception $e) {
             Log::error('获取可预约时间段失败', [
                 'coach_id' => $coachId,
@@ -334,7 +598,7 @@ class CoachService
                 OrderStatus::ACCEPTED->value,
                 OrderStatus::DEPARTED->value,
                 OrderStatus::ARRIVED->value,
-                OrderStatus::SERVING->value,
+                OrderStatus::SERVICING->value,
             ])
             ->select(['id', 'service_start_time', 'service_end_time', 'state'])
             ->get()
@@ -345,7 +609,7 @@ class CoachService
      * 生成可用时间段列表
      *
      * @param  string  $date  日期
-     * @param  array  $timeRanges  班时间段
+     * @param  array  $timeRanges  班时间段
      * @param  array  $dayOrders  当天订单
      * @param  bool  $isToday  是否是当天
      * @return array 可用时间段列表
@@ -360,10 +624,10 @@ class CoachService
         $currentTime = now();
 
         foreach ($timeRanges as $range) {
-            $start = Carbon::parse($date.' '.$range['start_time']);
-            $end = Carbon::parse($date.' '.$range['end_time']);
+            $start = Carbon::parse($date . ' ' . $range['start_time']);
+            $end = Carbon::parse($date . ' ' . $range['end_time']);
 
-            // 果是当天且开始时间已过,从下一个30分钟时间点开始
+            // 果是当天且开始时间已过,从下一个30分钟时间点开始
             if ($isToday && $start->lt($currentTime)) {
                 $start = $currentTime->copy()->addMinutes(30)->floorMinutes(30);
                 // 如果调整后的开始时间已超过结束时间,跳过此时间段
@@ -440,7 +704,8 @@ class CoachService
             // 检查时间段是否重叠
             if (($slotStart >= $orderStart && $slotStart < $orderEnd) ||
                 ($slotEnd > $orderStart && $slotEnd <= $orderEnd) ||
-                ($slotStart <= $orderStart && $slotEnd >= $orderEnd)) {
+                ($slotStart <= $orderStart && $slotEnd >= $orderEnd)
+            ) {
                 return true;
             }
         }
@@ -457,8 +722,8 @@ class CoachService
         $coachSchedule = $this->getSchedule($coachId, $serviceDateTime->format('Y-m-d'));
 
         foreach ($coachSchedule['time_slots'] as $slot) {
-            $slotStart = Carbon::parse($serviceDateTime->format('Y-m-d').' '.$slot['start_time']);
-            $slotEnd = Carbon::parse($serviceDateTime->format('Y-m-d').' '.$slot['end_time']);
+            $slotStart = Carbon::parse($serviceDateTime->format('Y-m-d') . ' ' . $slot['start_time']);
+            $slotEnd = Carbon::parse($serviceDateTime->format('Y-m-d') . ' ' . $slot['end_time']);
 
             if ($serviceDateTime->between($slotStart, $slotEnd)) {
                 return true;
@@ -467,4 +732,27 @@ class CoachService
 
         return false;
     }
+
+    /**
+     * 获取技师的地理位置信
+     *
+     * @param string $key Redis中的位置键名
+     * @return array|null 位置信息
+     */
+    protected function getCoachLocation(string $key): ?array
+    {
+        return Redis::geopos('coach_locations', $key);
+    }
+
+    /**
+     * 计算两点之间的距离
+     *
+     * @param string $from 起点位置键名
+     * @param string $to 终点位置键名
+     * @return float|null 距离(km)
+     */
+    protected function calculateDistance(string $from, string $to): ?float
+    {
+        return Redis::geodist('coach_locations', $from, $to, 'km');
+    }
 }