Добавлены новые файлы: .gitignore для исключения временных файлов, fill_db.py для заполнения базы данных тестовыми данными, main.py с основным функционалом FastAPI, models.py с API моделями и requirements.txt для зависимостей проекта.
This commit is contained in:
commit
37c855c601
121
.gitignore
vendored
Normal file
121
.gitignore
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# SQLite database
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
161
fill_db.py
Normal file
161
fill_db.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import random
|
||||||
|
from uuid import uuid4
|
||||||
|
from sqlmodel import Session
|
||||||
|
from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Transaction
|
||||||
|
from sqlalchemy import text
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
# Константа: список user_ids
|
||||||
|
USER_IDS = [1001, 256844410, 426261192, 564746427]
|
||||||
|
|
||||||
|
# Примеры телефонов
|
||||||
|
PHONES = [
|
||||||
|
"+79991234567",
|
||||||
|
"+79997654321",
|
||||||
|
"+79993456789",
|
||||||
|
"+79992345678",
|
||||||
|
"+79994561234"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Примеры описаний
|
||||||
|
DESCRIPTIONS = [
|
||||||
|
"Партнёр по рекламе",
|
||||||
|
"Блогер",
|
||||||
|
"Тестовая ссылка",
|
||||||
|
"Промо акция",
|
||||||
|
"VIP клиент",
|
||||||
|
"Реклама в Telegram от 01.05.2025",
|
||||||
|
"Пост в Instagram от 15.04.2025",
|
||||||
|
"Рассылка в ВК от 24.02.2025",
|
||||||
|
"Промо в Facebook от 10.03.2025",
|
||||||
|
"Пост в Twitter от 05.03.2025",
|
||||||
|
"Реклама в YouTube от 12.04.2025",
|
||||||
|
"Промо в TikTok от 20.04.2025",
|
||||||
|
"Пост в LinkedIn от 18.03.2025",
|
||||||
|
"Реклама в Одноклассниках от 22.03.2025",
|
||||||
|
"Промо в Pinterest от 30.03.2025",
|
||||||
|
"Пост в Reddit от 02.04.2025",
|
||||||
|
"Реклама в Dzen от 11.04.2025",
|
||||||
|
"Промо в Medium от 08.04.2025",
|
||||||
|
"Пост в WhatsApp от 14.04.2025",
|
||||||
|
"Реклама в Discord от 21.04.2025"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Примеры имен и логинов
|
||||||
|
NAMES = [
|
||||||
|
"Иван Иванов",
|
||||||
|
"Петр Петров",
|
||||||
|
"Сергей Сергеев",
|
||||||
|
"Анна Смирнова"
|
||||||
|
]
|
||||||
|
LOGINS = [
|
||||||
|
"ivanov1001",
|
||||||
|
"petrov256",
|
||||||
|
"serg426",
|
||||||
|
"anna564"
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Загрузка mock-данных ---
|
||||||
|
|
||||||
|
ALL_DESCRIPTIONS = DESCRIPTIONS
|
||||||
|
|
||||||
|
# ---
|
||||||
|
def get_date_list(days=7):
|
||||||
|
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
return [today - timedelta(days=i) for i in range(days, -1, -1)]
|
||||||
|
|
||||||
|
def fill_db():
|
||||||
|
date_list = get_date_list(7) # 8 дней: от недели назад до сегодня
|
||||||
|
with Session(AUTH_DB_ENGINE) as session:
|
||||||
|
# Очистка таблиц
|
||||||
|
session.execute(text("DELETE FROM sale"))
|
||||||
|
session.execute(text("DELETE FROM ref"))
|
||||||
|
session.execute(text("DELETE FROM tgagent"))
|
||||||
|
session.commit()
|
||||||
|
# 1. TgAgents
|
||||||
|
tg_agents = []
|
||||||
|
for i, tg_agent_id in enumerate(USER_IDS):
|
||||||
|
dt = random.choice(date_list)
|
||||||
|
tg_agent = TgAgent(
|
||||||
|
tg_id=tg_agent_id,
|
||||||
|
chat_id=tg_agent_id, # chat_id совпадает с tg_id
|
||||||
|
phone=PHONES[i % len(PHONES)],
|
||||||
|
name=NAMES[i % len(NAMES)],
|
||||||
|
login=LOGINS[i % len(LOGINS)],
|
||||||
|
create_dttm=dt,
|
||||||
|
update_dttm=dt
|
||||||
|
)
|
||||||
|
session.add(tg_agent)
|
||||||
|
tg_agents.append(tg_agent)
|
||||||
|
session.commit()
|
||||||
|
for tg_agent in tg_agents:
|
||||||
|
session.refresh(tg_agent)
|
||||||
|
|
||||||
|
# 2. Refs (минимум 22 на агента)
|
||||||
|
refs = []
|
||||||
|
desc_count = len(ALL_DESCRIPTIONS)
|
||||||
|
for tg_agent in tg_agents:
|
||||||
|
ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27
|
||||||
|
for j in range(ref_count):
|
||||||
|
ref_val = str(uuid4())
|
||||||
|
desc_val = ALL_DESCRIPTIONS[(j % desc_count)]
|
||||||
|
dt = random.choice(date_list)
|
||||||
|
ref = Ref(
|
||||||
|
tg_agent_id=tg_agent.id,
|
||||||
|
ref=ref_val,
|
||||||
|
description=desc_val,
|
||||||
|
create_dttm=dt,
|
||||||
|
update_dttm=dt
|
||||||
|
)
|
||||||
|
session.add(ref)
|
||||||
|
refs.append(ref)
|
||||||
|
session.commit()
|
||||||
|
for ref in refs:
|
||||||
|
session.refresh(ref)
|
||||||
|
|
||||||
|
# 3. Sales (минимум 20 на каждый ref)
|
||||||
|
for ref in refs:
|
||||||
|
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
|
||||||
|
for _ in range(sale_count):
|
||||||
|
cost = round(random.uniform(100, 1000), 2)
|
||||||
|
crediting = round(cost * random.uniform(0.5, 1.0), 2)
|
||||||
|
dt = random.choice(date_list)
|
||||||
|
sale = Sale(
|
||||||
|
cost=cost,
|
||||||
|
crediting=crediting,
|
||||||
|
ref=ref.id,
|
||||||
|
sale_id=str(uuid4()),
|
||||||
|
create_dttm=dt,
|
||||||
|
update_dttm=dt
|
||||||
|
)
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# 4. Transactions (только withdrawal на агента)
|
||||||
|
TRANSACTION_STATUSES = ['process', 'done', 'error', 'waiting']
|
||||||
|
for tg_agent in tg_agents:
|
||||||
|
withdrawal_count = random.randint(5, int(5 * 1.25)) # от 5 до 6
|
||||||
|
used_statuses = set()
|
||||||
|
for i in range(withdrawal_count):
|
||||||
|
dt = random.choice(date_list)
|
||||||
|
# Гарантируем, что каждый статус будет использован хотя бы раз
|
||||||
|
if len(used_statuses) < len(TRANSACTION_STATUSES):
|
||||||
|
status = TRANSACTION_STATUSES[len(used_statuses)]
|
||||||
|
used_statuses.add(status)
|
||||||
|
else:
|
||||||
|
status = random.choice(TRANSACTION_STATUSES)
|
||||||
|
transaction = Transaction(
|
||||||
|
transaction_id=str(uuid4()),
|
||||||
|
sum=round(random.uniform(200, 3000), 2),
|
||||||
|
tg_agent_id=tg_agent.id,
|
||||||
|
status=status,
|
||||||
|
create_dttm=dt,
|
||||||
|
update_dttm=dt
|
||||||
|
)
|
||||||
|
session.add(transaction)
|
||||||
|
session.commit()
|
||||||
|
print("База успешно заполнена!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fill_db()
|
||||||
448
main.py
Normal file
448
main.py
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
from fastapi import FastAPI, Depends, HTTPException, status, Query
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
from sqlmodel import SQLModel, Field, create_engine, Session, select
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from models import RefAddRequest, RefResponse, RegisterRequest, Token, TokenRequest
|
||||||
|
from uuid import uuid4
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
|
||||||
|
|
||||||
|
#SQLModel
|
||||||
|
class TgAgent(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
tg_id: int = Field(index=True, unique=True)
|
||||||
|
chat_id: Optional[int] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
login: Optional[str] = None
|
||||||
|
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
class Ref(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
tg_agent_id: int = Field(foreign_key="tgagent.id")
|
||||||
|
ref: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
class Sale(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
cost: float
|
||||||
|
crediting: float # сколько начислено за продажу
|
||||||
|
ref: int = Field(foreign_key="ref.id")
|
||||||
|
sale_id: str
|
||||||
|
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
class Transaction(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
transaction_id: str = Field(default_factory=lambda: str(uuid4()), index=True, unique=True)
|
||||||
|
sum: float
|
||||||
|
tg_agent_id: int = Field(foreign_key="tgagent.id")
|
||||||
|
status: str # 'process' || 'done' || 'error' || 'waiting'
|
||||||
|
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
# Создание движка базы данных
|
||||||
|
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
|
||||||
|
SQLModel.metadata.create_all(AUTH_DB_ENGINE)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
||||||
|
|
||||||
|
# FastAPI app
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# CRUD
|
||||||
|
|
||||||
|
def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
|
||||||
|
statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
|
||||||
|
return db.exec(statement).first()
|
||||||
|
|
||||||
|
# Dependency
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
with Session(AUTH_DB_ENGINE) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
# Авторизация
|
||||||
|
async def get_current_tg_agent(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
# Ожидаем токен вида 'session_for_{tg_id}'
|
||||||
|
if not token.startswith("session_for_"):
|
||||||
|
raise credentials_exception
|
||||||
|
try:
|
||||||
|
tg_id = int(token.replace("session_for_", ""))
|
||||||
|
except Exception:
|
||||||
|
raise credentials_exception
|
||||||
|
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||||
|
if tg_agent is None:
|
||||||
|
raise credentials_exception
|
||||||
|
return tg_agent
|
||||||
|
|
||||||
|
# Регистрация
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/register", tags=["partner-tg"])
|
||||||
|
def register(req: RegisterRequest, db: Session = Depends(get_db)):
|
||||||
|
tg_id = req.tg_id
|
||||||
|
chat_id = req.chat_id
|
||||||
|
phone = req.phone
|
||||||
|
name = getattr(req, 'name', None)
|
||||||
|
login = getattr(req, 'login', None)
|
||||||
|
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}')
|
||||||
|
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||||
|
if tg_agent:
|
||||||
|
raise HTTPException(status_code=400, detail="tg_id already registered")
|
||||||
|
new_tg_agent = TgAgent(tg_id=tg_id, chat_id=chat_id, phone=phone, name=name, login=login)
|
||||||
|
db.add(new_tg_agent)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_tg_agent)
|
||||||
|
return {"msg": "TgAgent registered successfully"}
|
||||||
|
|
||||||
|
def authenticate_tg_agent(engine, tg_id: int):
|
||||||
|
with Session(engine) as db:
|
||||||
|
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||||
|
if not tg_agent:
|
||||||
|
return None
|
||||||
|
return tg_agent
|
||||||
|
|
||||||
|
# Защищённый эндпоинт
|
||||||
|
@app.get("/protected", tags=["partner-tg"])
|
||||||
|
def protected_route(current_tg_agent: TgAgent = Depends(get_current_tg_agent)):
|
||||||
|
return {"msg": f"Hello, {current_tg_agent.tg_id}! This is a protected route."}
|
||||||
|
|
||||||
|
# Авторизация
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/token", response_model=Token, tags=["partner-tg"])
|
||||||
|
async def login_for_access_token(req: TokenRequest):
|
||||||
|
tg_id = req.tg_id
|
||||||
|
tg_agent = authenticate_tg_agent(AUTH_DB_ENGINE, tg_id)
|
||||||
|
if not tg_agent:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect tg_id",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
access_token = f"session_for_{tg_agent.tg_id}"
|
||||||
|
return Token(access_token=access_token, token_type="bearer")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/ref", response_model=list[RefResponse], tags=["partner-tg"])
|
||||||
|
def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||||
|
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||||
|
return [RefResponse(ref=r.ref, description=r.description or "") for r in refs]
|
||||||
|
|
||||||
|
@app.post("/ref/add", tags=["partner-tg"])
|
||||||
|
def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||||
|
new_ref = Ref(
|
||||||
|
tg_agent_id=current_tg_agent.id,
|
||||||
|
ref=str(uuid4()),
|
||||||
|
description=req.description
|
||||||
|
)
|
||||||
|
db.add(new_ref)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_ref)
|
||||||
|
return {"ref": new_ref.ref}
|
||||||
|
|
||||||
|
@app.get("/ref/stat", tags=["partner-tg"])
|
||||||
|
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||||
|
# 1. Получаем все реферальные ссылки пользователя
|
||||||
|
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||||
|
result = []
|
||||||
|
for ref in refs:
|
||||||
|
# 2. Для каждой ссылки считаем продажи и сумму
|
||||||
|
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||||||
|
sales_count = len(sales)
|
||||||
|
income = sum(sale.crediting for sale in sales)
|
||||||
|
result.append({
|
||||||
|
"description": ref.description or "",
|
||||||
|
"sales": sales_count,
|
||||||
|
"income": income
|
||||||
|
})
|
||||||
|
return {"refData": result}
|
||||||
|
|
||||||
|
@app.get("/stat", tags=["partner-tg"])
|
||||||
|
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||||
|
# 1. Получаем все реферальные ссылки пользователя
|
||||||
|
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||||
|
ref_ids = [r.id for r in refs]
|
||||||
|
|
||||||
|
# 2. Считаем totalSales (продажи по всем рефам пользователя)
|
||||||
|
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||||||
|
totalSales = len(total_sales)
|
||||||
|
totalIncome = sum(sale.crediting for sale in total_sales)
|
||||||
|
withdrawals = db.exec(
|
||||||
|
select(Transaction).where(
|
||||||
|
Transaction.tg_agent_id == current_tg_agent.id
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
availableWithdrawal = totalIncome - sum(t.sum for t in withdrawals)
|
||||||
|
return {
|
||||||
|
"totalSales": totalSales,
|
||||||
|
"totalIncome": totalIncome,
|
||||||
|
"availableWithdrawal": availableWithdrawal
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/dashboard/cards", tags=["bff"])
|
||||||
|
def get_dashboard_cards(db: Session = Depends(get_db)):
|
||||||
|
# 1. Общий доход - сумма всех Sale.cost
|
||||||
|
total_revenue = db.exec(select(Sale)).all()
|
||||||
|
totalRevenue = sum(sale.cost for sale in total_revenue)
|
||||||
|
|
||||||
|
# 2. Общие выплаты - сумма всех Sale.crediting
|
||||||
|
totalPayouts = sum(sale.crediting for sale in total_revenue)
|
||||||
|
|
||||||
|
# 3. Активные рефералы - количество уникальных TgAgent.tg_id
|
||||||
|
unique_agents = db.exec(select(TgAgent.tg_id)).all()
|
||||||
|
activeReferrals = len(set(unique_agents))
|
||||||
|
|
||||||
|
# 4. Ожидающие выплаты - разница между суммой всех Sale.crediting и суммой всех Transaction.sum
|
||||||
|
all_transactions = db.exec(select(Transaction)).all()
|
||||||
|
totalTransactions = sum(t.sum for t in all_transactions)
|
||||||
|
pendingPayouts = totalPayouts - totalTransactions
|
||||||
|
|
||||||
|
# 5. Количество продаж
|
||||||
|
totalSales = len(total_revenue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"totalRevenue": totalRevenue,
|
||||||
|
"totalPayouts": totalPayouts,
|
||||||
|
"activeReferrals": activeReferrals,
|
||||||
|
"pendingPayouts": pendingPayouts,
|
||||||
|
"totalSales": totalSales
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/dashboard/chart/total", tags=["bff"])
|
||||||
|
def get_dashboard_chart_total(db: Session = Depends(get_db)):
|
||||||
|
# Группируем продажи по дате (день)
|
||||||
|
result = db.exec(
|
||||||
|
select(
|
||||||
|
func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'),
|
||||||
|
func.sum(Sale.cost).label('revenue'),
|
||||||
|
func.count(Sale.id).label('sales')
|
||||||
|
).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||||||
|
.order_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||||||
|
).all()
|
||||||
|
# Преобразуем результат в нужный формат
|
||||||
|
data = [
|
||||||
|
{"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
|
@app.get("/dashboard/chart/agent", tags=["bff"])
|
||||||
|
def get_dashboard_chart_agent(db: Session = Depends(get_db)):
|
||||||
|
# Получаем всех агентов
|
||||||
|
agents = db.exec(select(TgAgent)).all()
|
||||||
|
result = []
|
||||||
|
for agent in agents:
|
||||||
|
# Получаем все рефы этого агента
|
||||||
|
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
|
||||||
|
ref_ids = [r.id for r in refs]
|
||||||
|
if not ref_ids:
|
||||||
|
result.append({
|
||||||
|
"name": agent.name or f"Агент {agent.id}",
|
||||||
|
"count": 0,
|
||||||
|
"sum": 0.0
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
# Получаем все продажи по этим рефам
|
||||||
|
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||||||
|
sales_count = len(sales)
|
||||||
|
sales_sum = sum(sale.cost for sale in sales)
|
||||||
|
result.append({
|
||||||
|
"name": agent.name or f"Агент {agent.id}",
|
||||||
|
"count": sales_count,
|
||||||
|
"sum": sales_sum
|
||||||
|
})
|
||||||
|
return JSONResponse(content=result)
|
||||||
|
|
||||||
|
@app.get("/stat/agents", tags=["bff"])
|
||||||
|
def get_agents_stat(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
date_start: str = Query(None),
|
||||||
|
date_end: str = Query(None),
|
||||||
|
):
|
||||||
|
agents_query = select(TgAgent)
|
||||||
|
if date_start:
|
||||||
|
agents_query = agents_query.where(TgAgent.create_dttm >= date_start)
|
||||||
|
if date_end:
|
||||||
|
agents_query = agents_query.where(TgAgent.create_dttm <= date_end)
|
||||||
|
agents = db.exec(agents_query).all()
|
||||||
|
result = []
|
||||||
|
for agent in agents:
|
||||||
|
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
|
||||||
|
ref_ids = [r.id for r in refs]
|
||||||
|
ref_count = len(ref_ids)
|
||||||
|
if not ref_ids:
|
||||||
|
result.append({
|
||||||
|
"name": agent.name or f"Агент {agent.id}",
|
||||||
|
"refCount": 0,
|
||||||
|
"salesCount": 0,
|
||||||
|
"salesSum": 0.0,
|
||||||
|
"crediting": 0.0
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||||||
|
sales_count = len(sales)
|
||||||
|
sales_sum = sum(sale.cost for sale in sales)
|
||||||
|
crediting_sum = sum(sale.crediting for sale in sales)
|
||||||
|
result.append({
|
||||||
|
"name": agent.name or f"Агент {agent.id}",
|
||||||
|
"refCount": ref_count,
|
||||||
|
"salesCount": sales_count,
|
||||||
|
"salesSum": sales_sum,
|
||||||
|
"crediting": crediting_sum
|
||||||
|
})
|
||||||
|
return JSONResponse(content=result)
|
||||||
|
|
||||||
|
@app.get("/stat/referrals", tags=["bff"])
|
||||||
|
def get_referrals_stat(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
date_start: str = Query(None),
|
||||||
|
date_end: str = Query(None),
|
||||||
|
):
|
||||||
|
refs_query = select(Ref)
|
||||||
|
if date_start:
|
||||||
|
refs_query = refs_query.where(Ref.create_dttm >= date_start)
|
||||||
|
if date_end:
|
||||||
|
refs_query = refs_query.where(Ref.create_dttm <= date_end)
|
||||||
|
refs = db.exec(refs_query).all()
|
||||||
|
result = []
|
||||||
|
for ref in refs:
|
||||||
|
agent = db.exec(select(TgAgent).where(TgAgent.id == ref.tg_agent_id)).first()
|
||||||
|
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||||||
|
sales_count = len(sales)
|
||||||
|
sales_sum = sum(sale.cost for sale in sales)
|
||||||
|
result.append({
|
||||||
|
"ref": ref.ref,
|
||||||
|
"agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}",
|
||||||
|
"description": ref.description or "",
|
||||||
|
"salesSum": sales_sum,
|
||||||
|
"salesCount": sales_count
|
||||||
|
})
|
||||||
|
return JSONResponse(content=result)
|
||||||
|
|
||||||
|
@app.get("/stat/sales", tags=["bff"])
|
||||||
|
def get_sales_stat(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
date_start: str = Query(None),
|
||||||
|
date_end: str = Query(None),
|
||||||
|
):
|
||||||
|
sales_query = select(Sale)
|
||||||
|
if date_start:
|
||||||
|
sales_query = sales_query.where(Sale.create_dttm >= date_start)
|
||||||
|
if date_end:
|
||||||
|
sales_query = sales_query.where(Sale.create_dttm <= date_end)
|
||||||
|
sales = db.exec(sales_query).all()
|
||||||
|
ref_ids = list(set(sale.ref for sale in sales))
|
||||||
|
refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else []
|
||||||
|
ref_map = {ref.id: ref for ref in refs}
|
||||||
|
agent_ids = list(set(ref.tg_agent_id for ref in refs)) if refs else []
|
||||||
|
agents = db.exec(select(TgAgent).where(TgAgent.id.in_(agent_ids))).all() if agent_ids else []
|
||||||
|
agent_map = {agent.id: agent for agent in agents}
|
||||||
|
result = []
|
||||||
|
for sale in sales:
|
||||||
|
ref_obj = ref_map.get(sale.ref)
|
||||||
|
agent_obj = agent_map.get(ref_obj.tg_agent_id) if ref_obj else None
|
||||||
|
result.append({
|
||||||
|
"saleId": sale.sale_id,
|
||||||
|
"cost": sale.cost,
|
||||||
|
"crediting": sale.crediting,
|
||||||
|
"ref": ref_obj.ref if ref_obj else None,
|
||||||
|
"name": agent_obj.name if agent_obj else None
|
||||||
|
})
|
||||||
|
return JSONResponse(content=result)
|
||||||
|
|
||||||
|
@app.get("/billing/cards", tags=["bff"])
|
||||||
|
def get_billing_cards(db: Session = Depends(get_db)):
|
||||||
|
# 1. cost - сумма всех Sale.cost
|
||||||
|
sales = db.exec(select(Sale)).all()
|
||||||
|
cost = sum(sale.cost for sale in sales)
|
||||||
|
# 2. crediting - сумма всех Sale.crediting
|
||||||
|
crediting = sum(sale.crediting for sale in sales)
|
||||||
|
# 3. pendingPayouts - разница между crediting и суммой всех Transaction.sum
|
||||||
|
transactions = db.exec(select(Transaction)).all()
|
||||||
|
total_transactions = sum(t.sum for t in transactions)
|
||||||
|
pendingPayouts = crediting - total_transactions
|
||||||
|
return {
|
||||||
|
"cost": cost,
|
||||||
|
"crediting": crediting,
|
||||||
|
"pendingPayouts": pendingPayouts
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/billing/payouts/transactions", tags=["bff"])
|
||||||
|
def get_billing_payouts_transactions(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
date_start: str = Query(None),
|
||||||
|
date_end: str = Query(None),
|
||||||
|
):
|
||||||
|
query = select(Transaction)
|
||||||
|
if date_start:
|
||||||
|
query = query.where(Transaction.create_dttm >= date_start)
|
||||||
|
if date_end:
|
||||||
|
query = query.where(Transaction.create_dttm <= date_end)
|
||||||
|
transactions = db.exec(query).all()
|
||||||
|
result = []
|
||||||
|
for t in transactions:
|
||||||
|
agent = db.exec(select(TgAgent).where(TgAgent.id == t.tg_agent_id)).first()
|
||||||
|
result.append({
|
||||||
|
"id": t.transaction_id,
|
||||||
|
"sum": t.sum,
|
||||||
|
"agent": agent.name if agent else None,
|
||||||
|
"status": t.status,
|
||||||
|
"create_dttm": t.create_dttm,
|
||||||
|
"update_dttm": t.update_dttm,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.get("/billing/chart/stat", tags=["bff"])
|
||||||
|
def get_billing_chart_stat(db: Session = Depends(get_db)):
|
||||||
|
# Группируем транзакции по дате (день) и статусу
|
||||||
|
result = db.exec(
|
||||||
|
select(
|
||||||
|
func.strftime('%Y-%m-%d', Transaction.create_dttm).label('date'),
|
||||||
|
Transaction.status.label('status'),
|
||||||
|
func.count(Transaction.id).label('count')
|
||||||
|
).group_by(
|
||||||
|
func.strftime('%Y-%m-%d', Transaction.create_dttm),
|
||||||
|
Transaction.status
|
||||||
|
).order_by(
|
||||||
|
func.strftime('%Y-%m-%d', Transaction.create_dttm),
|
||||||
|
Transaction.status
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
data = [
|
||||||
|
{"date": row.date, "status": row.status, "count": row.count}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
|
@app.get("/billing/chart/pie", tags=["bff"])
|
||||||
|
def get_billing_chart_pie(db: Session = Depends(get_db)):
|
||||||
|
result = db.exec(
|
||||||
|
select(
|
||||||
|
Transaction.status.label('status'),
|
||||||
|
func.count(Transaction.id).label('count')
|
||||||
|
).group_by(Transaction.status)
|
||||||
|
).all()
|
||||||
|
data = [
|
||||||
|
{"status": row.status, "count": row.count}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
return JSONResponse(content=data)
|
||||||
31
models.py
Normal file
31
models.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#API models
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
class RefResponse(BaseModel):
|
||||||
|
ref: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
class RefAddRequest(BaseModel):
|
||||||
|
description: str
|
||||||
|
|
||||||
|
class TokenRequest(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
|
||||||
|
class RegisterRequest(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
chat_id: Optional[int] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
login: Optional[str] = None
|
||||||
146
requirements.txt
Normal file
146
requirements.txt
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile pyproject.toml -o requirements.txt
|
||||||
|
annotated-types==0.7.0
|
||||||
|
# via pydantic
|
||||||
|
anyio==4.8.0
|
||||||
|
# via
|
||||||
|
# httpx
|
||||||
|
# starlette
|
||||||
|
# watchfiles
|
||||||
|
bcrypt==4.2.1
|
||||||
|
# via passlib
|
||||||
|
behave==1.2.6
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
certifi==2024.12.14
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
cffi==1.17.1
|
||||||
|
# via cryptography
|
||||||
|
charset-normalizer==3.4.2
|
||||||
|
# via requests
|
||||||
|
click==8.1.8
|
||||||
|
# via
|
||||||
|
# rich-toolkit
|
||||||
|
# typer
|
||||||
|
# uvicorn
|
||||||
|
cryptography==44.0.0
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
dnspython==2.7.0
|
||||||
|
# via email-validator
|
||||||
|
email-validator==2.2.0
|
||||||
|
# via fastapi
|
||||||
|
fastapi==0.115.6
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
fastapi-cli==0.0.7
|
||||||
|
# via fastapi
|
||||||
|
greenlet==3.1.1
|
||||||
|
# via sqlalchemy
|
||||||
|
h11==0.14.0
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# uvicorn
|
||||||
|
httpcore==1.0.7
|
||||||
|
# via httpx
|
||||||
|
httptools==0.6.4
|
||||||
|
# via uvicorn
|
||||||
|
httpx==0.28.1
|
||||||
|
# via fastapi
|
||||||
|
idna==3.10
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# email-validator
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
jinja2==3.1.5
|
||||||
|
# via fastapi
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
# via rich
|
||||||
|
markupsafe==3.0.2
|
||||||
|
# via jinja2
|
||||||
|
mdurl==0.1.2
|
||||||
|
# via markdown-it-py
|
||||||
|
parse==1.20.2
|
||||||
|
# via
|
||||||
|
# behave
|
||||||
|
# parse-type
|
||||||
|
parse-type==0.6.4
|
||||||
|
# via behave
|
||||||
|
passlib==1.7.4
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
psycopg2==2.9.10
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
pycparser==2.22
|
||||||
|
# via cffi
|
||||||
|
pydantic==2.10.5
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# pydantic-settings
|
||||||
|
# sqlmodel
|
||||||
|
pydantic-core==2.27.2
|
||||||
|
# via pydantic
|
||||||
|
pydantic-settings==2.9.1
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
pygments==2.19.1
|
||||||
|
# via rich
|
||||||
|
pyjwt==2.10.1
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
# via
|
||||||
|
# pydantic-settings
|
||||||
|
# uvicorn
|
||||||
|
python-multipart==0.0.20
|
||||||
|
# via fastapi
|
||||||
|
pyyaml==6.0.2
|
||||||
|
# via uvicorn
|
||||||
|
requests==2.32.3
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
rich==13.9.4
|
||||||
|
# via
|
||||||
|
# rich-toolkit
|
||||||
|
# typer
|
||||||
|
rich-toolkit==0.12.0
|
||||||
|
# via fastapi-cli
|
||||||
|
ruff==0.9.1
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
shellingham==1.5.4
|
||||||
|
# via typer
|
||||||
|
six==1.17.0
|
||||||
|
# via
|
||||||
|
# behave
|
||||||
|
# parse-type
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
sqlalchemy==2.0.37
|
||||||
|
# via sqlmodel
|
||||||
|
sqlmodel==0.0.22
|
||||||
|
# via epai-auth (pyproject.toml)
|
||||||
|
starlette==0.41.3
|
||||||
|
# via fastapi
|
||||||
|
typer==0.15.1
|
||||||
|
# via fastapi-cli
|
||||||
|
typing-extensions==4.12.2
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# fastapi
|
||||||
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
|
# rich-toolkit
|
||||||
|
# sqlalchemy
|
||||||
|
# typer
|
||||||
|
# typing-inspection
|
||||||
|
typing-inspection==0.4.0
|
||||||
|
# via pydantic-settings
|
||||||
|
urllib3==2.4.0
|
||||||
|
# via requests
|
||||||
|
uvicorn==0.34.0
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# fastapi-cli
|
||||||
|
#uvloop==0.21.0
|
||||||
|
# via uvicorn
|
||||||
|
watchfiles==1.0.4
|
||||||
|
# via uvicorn
|
||||||
|
websockets==14.1
|
||||||
|
# via uvicorn
|
||||||
Loading…
x
Reference in New Issue
Block a user