refaktorálás javításai
This commit is contained in:
269
backend/app/services/marketplace_service.py
Normal file
269
backend/app/services/marketplace_service.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/services/marketplace_service.py
|
||||
"""
|
||||
Marketplace Service – Verifikált Szerviz Értékelések (Social 3) logikája.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from sqlalchemy import select, and_, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.models.social import ServiceReview
|
||||
from app.models.service import ServiceProfile
|
||||
from app.models.identity import User
|
||||
from app.models.audit import FinancialLedger
|
||||
from app.models.system import SystemParameter
|
||||
from app.schemas.social import ServiceReviewCreate, ServiceReviewResponse
|
||||
from app.services.system_service import get_system_parameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_verified_review(
|
||||
db: AsyncSession,
|
||||
service_id: int,
|
||||
user_id: int,
|
||||
transaction_id: uuid.UUID,
|
||||
review_data: ServiceReviewCreate,
|
||||
) -> ServiceReviewResponse:
|
||||
"""
|
||||
Verifikált szerviz értékelés létrehozása.
|
||||
Csak igazolt pénzügyi tranzakció után, időablakon belül, egy tranzakcióra egyszer.
|
||||
|
||||
Args:
|
||||
db: AsyncSession
|
||||
service_id: A szerviz ID (service_profiles.id)
|
||||
user_id: A felhasználó ID (users.id)
|
||||
transaction_id: A pénzügyi tranzakció UUID (financial_ledger.transaction_id)
|
||||
review_data: Értékelési adatok (ratings, comment)
|
||||
|
||||
Returns:
|
||||
ServiceReviewResponse
|
||||
|
||||
Raises:
|
||||
ValueError: Ha a validáció sikertelen.
|
||||
IntegrityError: Ha a tranzakció már értékelve van.
|
||||
"""
|
||||
# 1. Ellenőrzés: Létezik‑e a szerviz?
|
||||
service = await db.get(ServiceProfile, service_id)
|
||||
if not service:
|
||||
raise ValueError(f"Service {service_id} not found")
|
||||
|
||||
# 2. Ellenőrzés: Létezik‑e a felhasználó?
|
||||
user = await db.get(User, user_id)
|
||||
if not user:
|
||||
raise ValueError(f"User {user_id} not found")
|
||||
|
||||
# 3. Ellenőrzés: A tranzakció létezik és a felhasználóhoz tartozik?
|
||||
stmt = select(FinancialLedger).where(
|
||||
FinancialLedger.transaction_id == transaction_id,
|
||||
FinancialLedger.user_id == user_id
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
transaction = result.scalar_one_or_none()
|
||||
if not transaction:
|
||||
raise ValueError(f"Transaction {transaction_id} not found or does not belong to user {user_id}")
|
||||
|
||||
# 4. Ellenőrzés: A tranzakció időpontja a REVIEW_WINDOW_DAYS‑on belül van?
|
||||
window_days = await get_system_parameter(db, "REVIEW_WINDOW_DAYS", default=30)
|
||||
window_limit = datetime.now() - timedelta(days=window_days)
|
||||
if transaction.created_at < window_limit:
|
||||
raise ValueError(f"Transaction is older than {window_days} days, review window expired")
|
||||
|
||||
# 5. Ellenőrzés: Már létezik‑e értékelés ehhez a tranzakcióhoz?
|
||||
existing_review = await db.execute(
|
||||
select(ServiceReview).where(ServiceReview.transaction_id == transaction_id)
|
||||
)
|
||||
if existing_review.scalar_one_or_none():
|
||||
raise IntegrityError(f"Transaction {transaction_id} already has a review")
|
||||
|
||||
# 6. Értékelési dimenziók validálása (1‑10)
|
||||
ratings = [
|
||||
review_data.price_rating,
|
||||
review_data.quality_rating,
|
||||
review_data.time_rating,
|
||||
review_data.communication_rating
|
||||
]
|
||||
for rating in ratings:
|
||||
if not (1 <= rating <= 10):
|
||||
raise ValueError("All ratings must be between 1 and 10")
|
||||
|
||||
# 7. ServiceReview létrehozása
|
||||
review = ServiceReview(
|
||||
service_id=service_id,
|
||||
user_id=user_id,
|
||||
transaction_id=transaction_id,
|
||||
price_rating=review_data.price_rating,
|
||||
quality_rating=review_data.quality_rating,
|
||||
time_rating=review_data.time_rating,
|
||||
communication_rating=review_data.communication_rating,
|
||||
comment=review_data.comment,
|
||||
is_verified=True
|
||||
)
|
||||
db.add(review)
|
||||
await db.commit()
|
||||
await db.refresh(review)
|
||||
|
||||
# 8. Háttér‑aggregátor indítása (aszinkron)
|
||||
asyncio.create_task(update_service_rating_aggregates(db, service_id))
|
||||
|
||||
logger.info(f"Verified review created: id={review.id}, service={service_id}, user={user_id}")
|
||||
|
||||
return ServiceReviewResponse.from_orm(review)
|
||||
|
||||
|
||||
async def update_service_rating_aggregates(db: AsyncSession, service_id: int) -> None:
|
||||
"""
|
||||
Frissíti a szerviz aggregált értékelési adatait (service_profiles táblában).
|
||||
Ez a függvény háttérben futhat (pl. Celery vagy asyncio task).
|
||||
"""
|
||||
# Összes verifikált értékelés lekérdezése a szervizhez
|
||||
stmt = select(
|
||||
func.count(ServiceReview.id).label("count"),
|
||||
func.avg(ServiceReview.price_rating).label("price_avg"),
|
||||
func.avg(ServiceReview.quality_rating).label("quality_avg"),
|
||||
func.avg(ServiceReview.time_rating).label("time_avg"),
|
||||
func.avg(ServiceReview.communication_rating).label("communication_avg"),
|
||||
func.max(ServiceReview.created_at).label("last_review_at")
|
||||
).where(
|
||||
and_(
|
||||
ServiceReview.service_id == service_id,
|
||||
ServiceReview.is_verified == True
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
row = result.fetchone()
|
||||
|
||||
if not row or row.count == 0:
|
||||
# Nincs értékelés, alapértékek
|
||||
price_avg = quality_avg = time_avg = communication_avg = None
|
||||
count = 0
|
||||
last_review_at = None
|
||||
else:
|
||||
count = row.count
|
||||
price_avg = float(row.price_avg) if row.price_avg else None
|
||||
quality_avg = float(row.quality_avg) if row.quality_avg else None
|
||||
time_avg = float(row.time_avg) if row.time_avg else None
|
||||
communication_avg = float(row.communication_avg) if row.communication_avg else None
|
||||
last_review_at = row.last_review_at
|
||||
|
||||
# Trust‑score súlyozás: a felhasználók trust‑score‑jának átlaga
|
||||
trust_stmt = select(func.avg(User.trust_score)).join(
|
||||
ServiceReview, ServiceReview.user_id == User.id
|
||||
).where(
|
||||
and_(
|
||||
ServiceReview.service_id == service_id,
|
||||
ServiceReview.is_verified == True
|
||||
)
|
||||
)
|
||||
trust_result = await db.execute(trust_stmt)
|
||||
avg_trust = trust_result.scalar() or 50.0 # alapérték 50
|
||||
|
||||
# Trust‑score befolyási tényező
|
||||
trust_factor = await get_system_parameter(db, "TRUST_SCORE_INFLUENCE_FACTOR", default=1.0)
|
||||
trust_weight = 1.0 + (avg_trust / 100.0) * trust_factor
|
||||
|
||||
# Súlyozott összpontszám számítása
|
||||
weights = await get_system_parameter(db, "REVIEW_RATING_WEIGHTS", default={
|
||||
"price": 0.25,
|
||||
"quality": 0.35,
|
||||
"time": 0.20,
|
||||
"communication": 0.20
|
||||
})
|
||||
weighted_score = 0.0
|
||||
if price_avg:
|
||||
weighted_score += price_avg * weights.get("price", 0.25)
|
||||
if quality_avg:
|
||||
weighted_score += quality_avg * weights.get("quality", 0.35)
|
||||
if time_avg:
|
||||
weighted_score += time_avg * weights.get("time", 0.20)
|
||||
if communication_avg:
|
||||
weighted_score += communication_avg * weights.get("communication", 0.20)
|
||||
|
||||
weighted_score *= trust_weight
|
||||
|
||||
# ServiceProfile frissítése
|
||||
service = await db.get(ServiceProfile, service_id)
|
||||
if service:
|
||||
service.rating_verified_count = count
|
||||
service.rating_price_avg = price_avg
|
||||
service.rating_quality_avg = quality_avg
|
||||
service.rating_time_avg = time_avg
|
||||
service.rating_communication_avg = communication_avg
|
||||
service.rating_overall = weighted_score
|
||||
service.last_review_at = last_review_at
|
||||
await db.commit()
|
||||
logger.debug(f"Updated rating aggregates for service {service_id}: count={count}, overall={weighted_score:.2f}")
|
||||
|
||||
|
||||
async def get_service_reviews(
|
||||
db: AsyncSession,
|
||||
service_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
verified_only: bool = True
|
||||
) -> Tuple[List[ServiceReviewResponse], int]:
|
||||
"""
|
||||
Szerviz értékeléseinek lapozható listázása.
|
||||
|
||||
Args:
|
||||
db: AsyncSession
|
||||
service_id: A szerviz ID
|
||||
skip: Lapozási offset
|
||||
limit: Maximális darabszám
|
||||
verified_only: Csak verifikált értékelések
|
||||
|
||||
Returns:
|
||||
(reviews, total_count)
|
||||
"""
|
||||
conditions = [ServiceReview.service_id == service_id]
|
||||
if verified_only:
|
||||
conditions.append(ServiceReview.is_verified == True)
|
||||
|
||||
# Összes darabszám
|
||||
count_stmt = select(func.count(ServiceReview.id)).where(*conditions)
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar()
|
||||
|
||||
# Lapozott lekérdezés
|
||||
stmt = select(ServiceReview).where(*conditions).order_by(
|
||||
ServiceReview.created_at.desc()
|
||||
).offset(skip).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
reviews = result.scalars().all()
|
||||
|
||||
return [ServiceReviewResponse.from_orm(r) for r in reviews], total
|
||||
|
||||
|
||||
async def can_user_review_service(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
service_id: int
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Ellenőrzi, hogy a felhasználó értékelheti‑e a szervizt.
|
||||
|
||||
Returns:
|
||||
(can_review, reason)
|
||||
"""
|
||||
# 1. Van‑e már értékelése a szervizre?
|
||||
existing_stmt = select(ServiceReview).where(
|
||||
ServiceReview.user_id == user_id,
|
||||
ServiceReview.service_id == service_id
|
||||
)
|
||||
existing = await db.execute(existing_stmt)
|
||||
if existing.scalar_one_or_none():
|
||||
return False, "User already reviewed this service"
|
||||
|
||||
# 2. Van‑e a felhasználónak tranzakciója a szervizzel?
|
||||
# Megjegyzés: A tranzakció‑szerviz kapcsolat jelenleg nincs tárolva.
|
||||
# Ehhez a FinancialLedger‑ben kellene egy service_id mező, vagy
|
||||
# egy kapcsolótábla. Most csak annyit ellenőrzünk, hogy van‑e bármilyen
|
||||
# tranzakció a felhasználónak, ami még nem értékelt.
|
||||
# TODO: Később pontosítani a tranzakció‑szerviz kapcsolatot.
|
||||
|
||||
return True, None
|
||||
Reference in New Issue
Block a user