# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py from fastapi import APIRouter, Depends, HTTPException, Body, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc, func, and_ from typing import List, Optional from datetime import datetime, timedelta from app.db.session import get_db from app.api.deps import get_current_user from app.models.identity import User from app.models import UserStats, PointsLedger, LevelConfig, UserContribution, Badge, UserBadge, Season from app.models.system import SystemParameter, ParameterScope from app.models.marketplace.service import ServiceStaging from app.schemas.gamification import SeasonResponse, UserStatResponse, LeaderboardEntry router = APIRouter() # -- SEGÉDFÜGGVÉNY A RENDSZERBEÁLLÍTÁSOKHOZ -- async def get_system_param(db: AsyncSession, key: str, default_value): stmt = select(SystemParameter).where(SystemParameter.key == key) res = (await db.execute(stmt)).scalar_one_or_none() return res.value if res else default_value @router.get("/my-stats") async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)): stmt = select(UserStats).where(UserStats.user_id == current_user.id) stats = (await db.execute(stmt)).scalar_one_or_none() if not stats: return {"total_xp": 0, "current_level": 1, "penalty_points": 0, "services_submitted": 0} return stats @router.get("/leaderboard") async def get_leaderboard( limit: int = 10, season_id: Optional[int] = None, db: AsyncSession = Depends(get_db) ): """Vezetőlista - globális vagy szezonális""" if season_id: # Szezonális vezetőlista stmt = ( select( User.email, func.sum(UserContribution.points_awarded).label("total_points"), func.sum(UserContribution.xp_awarded).label("total_xp") ) .join(UserContribution, User.id == UserContribution.user_id) .where(UserContribution.season_id == season_id) .group_by(User.id) .order_by(desc("total_points")) .limit(limit) ) else: # Globális vezetőlista stmt = ( select(User.email, UserStats.total_xp, UserStats.current_level) .join(UserStats, User.id == UserStats.user_id) .order_by(desc(UserStats.total_xp)) .limit(limit) ) result = await db.execute(stmt) if season_id: return [ {"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "points": r[1], "xp": r[2]} for r in result.all() ] else: return [ {"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]} for r in result.all() ] @router.get("/seasons") async def get_seasons( active_only: bool = True, db: AsyncSession = Depends(get_db) ): """Szezonok listázása""" stmt = select(Season) if active_only: stmt = stmt.where(Season.is_active == True) result = await db.execute(stmt) seasons = result.scalars().all() return [ { "id": s.id, "name": s.name, "start_date": s.start_date, "end_date": s.end_date, "is_active": s.is_active } for s in seasons ] @router.get("/my-contributions") async def get_my_contributions( season_id: Optional[int] = None, limit: int = 50, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Felhasználó hozzájárulásainak listázása""" stmt = select(UserContribution).where(UserContribution.user_id == current_user.id) if season_id: stmt = stmt.where(UserContribution.season_id == season_id) stmt = stmt.order_by(desc(UserContribution.created_at)).limit(limit) result = await db.execute(stmt) contributions = result.scalars().all() return [ { "id": c.id, "contribution_type": c.contribution_type, "entity_type": c.entity_type, "entity_id": c.entity_id, "points_awarded": c.points_awarded, "xp_awarded": c.xp_awarded, "status": c.status, "created_at": c.created_at } for c in contributions ] @router.get("/season-standings/{season_id}") async def get_season_standings( season_id: int, limit: int = 20, db: AsyncSession = Depends(get_db) ): """Szezon állása - top hozzájárulók""" # Aktuális szezon ellenőrzése season_stmt = select(Season).where(Season.id == season_id) season = (await db.execute(season_stmt)).scalar_one_or_none() if not season: raise HTTPException(status_code=404, detail="Season not found") # Top hozzájárulók lekérdezése stmt = ( select( User.email, func.sum(UserContribution.points_awarded).label("total_points"), func.sum(UserContribution.xp_awarded).label("total_xp"), func.count(UserContribution.id).label("contribution_count") ) .join(UserContribution, User.id == UserContribution.user_id) .where( and_( UserContribution.season_id == season_id, UserContribution.status == "approved" ) ) .group_by(User.id) .order_by(desc("total_points")) .limit(limit) ) result = await db.execute(stmt) standings = result.all() # Szezonális jutalmak konfigurációja season_config = await get_system_param( db, "seasonal_competition_config", { "top_contributors_count": 10, "rewards": { "first_place": {"credits": 1000, "badge": "season_champion"}, "second_place": {"credits": 500, "badge": "season_runner_up"}, "third_place": {"credits": 250, "badge": "season_bronze"}, "top_10": {"credits": 100, "badge": "season_elite"} } } ) return { "season": { "id": season.id, "name": season.name, "start_date": season.start_date, "end_date": season.end_date }, "standings": [ { "rank": idx + 1, "user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "points": r[1], "xp": r[2], "contributions": r[3], "reward": get_season_reward(idx + 1, season_config) } for idx, r in enumerate(standings) ], "config": season_config } def get_season_reward(rank: int, config: dict) -> dict: """Szezonális jutalom meghatározása a rang alapján""" rewards = config.get("rewards", {}) if rank == 1: return rewards.get("first_place", {}) elif rank == 2: return rewards.get("second_place", {}) elif rank == 3: return rewards.get("third_place", {}) elif rank <= config.get("top_contributors_count", 10): return rewards.get("top_10", {}) else: return {} @router.get("/self-defense-status") async def get_self_defense_status( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Önvédelmi rendszer státusz lekérdezése""" stmt = select(UserStats).where(UserStats.user_id == current_user.id) stats = (await db.execute(stmt)).scalar_one_or_none() if not stats: return { "penalty_level": 0, "restrictions": [], "recovery_progress": 0, "can_submit_services": True } # Önvédelmi büntetések konfigurációja penalty_config = await get_system_param( db, "self_defense_penalties", { "level_minus_1": {"restrictions": ["no_service_submissions"], "duration_days": 7}, "level_minus_2": {"restrictions": ["no_service_submissions", "no_reviews"], "duration_days": 30}, "level_minus_3": {"restrictions": ["no_service_submissions", "no_reviews", "no_messaging"], "duration_days": 365} } ) # Büntetési szint meghatározása (egyszerűsített logika) penalty_level = 0 if stats.penalty_points >= 1000: penalty_level = -3 elif stats.penalty_points >= 500: penalty_level = -2 elif stats.penalty_points >= 100: penalty_level = -1 restrictions = [] if penalty_level < 0: level_key = f"level_minus_{abs(penalty_level)}" restrictions = penalty_config.get(level_key, {}).get("restrictions", []) return { "penalty_level": penalty_level, "penalty_points": stats.penalty_points, "restrictions": restrictions, "recovery_progress": min(stats.total_xp / 10000 * 100, 100) if penalty_level < 0 else 100, "can_submit_services": "no_service_submissions" not in restrictions } # --- AZ ÚJ, DINAMIKUS BEKÜLDŐ VÉGPONT (Gamification 2.0 kompatibilis) --- @router.post("/submit-service") async def submit_new_service( name: str = Body(...), city: str = Body(...), address: str = Body(...), contact_phone: Optional[str] = Body(None), website: Optional[str] = Body(None), description: Optional[str] = Body(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): # 1. Önvédelmi státusz ellenőrzése defense_status = await get_self_defense_status(db, current_user) if not defense_status["can_submit_services"]: raise HTTPException( status_code=403, detail="Önvédelmi korlátozás miatt nem küldhetsz be új szerviz adatokat." ) # 2. Beállítások lekérése az Admin által vezérelt táblából submission_rewards = await get_system_param( db, "service_submission_rewards", {"points": 50, "xp": 100, "social_credits": 10} ) contribution_config = await get_system_param( db, "contribution_types_config", { "service_submission": {"points": 50, "xp": 100, "weight": 1.0} } ) # 3. Aktuális szezon lekérdezése season_stmt = select(Season).where( and_( Season.is_active == True, Season.start_date <= datetime.utcnow().date(), Season.end_date >= datetime.utcnow().date() ) ).limit(1) season_result = await db.execute(season_stmt) current_season = season_result.scalar_one_or_none() # 4. Felhasználó statisztikák stmt = select(UserStats).where(UserStats.user_id == current_user.id) stats = (await db.execute(stmt)).scalar_one_or_none() user_lvl = stats.current_level if stats else 1 # 5. Trust score számítás a szint alapján trust_weight = min(20 + (user_lvl * 6), 90) # 6. Nyers adat beküldése a Robotoknak (Staging) import hashlib f_print = hashlib.md5(f"{name.lower()}{city.lower()}{address.lower()}".encode()).hexdigest() new_staging = ServiceStaging( name=name, city=city, address_line1=address, contact_phone=contact_phone, website=website, description=description, fingerprint=f_print, status="pending", trust_score=trust_weight, submitted_by=current_user.id, raw_data={ "submitted_by_user": current_user.id, "user_level": user_lvl, "submitted_at": datetime.utcnow().isoformat() } ) db.add(new_staging) await db.flush() # Get the ID # 7. UserContribution létrehozása contribution = UserContribution( user_id=current_user.id, season_id=current_season.id if current_season else None, contribution_type="service_submission", entity_type="service_staging", entity_id=new_staging.id, points_awarded=submission_rewards.get("points", 50), xp_awarded=submission_rewards.get("xp", 100), status="pending", # Robot 5 jóváhagyására vár metadata={ "service_name": name, "city": city, "staging_id": new_staging.id }, created_at=datetime.utcnow() ) db.add(contribution) # 8. PointsLedger bejegyzés ledger = PointsLedger( user_id=current_user.id, points=submission_rewards.get("points", 50), xp=submission_rewards.get("xp", 100), source_type="service_submission", source_id=new_staging.id, description=f"Szerviz beküldés: {name}", created_at=datetime.utcnow() ) db.add(ledger) # 9. UserStats frissítése if stats: stats.total_points += submission_rewards.get("points", 50) stats.total_xp += submission_rewards.get("xp", 100) stats.services_submitted += 1 stats.updated_at = datetime.utcnow() else: # Ha nincs még UserStats, létrehozzuk stats = UserStats( user_id=current_user.id, total_points=submission_rewards.get("points", 50), total_xp=submission_rewards.get("xp", 100), services_submitted=1, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db.add(stats) try: await db.commit() return { "status": "success", "message": "Szerviz beküldve a rendszerbe elemzésre!", "xp_earned": submission_rewards.get("xp", 100), "points_earned": submission_rewards.get("points", 50), "staging_id": new_staging.id, "season_id": current_season.id if current_season else None } except Exception as e: await db.rollback() raise HTTPException(status_code=400, detail=f"Hiba a beküldés során: {str(e)}") # --- Gamification 2.0 API végpontok (Frontend/Mobil) --- @router.get("/me", response_model=UserStatResponse) async def get_my_gamification_stats( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Visszaadja a bejelentkezett felhasználó aktuális statisztikáit. Ha nincs rekord, alapértelmezett értékekkel tér vissza. """ stmt = select(UserStats).where(UserStats.user_id == current_user.id) stats = (await db.execute(stmt)).scalar_one_or_none() if not stats: # Alapértelmezett statisztika return UserStatResponse( user_id=current_user.id, total_xp=0, current_level=1, restriction_level=0, penalty_quota_remaining=0, banned_until=None ) return UserStatResponse.from_orm(stats) @router.get("/seasons/active", response_model=SeasonResponse) async def get_active_season( db: AsyncSession = Depends(get_db) ): """ Visszaadja az éppen aktív szezont. """ stmt = select(Season).where(Season.is_active == True) season = (await db.execute(stmt)).scalar_one_or_none() if not season: raise HTTPException(status_code=404, detail="No active season found") return SeasonResponse.from_orm(season) @router.get("/leaderboard", response_model=List[LeaderboardEntry]) async def get_leaderboard_top10( limit: int = Query(10, ge=1, le=100), db: AsyncSession = Depends(get_db) ): """ Visszaadja a top felhasználókat total_xp alapján csökkenő sorrendben. """ stmt = ( select(UserStats, User.email) .join(User, UserStats.user_id == User.id) .order_by(desc(UserStats.total_xp)) .limit(limit) ) result = await db.execute(stmt) rows = result.all() leaderboard = [] for stats, email in rows: leaderboard.append( LeaderboardEntry( user_id=stats.user_id, username=email, # email használata username helyett total_xp=stats.total_xp, current_level=stats.current_level ) ) return leaderboard