343 lines
13 KiB
Python
343 lines
13 KiB
Python
# /opt/docker/dev/service_finder/backend/app/services/trust_engine.py
|
|
"""
|
|
Gondos Gazda Index (Trust Score) számítási motor.
|
|
Dinamikusan betölti a súlyozási paramétereket a SystemParameter rendszerből.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime, timedelta
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.models.identity import User, UserTrustProfile
|
|
from app.models import Vehicle, VehicleOwnership
|
|
from app.models.marketplace.service import Cost
|
|
from app.models.system import SystemParameter, ParameterScope
|
|
from app.services.system_service import SystemService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TrustEngine:
|
|
"""
|
|
A Gondos Gazda Index számításáért felelős motor.
|
|
A számítás három komponensből áll:
|
|
1. Maintenance Score - Karbantartási időzítés pontossága
|
|
2. Quality Score - Szerviz minősége (ár/érték arány)
|
|
3. Preventive Score - Megelőző intézkedések (pl. idő előtti cserék)
|
|
|
|
Minden komponens súlyozása a SystemParameter rendszerből származik.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.system_service = SystemService()
|
|
|
|
async def calculate_user_trust(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int,
|
|
force_recalculate: bool = False
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Kiszámolja a felhasználó trust score-ját és elmenti a UserTrustProfile táblába.
|
|
|
|
:param db: Adatbázis munkamenet
|
|
:param user_id: A felhasználó azonosítója
|
|
:param force_recalculate: Ha True, akkor újraszámolja még akkor is, ha friss
|
|
:return: A számított trust adatok szótárban
|
|
"""
|
|
logger.info(f"Trust számítás indítása user_id={user_id}")
|
|
|
|
# 1. Ellenőrizzük, hogy szükséges-e újraszámolni
|
|
trust_profile = await self._get_or_create_trust_profile(db, user_id)
|
|
|
|
if not force_recalculate:
|
|
# Ha a számítás kevesebb mint 24 órája történt, visszaadjuk a meglévőt
|
|
time_threshold = datetime.utcnow() - timedelta(hours=24)
|
|
if trust_profile.last_calculated and trust_profile.last_calculated > time_threshold:
|
|
logger.debug(f"Trust score már friss (last_calculated={trust_profile.last_calculated}), visszaadjuk")
|
|
return self._format_trust_response(trust_profile)
|
|
|
|
# 2. Lekérjük a súlyozási paramétereket
|
|
weights = await self._get_trust_weights(db, user_id)
|
|
tolerance_km = await self._get_tolerance_km(db, user_id)
|
|
|
|
# 3. Számoljuk ki a részpontszámokat
|
|
maintenance_score = await self._calculate_maintenance_score(db, user_id, tolerance_km)
|
|
quality_score = await self._calculate_quality_score(db, user_id)
|
|
preventive_score = await self._calculate_preventive_score(db, user_id)
|
|
|
|
# 4. Összesített trust score számítása súlyozással
|
|
trust_score = int(
|
|
(maintenance_score * weights["maintenance"] +
|
|
quality_score * weights["quality"] +
|
|
preventive_score * weights["preventive"]) * 100
|
|
)
|
|
# Korlátozzuk 0-100 közé
|
|
trust_score = max(0, min(100, trust_score))
|
|
|
|
# 5. Frissítjük a trust profile-t
|
|
trust_profile.trust_score = trust_score
|
|
trust_profile.maintenance_score = float(maintenance_score)
|
|
trust_profile.quality_score = float(quality_score)
|
|
trust_profile.preventive_score = float(preventive_score)
|
|
trust_profile.last_calculated = datetime.utcnow()
|
|
|
|
db.add(trust_profile)
|
|
await db.commit()
|
|
|
|
logger.info(f"Trust számítás kész user_id={user_id}: score={trust_score}")
|
|
|
|
return {
|
|
"trust_score": trust_score,
|
|
"maintenance_score": float(maintenance_score),
|
|
"quality_score": float(quality_score),
|
|
"preventive_score": float(preventive_score),
|
|
"weights": weights,
|
|
"tolerance_km": tolerance_km,
|
|
"last_calculated": trust_profile.last_calculated.isoformat() if trust_profile.last_calculated else None,
|
|
}
|
|
|
|
async def _get_or_create_trust_profile(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int
|
|
) -> UserTrustProfile:
|
|
"""Lekéri vagy létrehozza a felhasználó trust profile-ját."""
|
|
stmt = select(UserTrustProfile).where(UserTrustProfile.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if profile is None:
|
|
profile = UserTrustProfile(
|
|
user_id=user_id,
|
|
trust_score=0,
|
|
maintenance_score=0.0,
|
|
quality_score=0.0,
|
|
preventive_score=0.0,
|
|
last_calculated=datetime.utcnow()
|
|
)
|
|
db.add(profile)
|
|
await db.flush()
|
|
|
|
return profile
|
|
|
|
async def _get_trust_weights(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int
|
|
) -> Dict[str, float]:
|
|
"""Lekéri a súlyozási paramétereket hierarchikusan."""
|
|
# A user region_code-ját és country_code-ját lekérjük a User táblából
|
|
stmt = select(User).where(User.id == user_id)
|
|
result = await db.execute(stmt)
|
|
user = result.scalar_one_or_none()
|
|
|
|
region_id = user.region_code if user else None
|
|
country_code = user.region_code[:2] if user and user.region_code else None # pl. "HU" az első 2 karakter
|
|
|
|
# Súlyok lekérése
|
|
weight_m = await self.system_service.get_scoped_parameter(
|
|
db, "TRUST_WEIGHT_MAINTENANCE",
|
|
user_id=str(user_id), region_id=region_id, country_code=country_code,
|
|
default=0.4
|
|
)
|
|
weight_q = await self.system_service.get_scoped_parameter(
|
|
db, "TRUST_WEIGHT_QUALITY",
|
|
user_id=str(user_id), region_id=region_id, country_code=country_code,
|
|
default=0.3
|
|
)
|
|
weight_p = await self.system_service.get_scoped_parameter(
|
|
db, "TRUST_WEIGHT_PREVENTIVE",
|
|
user_id=str(user_id), region_id=region_id, country_code=country_code,
|
|
default=0.3
|
|
)
|
|
|
|
# A JSON értékből kinyerjük a számot (ha dict formátumban van)
|
|
if isinstance(weight_m, dict):
|
|
weight_m = weight_m.get("value", 0.4)
|
|
if isinstance(weight_q, dict):
|
|
weight_q = weight_q.get("value", 0.3)
|
|
if isinstance(weight_p, dict):
|
|
weight_p = weight_p.get("value", 0.3)
|
|
|
|
# Normalizáljuk, hogy összegük 1 legyen
|
|
total = weight_m + weight_q + weight_p
|
|
if total > 0:
|
|
weight_m /= total
|
|
weight_q /= total
|
|
weight_p /= total
|
|
|
|
return {
|
|
"maintenance": float(weight_m),
|
|
"quality": float(weight_q),
|
|
"preventive": float(weight_p)
|
|
}
|
|
|
|
async def _get_tolerance_km(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int
|
|
) -> int:
|
|
"""Lekéri a tolerancia km-t a karbantartási időzítéshez."""
|
|
stmt = select(User).where(User.id == user_id)
|
|
result = await db.execute(stmt)
|
|
user = result.scalar_one_or_none()
|
|
|
|
region_id = user.region_code if user else None
|
|
country_code = user.region_code[:2] if user and user.region_code else None
|
|
|
|
tolerance = await self.system_service.get_scoped_parameter(
|
|
db, "TRUST_MAINTENANCE_TOLERANCE_KM",
|
|
user_id=str(user_id), region_id=region_id, country_code=country_code,
|
|
default=1000
|
|
)
|
|
|
|
if isinstance(tolerance, dict):
|
|
tolerance = tolerance.get("value", 1000)
|
|
|
|
return int(tolerance)
|
|
|
|
async def _calculate_maintenance_score(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int,
|
|
tolerance_km: int
|
|
) -> float:
|
|
"""
|
|
Karbantartási időzítés pontosságának számítása.
|
|
Összehasonlítja a tényleges karbantartási költségeket az odometer állásokkal.
|
|
"""
|
|
# 1. Lekérjük a felhasználó járműveit
|
|
stmt = (
|
|
select(Vehicle)
|
|
.join(VehicleOwnership, VehicleOwnership.vehicle_id == Vehicle.id)
|
|
.where(VehicleOwnership.user_id == user_id)
|
|
.where(VehicleOwnership.is_active == True)
|
|
)
|
|
result = await db.execute(stmt)
|
|
vehicles = result.scalars().all()
|
|
|
|
if not vehicles:
|
|
logger.debug(f"Nincs aktív jármű a user_id={user_id} számára, maintenance_score=0.5")
|
|
return 0.5 # Alapértelmezett közepes érték
|
|
|
|
total_score = 0.0
|
|
vehicle_count = 0
|
|
|
|
for vehicle in vehicles:
|
|
# 2. Lekérjük a MAINTENANCE kategóriájú költségeket
|
|
stmt_costs = (
|
|
select(Cost)
|
|
.where(Cost.vehicle_id == vehicle.id)
|
|
.where(Cost.category == "MAINTENANCE")
|
|
.where(Cost.is_deleted == False)
|
|
.order_by(Cost.occurrence_date)
|
|
)
|
|
result_costs = await db.execute(stmt_costs)
|
|
maintenance_costs = result_costs.scalars().all()
|
|
|
|
if not maintenance_costs:
|
|
continue # Nincs karbantartási költség, nem számítunk bele
|
|
|
|
# 3. Összehasonlítjuk az odometer állásokkal
|
|
vehicle_score = await self._calculate_vehicle_maintenance_score(
|
|
db, vehicle, maintenance_costs, tolerance_km
|
|
)
|
|
total_score += vehicle_score
|
|
vehicle_count += 1
|
|
|
|
if vehicle_count == 0:
|
|
return 0.5
|
|
|
|
return total_score / vehicle_count
|
|
|
|
async def _calculate_vehicle_maintenance_score(
|
|
self,
|
|
db: AsyncSession,
|
|
vehicle: Vehicle,
|
|
maintenance_costs: list,
|
|
tolerance_km: int
|
|
) -> float:
|
|
"""Egy jármű karbantartási pontszámának számítása."""
|
|
# Egyszerűsített implementáció: csak ellenőrizzük, hogy vannak-e karbantartási költségek
|
|
# és hogy az odometer növekedése nem túl nagy a költségek között
|
|
# (Valós implementációban összehasonlítanánk a gyártói ajánlásokkal)
|
|
|
|
if len(maintenance_costs) < 2:
|
|
# Kevesebb mint 2 karbantartás, nem tudunk trendet elemezni
|
|
return 0.7
|
|
|
|
# Átlagos időköz a karbantartások között (km-ben)
|
|
total_km_gap = 0
|
|
gap_count = 0
|
|
|
|
for i in range(1, len(maintenance_costs)):
|
|
prev_cost = maintenance_costs[i-1]
|
|
curr_cost = maintenance_costs[i]
|
|
|
|
if prev_cost.odometer_km and curr_cost.odometer_km:
|
|
gap = curr_cost.odometer_km - prev_cost.odometer_km
|
|
total_km_gap += gap
|
|
gap_count += 1
|
|
|
|
if gap_count == 0:
|
|
return 0.7
|
|
|
|
avg_gap = total_km_gap / gap_count
|
|
|
|
# Ideális karbantartási intervallum (pl. 15,000 km)
|
|
ideal_interval = 15000
|
|
|
|
# Pontszám: minél közelebb van az ideálishoz, annál magasabb
|
|
deviation = abs(avg_gap - ideal_interval)
|
|
if deviation <= tolerance_km:
|
|
score = 1.0
|
|
elif deviation <= ideal_interval * 0.5: # 50%-nál kisebb eltérés
|
|
score = 0.8
|
|
elif deviation <= ideal_interval: # 100%-nál kisebb eltérés
|
|
score = 0.5
|
|
else:
|
|
score = 0.2
|
|
|
|
return score
|
|
|
|
async def _calculate_quality_score(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int
|
|
) -> float:
|
|
"""
|
|
Szerviz minőségének számítása (ár/érték arány).
|
|
Egyszerűsített implementáció: átlagos értékelések alapján.
|
|
"""
|
|
# Jelenlegi implementáció: minden felhasználó kap egy alap pontszámot
|
|
# Valós implementációban a szervizek értékeléseit és árait elemeznénk
|
|
return 0.75 # Alapértelmezett közepes érték
|
|
|
|
async def _calculate_preventive_score(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: int
|
|
) -> float:
|
|
"""
|
|
Megelőző intézkedések pontszáma.
|
|
Egyszerűsített implementáció: idő előtti alkatrész cserék száma.
|
|
"""
|
|
# Jelenlegi implementáció: minden felhasználó kap egy alap pontszámot
|
|
# Valós implementációban a PREVENTIVE kategóriájú költségeket elemeznénk
|
|
return 0.6 # Alapértelmezett közepes érték
|
|
|
|
def _format_trust_response(self, profile: UserTrustProfile) -> Dict[str, Any]:
|
|
"""Formázza a trust profile-t válaszként."""
|
|
return {
|
|
"trust_score": profile.trust_score,
|
|
"maintenance_score": float(profile.maintenance_score),
|
|
"quality_score": float(profile.quality_score),
|
|
"preventive_score": float(profile.preventive_score),
|
|
"weights": {}, # Üres, mert nem számoltuk újra
|
|
"tolerance_km": None,
|
|
"last_calculated": profile.last_calculated.isoformat() if profile.last_calculated else None,
|
|
} |