# /opt/docker/dev/service_finder/backend/app/services/search_service.py import logging from typing import List, Optional, Dict, Any from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from geoalchemy2.functions import ST_Distance, ST_MakePoint, ST_DWithin from app.models.service import ServiceProfile, ExpertiseTag from app.models.organization import Organization from app.models.identity import User from app.services.config_service import config logger = logging.getLogger("Search-Service-2.4-Agnostic") class SearchService: """ Sentinel Master Search Service 2.4. Csomag-agnosztikus rangsoroló motor. Minden üzleti logika (súlyozás, prioritás, láthatóság) az adatbázisból jön. """ @staticmethod async def find_nearby_services( db: AsyncSession, lat: float, lon: float, current_user: User, expertise_key: str = None ): try: # 1. HIERARCHIKUS RANGSOROLÁSI SZABÁLYOK LEKÉRÉSE # A config_service automatikusan a legspecifikusabbat adja vissza: # 1. User-specifikus (Céges egyedi beállítás) # 2. Package-specifikus (pl. 'free', 'premium', 'ultra_gold') # 3. Global (Alapértelmezett) user_tier = current_user.tier_name if current_user.role in [UserRole.superadmin, UserRole.admin]: user_tier = "vip" ranking_rules = await config.get_setting( db, "RANKING_RULES", user_id=current_user.id, # Ez a belső config_service-ben kezeli a hierarchiát package_slug=user_tier, default={ "ad_weight": 5000, "partner_weight": 1000, "trust_weight": 10, "dist_penalty": 20, "pref_weight": 0, "can_use_prefs": False, "search_radius_km": 30 } ) # 2. PREFERENCIÁK (Ha a szabályrendszer engedi) user_prefs = {"networks": [], "ids": []} if ranking_rules.get("can_use_prefs", False): user_prefs = await config.get_setting( db, "USER_SEARCH_PREFERENCES", scope_level="user", scope_id=str(current_user.id), default={"networks": [], "ids": []} ) # 3. TÉRBELI LEKÉRDEZÉS user_point = ST_MakePoint(lon, lat) radius_m = ranking_rules.get("search_radius_km", 30) * 1000 distance_col = ST_Distance(ServiceProfile.location, user_point).label("distance_meters") stmt = ( select(ServiceProfile, Organization, distance_col) .join(Organization, ServiceProfile.organization_id == Organization.id) .where( and_( ST_DWithin(ServiceProfile.location, user_point, radius_m), ServiceProfile.is_active == True ) ) ) if expertise_key: stmt = stmt.join(ServiceProfile.expertises).join(ExpertiseTag).where(ExpertiseTag.key == expertise_key) result = await db.execute(stmt) rows = result.all() # 4. UNIVERZÁLIS PONTOZÁS (Súlyozott mátrix alapján) final_results = [] r = ranking_rules # Rövidítés a számításhoz for s_prof, org, dist_m in rows: dist_km = dist_m / 1000.0 score = 0 # --- PONTOZÁSI LOGIKA (Nincsenek fix csomagnevek!) --- # A. Hirdetési súly if s_prof.is_advertiser: score += r.get("ad_weight", 0) # B. Partner (Minősített) súly if s_prof.is_verified_partner: score += r.get("partner_weight", 0) # C. Minőség (Trust Score) súly score += (s_prof.trust_score * r.get("trust_weight", 0)) # D. Egyéni/Céges preferencia súly (Csak ha engedélyezett) if r.get("can_use_prefs"): if s_prof.network_slug in user_prefs.get("networks", []): score += r.get("pref_weight", 0) if str(org.id) in user_prefs.get("ids", []): score += r.get("pref_weight", 0) * 1.2 # E. Távolság büntetés score -= (dist_km * r.get("dist_penalty", 0)) final_results.append({ "id": org.id, "name": org.full_name, "trust_score": s_prof.trust_score, "distance_km": round(dist_km, 2), "rank_score": round(score, 2), "flags": { "is_ad": s_prof.is_advertiser, "is_partner": s_prof.is_verified_partner, "is_favorite": str(org.id) in user_prefs.get("ids", []) } }) # 5. RENDEZÉS return sorted(final_results, key=lambda x: x['rank_score'], reverse=True) except Exception as e: logger.error(f"Search Service 2.4 Critical Error: {e}") raise e