STABLE: Final schema sync, optimized gitignore
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)}")
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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...")
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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']))
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user