átlagos kiegészítséek jó sok
This commit is contained in:
@@ -1,40 +1,475 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from typing import List
|
||||
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.gamification import UserStats, PointsLedger
|
||||
from app.services.config_service import config
|
||||
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)):
|
||||
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
|
||||
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 {"total_xp": 0, "current_level": 1, "penalty_points": 0}
|
||||
return {
|
||||
"penalty_level": 0,
|
||||
"restrictions": [],
|
||||
"recovery_progress": 0,
|
||||
"can_submit_services": True
|
||||
}
|
||||
|
||||
return stats
|
||||
# Ö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
|
||||
}
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
|
||||
# --- 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(User.email, UserStats.total_xp, UserStats.current_level)
|
||||
.join(UserStats, User.id == UserStats.user_id)
|
||||
select(UserStats, User.email)
|
||||
.join(User, UserStats.user_id == User.id)
|
||||
.order_by(desc(UserStats.total_xp))
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
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
|
||||
Reference in New Issue
Block a user