gemification_admin bekötve
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user