Добавлены новые файлы: .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