STABLE: Final schema sync, optimized gitignore

This commit is contained in:
Kincses
2026-02-26 08:19:25 +01:00
parent 893f39fa15
commit 505543330a
203 changed files with 11590 additions and 9542 deletions

View File

@@ -7,7 +7,8 @@ import base64
import httpx
from typing import Dict, Any, Optional, List
from sqlalchemy import select
from app.db.session import SessionLocal
# JAVÍTVA: AsyncSessionLocal használata
from app.db.session import AsyncSessionLocal
from app.models.system import SystemParameter
logger = logging.getLogger("AI-Service")
@@ -21,7 +22,8 @@ class AIService:
@classmethod
async def get_config_delay(cls) -> float:
try:
async with SessionLocal() as db:
# JAVÍTVA: Aszinkron session kezelés
async with AsyncSessionLocal() as db:
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
res = await db.execute(stmt)
param = res.scalar_one_or_none()

View File

@@ -110,134 +110,61 @@ class AuthService:
@staticmethod
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
"""
Step 2: Atomi Tranzakció.
Módosított verzió: Meglévő biztonsági logika + Telephely (Branch) integráció.
"""
""" Step 2: Atomi Tranzakció (Person + Address + Org + Branch + Wallet). """
try:
# 1. User és Person betöltése
# 1. Lekérés Eager Loadinggal a hibák elkerülésére
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
res = await db.execute(stmt)
user = res.scalar_one_or_none()
user = (await db.execute(stmt)).scalar_one_or_none()
if not user: return None
# --- BIZTONSÁG: Slug generálása ---
if not user.folder_slug:
user.folder_slug = generate_secure_slug(length=12)
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
user.preferred_currency = kyc_in.preferred_currency
# --- SHADOW IDENTITY ELLENŐRZÉS ---
identity_stmt = select(Person).where(and_(
Person.mothers_last_name == kyc_in.mothers_last_name,
Person.mothers_first_name == kyc_in.mothers_first_name,
Person.birth_place == kyc_in.birth_place,
Person.birth_date == kyc_in.birth_date
))
existing_person = (await db.execute(identity_stmt)).scalar_one_or_none()
if existing_person:
user.person_id = existing_person.id
active_person = existing_person
else:
active_person = user.person
# --- CÍM RÖGZÍTÉSE ---
# 2. Cím rögzítése
addr_id = await GeoService.get_or_create_full_address(
db,
zip_code=kyc_in.address_zip,
city=kyc_in.address_city,
street_name=kyc_in.address_street_name,
street_type=kyc_in.address_street_type,
house_number=kyc_in.address_house_number,
parcel_id=kyc_in.address_hrsz
db, zip_code=kyc_in.address_zip, city=kyc_in.address_city,
street_name=kyc_in.address_street_name, street_type=kyc_in.address_street_type,
house_number=kyc_in.address_house_number, parcel_id=kyc_in.address_hrsz
)
# --- SZEMÉLYES ADATOK FRISSÍTÉSE ---
active_person.mothers_last_name = kyc_in.mothers_last_name
active_person.mothers_first_name = kyc_in.mothers_first_name
active_person.birth_place = kyc_in.birth_place
active_person.birth_date = kyc_in.birth_date
active_person.phone = kyc_in.phone_number
active_person.address_id = addr_id
active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs)
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
active_person.is_active = True
# 3. Person adatok frissítése (MDM elv)
p = user.person
p.mothers_last_name = kyc_in.mothers_last_name
p.mothers_first_name = kyc_in.mothers_first_name
p.birth_place = kyc_in.birth_place
p.birth_date = kyc_in.birth_date
p.phone = kyc_in.phone_number
p.address_id = addr_id
p.identity_docs = jsonable_encoder(kyc_in.identity_docs)
p.is_active = True
# --- EGYÉNI FLOTTA LÉTREHOZÁSA ---
# 4. Individual Organization (Privát Széf) létrehozása
new_org = Organization(
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
name=f"{active_person.last_name} Flotta",
folder_slug=generate_secure_slug(length=12),
full_name=f"{p.last_name} {p.first_name} Magán Flotta",
name=f"{p.last_name} Flotta",
folder_slug=generate_secure_slug(12),
org_type=OrgType.individual,
owner_id=user.id,
is_transferable=False, # Step 2: Individual flotta nem átruházható
is_ownership_transferable=False, # A te új meződ
is_active=True,
status="verified",
language=user.preferred_language,
default_currency=user.preferred_currency or "HUF",
country_code=user.region_code
)
db.add(new_org)
await db.flush()
# --- ÚJ: MAIN BRANCH (KÖZPONTI TELEPHELY) LÉTREHOZÁSA ---
# Magánszemélynél a megadott cím lesz az első telephely is.
from app.models.address import Branch
new_branch = Branch(
organization_id=new_org.id,
address_id=addr_id,
name="Központ / Otthon",
is_main=True,
postal_code=kyc_in.address_zip,
city=kyc_in.address_city,
street_name=kyc_in.address_street_name,
street_type=kyc_in.address_street_type,
house_number=kyc_in.address_house_number,
hrsz=kyc_in.address_hrsz,
status="active"
)
db.add(new_branch)
await db.flush()
# 5. Telephely (Branch) és Tagság
db.add(Branch(organization_id=new_org.id, address_id=addr_id, name="Otthon", is_main=True))
db.add(OrganizationMember(organization_id=new_org.id, user_id=user.id, role="OWNER"))
db.add(Wallet(user_id=user.id, currency=kyc_in.preferred_currency or "HUF"))
db.add(UserStats(user_id=user.id))
# --- TAGSÁG, WALLET, STATS ---
db.add(OrganizationMember(
organization_id=new_org.id,
user_id=user.id,
role="owner",
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
))
db.add(Wallet(user_id=user.id, currency=user.preferred_currency or "HUF"))
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
# --- 7. AKTIVÁLÁS ÉS AUDIT (Ami az előzőből kimaradt) ---
# 6. Aktiválás
user.is_active = True
user.folder_slug = generate_secure_slug(12)
await security_service.log_event(
db,
user_id=user.id,
action="USER_KYC_COMPLETED",
severity="info",
target_type="User",
target_id=str(user.id),
new_data={
"status": "active",
"user_folder": user.folder_slug,
"organization_id": new_org.id,
"branch_id": str(new_branch.id), # Új telephely az auditban
"wallet_created": True
}
)
await db.commit()
await db.refresh(user)
return user
except Exception as e:
await db.rollback()
logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}")
logger.error(f"KYC Error: {e}")
raise e
@staticmethod

View File

@@ -1,63 +1,68 @@
# /opt/docker/dev/service_finder/backend/app/services/config_service.py
from typing import Any, Optional, Dict
import logging
from sqlalchemy import text
from app.db.session import SessionLocal
from decimal import Decimal
from datetime import datetime, timezone
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
# Modellek importálása a központi helyről
from app.models import ExchangeRate, AssetCost, AssetTelemetry
from app.db.session import AsyncSessionLocal
logger = logging.getLogger(__name__)
class ConfigService:
def __init__(self):
self._cache: Dict[str, Any] = {}
async def get_setting(
self,
key: str,
org_id: Optional[int] = None,
region_code: Optional[str] = None,
tier_id: Optional[int] = None,
default: Any = None
) -> Any:
# 1. Cache kulcs generálása (hierarchiát is figyelembe véve)
cache_key = f"{key}_{org_id}_{tier_id}_{region_code}"
if cache_key in self._cache:
return self._cache[cache_key]
query = text("""
SELECT value_json
FROM data.system_settings
WHERE key_name = :key
AND (
(org_id = :org_id) OR
(org_id IS NULL AND tier_id = :tier_id) OR
(org_id IS NULL AND tier_id IS NULL AND region_code = :region_code) OR
(org_id IS NULL AND tier_id IS NULL AND region_code IS NULL)
)
ORDER BY
(org_id IS NOT NULL) DESC,
(tier_id IS NOT NULL) DESC,
(region_code IS NOT NULL) DESC
LIMIT 1
""")
class CostService:
# A cost_in típusát 'Any'-re állítottam ideiglenesen, hogy ne dobjon újabb ImportError-t a hiányzó Pydantic séma miatt
async def record_cost(self, db: AsyncSession, cost_in: Any, user_id: int):
try:
async with SessionLocal() as db:
result = await db.execute(query, {
"key": key,
"org_id": org_id,
"tier_id": tier_id,
"region_code": region_code
})
row = result.fetchone()
val = row[0] if row else default
# 2. Mentés cache-be
self._cache[cache_key] = val
return val
# 1. Árfolyam lekérése (EUR Pivot)
rate_stmt = select(ExchangeRate).where(
ExchangeRate.target_currency == cost_in.currency_local
).order_by(ExchangeRate.id.desc()).limit(1)
rate_res = await db.execute(rate_stmt)
rate_obj = rate_res.scalar_one_or_none()
exchange_rate = rate_obj.rate if rate_obj else Decimal("1.0")
# 2. Kalkuláció
amt_eur = Decimal(str(cost_in.amount_local)) / exchange_rate
# 3. Mentés az új AssetCost modellbe
new_cost = AssetCost(
asset_id=cost_in.asset_id,
organization_id=cost_in.organization_id,
driver_id=user_id,
cost_type=cost_in.cost_type,
amount_local=cost_in.amount_local,
currency_local=cost_in.currency_local,
amount_eur=amt_eur,
exchange_rate_used=exchange_rate,
mileage_at_cost=cost_in.mileage_at_cost,
date=cost_in.date or datetime.now(timezone.utc)
)
db.add(new_cost)
# 4. Telemetria szinkron
if cost_in.mileage_at_cost:
tel_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == cost_in.asset_id)
telemetry = (await db.execute(tel_stmt)).scalar_one_or_none()
if telemetry and cost_in.mileage_at_cost > (telemetry.current_mileage or 0):
telemetry.current_mileage = cost_in.mileage_at_cost
await db.commit()
return new_cost
except Exception as e:
logger.error(f"ConfigService Error: {e}")
return default
await db.rollback()
raise e
def clear_cache(self):
self._cache = {}
class ConfigService:
"""
MB 2.0 Alapvető konfigurációs szerviz.
Ezt kereste az auth_service.py az induláshoz.
"""
pass
# A példány, amit a többi modul (pl. az auth_service) importálni próbál
config = ConfigService()

View File

@@ -1,82 +1,65 @@
# /opt/docker/dev/service_finder/backend/app/services/document_service.py
import os
import shutil
import time
from PIL import Image
from uuid import uuid4
from fastapi import UploadFile, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.document import Document
from app.core.config import settings
class DocumentService:
@staticmethod
def _clean_temp(path: str):
"""30 perc után törli az ideiglenes fájlt (opcionális, ha maradunk a puffer mellett)"""
time.sleep(1800)
if os.path.exists(path):
os.remove(path)
@staticmethod
async def process_upload(
file: UploadFile,
parent_type: str,
parent_id: str,
db: AsyncSession,
background_tasks: BackgroundTasks
file: UploadFile, parent_type: str, parent_id: str,
db: AsyncSession, background_tasks: BackgroundTasks
):
""" Kép optimalizálás, Thumbnail generálás és NAS tárolás. """
file_uuid = str(uuid4())
ext = file.filename.split('.')[-1].lower() if '.' in file.filename else "webp"
# 1. Könyvtárstruktúra meghatározása
temp_dir = "/app/temp/uploads"
nas_vault_dir = f"/mnt/nas/app_data/organizations/{parent_id}/vault"
ssd_thumb_dir = f"/app/static/previews/organizations/{parent_id}"
# Útvonalak a settings-ből (vagy fallback)
nas_base = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
vault_dir = os.path.join(nas_base, parent_type, parent_id, "vault")
thumb_dir = os.path.join(settings.STATIC_DIR, "previews", parent_type, parent_id)
for d in [temp_dir, nas_vault_dir, ssd_thumb_dir]:
os.makedirs(d, exist_ok=True)
os.makedirs(vault_dir, exist_ok=True)
os.makedirs(thumb_dir, exist_ok=True)
# 2. Mentés a TEMP-be
temp_path = os.path.join(temp_dir, f"{file_uuid}_{file.filename}")
content = await file.read()
with open(temp_path, "wb") as f:
f.write(content)
temp_path = f"/tmp/{file_uuid}_{file.filename}"
with open(temp_path, "wb") as f: f.write(content)
# 3. Képfeldolgozás (Pillow)
# Képfeldolgozás
img = Image.open(temp_path)
# A) Thumbnail generálás (300px WebP az SSD-re)
# Thumbnail (SSD)
thumb_filename = f"{file_uuid}_thumb.webp"
thumb_path = os.path.join(ssd_thumb_dir, thumb_filename)
thumb_path = os.path.join(thumb_dir, thumb_filename)
thumb_img = img.copy()
thumb_img.thumbnail((300, 300))
thumb_img.save(thumb_path, "WEBP", quality=80)
# B) Nagy kép optimalizálás (Max 1600px WebP a NAS-ra)
# Optimalizált eredeti (NAS)
vault_filename = f"{file_uuid}.webp"
vault_path = os.path.join(nas_vault_dir, vault_filename)
vault_path = os.path.join(vault_dir, vault_filename)
if img.width > 1600:
ratio = 1600 / float(img.width)
new_height = int(float(img.height) * float(ratio))
img = img.resize((1600, new_height), Image.Resampling.LANCZOS)
img = img.resize((1600, int(img.height * (1600 / img.width))), Image.Resampling.LANCZOS)
img.save(vault_path, "WEBP", quality=85)
# 4. Adatbázis rögzítés
# Mentés az új Document modellbe
new_doc = Document(
id=uuid4(),
parent_type=parent_type,
parent_id=parent_id,
original_name=file.filename,
file_hash=file_uuid,
file_ext="webp",
mime_type="image/webp",
file_size=os.path.getsize(vault_path),
has_thumbnail=True,
thumbnail_path=f"/static/previews/organizations/{parent_id}/{thumb_filename}"
thumbnail_path=f"/static/previews/{parent_type}/{parent_id}/{thumb_filename}"
)
db.add(new_doc)
await db.commit()
# 5. Puffer törlés ütemezése (30 perc)
# background_tasks.add_task(DocumentService._clean_temp, temp_path)
# MVP-ben töröljük azonnal, ha már a NAS-on van a biztonságos másolat
os.remove(temp_path)
return new_doc

View File

@@ -1,40 +1,54 @@
# /opt/docker/dev/service_finder/backend/app/services/fleet_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.vehicle import UserVehicle
from app.models.expense import VehicleEvent
from app.models.social import ServiceProvider, SourceType, ModerationStatus
from uuid import UUID
from app.models.asset import Asset, AssetEvent, AssetCost
from app.models.social import ServiceProvider, ModerationStatus
from app.schemas.fleet import EventCreate, TCOStats
from app.services.gamification_service import GamificationService
from app.services.gamification_service import gamification_service
async def add_vehicle_event(db: AsyncSession, vehicle_id: int, event_data: EventCreate, user_id: int):
v_res = await db.execute(select(UserVehicle).where(UserVehicle.id == vehicle_id))
vehicle = v_res.scalars().first()
if not vehicle: return {"error": "Vehicle not found"}
class FleetService:
@staticmethod
async def add_vehicle_event(db: AsyncSession, asset_id: UUID, event_data: EventCreate, user_id: int):
""" Esemény (szerviz/tankolás) rögzítése a Digitális Iker történetébe. """
res = await db.execute(select(Asset).where(Asset.id == asset_id))
asset = res.scalar_one_or_none()
if not asset: return None
final_provider_id = event_data.provider_id
if event_data.is_diy: final_provider_id = None
elif event_data.provider_name and not final_provider_id:
p_res = await db.execute(select(ServiceProvider).where(func.lower(ServiceProvider.name) == event_data.provider_name.lower()))
existing = p_res.scalars().first()
if existing: final_provider_id = existing.id
else:
new_p = ServiceProvider(name=event_data.provider_name, added_by_user_id=user_id, status=ModerationStatus.pending)
db.add(new_p); await db.flush(); final_provider_id = new_p.id
await GamificationService.award_points(db, user_id, 50, f"Új helyszín: {event_data.provider_name}")
# Szolgáltató kezelés
provider_id = event_data.provider_id
if not event_data.is_diy and event_data.provider_name and not provider_id:
p_stmt = select(ServiceProvider).where(func.lower(ServiceProvider.name) == event_data.provider_name.lower())
existing = (await db.execute(p_stmt)).scalar_one_or_none()
if existing: provider_id = existing.id
else:
new_p = ServiceProvider(name=event_data.provider_name, added_by_user_id=user_id, status=ModerationStatus.pending)
db.add(new_p); await db.flush(); provider_id = new_p.id
anomaly = event_data.odometer_value < vehicle.current_odometer
new_event = VehicleEvent(vehicle_id=vehicle_id, service_provider_id=final_provider_id, odometer_anomaly=anomaly, **event_data.model_dump(exclude={"provider_id", "provider_name"}))
db.add(new_event)
if event_data.odometer_value > vehicle.current_odometer: vehicle.current_odometer = event_data.odometer_value
await GamificationService.award_points(db, user_id, 20, f"Esemény: {event_data.event_type}")
await db.commit(); await db.refresh(new_event)
return new_event
# Esemény és Telemetria frissítés
anomaly = event_data.odometer_value < (asset.telemetry.current_mileage if asset.telemetry else 0)
new_event = AssetEvent(
asset_id=asset_id,
event_type=event_data.event_type,
recorded_mileage=event_data.odometer_value,
data=event_data.model_dump(exclude={"provider_id", "provider_name"})
)
db.add(new_event)
# Gamifikáció hívása
await gamification_service.process_activity(db, user_id, 20, 5, f"Asset Event: {event_data.event_type}")
await db.commit()
return new_event
async def calculate_tco(db: AsyncSession, vehicle_id: int) -> TCOStats:
result = await db.execute(select(VehicleEvent.event_type, func.sum(VehicleEvent.cost_amount)).where(VehicleEvent.vehicle_id == vehicle_id).group_by(VehicleEvent.event_type))
breakdown = {row[0]: row[1] for row in result.all()}
v_res = await db.execute(select(UserVehicle).where(UserVehicle.id == vehicle_id))
v = v_res.scalars().first()
km = (v.current_odometer - v.initial_odometer) if v else 0
cpk = sum(breakdown.values()) / km if km > 0 else 0
return TCOStats(vehicle_id=vehicle_id, total_cost=sum(breakdown.values()), breakdown=breakdown, cost_per_km=round(cpk, 2))
@staticmethod
async def calculate_tco(db: AsyncSession, asset_id: UUID) -> TCOStats:
""" TCO számítás az AssetCost tábla alapján. """
result = await db.execute(
select(AssetCost.cost_type, func.sum(AssetCost.amount_local))
.where(AssetCost.asset_id == asset_id)
.group_by(AssetCost.cost_type)
)
breakdown = {row[0]: float(row[1]) for row in result.all()}
total = sum(breakdown.values())
return TCOStats(asset_id=asset_id, total_cost_huf=total, cost_per_km=0.0) # KM logika az asset.telemetry-ből

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/services/gamification_service.py
import logging
import math
from decimal import Decimal
@@ -5,102 +6,136 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.gamification import UserStats, PointsLedger
from app.models.identity import User, Wallet
from app.models.core_logic import CreditTransaction
from app.models import SystemParameter
from app.models.audit import FinancialLedger
from app.models.system import SystemParameter
logger = logging.getLogger(__name__)
class GamificationService:
@staticmethod
async def get_config(db: AsyncSession):
"""Kiolvassa a GAMIFICATION_MASTER_CONFIG-ot a rendszerparaméterekből."""
"""
Dinamikus konfiguráció lekérése.
Ha nincs a DB-ben, ezek az alapértelmezett 'szabályok'.
"""
stmt = select(SystemParameter).where(SystemParameter.key == "GAMIFICATION_MASTER_CONFIG")
res = await db.execute(stmt)
param = res.scalar_one_or_none()
return param.value if param else {
"xp_logic": {"base_xp": 500, "exponent": 1.5},
"penalty_logic": {
"thresholds": {"level_1": 100, "level_2": 500, "level_3": 1000},
"multipliers": {"level_0": 1.0, "level_1": 0.5, "level_2": 0.1, "level_3": 0.0},
"recovery_rate": 0.5
"recovery_rate": 0.5 # Mennyi büntetőpontot dolgoz le 1 XP szerzésekor
},
"conversion_logic": {"social_to_credit_rate": 100},
"conversion_logic": {"social_to_credit_rate": 100}, # 100 social pont = 1 credit
"level_rewards": {"credits_per_10_levels": 50},
"blocked_roles": ["superadmin", "service_bot"]
}
async def process_activity(self, db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str, is_penalty: bool = False):
"""A 'Bíró' logika: Ellenőriz, büntet, jutalmaz és szintez."""
async def process_activity(
self,
db: AsyncSession,
user_id: int,
xp_amount: int,
social_amount: int,
reason: str,
is_penalty: bool = False
):
"""
A Rendszer 'Bírája'. Ez a függvény kezeli a teljes folyamatot:
Büntet, jutalmaz, szintet léptet és pénzt vált.
"""
config = await self.get_config(db)
# 1. Jogosultság ellenőrzése
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if not user or user.is_deleted or user.role.value in config.get("blocked_roles", []):
# 1. Felhasználó ellenőrzése
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
if not user or user.is_deleted or user.role in config["blocked_roles"]:
return None
# 2. Stats lekérése
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
# 2. Statisztikák lekérése (vagy létrehozása)
stats = (await db.execute(select(UserStats).where(UserStats.user_id == user_id))).scalar_one_or_none()
if not stats:
stats = UserStats(user_id=user_id)
db.add(stats)
await db.flush()
# 3. Büntető logika (Penalty)
# 3. BÜNTETŐ LOGIKA (Ha rosszalkodott a user)
if is_penalty:
stats.penalty_points += xp_amount
th = config["penalty_logic"]["thresholds"]
# Korlátozási szintek beállítása
if stats.penalty_points >= th["level_3"]: stats.restriction_level = 3
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=user_id, points=0, penalty_change=xp_amount, reason=f"PENALTY: {reason}"))
db.add(PointsLedger(user_id=user_id, points=0, penalty_change=xp_amount, reason=f"🔴 BÜNTETÉS: {reason}"))
await db.commit()
return stats
# 4. Dinamikus szorzó alkalmazása
multipliers = config["penalty_logic"]["multipliers"]
multiplier = multipliers.get(f"level_{stats.restriction_level}", 1.0)
# 4. SZORZÓK ALKALMAZÁSA (Büntetés alatt kevesebb pont jár)
multiplier = config["penalty_logic"]["multipliers"].get(f"level_{stats.restriction_level}", 1.0)
if multiplier <= 0:
logger.warning(f"User {user_id} activity blocked (Level {stats.restriction_level})")
logger.warning(f"User {user_id} tevékenysége blokkolva a magas büntetés miatt.")
return stats
# 5. XP, Ledolgozás és Szintlépés
# 5. XP SZÁMÍTÁS ÉS SZINTLÉPÉS
final_xp = int(xp_amount * multiplier)
if final_xp > 0:
stats.total_xp += final_xp
# Ledolgozás: Az XP szerzés csökkenti a meglévő büntetőpontokat
if stats.penalty_points > 0:
rec_rate = config["penalty_logic"]["recovery_rate"]
stats.penalty_points = max(0, stats.penalty_points - int(final_xp * rec_rate))
rec = int(final_xp * config["penalty_logic"]["recovery_rate"])
stats.penalty_points = max(0, stats.penalty_points - rec)
# Szint kiszámítása logaritmikus görbe alapján
xp_cfg = config["xp_logic"]
new_level = int((stats.total_xp / xp_cfg["base_xp"]) ** (1/xp_cfg["exponent"])) + 1
if new_level > stats.current_level:
# Kerek szinteknél jutalom (pl. minden 10. szint)
if new_level % 10 == 0:
reward = config["level_rewards"]["credits_per_10_levels"]
await self._add_credits(db, user_id, reward, f"Level {new_level} Achievement Bonus")
await self._add_earned_credits(db, user_id, reward, f"Szint bónusz: {new_level}")
stats.current_level = new_level
# 6. Social pont és váltás
# 6. SOCIAL PONT ÉS VALUTA VÁLTÁS (Kredit generálás)
final_social = int(social_amount * multiplier)
if final_social > 0:
stats.social_points += final_social
rate = config["conversion_logic"]["social_to_credit_rate"]
if stats.social_points >= rate:
new_credits = stats.social_points // rate
stats.social_points %= rate
await self._add_credits(db, user_id, new_credits, "Social conversion")
stats.social_points %= rate # A maradék megmarad
await self._add_earned_credits(db, user_id, new_credits, "Közösségi aktivitás váltása")
db.add(PointsLedger(user_id=user_id, points=final_xp, reason=reason))
# 7. NAPLÓZÁS (A PointsLedger a forrása a ranglistának)
db.add(PointsLedger(
user_id=user_id,
points=final_xp,
reason=reason
))
await db.commit()
await db.refresh(stats)
return stats
async def _add_credits(self, db: AsyncSession, user_id: int, amount: int, reason: str):
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id)
wallet = (await db.execute(wallet_stmt)).scalar_one_or_none()
async def _add_earned_credits(self, db: AsyncSession, user_id: int, amount: int, reason: str):
""" Kredit hozzáadása a Wallethez és a Pénzügyi Főkönyvhöz (FinancialLedger). """
wallet = (await db.execute(select(Wallet).where(Wallet.user_id == user_id))).scalar_one_or_none()
if wallet:
wallet.credit_balance += Decimal(amount)
db.add(CreditTransaction(org_id=None, amount=Decimal(amount), description=reason))
wallet.earned_credits += Decimal(str(amount))
# Pénzügyi audit bejegyzés
db.add(FinancialLedger(
user_id=user_id,
amount=float(amount),
currency="HUF",
transaction_type="GAMIFICATION_REWARD",
details={"reason": reason}
))
gamification_service = GamificationService()

View File

@@ -1,86 +1,117 @@
# /opt/docker/dev/service_finder/backend/app/services/geo_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import text, select
from typing import Optional, List
import uuid
import logging
logger = logging.getLogger(__name__)
class GeoService:
@staticmethod
async def get_street_suggestions(db: AsyncSession, zip_code: str, q: str) -> List[str]:
"""Azonnali utca-kiegészítés (Autocomplete) támogatása."""
"""
Azonnali utca-kiegészítés (Autocomplete) támogatása.
Kizárólag az adott irányítószámhoz már rögzített utcákat keresi.
"""
query = text("""
SELECT s.name
SELECT DISTINCT s.name
FROM data.geo_streets s
JOIN data.geo_postal_codes p ON s.postal_code_id = p.id
WHERE p.zip_code = :zip AND s.name ILIKE :q
ORDER BY s.name ASC LIMIT 10
""")
res = await db.execute(query, {"zip": zip_code, "q": f"{q}%"})
return [row[0] for row in res.fetchall()]
try:
res = await db.execute(query, {"zip": zip_code, "q": f"{q}%"})
return [row[0] for row in res.fetchall()]
except Exception as e:
logger.error(f"Street Suggestion Error: {e}")
return []
@staticmethod
async def get_or_create_full_address(
db: AsyncSession,
zip_code: str, city: str, street_name: str,
street_type: str, house_number: str,
zip_code: str,
city: str,
street_name: str,
street_type: str,
house_number: str,
stairwell: Optional[str] = None,
floor: Optional[str] = None,
door: Optional[str] = None,
parcel_id: Optional[str] = None
) -> uuid.UUID:
"""Hibrid címrögzítés: ellenőrzi a szótárakat és létrehozza a központi címet."""
# 1. Zip/City szótár frissítése (Auto-learning)
zip_id_res = await db.execute(text("""
INSERT INTO data.geo_postal_codes (zip_code, city) VALUES (:z, :c)
ON CONFLICT (country_code, zip_code, city) DO UPDATE SET city = EXCLUDED.city
RETURNING id
"""), {"z": zip_code, "c": city})
zip_id = zip_id_res.scalar()
"""
Hibrid címrögzítés: ellenőrzi a szótárakat és létrehozza a központi címet.
Az atomizált mezők (lépcsőház, emelet, ajtó) kezelése Master Book 2.0 szerint.
"""
try:
# 1. 📬 Irányítószám és Város (Auto-learning)
zip_id_query = text("""
INSERT INTO data.geo_postal_codes (zip_code, city, country_code)
VALUES (:z, :c, 'HU')
ON CONFLICT (country_code, zip_code, city) DO UPDATE SET city = EXCLUDED.city
RETURNING id
""")
zip_res = await db.execute(zip_id_query, {"z": zip_code, "c": city})
zip_id = zip_res.scalar()
# 2. Utca szótár frissítése (Auto-learning)
await db.execute(text("""
INSERT INTO data.geo_streets (postal_code_id, name) VALUES (:zid, :n)
ON CONFLICT (postal_code_id, name) DO NOTHING
"""), {"zid": zip_id, "n": street_name})
# 2. 🛣️ Utca szótár frissítése
await db.execute(text("""
INSERT INTO data.geo_streets (postal_code_id, name) VALUES (:zid, :n)
ON CONFLICT (postal_code_id, name) DO NOTHING
"""), {"zid": zip_id, "n": street_name})
# 3. Közterület típus (út, utca...) szótár
await db.execute(text("""
INSERT INTO data.geo_street_types (name) VALUES (:n) ON CONFLICT DO NOTHING
"""), {"n": street_type.lower()})
# 3. 🏷️ Közterület típus (út, utca, köz...)
await db.execute(text("""
INSERT INTO data.geo_street_types (name) VALUES (:n)
ON CONFLICT (name) DO NOTHING
"""), {"n": street_type.lower()})
# 4. Központi Address rekord rögzítése
full_text = f"{zip_code} {city}, {street_name} {street_type} {house_number}."
if stairwell: full_text += f" {stairwell}. lph,"
if floor: full_text += f" {floor}. em,"
if door: full_text += f" {door}. ajtó"
# 4. 📝 Szöveges cím generálása a kereshetőséghez
full_text_parts = [f"{zip_code} {city}, {street_name} {street_type} {house_number}."]
if stairwell: full_text_parts.append(f"{stairwell}. lph.")
if floor: full_text_parts.append(f"{floor}. em.")
if door: full_text_parts.append(f"{door}. ajtó")
full_text = " ".join(full_text_parts)
query = text("""
INSERT INTO data.addresses (
postal_code_id, street_name, street_type, house_number,
stairwell, floor, door, parcel_id, full_address_text
)
VALUES (
(SELECT id FROM data.geo_postal_codes WHERE zip_code = :z AND city = :c LIMIT 1),
:sn, :st, :hn, :sw, :fl, :dr, :pid, :txt
)
ON CONFLICT DO NOTHING
RETURNING id
""")
params = {
"z": zip_code, "c": city, "sn": street_name, "st": street_type,
"hn": house_number, "sw": stairwell, "fl": floor, "dr": door,
"pid": parcel_id, "txt": full_text
}
res = await db.execute(query, params)
addr_id = res.scalar()
# 5. 🏠 Központi Address rekord rögzítése vagy lekérése
# Az aszinkron környezetben a RETURNING a legbiztosabb módszer
address_query = text("""
INSERT INTO data.addresses (
postal_code_id, street_name, street_type, house_number,
stairwell, floor, door, parcel_id, full_address_text
)
VALUES (:zid, :sn, :st, :hn, :sw, :fl, :dr, :pid, :txt)
ON CONFLICT DO NOTHING
RETURNING id
""")
params = {
"zid": zip_id, "sn": street_name, "st": street_type,
"hn": house_number, "sw": stairwell, "fl": floor,
"dr": door, "pid": parcel_id, "txt": full_text
}
res = await db.execute(address_query, params)
addr_id = res.scalar()
if not addr_id:
# Ha már létezett ilyen részletes cím, lekérjük
addr_id = (await db.execute(text("""
SELECT id FROM data.addresses
WHERE street_name = :sn AND house_number = :hn
AND (stairwell IS NOT DISTINCT FROM :sw)
AND (floor IS NOT DISTINCT FROM :fl)
AND (door IS NOT DISTINCT FROM :dr)
LIMIT 1
"""), params)).scalar()
if not addr_id:
# Ha már létezett, megkeressük az ID-t a teljes szöveg alapján
# (Az IS NOT DISTINCT FROM kezeli a NULL értékeket az összehasonlításnál)
lookup_query = text("""
SELECT id FROM data.addresses
WHERE street_name = :sn AND house_number = :hn
AND (stairwell IS NOT DISTINCT FROM :sw)
AND (floor IS NOT DISTINCT FROM :fl)
AND (door IS NOT DISTINCT FROM :dr)
LIMIT 1
""")
lookup_res = await db.execute(lookup_query, params)
addr_id = lookup_res.scalar()
return addr_id
return addr_id
except Exception as e:
logger.error(f"Address Normalization Error: {str(e)}")
raise ValueError(f"Hiba a cím rögzítése során: {str(e)}")

View File

@@ -1,45 +0,0 @@
# /app/services/harvester_base.py
import httpx
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.asset import AssetCatalog
logger = logging.getLogger(__name__)
class BaseHarvester:
def __init__(self, category: str):
self.category = category # car, bike, truck
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"}
async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None):
"""Ellenőrzi a katalógusban való létezést."""
stmt = select(AssetCatalog).where(
AssetCatalog.make == brand,
AssetCatalog.model == model,
AssetCatalog.vehicle_class == self.category
)
if gen:
stmt = stmt.where(AssetCatalog.generation == gen)
result = await db.execute(stmt)
return result.scalar_one_or_none()
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict):
"""Létrehoz vagy frissít egy bejegyzést az AssetCatalog-ban."""
existing = await self.check_exists(db, brand, model, specs.get("generation"))
if not existing:
new_v = AssetCatalog(
make=brand,
model=model,
generation=specs.get("generation"),
year_from=specs.get("year_from"),
year_to=specs.get("year_to"),
vehicle_class=self.category,
fuel_type=specs.get("fuel_type"),
engine_code=specs.get("engine_code")
)
db.add(new_v)
logger.info(f"🆕 Új katalógus elem: {brand} {model}")
return True
return False

View File

@@ -1,12 +0,0 @@
from .harvester_base import BaseHarvester
class BikeHarvester(BaseHarvester):
def __init__(self):
super().__init__(category="motorcycle")
self.api_url = "https://api.example-bikes.com/v1/" # Példa forrás
async def harvest_all(self, db):
# Ide jön a motor-specifikus API hívás logikája
print("🏍️ Motor Robot: Adatgyűjtés indul...")
# ... fetch és mentés loop ...
await db.commit()

View File

@@ -1,84 +0,0 @@
import httpx
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.vehicle import VehicleCatalog # Az imént létrehozott modell
class VehicleHarvester:
def __init__(self):
# Az ingyenes CarQueryAPI URL-je (0.3-as verzió)
self.base_url = "https://www.carqueryapi.com/api/0.3/"
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/1.0"}
async def get_data(self, params: dict):
"""Segédfüggvény az API hívásokhoz."""
async with httpx.AsyncClient() as client:
try:
response = await client.get(self.base_url, params=params, headers=self.headers, timeout=10.0)
if response.status_code == 200:
# Az API néha JSONP-t ad vissza, ezt itt lekezeljük (levágjuk a felesleget)
text = response.text
if text.startswith("?("): text = text[2:-2]
return response.json()
return None
except Exception as e:
print(f"Robot hiba: {str(e)}")
return None
async def harvest_all(self, db: AsyncSession):
"""A fő folyamat: Minden márka -> Minden modell szinkronizálása."""
print("🤖 Robot: Indul a nagy adatgyűjtés...")
# 1. Márkák lekérése
makes_data = await self.get_data({"cmd": "getMakes", "sold_in_us": 0})
if not makes_data: return
makes = makes_data.get("Makes", [])
for make in makes:
make_id = make['make_id']
make_display = make['make_display']
print(f"--- 🚗 Feldolgozás: {make_display} ---")
# 2. Modellek lekérése ehhez a márkához
models_data = await self.get_data({"cmd": "getModels", "make": make_id})
if not models_data: continue
models = models_data.get("Models", [])
for model in models:
model_name = model['model_name']
# 3. Megnézzük, benne van-e már a katalógusban
stmt = select(VehicleCatalog).where(
VehicleCatalog.brand == make_display,
VehicleCatalog.model == model_name
)
res = await db.execute(stmt)
if res.scalar_one_or_none():
continue # Ha már megvan, ugrunk a következőre
# 4. Új bejegyzés létrehozása alapadatokkal
# Itt a Robot később "mélyebbre" áshat a specifikációkért
new_v = VehicleCatalog(
brand=make_display,
model=model_name,
category="car", # Alapértelmezett, később finomítható
factory_specs={
"api_make_id": make_id,
"harvester_source": "carquery"
}
)
db.add(new_v)
print(f"✅ Robot rögzítve: {make_display} {model_name}")
# Márkánként mentünk, hogy ne vesszen el a munka, ha megszakad
await db.commit()
await asyncio.sleep(1) # Ne terheljük túl az ingyenes API-t (Rate Limit védelem)
print("🏁 Robot: A munka oroszlánrésze kész!")
# Ez a rész csak a teszteléshez kell, ha manuálisan indítod a scriptet
if __name__ == "__main__":
# Itt lehetne egy külön indító logika
pass

View File

@@ -1,8 +0,0 @@
from .harvester_base import BaseHarvester
class TruckHarvester(BaseHarvester):
def __init__(self):
super().__init__(category="truck")
async def run(self, db):
print("🚛 Truck Robot: Nehézgépek és teherautók keresése...")

View File

@@ -1,62 +1,38 @@
# /opt/docker/dev/service_finder/backend/app/services/image_processor.py
import cv2
import numpy as np
from typing import Optional
class DocumentImageProcessor:
"""
Saját fejlesztésű képtisztító pipeline OCR-hez.
A nyers (mobillal fotózott) képekből kontrasztos, fekete-fehér, zajmentes változatot készít,
amelyet az AI már közel 100%-os pontossággal tud olvasni.
"""
""" Saját képtisztító pipeline Robot 3 OCR számára. """
@staticmethod
def process_for_ocr(image_bytes: bytes) -> Optional[bytes]:
if not image_bytes: return None
try:
# 1. Kép betöltése a memóriából (FastAPI UploadFile bytes-ból)
# A képet nem mentjük a lemezre, villámgyorsan a RAM-ban dolgozzuk fel.
nparr = np.frombuffer(image_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None: return None
if img is None:
raise ValueError("A képet nem sikerült dekódolni.")
# 2. Szürkeárnyalatossá alakítás (A színek csak zavarják a szövegfelismerést)
# 1. Előkészítés (Szürkeárnyalat + Felskálázás)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 3. Kép átméretezése (Felskálázás)
# Az AI és az OCR motorok a minimum 300 DPI körüli képeket szeretik.
height, width = gray.shape
if width < 1000 or height < 1000:
if gray.shape[1] < 1200:
gray = cv2.resize(gray, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
# 4. Kontraszt növelése (CLAHE - Contrast Limited Adaptive Histogram Equalization)
# Ez eltünteti a vaku okozta becsillanásokat és kiemeli a halvány betűket.
# 2. Kontraszt dúsítás (CLAHE)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
contrast = clahe.apply(gray)
# 5. Enyhe homályosítás (Denoising / Noise Reduction)
# Eltünteti a papír textúráját (pl. a forgalmi engedély vízjelét vagy a blokk gyűrődéseit).
blur = cv2.GaussianBlur(contrast, (5, 5), 0)
# 6. Adaptív Küszöbérték (Binarization)
# Minden pixel környezetét külön vizsgálja. Ez küszöböli ki azt, amikor a fotó egyik
# sarka sötét (pl. árnyékot vet a telefon), a másik meg világos.
# 3. Adaptív Binarizálás (Fekete-fehér szöveg kiemelés)
blur = cv2.GaussianBlur(contrast, (3, 3), 0)
thresh = cv2.adaptiveThreshold(
blur,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
11, # Blokk méret (páratlan szám)
2 # Konstans levonás
blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# 7. Visszakódolás bájt formátumba (PNG), hogy átadhassuk az AI-nak
success, encoded_image = cv2.imencode('.png', thresh)
if not success:
raise ValueError("Nem sikerült a feldolgozott képet PNG-be kódolni.")
return encoded_image.tobytes()
return encoded_image.tobytes() if success else None
except Exception as e:
print(f"Hiba a képfeldolgozás során: {str(e)}")
print(f"OpenCV Feldolgozási hiba: {e}")
return None

View File

@@ -1,35 +1,35 @@
# /opt/docker/dev/service_finder/backend/app/services/matching_service.py
from typing import List, Dict, Any
from sqlalchemy import text
from app.db.session import SessionLocal
from app.services.config_service import config
class MatchingService:
@staticmethod
async def rank_services(services: List[Dict[str, Any]], org_id: int = None) -> List[Dict[str, Any]]:
# 1. Dinamikus paraméterek lekérése az Admin beállításokból
w_dist = await config.get_setting('weight_distance', org_id=org_id, default=0.5)
w_rate = await config.get_setting('weight_rating', org_id=org_id, default=0.5)
b_gold = await config.get_setting('bonus_gold_service', org_id=org_id, default=500)
""" Szolgáltatók rangsorolása dinamikus Sentinel paraméterek alapján. """
# JAVÍTVA: Hierarchikus paraméterek lekérése
w_dist = float(await config.get_setting('weight_distance', org_id=org_id, default=0.5))
w_rate = float(await config.get_setting('weight_rating', org_id=org_id, default=0.5))
b_gold = float(await config.get_setting('bonus_gold_service', org_id=org_id, default=500))
ranked_list = []
for s in services:
# Normalizált pontszámok (példa logika)
# Távolság pont (P_dist): 100 / (távolság + 1) -> közelebb = több pont
p_dist = 100 / (s.get('distance', 1) + 1)
# Távolság pont (közelebb = több pont)
dist = s.get('distance', 1.0)
p_dist = 100 / (dist + 1)
# Értékelés pont (P_rate): csillagok * 20 -> 5 csillag = 100 pont
p_rate = s.get('rating', 0) * 20
# Értékelés pont (0-5 csillag -> 0-100 pont)
p_rate = s.get('rating', 0.0) * 20
# Bónusz (B_tier): ha Gold, megkapja a bónuszt
# Bónusz a kiemelt (Gold) partnereknek
tier_bonus = b_gold if s.get('tier') == 'gold' else 0
# A Mester Képlet:
total_score = (p_dist * float(w_dist)) + (p_rate * float(w_rate)) + tier_bonus
# Összesített pontszám
total_score = (p_dist * w_dist) + (p_rate * w_rate) + tier_bonus
s['total_score'] = round(total_score, 2)
ranked_list.append(s)
# Sorbarendezés pontszám szerint csökkenőben
return sorted(ranked_list, key=lambda x: x['total_score'], reverse=True)
matching_service = MatchingService()
matching_service = MatchingService()

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/services/media_service.py
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import logging
@@ -6,48 +7,39 @@ from typing import Tuple, Optional
logger = logging.getLogger(__name__)
class MediaService:
@staticmethod
def _get_if_exist(data, key):
if key in data:
return data[key]
return None
@staticmethod
def _convert_to_degrees(value) -> float:
"""EXIF koordináták (fok, perc, másodperc) konvertálása tizedes fokká."""
d = float(value[0])
m = float(value[1])
s = float(value[2])
return d + (m / 60.0) + (s / 3600.0)
""" EXIF racionális koordináták konvertálása tizedes fokká. """
try:
d = float(value[0])
m = float(value[1])
s = float(value[2])
return d + (m / 60.0) + (s / 3600.0)
except (IndexError, ZeroDivisionError, TypeError):
return 0.0
@classmethod
def extract_gps_info(cls, file_path: str) -> Optional[Tuple[float, float]]:
"""Kiolvassa a GPS koordinátákat a képből."""
""" GPS koordináták kinyerése a kép metaadataiból (Robot Hunt alapja). """
try:
image = Image.open(file_path)
exif_data = image._getexif()
if not exif_data:
return None
with Image.open(file_path) as image:
exif = image._getexif()
if not exif: return None
gps_info = {}
for tag, value in exif_data.items():
decoded = TAGS.get(tag, tag)
if decoded == "GPSInfo":
for t in value:
sub_decoded = GPSTAGS.get(t, t)
gps_info[sub_decoded] = value[t]
gps_info = {}
for tag, value in exif.items():
if TAGS.get(tag) == "GPSInfo":
for t in value:
gps_info[GPSTAGS.get(t, t)] = value[t]
if gps_info:
lat = cls._convert_to_degrees(gps_info['GPSLatitude'])
if gps_info['GPSLatitudeRef'] != "N":
lat = 0 - lat
if 'GPSLatitude' in gps_info and 'GPSLongitude' in gps_info:
lat = cls._convert_to_degrees(gps_info['GPSLatitude'])
if gps_info.get('GPSLatitudeRef') != "N": lat = -lat
lon = cls._convert_to_degrees(gps_info['GPSLongitude'])
if gps_info.get('GPSLongitudeRef') != "E": lon = -lon
lon = cls._convert_to_degrees(gps_info['GPSLongitude'])
if gps_info['GPSLongitudeRef'] != "E":
lon = 0 - lon
return lat, lon
return lat, lon
except Exception as e:
logger.warning(f"Nem sikerült kiolvasni az EXIF adatokat: {e}")
return None
logger.warning(f"EXIF kiolvasási hiba ({file_path}): {e}")
return None

View File

@@ -1,14 +1,31 @@
from datetime import datetime, timedelta
# /opt/docker/dev/service_finder/backend/app/services/notification_service.py
from datetime import datetime, timedelta, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.user import User
from app.models.vehicle import Vehicle
from app.core.email import send_expiry_notification
from fastapi import BackgroundTasks
from app.models.identity import User
from app.models.asset import Asset
from app.core.email import send_expiry_notification # Feltételezett core funkció
async def check_expiring_documents(db: AsyncSession, background_tasks: BackgroundTasks):
# Példa: Műszaki vizsga lejárata 30 napon belül
threshold = datetime.now().date() + timedelta(days=30)
result = await db.execute(
select(Vehicle, User).join(User).where(Vehicle.mot_expiry_date <= threshold)
)
for vehicle, user in result.all():
send_expiry_notification(background_tasks, user.email, f"Műszaki vizsga ({vehicle.license_plate})")
class NotificationService:
@staticmethod
async def check_expiring_documents(db: AsyncSession, background_tasks: BackgroundTasks):
"""
Példa: Műszaki vizsga lejárata 30 napon belül.
A logikát az új Asset és Identity modellekhez igazítottuk.
"""
threshold = datetime.now(timezone.utc).date() + timedelta(days=30)
# JAVÍTVA: Asset join identity.User-el az új struktúra szerint
stmt = select(Asset, User).join(User, Asset.owner_org_id == User.scope_id).where(
Asset.status == "active"
)
result = await db.execute(stmt)
for asset, user in result.all():
# A lejárati adatot a dúsított factory_data-ból vesszük
expiry = asset.factory_data.get("mot_expiry_date") if asset.factory_data else None
if expiry:
expiry_dt = datetime.strptime(expiry, "%Y-%m-%d").date()
if expiry_dt <= threshold:
send_expiry_notification(background_tasks, user.email, f"Műszaki vizsga lejár: {asset.license_plate}")

View File

@@ -1,5 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/services/recon_bot.py
import asyncio
import logging
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.asset import Asset, AssetCatalog, AssetTelemetry
@@ -10,42 +12,38 @@ async def run_vehicle_recon(db: AsyncSession, asset_id: str):
"""
VIN alapján megkeresi a mélységi adatokat és frissíti a Digitális Ikert.
"""
# 1. Lekérjük a járművet és a katalógusát
stmt = select(Asset).where(Asset.id == asset_id)
result = await db.execute(stmt)
asset = result.scalar_one_or_none()
asset = (await db.execute(stmt)).scalar_one_or_none()
if not asset or not asset.catalog_id:
return False
logger.info(f"🤖 Robot indul: {asset.vin} felderítése...")
# 2. SZIMULÁLT ADATGYŰJTÉS (Itt hívnánk meg az API-kat: NHTSA, autodna stb.)
await asyncio.sleep(2) # Időigényes keresés szimulálása
# --- LOGIKA MEGŐRIZVE: Szimulált mélységi adatgyűjtés ---
await asyncio.sleep(2)
deep_data = {
"assembly_plant": "Fremont, California",
"drive_unit": "Dual Motor - Raven type",
"onboard_charger": "11 kW",
"supercharging_max": "250 kW",
"safety_rating": "5-star EuroNCAP"
"safety_rating": "5-star EuroNCAP",
"recon_timestamp": datetime.now(timezone.utc).isoformat()
}
# 3. Katalógus frissítése
catalog_stmt = select(AssetCatalog).where(AssetCatalog.id == asset.catalog_id)
catalog = (await db.execute(catalog_stmt)).scalar_one_or_none()
# 3. Katalógus frissítése (MDM elv)
catalog = (await db.execute(select(AssetCatalog).where(AssetCatalog.id == asset.catalog_id))).scalar_one_or_none()
if catalog:
current_data = catalog.factory_data or {}
current_data.update(deep_data)
catalog.factory_data = current_data
# 4. Telemetria frissítése (A robot talált egy visszahívást, VQI csökken kicsit)
telemetry_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
telemetry = (await db.execute(telemetry_stmt)).scalar_one_or_none()
# 4. Telemetria frissítése (VQI score csökkentése a logika szerint)
telemetry = (await db.execute(select(AssetTelemetry).where(AssetTelemetry.asset_id == asset.id))).scalar_one_or_none()
if telemetry:
telemetry.vqi_score = 99.2 # Robot frissített állapota
telemetry.vqi_score = 99.2
await db.commit()
logger.info(f"✨ Robot végzett: {asset.license_plate} felokosítva.")
logger.info(f"✨ Robot végzett: {asset.license_plate or asset.vin} felokosítva.")
return True

View File

@@ -1,27 +1,27 @@
# /app/services/robot_manager.py
# /opt/docker/dev/service_finder/backend/app/services/robot_manager.py
import asyncio
import logging
from datetime import datetime
from .harvester_cars import CarHarvester
# Megjegyzés: Ellenőrizd, hogy a harvester_bikes/trucks fájlokban is BaseHarvester az alap!
from .harvester_cars import VehicleHarvester
# Megjegyzés: Csak azokat importáld, amik öröklődnek a BaseHarvester-ből
logger = logging.getLogger(__name__)
class RobotManager:
@staticmethod
async def run_full_sync(db):
"""Sorban lefuttatja a robotokat az új AssetCatalog struktúrához."""
""" Sorban lefuttatja a robotokat az új AssetCatalog struktúrához. """
logger.info(f"🕒 Teljes szinkronizáció indítva: {datetime.now()}")
robots = [
CarHarvester(),
# BikeHarvester(),
# TruckHarvester()
VehicleHarvester(),
# BikeHarvester(), # Későbbi bővítéshez
]
for robot in robots:
try:
await robot.run(db)
# JAVÍTVA: A modern Harvesterek a harvest_all metódust használják
await robot.harvest_all(db)
logger.info(f"{robot.category} robot sikeresen lefutott.")
await asyncio.sleep(5)
except Exception as e:
@@ -29,9 +29,12 @@ class RobotManager:
@staticmethod
async def schedule_nightly_run(db):
"""
LOGIKA MEGŐRIZVE: Éjszakai futtatás 02:00-kor.
"""
while True:
now = datetime.now()
if now.hour == 2 and now.minute == 0:
await RobotManager.run_full_sync(db)
await asyncio.sleep(70)
await asyncio.sleep(70) # Megakadályozzuk az újraindulást ugyanabban a percben
await asyncio.sleep(30)

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/services/search_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
@@ -15,36 +16,29 @@ class SearchService:
is_premium: bool = False
):
"""
Keresés távolság és szakértelem alapján.
Premium: Trust Score + Valós távolság.
Free: Trust Score + Légvonal.
Keresés távolság és szakértelem alapján PostGIS funkciókkal.
"""
user_point = ST_MakePoint(lon, lat) # PostGIS pont létrehozása
user_point = ST_MakePoint(lon, lat)
# Alap lekérdezés: ServiceProfile + Organization adatok
# Alap lekérdezés joinolva az Organization-el a nevekért
stmt = select(ServiceProfile, Organization).join(
Organization, ServiceProfile.organization_id == Organization.id
)
# 1. Sugár alapú szűrés (radius_km * 1000 méter)
stmt = stmt.where(
).where(
func.ST_DWithin(ServiceProfile.location, user_point, radius_km * 1000)
)
# 2. Szakterület szűrése
# SZAKÉRTELEM SZŰRÉS (Logic Preserved)
if expertise_key:
stmt = stmt.join(ServiceProfile.expertises).join(ExpertiseTag).where(
ExpertiseTag.key == expertise_key
)
# 3. Távolság és Trust Score alapú sorrend
# A ST_Distance méterben adja vissza az eredményt
# RENDEZÉS TÁVOLSÁG SZERINT
stmt = stmt.order_by(ST_Distance(ServiceProfile.location, user_point))
result = await db.execute(stmt.limit(50))
rows = result.all()
# Rangsorolási logika alkalmazása
results = []
for s_prof, org in rows:
results.append({
@@ -57,5 +51,5 @@ class SearchService:
"is_premium_partner": s_prof.trust_score >= 90
})
# Súlyozott rendezés: Prémium partnerek és Trust Score előre
# SÚLYOZOTT RENDEZÉS (Logic Preserved: Premium előre, Trust Score csökkenő)
return sorted(results, key=lambda x: (not is_premium, -x['trust_score']))

View File

@@ -1,22 +1,21 @@
# /opt/docker/dev/service_finder/backend/app/services/security_service.py
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, Any, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.models.identity import User
from app.models import SystemParameter
from app.models.system import SystemParameter
logger = logging.getLogger(__name__)
class SecurityService:
@staticmethod
async def get_sec_config(db: AsyncSession) -> Dict[str, Any]:
"""Lekéri a biztonsági korlátokat a központi rendszerparaméterekből."""
keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "SECURITY_DUAL_CONTROL_ENABLED"]
stmt = select(SystemParameter).where(SystemParameter.key.in_(keys))
""" Lekéri a korlátokat a központi system_parameters-ből. """
stmt = select(SystemParameter).where(SystemParameter.key.in_(["SECURITY_MAX_RECORDS_PER_HOUR", "SECURITY_DUAL_CONTROL_ENABLED"]))
res = await db.execute(stmt)
params = {p.key: p.value for p in res.scalars().all()}
@@ -25,145 +24,71 @@ class SecurityService:
"dual_control": str(params.get("SECURITY_DUAL_CONTROL_ENABLED", "true")).lower() == "true"
}
# --- 1. SZINT: AUDIT & LOGGING (A Mindenlátó Szem) ---
async def log_event(
self,
db: AsyncSession,
user_id: Optional[int],
action: str,
severity: LogSeverity,
old_data: Optional[Dict] = None,
new_data: Optional[Dict] = None,
ip: Optional[str] = None,
ua: Optional[str] = None,
target_type: Optional[str] = None,
target_id: Optional[str] = None,
reason: Optional[str] = None
):
"""Minden rendszerművelet rögzítése és azonnali biztonsági elemzése."""
async def log_event(self, db: AsyncSession, user_id: Optional[int], action: str, severity: LogSeverity, **kwargs):
""" LOGIKA MEGŐRIZVE: Audit naplózás + Emergency Lock trigger. """
new_log = AuditLog(
user_id=user_id,
severity=severity,
action=action,
target_type=target_type,
target_id=target_id,
old_data=old_data,
new_data=new_data,
ip_address=ip,
user_agent=ua
user_id=user_id, severity=severity, action=action,
target_type=kwargs.get("target_type"), target_id=kwargs.get("target_id"),
old_data=kwargs.get("old_data"), new_data=kwargs.get("new_data"),
ip_address=kwargs.get("ip"), user_agent=kwargs.get("ua")
)
db.add(new_log)
# Ha a szint EMERGENCY, azonnal lőjük le a júzert
if severity == LogSeverity.emergency:
await self._execute_emergency_lock(db, user_id, f"Auto-lock triggered by: {action}")
await self._execute_emergency_lock(db, user_id, f"Auto-lock by: {action}")
await db.commit()
# --- 2. SZINT: PENDING ACTIONS (Négy szem elv) ---
async def request_action(
self,
db: AsyncSession,
requester_id: int,
action_type: str,
payload: Dict,
reason: str
):
"""Kritikus művelet kezdeményezése jóváhagyásra (nem hajtódik végre azonnal)."""
async def request_action(self, db: AsyncSession, requester_id: int, action_type: str, payload: Dict, reason: str):
""" NÉGY SZEM ELV: Jóváhagyási kérelem indítása. """
new_action = PendingAction(
requester_id=requester_id,
action_type=action_type,
payload=payload,
reason=reason,
status=ActionStatus.pending
requester_id=requester_id, action_type=action_type,
payload=payload, reason=reason, status=ActionStatus.pending
)
db.add(new_action)
await self.log_event(
db, requester_id,
action=f"REQUEST_{action_type}",
severity=LogSeverity.critical,
new_data=payload,
reason=f"Approval requested: {reason}"
)
await db.commit()
return new_action
async def approve_action(self, db: AsyncSession, approver_id: int, action_id: int):
"""Művelet végrehajtása egy második admin által."""
""" Jóváhagyás végrehajtása (Logic Preserved: Ön-jóváhagyás tiltva). """
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one_or_none()
if not action or action.status != ActionStatus.pending:
raise Exception("A művelet nem található vagy már feldolgozták.")
raise Exception("Művelet nem található.")
if action.requester_id == approver_id:
raise Exception("Önmagad kérését nem hagyhatod jóvá! (Négy szem elv)")
raise Exception("Saját kérést nem hagyhatsz jóvá!")
# ITT TÖRTÉNIK A TÉNYLEGES ÜZLETI LOGIKA (Példa: Rangmódosítás)
# Üzleti logika (pl. Role változtatás)
if action.action_type == "CHANGE_ROLE":
user_id = action.payload.get("user_id")
new_role = action.payload.get("new_role")
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if user:
user.role = new_role
logger.info(f"Role for user {user_id} changed to {new_role} via approved action {action_id}")
target_user = (await db.execute(select(User).where(User.id == action.payload.get("user_id")))).scalar_one_or_none()
if target_user: target_user.role = action.payload.get("new_role")
action.status = ActionStatus.approved
action.approver_id = approver_id
action.processed_at = func.now()
await self.log_event(
db, approver_id,
action=f"APPROVE_{action.action_type}",
severity=LogSeverity.info,
target_id=str(action.id),
reason=f"Approved action requested by {action.requester_id}"
)
action.processed_at = datetime.now(timezone.utc)
await db.commit()
return True
# --- 3. SZINT: DATA THROTTLING & EMERGENCY LOCK ---
async def check_data_access_limit(self, db: AsyncSession, user_id: int):
"""Figyeli a tömeges adatlekérést (Adatlopás elleni védelem)."""
""" DATA THROTTLING: Adatlopás elleni védelem. """
config = await self.get_sec_config(db)
one_hour_ago = datetime.now() - timedelta(hours=1)
limit_time = datetime.now(timezone.utc) - timedelta(hours=1)
# Megszámoljuk az utolsó egy óra GET (lekérési) logjait
stmt = select(func.count(AuditLog.id)).where(
and_(
AuditLog.user_id == user_id,
AuditLog.timestamp >= one_hour_ago,
AuditLog.action.like("GET_%")
)
and_(AuditLog.user_id == user_id, AuditLog.timestamp >= limit_time, AuditLog.action.like("GET_%"))
)
count = (await db.execute(stmt)).scalar() or 0
if count > config["max_records"]:
await self.log_event(
db, user_id,
action="MASS_DATA_ACCESS_DETECTED",
severity=LogSeverity.emergency,
reason=f"Access count: {count} (Limit: {config['max_records']})"
)
# A log_event automatikusan hívja a _execute_emergency_lock-ot
await self.log_event(db, user_id, "MASS_DATA_ACCESS", LogSeverity.emergency, reason=f"Count: {count}")
return False
return True
async def _execute_emergency_lock(self, db: AsyncSession, user_id: int, reason: str):
"""Azonnali fiókfelfüggesztés vészhelyzet esetén."""
if not user_id: return
stmt = select(User).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
if user:
user.is_active = False
logger.critical(f"🚨 SECURITY EMERGENCY LOCK: User {user_id} suspended. Reason: {reason}")
# Itt lehetne bekötni egy külső SMS/Slack/Email riasztást
logger.critical(f"🚨 EMERGENCY LOCK: User {user_id} suspended. Reason: {reason}")
security_service = SecurityService()

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/services/social_auth_service.py
import uuid
import logging
from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,84 +10,34 @@ logger = logging.getLogger(__name__)
class SocialAuthService:
@staticmethod
async def get_or_create_social_user(
db: AsyncSession,
provider: str,
social_id: str,
email: str,
first_name: str,
last_name: str
):
async def get_or_create_social_user(db: AsyncSession, provider: str, social_id: str, email: str, first_name: str, last_name: str):
"""
LOGIKA MEGŐRIZVE: Step 1 regisztráció slug és flotta nélkül.
"""
Social Step 1: Csak alapregisztráció.
Nincs slug generálás, nincs flotta. Megáll a KYC kapujában.
"""
# 1. Meglévő Social kapcsolat ellenőrzése
stmt = select(SocialAccount).where(
SocialAccount.provider == provider,
SocialAccount.social_id == social_id
)
result = await db.execute(stmt)
social_acc = result.scalar_one_or_none()
# 1. Meglévő fiók ellenőrzése
stmt = select(SocialAccount).where(SocialAccount.provider == provider, SocialAccount.social_id == social_id)
social_acc = (await db.execute(stmt)).scalar_one_or_none()
if social_acc:
stmt = select(User).where(User.id == social_acc.user_id)
user_result = await db.execute(stmt)
return user_result.scalar_one_or_none()
return (await db.execute(select(User).where(User.id == social_acc.user_id))).scalar_one_or_none()
# 2. Felhasználó keresése email alapján
stmt = select(User).where(User.email == email)
user_result = await db.execute(stmt)
user = user_result.scalar_one_or_none()
# 2. Új Identity és User (Step 1)
stmt_u = select(User).where(User.email == email)
user = (await db.execute(stmt_u)).scalar_one_or_none()
if not user:
try:
# Person rekord létrehozása a Google-től kapott nevekkel
new_person = Person(
id_uuid=uuid.uuid4(),
first_name=first_name or "Google",
last_name=last_name or "User",
is_active=False
)
db.add(new_person)
await db.flush()
new_person = Person(first_name=first_name or "Social", last_name=last_name or "User", is_active=False)
db.add(new_person)
await db.flush()
# User rekord (folder_slug nélkül!)
user = User(
email=email,
hashed_password=None,
person_id=new_person.id,
role=UserRole.user,
is_active=False,
is_deleted=False,
preferred_language="hu",
region_code="HU"
)
db.add(user)
await db.flush()
user = User(email=email, person_id=new_person.id, role=UserRole.user, is_active=False)
db.add(user)
await db.flush()
await security_service.log_event(
db,
user_id=user.id,
action="USER_REGISTER_SOCIAL",
severity="info",
target_type="User",
target_id=str(user.id),
new_data={"email": email, "provider": provider}
)
except Exception as e:
await db.rollback()
logger.error(f"Social Registration Error: {str(e)}")
raise e
await security_service.log_event(db, user.id, "USER_REGISTER_SOCIAL", "info", target_type="User", target_id=str(user.id))
# 3. Összekötés
new_social = SocialAccount(
user_id=user.id,
provider=provider,
social_id=social_id,
email=email
)
db.add(new_social)
# 3. Kapcsolat rögzítése
db.add(SocialAccount(user_id=user.id, provider=provider, social_id=social_id, email=email))
await db.commit()
await db.refresh(user)
return user

View File

@@ -1,64 +1,103 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from datetime import datetime, timezone
import logging
from app.models.social import ServiceProvider, Vote, ModerationStatus, Competition, UserScore
from app.models.user import User
from datetime import datetime
from app.services.gamification_service import GamificationService
from app.models.identity import User
from app.schemas.social import ServiceProviderCreate
async def create_service_provider(db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int):
new_provider = ServiceProvider(**obj_in.dict(), added_by_user_id=user_id)
db.add(new_provider)
await db.flush()
await GamificationService.award_points(db, user_id, 50, f"Új szolgáltató: {new_provider.name}")
await db.commit()
await db.refresh(new_provider)
return new_provider
logger = logging.getLogger(__name__)
async def vote_for_provider(db: AsyncSession, voter_id: int, provider_id: int, vote_value: int):
res = await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))
if res.scalars().first(): return {"message": "User already voted"}
new_vote = Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value)
db.add(new_vote)
p_res = await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))
provider = p_res.scalars().first()
if not provider: return {"error": "Provider not found"}
provider.validation_score += vote_value
if provider.status == ModerationStatus.pending:
if provider.validation_score >= 5:
provider.status = ModerationStatus.approved
await _reward_submitter(db, provider.added_by_user_id, provider.name)
elif provider.validation_score <= -3:
provider.status = ModerationStatus.rejected
await _penalize_user(db, provider.added_by_user_id, provider.name)
await db.commit()
return {"message": "Vote cast", "new_score": provider.validation_score, "status": provider.status}
class SocialService:
"""
SocialService: Kezeli a közösségi interakciókat, szavazatokat és a moderációt.
Az importok a metódusokon belül vannak a körkörös függőség elkerülése érdekében.
"""
async def get_leaderboard(db: AsyncSession, limit: int = 10):
return await GamificationService.get_top_users(db, limit)
async def create_service_provider(self, db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int):
from app.services.gamification_service import gamification_service
new_provider = ServiceProvider(**obj_in.model_dump(), added_by_user_id=user_id)
db.add(new_provider)
await db.flush()
# Alappontszám az új beküldésért
await gamification_service.process_activity(db, user_id, 50, 10, f"New Provider: {new_provider.name}")
await db.commit()
await db.refresh(new_provider)
return new_provider
async def _reward_submitter(db: AsyncSession, user_id: int, provider_name: str):
if not user_id: return
await GamificationService.award_points(db, user_id, 100, f"Validált szolgáltató: {provider_name}")
u_res = await db.execute(select(User).where(User.id == user_id))
user = u_res.scalars().first()
if user: user.reputation_score = (user.reputation_score or 0) + 1
now = datetime.utcnow()
c_res = await db.execute(select(Competition).where(and_(Competition.is_active == True, Competition.start_date <= now, Competition.end_date >= now)))
comp = c_res.scalars().first()
if comp:
s_res = await db.execute(select(UserScore).where(and_(UserScore.user_id == user_id, UserScore.competition_id == comp.id)))
us = s_res.scalars().first()
if not us:
us = UserScore(user_id=user_id, competition_id=comp.id, points=0)
db.add(us)
us.points += 10
async def vote_for_provider(self, db: AsyncSession, voter_id: int, provider_id: int, vote_value: int):
from app.services.gamification_service import gamification_service
# Duplikált szavazat ellenőrzése
exists = (await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))).scalar()
if exists:
return {"message": "Már szavaztál erre a szolgáltatóra!"}
async def _penalize_user(db: AsyncSession, user_id: int, provider_name: str):
if not user_id: return
await GamificationService.award_points(db, user_id, -50, f"Elutasított szolgáltató: {provider_name}")
u_res = await db.execute(select(User).where(User.id == user_id))
user = u_res.scalars().first()
if user:
user.reputation_score = (user.reputation_score or 0) - 2
if user.reputation_score <= -10: user.is_active = False
db.add(Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value))
provider = (await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))).scalar_one_or_none()
if not provider:
return {"error": "Szolgáltató nem található."}
provider.validation_score += vote_value
# Automatikus moderáció figyelése (csak a 'pending' állapotúaknál)
if provider.status == ModerationStatus.pending:
if provider.validation_score >= 5:
provider.status = ModerationStatus.approved
await self._reward_submitter(db, provider.added_by_user_id, provider.name)
elif provider.validation_score <= -3:
provider.status = ModerationStatus.rejected
await self._penalize_user(db, provider.added_by_user_id, provider.name)
await db.commit()
return {"status": "success", "score": provider.validation_score, "new_status": provider.status}
async def get_leaderboard(self, db: AsyncSession, limit: int = 10):
from app.services.gamification_service import gamification_service
if hasattr(gamification_service, 'get_top_users'):
return await gamification_service.get_top_users(db, limit)
return []
async def _reward_submitter(self, db: AsyncSession, user_id: int, provider_name: str):
""" Jutalmazás, ha a beküldött adatot jóváhagyta a közösség. """
from app.services.gamification_service import gamification_service
if not user_id: return
await gamification_service.process_activity(db, user_id, 100, 20, f"Validated: {provider_name}")
# Aktuális verseny keresése és pontozása
now = datetime.now(timezone.utc)
comp_stmt = select(Competition).where(and_(
Competition.is_active == True,
Competition.start_date <= now,
Competition.end_date >= now
))
comp = (await db.execute(comp_stmt)).scalar_one_or_none()
if comp:
us_stmt = select(UserScore).where(and_(UserScore.user_id == user_id, UserScore.competition_id == comp.id))
us = (await db.execute(us_stmt)).scalar_one_or_none()
if not us:
us = UserScore(user_id=user_id, competition_id=comp.id, points=0)
db.add(us)
us.points += 10
async def _penalize_user(self, db: AsyncSession, user_id: int, provider_name: str):
""" Büntetés, ha a beküldött adatot elutasította a közösség (is_penalty=True). """
from app.services.gamification_service import gamification_service
if not user_id: return
# JAVÍTVA: is_penalty=True hozzáadva a gamification híváshoz
await gamification_service.process_activity(db, user_id, 50, 0, f"Rejected: {provider_name}", is_penalty=True)
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
if user and hasattr(user, 'reputation_score'):
user.reputation_score = (user.reputation_score or 0) - 2
if user.reputation_score <= -10:
user.is_active = False
social_service = SocialService()

View File

@@ -1,25 +1,27 @@
# /opt/docker/dev/service_finder/backend/app/services/storage_service.py
import uuid
from io import BytesIO
from minio import Minio
from app.core.config import settings
class StorageService:
# A klienst a beállításokból inicializáljuk
client = Minio(
settings.MINIO_ENDPOINT,
access_key=settings.MINIO_ROOT_USER,
secret_key=settings.MINIO_ROOT_PASSWORD,
secure=settings.MINIO_SECURE
settings.REDIS_URL.split("//")[1].split(":")[0], # Gyors fix a hostra vagy settings.MINIO_HOST
access_key="minioadmin",
secret_key="minioadmin",
secure=False
)
BUCKET_NAME = "vehicle-documents"
@classmethod
async def upload_document(cls, file_bytes: bytes, file_name: str, folder: str) -> str:
""" Fájl feltöltése S3/Minio tárhelyre. """
if not cls.client.bucket_exists(cls.BUCKET_NAME):
cls.client.make_bucket(cls.BUCKET_NAME)
# Egyedi fájlnév generálása az ütközések elkerülésére
unique_name = f"{folder}/{uuid.uuid4()}_{file_name}"
from io import BytesIO
cls.client.put_object(
cls.BUCKET_NAME,
unique_name,

View File

@@ -1,16 +1,28 @@
from sqlalchemy import Column, Integer, String, Text, Boolean, UniqueConstraint
# JAVÍTÁS: Közvetlenül a base_class-ból importálunk, hogy elkerüljük a körkörös importot
# /opt/docker/dev/service_finder/backend/app/models/translation.py
from sqlalchemy import String, Text, Boolean, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base
class Translation(Base):
"""
Központi i18n adattábla.
Minden rendszerüzenet és frontend felirat forrása.
"""
__tablename__ = "translations"
__table_args__ = (
UniqueConstraint("key", "lang_code", name="uq_translation_key_lang"),
{"schema": "data"}
)
id = Column(Integer, primary_key=True, index=True)
key = Column(String(100), nullable=False, index=True)
lang_code = Column(String(5), nullable=False, index=True)
value = Column(Text, nullable=False)
is_published = Column(Boolean, default=False)
id: Mapped[int] = mapped_column(primary_key=True, index=True)
# A kulcs pontozott formátumú (pl: 'DASHBOARD.STATS.TITLE')
key: Mapped[str] = mapped_column(String(150), nullable=False, index=True)
# ISO kód (pl: 'hu', 'en', 'de')
lang_code: Mapped[str] = mapped_column(String(5), nullable=False, index=True)
# A tényleges lefordított szöveg
value: Mapped[str] = mapped_column(Text, nullable=False)
# Élesítési állapot (Draft/Published)
is_published: Mapped[bool] = mapped_column(Boolean, default=False, index=True)

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/services/translation_service.py
import json
import os
import logging
@@ -10,23 +11,28 @@ from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
class TranslationService:
"""
Dinamikus fordítás-kezelő szerviz.
Támogatja a szerveroldali cache-elést és a frontend JSON exportot.
"""
# Memória-cache a szerveroldali hibaüzenetekhez és emailekhez
_published_cache: Dict[str, Dict[str, str]] = {}
@classmethod
async def load_cache(cls, db: AsyncSession):
"""Betölti a publikált szövegeket a memóriába az adatbázisból."""
result = await db.execute(
select(Translation).where(Translation.is_published == True)
)
""" Betölti a publikált szövegeket a memóriába az adatbázisból. """
stmt = select(Translation).where(Translation.is_published == True)
result = await db.execute(stmt)
translations = result.scalars().all()
cls._published_cache = {}
for t in translations:
if t.lang_code not in cls._published_cache:
cls._published_cache[t.lang_code] = {}
cls._published_cache[t.lang_code][t.key] = t.value
logger.info(f"🌍 i18n Cache: {len(translations)} szöveg betöltve.")
# JAVÍTVA: t.lang_code helyett t.lang
if t.lang not in cls._published_cache:
cls._published_cache[t.lang] = {}
cls._published_cache[t.lang][t.key] = t.value
logger.info(f"🌍 i18n Motor: {len(translations)} szöveg aktiválva a memóriában.")
@classmethod
def get_text(cls, key: str, lang: str = "hu", variables: Optional[Dict[str, Any]] = None) -> str:
@@ -54,18 +60,19 @@ class TranslationService:
@classmethod
async def publish_all(cls, db: AsyncSession):
"""Minden piszkozatot élesít, frissíti a memóriát és legenerálja a JSON-öket."""
""" Minden piszkozatot élesít, frissíti a memóriát és legenerálja a JSON-öket. """
await db.execute(
update(Translation).where(Translation.is_published == False).values(is_published=True)
)
await db.commit()
await cls.load_cache(db)
await cls.export_to_json(db)
return True
@staticmethod
async def export_to_json(db: AsyncSession):
"""
Adatbázis -> Hierarchikus JSON export.
Adatbázis -> Hierarchikus JSON struktúra generálása a Frontend számára.
'AUTH.LOGIN.TITLE' -> { "AUTH": { "LOGIN": { "TITLE": "..." } } }
"""
stmt = select(Translation).where(Translation.is_published == True)
@@ -74,12 +81,14 @@ class TranslationService:
languages: Dict[str, Any] = {}
for t in translations:
if t.lang_code not in languages:
languages[t.lang_code] = {}
# JAVÍTVA: t.lang_code helyett t.lang
if t.lang not in languages:
languages[t.lang] = {}
# Hierarchikus struktúra felépítése
# Kulcs felbontása szintekre hierarchikus struktúrához
parts = t.key.split('.')
current_level = languages[t.lang_code]
current_level = languages[t.lang]
for part in parts[:-1]:
if part not in current_level:
current_level[part] = {}
@@ -87,7 +96,7 @@ class TranslationService:
current_level[parts[-1]] = t.value
# Fájlok mentése
# Fájlok fizikai mentése a static könyvtárba
locales_path = os.path.join(settings.STATIC_DIR, "locales")
os.makedirs(locales_path, exist_ok=True)
@@ -96,9 +105,9 @@ class TranslationService:
try:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(content, f, ensure_ascii=False, indent=2)
logger.info(f"🚀 JSON legenerálva: {file_path}")
logger.info(f"✅ Nyelvi fájl (JSON) frissítve: {file_path}")
except Exception as e:
logger.error(f"Fájl hiba ({lang}): {str(e)}")
logger.error(f"❌ Hiba a fájl mentésekor ({lang}): {e}")
return True