refaktorálás javításai

This commit is contained in:
Roo
2026-03-13 10:22:41 +00:00
parent 2d8d23f469
commit f53e0b53df
140 changed files with 7316 additions and 4579 deletions

View 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,
}