Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/db/repositories/user_repo.py
2026-07-17 17:17:06 +00:00

280 lines
10 KiB
Python
Executable file

from __future__ import annotations
from typing import Optional, List, Literal
from datetime import datetime, timedelta
from sqlalchemy import select, func, and_, or_, update
from sqlalchemy.orm import Session
from adapters.db.models.user import User
from adapters.db.repositories.base_repo import BaseRepository
from adapters.api.v1.schemas import QueryInfo
class UserRepository(BaseRepository[User]):
def __init__(self, db: Session) -> None:
super().__init__(db, User)
def get_by_email(self, email: str) -> Optional[User]:
stmt = select(User).where(User.email == email)
return self.db.execute(stmt).scalars().first()
def get_by_mobile(self, mobile: str) -> Optional[User]:
stmt = select(User).where(User.mobile == mobile)
return self.db.execute(stmt).scalars().first()
def get_signups_timeline_buckets(
self,
start_utc: datetime,
end_utc: datetime,
granularity: Literal["day", "week", "month"],
) -> List[tuple[datetime, int]]:
"""تعداد ثبت‌نام کاربران به‌ازای هر بازهٔ زمانی (PostgreSQL date_trunc)."""
if start_utc >= end_utc:
return []
if granularity == "day":
bucket = func.date_trunc("day", User.created_at)
elif granularity == "week":
bucket = func.date_trunc("week", User.created_at)
else:
bucket = func.date_trunc("month", User.created_at)
stmt = (
select(bucket.label("bucket"), func.count().label("cnt"))
.where(User.created_at >= start_utc, User.created_at < end_utc)
.group_by(bucket)
.order_by(bucket)
)
rows = self.db.execute(stmt).all()
out: List[tuple[datetime, int]] = []
for row in rows:
b = row.bucket
if b is None:
continue
out.append((b, int(row.cnt or 0)))
return out
def count_recently_active_users(self, within_minutes: int = 5) -> int:
"""تعداد کاربران فعالی که ضربان فعالیت در بازهٔ اخیر داشته‌اند (برآورد «کاربر آنلاین»)."""
cutoff = datetime.utcnow() - timedelta(minutes=max(1, min(within_minutes, 120)))
stmt = select(func.count()).select_from(User).where(
User.is_active == True,
User.last_activity_at.is_not(None),
User.last_activity_at >= cutoff,
)
return int(self.db.execute(stmt).scalar() or 0)
def touch_last_activity(self, user_id: int, throttle_seconds: int = 45) -> Optional[datetime]:
"""
به‌روزرسانی زمان آخرین فعالیت کاربر؛ اگر قبلاً در بازهٔ throttle به‌روز شده باشد، UPDATE زده نمی‌شود.
برمی‌گرداند timestamp فعالیت ثبت‌شده (جدید یا از DB در صورت throttle).
"""
now = datetime.utcnow()
threshold = now - timedelta(seconds=throttle_seconds)
stmt = (
update(User)
.where(User.id == user_id)
.where(or_(User.last_activity_at.is_(None), User.last_activity_at < threshold))
.values(last_activity_at=now)
)
result = self.db.execute(stmt)
self.db.commit()
if getattr(result, "rowcount", 0):
return now
u = self.get_by_id(user_id)
return u.last_activity_at if u else None
def get_by_referral_code(self, referral_code: str) -> Optional[User]:
stmt = select(User).where(User.referral_code == referral_code)
return self.db.execute(stmt).scalars().first()
def is_first_user(self) -> bool:
"""بررسی اینکه آیا این اولین کاربر سیستم است یا نه"""
stmt = select(func.count()).select_from(User)
count = self.db.execute(stmt).scalar() or 0
return count == 0
def create(self, *, email: str | None, mobile: str | None, password_hash: str, first_name: str | None, last_name: str | None, referral_code: str, referred_by_user_id: int | None = None, email_verified: bool = False) -> User:
# تعیین دسترسی‌های برنامه بر اساس اینکه آیا کاربر اول است یا نه
app_permissions = {"superadmin": True} if self.is_first_user() else {}
user = User(
email=email,
mobile=mobile,
password_hash=password_hash,
first_name=first_name,
last_name=last_name,
referral_code=referral_code,
referred_by_user_id=referred_by_user_id,
app_permissions=app_permissions,
email_verified=email_verified
)
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return user
def count_referred(self, referrer_user_id: int, start: str | None = None, end: str | None = None) -> int:
stmt = select(func.count()).select_from(User).where(User.referred_by_user_id == referrer_user_id)
if start is not None:
stmt = stmt.where(User.created_at >= func.cast(start, User.created_at.type))
if end is not None:
stmt = stmt.where(User.created_at < func.cast(end, User.created_at.type))
return int(self.db.execute(stmt).scalar() or 0)
def count_referred_between(self, referrer_user_id: int, start_dt, end_dt) -> int:
stmt = select(func.count()).select_from(User).where(
and_(
User.referred_by_user_id == referrer_user_id,
User.created_at >= start_dt,
User.created_at < end_dt,
)
)
return int(self.db.execute(stmt).scalar() or 0)
def count_referred_filtered(self, referrer_user_id: int, start_dt=None, end_dt=None, search: str | None = None) -> int:
stmt = select(func.count()).select_from(User).where(User.referred_by_user_id == referrer_user_id)
if start_dt is not None:
stmt = stmt.where(User.created_at >= start_dt)
if end_dt is not None:
stmt = stmt.where(User.created_at < end_dt)
if search:
like = f"%{search}%"
stmt = stmt.where(or_(User.first_name.ilike(like), User.last_name.ilike(like), User.email.ilike(like)))
return int(self.db.execute(stmt).scalar() or 0)
def list_referred(self, referrer_user_id: int, start_dt=None, end_dt=None, search: str | None = None, offset: int = 0, limit: int = 20):
stmt = select(User).where(User.referred_by_user_id == referrer_user_id)
if start_dt is not None:
stmt = stmt.where(User.created_at >= start_dt)
if end_dt is not None:
stmt = stmt.where(User.created_at < end_dt)
if search:
like = f"%{search}%"
stmt = stmt.where(or_(User.first_name.ilike(like), User.last_name.ilike(like), User.email.ilike(like)))
stmt = stmt.order_by(User.created_at.desc()).offset(offset).limit(limit)
return self.db.execute(stmt).scalars().all()
def get_support_operators(self) -> List[User]:
"""دریافت لیست تمام اپراتورهای پشتیبانی فعال"""
from sqlalchemy import cast, or_
from sqlalchemy.dialects.postgresql import JSONB
permissions = cast(User.app_permissions, JSONB)
stmt = select(User).where(
User.is_active == True,
or_(
permissions.contains({"support_operator": True}),
permissions.contains({"superadmin": True}),
),
)
return list(self.db.execute(stmt).scalars().all())
def is_support_operator(self, user_id: int) -> bool:
"""بررسی اینکه آیا کاربر یک اپراتور پشتیبانی است یا نه"""
user = self.get_by_id(user_id)
if not user or not user.is_active:
return False
if not user.app_permissions:
return False
# SuperAdmin هم می‌تواند اپراتور باشد
return bool(user.app_permissions.get("support_operator", False) or user.app_permissions.get("superadmin", False))
def to_dict(self, user: User, include_extended: bool = False) -> dict:
"""تبدیل User object به dictionary برای API response"""
# ساخت full_name
full_name = None
if user.first_name or user.last_name:
parts = [p for p in [user.first_name, user.last_name] if p]
full_name = " ".join(parts) if parts else None
# تعیین status از is_active و وضعیت تأیید
if not user.is_active:
status = "inactive"
elif not user.email_verified and not user.mobile_verified:
status = "pending"
else:
status = "active"
# تعیین role از app_permissions
role = "user"
if user.app_permissions:
if user.app_permissions.get("superadmin"):
role = "admin"
elif user.app_permissions.get("operator"):
role = "operator"
elif user.app_permissions.get("supervisor"):
role = "supervisor"
# شمارش کسب‌وکارها
businesses_count = 0
last_login_ip = None
last_login_at = None
if include_extended:
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
from adapters.db.repositories.business_repo import BusinessRepository
from adapters.db.repositories.api_key_repo import ApiKeyRepository
# شمارش کسب‌وکارها (هم مالک و هم عضو)
business_repo = BusinessRepository(self.db)
bp_repo = BusinessPermissionRepository(self.db)
# دریافت کسب‌وکارهایی که کاربر مالک آن‌هاست
owned_businesses = business_repo.get_by_owner_id(user.id)
owned_business_ids = {b.id for b in owned_businesses}
# دریافت کسب‌وکارهایی که کاربر عضو آن‌هاست (از طریق BusinessPermission با join=True)
member_permissions = bp_repo.get_user_member_businesses(user.id)
member_business_ids = {perm.business_id for perm in member_permissions}
# ترکیب و حذف تکراری
all_business_ids = owned_business_ids | member_business_ids
businesses_count = len(all_business_ids)
# آخرین ورود
api_repo = ApiKeyRepository(self.db)
from sqlalchemy import select, desc
from adapters.db.models.api_key import ApiKey
stmt = select(ApiKey).where(
ApiKey.user_id == user.id,
ApiKey.revoked_at.is_(None)
).order_by(desc(ApiKey.last_used_at)).limit(1)
last_key = self.db.execute(stmt).scalars().first()
if last_key:
last_login_ip = last_key.ip
last_login_at = last_key.last_used_at or last_key.created_at
result = {
"id": user.id,
"email": user.email,
"mobile": user.mobile,
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": full_name,
"is_active": user.is_active,
"status": status,
"role": role,
"referral_code": user.referral_code,
"referred_by_user_id": user.referred_by_user_id,
"app_permissions": user.app_permissions,
"created_at": user.created_at,
"updated_at": user.updated_at,
"signature_file_id": getattr(user, "signature_file_id", None),
}
if include_extended:
result.update({
"businesses_count": businesses_count,
"last_login_ip": last_login_ip,
"last_login_at": last_login_at,
"last_activity_at": user.last_activity_at,
})
return result
def query_admin_list(self, query_info: QueryInfo) -> tuple[list[User], int]:
"""لیست کاربران برای پنل مدیریت با فیلدهای مجازی UI."""
from app.services.user_list_query_service import query_users_admin
return query_users_admin(self.db, query_info)