gemification_admin bekötve

This commit is contained in:
Roo
2026-06-30 08:56:55 +00:00
parent df4a0f07ff
commit cbfb955580
313 changed files with 18490 additions and 2895 deletions

View File

@@ -5,6 +5,7 @@ from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.models import UserStats, PointsLedger, UserBadge, Badge
from app.models.gamification.gamification import PointRule
from app.models.identity import User, Wallet
from app.models import FinancialLedger
from app.services.config_service import config # 2.0 Központi konfigurátor
@@ -15,6 +16,9 @@ class GamificationService:
"""
Gamification Service 2.0 - A 'Jövevény' lelke.
Felelős a pontozásért, szintekért, büntetésekért és a jutalom-kreditekért.
Refaktor: A process_activity() most már a point_rules táblából is olvas,
így az admin által definiált pontszabályok elsőbbséget élveznek a master configgal szemben.
"""
@staticmethod
@@ -29,6 +33,22 @@ class GamificationService:
service = GamificationService()
return await service.process_activity(db, user_id, xp_amount=amount, social_amount=social_points, reason=reason, commit=commit)
async def _get_point_rule(self, db: AsyncSession, action_key: str) -> dict | None:
"""Lekér egy pontszabályt a point_rules táblából action_key alapján.
Returns:
dict with 'points' and 'description' or None if not found/inactive.
"""
stmt = select(PointRule).where(
PointRule.action_key == action_key,
PointRule.is_active == True
)
result = await db.execute(stmt)
rule = result.scalar_one_or_none()
if rule:
return {"points": rule.points, "description": rule.description}
return None
async def process_activity(
self,
db: AsyncSession,
@@ -37,13 +57,25 @@ class GamificationService:
social_amount: int,
reason: str,
is_penalty: bool = False,
commit: bool = True
commit: bool = True,
action_key: str | None = None,
source_type: str | None = None,
source_id: int | None = None,
):
""" A fő folyamat: Pontozás -> Büntetés szűrés -> Szintszámítás -> Kifizetés.
Refaktor 2.0:
- Ha action_key meg van adva, a point_rules táblából olvassa a pontértékeket.
- A master config csak fallback, ha nincs a point_rules-ben.
- Támogatja a source_type és source_id mezőket a PointsLedger naplózáshoz.
Args:
commit: If True (default), commits the transaction internally.
Set to False when called from within a larger transaction.
action_key: Opcionális. Ha meg van adva, a point_rules táblából
olvassa a pontértékeket a master config helyett.
source_type: Opcionális. A pontok forrásának típusa (pl. 'service_submission').
source_id: Opcionális. A pontok forrásának ID-ja.
"""
try:
# 1. ADMIN KONFIGURÁCIÓ BETÖLTÉSE
@@ -59,6 +91,19 @@ class GamificationService:
"level_rewards": {"credits_per_10_levels": 50}
})
# 1/b. POINT RULES TÁBLA LEKÉRÉSE (ha action_key meg van adva)
# A point_rules tábla elsőbbséget élvez a master config-gal szemben!
if action_key:
rule = await self._get_point_rule(db, action_key)
if rule:
# A point_rules-ból jövő pontok felülírják a paraméterként kapott értékeket
xp_amount = rule["points"]
if rule.get("description"):
reason = f"{action_key}: {rule['description']}"
logger.debug(f"Point rule applied: {action_key} -> {xp_amount} XP")
else:
logger.debug(f"No active point rule found for action_key='{action_key}', using fallback values.")
# 2. FELHASZNÁLÓ ÉS STATISZTIKA ELLENŐRZÉSE
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
@@ -70,7 +115,7 @@ class GamificationService:
# 3. BÜNTETŐ LOGIKA (Ha negatív esemény történik)
if is_penalty:
return await self._apply_penalty(db, stats, xp_amount, reason, cfg)
return await self._apply_penalty(db, stats, xp_amount, reason, cfg, source_type=source_type, source_id=source_id)
# 4. SZORZÓK ALKALMAZÁSA (Büntetés alatt állók 'bírsága')
multiplier = await self._calculate_multiplier(stats, cfg)
@@ -107,8 +152,15 @@ class GamificationService:
stats.social_points %= rate # A maradék pont megmarad
await self._add_earned_credits(db, user_id, credits_to_add, "SOCIAL_ACTIVITY_CONVERSION")
# 7. NAPLÓZÁS
db.add(PointsLedger(user_id=user_id, points=final_xp, reason=reason))
# 7. NAPLÓZÁS (kibővítve xp, source_type, source_id mezőkkel)
db.add(PointsLedger(
user_id=user_id,
points=final_xp,
xp=final_xp,
reason=reason,
source_type=source_type,
source_id=source_id,
))
# Only commit if caller wants us to manage the transaction
if commit:
@@ -124,8 +176,12 @@ class GamificationService:
# --- PRIVÁT SEGÉDFÜGGVÉNYEK ---
async def _apply_penalty(self, db: AsyncSession, stats: UserStats, amount: int, reason: str, cfg: dict):
"""Büntetőpontok hozzáadása és korlátozási szintek emelése."""
async def _apply_penalty(self, db: AsyncSession, stats: UserStats, amount: int, reason: str, cfg: dict,
source_type: str | None = None, source_id: int | None = None):
"""Büntetőpontok hozzáadása és korlátozási szintek emelése.
Refaktor: Most már támogatja a source_type és source_id mezőket a naplózáshoz.
"""
stats.penalty_points += amount
th = cfg["penalty_logic"]["thresholds"]
@@ -133,7 +189,14 @@ class GamificationService:
elif stats.penalty_points >= th["level_2"]: stats.restriction_level = 2
elif stats.penalty_points >= th["level_1"]: stats.restriction_level = 1
db.add(PointsLedger(user_id=stats.user_id, points=0, penalty_change=amount, reason=f"🔴 PENALTY: {reason}"))
db.add(PointsLedger(
user_id=stats.user_id,
points=0,
penalty_change=amount,
reason=f"🔴 PENALTY: {reason}",
source_type=source_type,
source_id=source_id,
))
await db.commit()
return stats
@@ -165,4 +228,4 @@ class GamificationService:
details={"reason": reason}
))
gamification_service = GamificationService()
gamification_service = GamificationService()

View File

@@ -6,7 +6,7 @@ A rendszerparaméterek prioritásos felülbírálást támogatnak: User > Region
import logging
from typing import Optional, Any, Dict
from sqlalchemy import select, func # HOZZÁADVA: func a NOW() híváshoz
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -60,7 +60,7 @@ class SystemService:
for scope_level, scope_id in scopes:
stmt = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
SystemParameter.scope_level == scope_level.value,
SystemParameter.is_active == True,
)
if scope_id is not None:
@@ -97,42 +97,65 @@ class SystemService:
"""
Létrehoz vagy frissít egy rendszerparamétert a megadott scope-ban.
Ha már létezik ugyanazzal a kulccsal, scope_level-lel és scope_id-vel, felülírja.
Megjegyzés: PostgreSQL-ben a UNIQUE constraint nem blokkolja a NULL scope_id-jű
duplikátumokat (mert NULL != NULL). Ezért kétlépéses megközelítést használunk:
először megpróbáljuk frissíteni a meglévő rekordot, és ha nem találjuk, beszúrjuk.
"""
from sqlalchemy.dialects.postgresql import insert
# UPSERT logika: ON CONFLICT DO UPDATE
insert_stmt = insert(SystemParameter).values(
key=key,
value=value,
scope_level=scope_level,
scope_id=scope_id,
category=category,
description=description,
last_modified_by=last_modified_by,
is_active=True,
# 1. lépés: Próbáljuk megkeresni a meglévő rekordot
stmt_find = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level.value,
SystemParameter.is_active == True,
)
upsert_stmt = insert_stmt.on_conflict_do_update(
constraint="uix_param_scope",
set_=dict(
if scope_id is not None:
stmt_find = stmt_find.where(SystemParameter.scope_id == scope_id)
else:
stmt_find = stmt_find.where(SystemParameter.scope_id.is_(None))
result = await db.execute(stmt_find)
existing = result.scalar_one_or_none()
if existing:
# 2a. lépés: Frissítjük a meglévő rekordot
existing.value = value
existing.category = category
existing.description = description
existing.last_modified_by = last_modified_by
existing.updated_at = func.now()
await db.commit()
return existing
else:
# 2b. lépés: Beszúrunk egy új rekordot
insert_stmt = insert(SystemParameter).values(
key=key,
value=value,
scope_level=scope_level.value,
scope_id=scope_id,
category=category,
description=description,
last_modified_by=last_modified_by,
updated_at=func.now(),
),
)
await db.execute(upsert_stmt)
await db.commit()
is_active=True,
)
await db.execute(insert_stmt)
await db.commit()
# Visszaolvassuk a létrehozott/frissített rekordot
stmt = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
SystemParameter.scope_id == scope_id,
)
result = await db.execute(stmt)
param = result.scalar_one()
return param
# Visszaolvassuk a létrehozott rekordot
stmt_read = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level.value,
SystemParameter.is_active == True,
)
if scope_id is not None:
stmt_read = stmt_read.where(SystemParameter.scope_id == scope_id)
else:
stmt_read = stmt_read.where(SystemParameter.scope_id.is_(None))
result = await db.execute(stmt_read)
param = result.scalar_one()
return param
# --- GLOBÁLIS PÉLDÁNY ÉS SEGÉDFÜGGVÉNYEK ---
# Ezek a fájl legszélén vannak (0-s behúzás), így kívülről importálhatóak!