refaktorálás javításai
This commit is contained in:
343
backend/app/services/trust_engine.py
Normal file
343
backend/app/services/trust_engine.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# /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.asset import Vehicle, VehicleOwnership
|
||||
from app.models.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,
|
||||
}
|
||||
Reference in New Issue
Block a user