'skill_professional', // 手法专业 'IMAGE' => 'image_excellent', // 形象优秀 ]; /** * 用户当前位置信息 */ private readonly ?float $latitude; private readonly ?float $longitude; private readonly int $radius; /** * 缓存相关常量 */ private const CACHE_TTL = 300; // 缓存时间5分钟 private const CACHE_TAGS = 'coach_groups'; private const CACHE_KEY_PREFIX = 'coach_groups:'; /** * 构造函数 */ public function __construct(?float $latitude = null, ?float $longitude = null, ?int $radius = null) { $this->latitude = $latitude; $this->longitude = $longitude; $this->radius = $radius ?? self::DEFAULT_RADIUS; } /** * 获取所有分组的技师数据 * * @return array{technical: array, star: array, newcomer: array} */ public function getAllGroups(): array { $cacheKey = $this->generateCacheKey(); return Cache::tags(self::CACHE_TAGS)->remember($cacheKey, self::CACHE_TTL, function () { return [ 'technical' => $this->getTechnicalGroup(), 'star' => $this->getStarGroup(), 'newcomer' => $this->getNewcomerGroup(), ]; }); } /** * 生成缓存键 */ private function generateCacheKey(): string { $params = [ 'lat' => $this->latitude, 'lng' => $this->longitude, 'rad' => $this->radius, ]; return self::CACHE_KEY_PREFIX . md5(serialize($params)); } /** * 获取技术组技师列表 * * @return array 技师列表 */ protected function getTechnicalGroup(): array { $tagId = $this->getTagId(self::TAG_CODES['SKILL']); if (!$tagId) { return []; } return $this->getCoachesByTag($tagId, 'skill'); } /** * 获取明星组技师列表 * * @return array 技师列表 */ protected function getStarGroup(): array { $tagId = $this->getTagId(self::TAG_CODES['IMAGE']); if (!$tagId) { return []; } return $this->getCoachesByTag($tagId, 'image'); } /** * 获取新人组技师列表 * * @return array 技师列表 */ protected function getNewcomerGroup(): array { // 获取基础查询构建器 $query = $this->getBaseCoachQuery(); // 添加新人组特有的排序 $coaches = $query->orderByDesc('newcomer_sort') ->orderByDesc('newcomer_sort_updated_at') ->orderByDesc('updated_at') ->limit(self::GROUP_LIMIT) ->get(); // 如果有位置信息,添加距离并过滤 if ($this->hasLocation()) { $coaches = $this->appendCoachesDistance($coaches); } return $this->formatCoaches($coaches); } /** * 获取基础的技师查询构建器 */ private function getBaseCoachQuery(): \Illuminate\Database\Eloquent\Builder { return CoachUser::query() ->where('state', TechnicianStatus::ACTIVE) ->whereHas('info') ->with(['info', 'statistic']); } /** * 根据标签获取技师列表 * * @param int $tagId 标签ID * @param string $rateField 评分字段名 * @return array 技师列表 */ private function getCoachesByTag(int $tagId, string $rateField): array { // 获取基础查询构建器 $query = $this->buildTagBasedQuery($tagId, $rateField); // 执行查询 $coaches = $query->get(); // 处理位置信息 if ($this->hasLocation()) { $coaches = $this->appendCoachesDistance($coaches); $coaches = $coaches->sortBy('distance')->values(); } return $this->formatCoaches($coaches); } /** * 构建基于标签的查询构建器 * * @param int $tagId 标签ID * @param string $rateField 评分字段名 * @return \Illuminate\Database\Eloquent\Builder 查询构建器 */ private function buildTagBasedQuery(int $tagId, string $rateField): \Illuminate\Database\Eloquent\Builder { // 构建JSON路径 $jsonPath = $this->buildJsonPath($tagId); // 获取基础查询 $query = $this->getBaseTagStatisticQuery($jsonPath); // 添加选择字段 return $this->addTagStatisticFields($query, $jsonPath, $rateField); } /** * 构建JSON路径 * * @param int $tagId 标签ID * @return string JSON路径 */ private function buildJsonPath(int $tagId): string { return sprintf('$."%d"', $tagId); } /** * 获取基础标签统计查询 */ private function getBaseTagStatisticQuery(string $jsonPath): \Illuminate\Database\Eloquent\Builder { return CoachStatistic::query() ->join('coach_users', 'coach_statistics.coach_id', '=', 'coach_users.id') ->where('coach_users.state', TechnicianStatus::ACTIVE) ->where('coach_statistics.comment_count', '>=', self::MIN_COMMENTS) ->whereRaw('JSON_EXTRACT(tag_statistics, ?) IS NOT NULL', [$jsonPath]); } /** * 添加标签统计相关字段 * * @param \Illuminate\Database\Eloquent\Builder $query 查询构建器 * @param string $jsonPath JSON路径 * @param string $rateField 评分字段名 * @return \Illuminate\Database\Eloquent\Builder 查询构建器 */ private function addTagStatisticFields($query, string $jsonPath, string $rateField): \Illuminate\Database\Eloquent\Builder { // 构建标签统计表达式 $countExpr = $this->buildTagCountExpression($jsonPath); $rateExpr = $this->buildTagRateExpression($countExpr); return $query->select([ 'coach_users.*', 'coach_statistics.comment_count', 'coach_statistics.tag_statistics', 'coach_statistics.avg_score', DB::raw("{$countExpr} as {$rateField}_count"), DB::raw("{$rateExpr} as {$rateField}_rate"), ]) ->orderByRaw("{$rateField}_rate * avg_score DESC") ->with(['info']) ->limit(self::GROUP_LIMIT); } /** * 构建标签计数表达式 * * @param string $jsonPath JSON路径 * @return string SQL表达式 */ private function buildTagCountExpression(string $jsonPath): string { return "CAST(JSON_EXTRACT(tag_statistics, '{$jsonPath}') AS UNSIGNED)"; } /** * 构建标签比率表达式 * * @param string $countExpr 计数表达式 * @return string SQL表达式 */ private function buildTagRateExpression(string $countExpr): string { return "({$countExpr} / comment_count * 100)"; } /** * 获取标签ID * * @param string $code 标签代码 * @return int|null 标签ID */ private function getTagId(string $code): ?int { static $tagCache = []; // 使用静态缓存避免重复查询 if (!isset($tagCache[$code])) { $tagCache[$code] = CoachCommentTag::where('code', $code)->value('id'); } return $tagCache[$code]; } /** * 为技师列表添加距离信息 * * @param Collection $coaches 技师集合 * @return Collection 添加了距离信息的技师集合 */ private function appendCoachesDistance(Collection $coaches): Collection { // 批量获取位置信息 $locations = $this->batchGetCoachLocations($coaches); // 批量计算距离 return $this->batchCalculateDistances($coaches, $locations); } /** * 批量获取技师位置信息 * * @param Collection $coaches 技师集合 * @return array 位置信息数组 [coach_id => [longitude, latitude]] */ private function batchGetCoachLocations(Collection $coaches): array { if ($coaches->isEmpty()) { return []; } // 批量生成位置键名 $locationKeys = $coaches->map(fn($coach) => $this->getLocationKey($coach->id))->toArray(); // 使用管道批量获取位置信息 $positions = Redis::pipeline(function ($pipe) use ($locationKeys) { foreach ($locationKeys as $key) { $pipe->geopos(self::REDIS_KEY, $key); } }); // 整理位置信息 $locations = []; foreach ($coaches as $index => $coach) { if (!empty($positions[$index][0])) { $locations[$coach->id] = $positions[$index][0]; } } return $locations; } /** * 批量计算技师距离 * * @param Collection $coaches 技师集合 * @param array $locations 位置信息数组 * @return Collection 添加了距离信息的技师集合 */ private function batchCalculateDistances(Collection $coaches, array $locations): Collection { if (empty($locations)) { return $coaches; } // 创建临时位置点 $tempKey = $this->createTempLocationPoint(); try { // 使用管道批量计算距离 $distances = Redis::pipeline(function ($pipe) use ($locations, $tempKey) { foreach ($locations as $coachId => $position) { $locationKey = $this->getLocationKey($coachId); $pipe->geodist(self::REDIS_KEY, $tempKey, $locationKey, 'km'); } }); // 为技师添加距离信息 $index = 0; foreach ($coaches as $coach) { if (isset($locations[$coach->id])) { $coach->distance = (float)$distances[$index++]; } } // 过滤并排序 return $coaches ->filter(fn($coach) => isset($coach->distance) && $coach->distance <= $this->radius) ->values(); } finally { $this->removeTempLocationPoint($tempKey); } } /** * 获取技师位置的Redis键名 * * @param int $coachId 技师ID * @return string Redis键名 */ private function getLocationKey(int $coachId): string { return sprintf('%d_%s', $coachId, self::LOCATION_TYPE); } /** * 创建临时位置点 * * @return string ���时点的键名 */ private function createTempLocationPoint(): string { $tempKey = sprintf('temp_%f_%f', $this->latitude, $this->longitude); Redis::geoadd( self::REDIS_KEY, $this->longitude, $this->latitude, $tempKey ); return $tempKey; } /** * 删除临时位置点 * * @param string $tempKey 临时点的键名 */ private function removeTempLocationPoint(string $tempKey): void { Redis::zrem(self::REDIS_KEY, $tempKey); } /** * 检查是否有位置信息 * * @return bool 是否有位置信息 */ private function hasLocation(): bool { return $this->latitude !== null && $this->longitude !== null; } /** * 格式化技师数据 * * @param Collection $coaches 技师集合 * @return array 格式化后的技师数组 */ protected function formatCoaches(Collection $coaches): array { return $coaches->map(function ($coach) { return [ ...$this->getBasicInfo($coach), ...$this->getLocationInfo($coach), ...$this->getEvaluationInfo($coach), ...$this->getPersonalInfo($coach), ]; })->toArray(); } /** * 获取技师基本信息 * * @param CoachUser $coach 技师模型 * @return array 基本信息数组 */ private function getBasicInfo($coach): array { return [ 'id' => $coach->id, 'name' => $coach->info->nickname ?? '未知', 'avatar' => $coach->info->portrait_images[0] ?? null, 'portrait_images' => collect($coach->info->portrait_images ?? [])->values()->toArray(), 'formal_photo' => $coach->formal_photo, ]; } /** * 获取技师地理和工作信息 * * @param CoachUser $coach 技师模型 * @return array 地理和工作信息数组 */ private function getLocationInfo($coach): array { $data = [ 'city' => $coach->info->intention_city ?? '', 'work_years' => (int)($coach->info->work_years ?? 0), ]; // 添加距离信息(如果有) if (isset($coach->distance)) { $data['distance'] = round($coach->distance, 1); } return $data; } /** * 获取技师评价信息 * * @param CoachUser $coach 技师模型 * @return array 评价信息数组 */ private function getEvaluationInfo($coach): array { return [ 'avg_score' => round(optional($coach->statistic)->avg_score ?? 0, 1), 'comment_count' => (int)(optional($coach->statistic)->comment_count ?? 0), ]; } /** * 获取技师个人信息 * * @param CoachUser $coach 技师模型 * @return array 个人信息数组 */ private function getPersonalInfo($coach): array { return [ 'skill_tags' => $coach->info->skill_tags ?? [], 'description' => $coach->info->description ?? '', 'gender' => $coach->info->gender ?? 0, 'age' => (int)($coach->info->age ?? 0), 'height' => (int)($coach->info->height ?? 0), 'weight' => (int)($coach->info->weight ?? 0), ]; } }