|
@@ -146,6 +146,7 @@ class CoachService
|
|
|
{
|
|
|
|
|
|
$user = MemberUser::findOrFail($userId);
|
|
|
+ abort_if(!$user, 401, '用户未登录');
|
|
|
abort_if($user->state !== UserStatus::OPEN->value, 400, '用户状态异常');
|
|
|
|
|
|
|
|
@@ -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)
|
|
|
{
|
|
|
-
|
|
|
+
|
|
|
$this->validateBasicRequirements(Auth::id());
|
|
|
|
|
|
-
|
|
|
- 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('用户状态异常');
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
$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, '技师不存在');
|
|
|
|
|
|
-
|
|
|
- $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);
|
|
|
|
|
|
-
|
|
|
- $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);
|