326 lines
10 KiB
Python
326 lines
10 KiB
Python
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, Path, Query
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.depends import get_session, verify_admin_token
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/all")
|
|
async def get_all_partners(
|
|
limit: int = Query(1000, ge=1, le=10000, description="Лимит результатов"),
|
|
offset: int = Query(0, ge=0, description="Смещение"),
|
|
admin=Depends(verify_admin_token),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Возвращает список всех партнёров со статистикой.
|
|
|
|
Структура ответа:
|
|
{
|
|
"total": int,
|
|
"items": [
|
|
{
|
|
"tg_id": int,
|
|
"balance": float,
|
|
"percent": float,
|
|
"code": str | None,
|
|
"method": str | None,
|
|
"referred_count": int
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
partners_sql = text(
|
|
"""
|
|
SELECT
|
|
p.partner_tg_id AS tg_id,
|
|
COALESCE(u.partner_balance, 0) AS partner_balance,
|
|
COALESCE(u.partner_percent, 0) AS partner_percent,
|
|
u.partner_code,
|
|
u.payout_method,
|
|
COUNT(p.joined_tg_id) as joined_count
|
|
FROM partners p
|
|
LEFT JOIN users u ON u.tg_id = p.partner_tg_id
|
|
WHERE p.partner_tg_id IS NOT NULL
|
|
GROUP BY p.partner_tg_id, u.partner_balance, u.partner_percent, u.partner_code, u.payout_method
|
|
ORDER BY partner_balance DESC
|
|
LIMIT :limit OFFSET :offset
|
|
"""
|
|
)
|
|
|
|
count_sql = text(
|
|
"""
|
|
SELECT COUNT(DISTINCT partner_tg_id) FROM partners
|
|
WHERE partner_tg_id IS NOT NULL
|
|
"""
|
|
)
|
|
|
|
result = await session.execute(partners_sql, {"limit": limit, "offset": offset})
|
|
partners = result.fetchall()
|
|
|
|
count_result = await session.execute(count_sql)
|
|
total = count_result.scalar() or 0
|
|
|
|
partners_list = [
|
|
{
|
|
"tg_id": int(partner[0]),
|
|
"balance": float(partner[1] or 0),
|
|
"percent": float(partner[2] or 0),
|
|
"code": partner[3] or None,
|
|
"method": partner[4] or None,
|
|
"referred_count": int(partner[5] or 0),
|
|
}
|
|
for partner in partners
|
|
]
|
|
|
|
return JSONResponse(content={"total": total, "items": partners_list})
|
|
|
|
|
|
@router.get("/stats/all")
|
|
async def get_partners_stats(
|
|
admin=Depends(verify_admin_token),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Возвращает общую статистику партнёрской программы.
|
|
|
|
Структура ответа:
|
|
{
|
|
"total_partners": int,
|
|
"total_referred": int,
|
|
"total_balance": float,
|
|
"top_partner_tg_id": int,
|
|
"top_partner_refs": int
|
|
}
|
|
"""
|
|
|
|
stats_sql = text(
|
|
"""
|
|
WITH partner_refs AS (
|
|
SELECT partner_tg_id, COUNT(DISTINCT joined_tg_id) AS ref_count
|
|
FROM partners
|
|
WHERE partner_tg_id IS NOT NULL
|
|
GROUP BY partner_tg_id
|
|
)
|
|
SELECT
|
|
(SELECT COUNT(*) FROM partner_refs) AS total_partners,
|
|
(SELECT COUNT(DISTINCT joined_tg_id) FROM partners WHERE partner_tg_id IS NOT NULL) AS total_referred,
|
|
(
|
|
SELECT COALESCE(SUM(u.partner_balance), 0.0)
|
|
FROM users u
|
|
WHERE u.tg_id IN (SELECT partner_tg_id FROM partner_refs)
|
|
) AS total_balance,
|
|
(SELECT partner_tg_id FROM partner_refs ORDER BY ref_count DESC LIMIT 1) AS top_partner_tg_id,
|
|
(SELECT ref_count FROM partner_refs ORDER BY ref_count DESC LIMIT 1) AS top_partner_refs
|
|
"""
|
|
)
|
|
|
|
stats_result = await session.execute(stats_sql)
|
|
stats_row = stats_result.fetchone()
|
|
|
|
if stats_row:
|
|
stats = {
|
|
"total_partners": int(stats_row[0] or 0),
|
|
"total_referred": int(stats_row[1] or 0),
|
|
"total_balance": float(stats_row[2] or 0.0),
|
|
"top_partner_tg_id": int(stats_row[3] or 0),
|
|
"top_partner_refs": int(stats_row[4] or 0),
|
|
}
|
|
else:
|
|
stats = {
|
|
"total_partners": 0,
|
|
"total_referred": 0,
|
|
"total_balance": 0.0,
|
|
"top_partner_tg_id": 0,
|
|
"top_partner_refs": 0,
|
|
}
|
|
|
|
return JSONResponse(content=stats)
|
|
|
|
|
|
@router.patch("/{tg_id}")
|
|
async def update_partner(
|
|
tg_id: int = Path(..., description="Telegram ID партнёра"),
|
|
balance: float = Query(..., description="Новый баланс партнёра"),
|
|
percent: float = Query(..., description="Новый процент партнёра"),
|
|
admin=Depends(verify_admin_token),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Обновляет данные партнёра (баланс и процент).
|
|
|
|
Структура ответа:
|
|
{
|
|
"success": bool,
|
|
"message": str
|
|
}
|
|
"""
|
|
|
|
try:
|
|
stmt = text(
|
|
"""
|
|
UPDATE users
|
|
SET partner_balance = :balance, partner_percent = :percent
|
|
WHERE tg_id = :tg_id
|
|
"""
|
|
)
|
|
|
|
result = await session.execute(stmt, {"tg_id": tg_id, "balance": balance, "percent": percent})
|
|
await session.commit()
|
|
|
|
if result.rowcount > 0:
|
|
return JSONResponse(
|
|
content={"success": True, "message": f"Партнёр {tg_id} успешно обновлён"},
|
|
status_code=200,
|
|
)
|
|
else:
|
|
return JSONResponse(
|
|
content={"success": False, "message": "Партнёр не найден"},
|
|
status_code=404,
|
|
)
|
|
except Exception as e:
|
|
await session.rollback()
|
|
return JSONResponse(
|
|
content={"success": False, "message": str(e)},
|
|
status_code=500,
|
|
)
|
|
|
|
|
|
@router.get("/{tg_id}")
|
|
async def get_partner_data(
|
|
tg_id: int = Path(..., description="Telegram ID партнёра"),
|
|
admin=Depends(verify_admin_token),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Возвращает партнёрские данные для указанного `tg_id`.
|
|
|
|
Структура ответа:
|
|
{
|
|
"tg_id": int,
|
|
"partner_balance": float,
|
|
"partner_percent": float,
|
|
"partner_code": str | None,
|
|
"payout_method": str | None,
|
|
"invited": [
|
|
{ "tg_id": int, "joined_at": str | None, "balance": float, "subs_count": int, "payments_count": int }
|
|
]
|
|
}
|
|
"""
|
|
|
|
meta_sql = text(
|
|
"""
|
|
SELECT
|
|
COALESCE(u.partner_balance, 0) AS partner_balance,
|
|
COALESCE(u.partner_percent, 0) AS partner_percent,
|
|
u.partner_code,
|
|
u.payout_method
|
|
FROM users u
|
|
WHERE u.tg_id = :tg_id
|
|
"""
|
|
)
|
|
|
|
invited_sql = text(
|
|
"""
|
|
SELECT
|
|
pr.joined_tg_id,
|
|
pr.created_at,
|
|
COALESCE(u.balance, 0) AS user_balance,
|
|
(
|
|
SELECT COUNT(*) FROM keys k
|
|
WHERE k.tg_id = pr.joined_tg_id
|
|
) AS subs_count,
|
|
(
|
|
SELECT COUNT(*) FROM payments pay
|
|
WHERE pay.tg_id = pr.joined_tg_id
|
|
AND lower(pay.status) = 'success'
|
|
) AS payments_count
|
|
FROM partners pr
|
|
LEFT JOIN users u ON u.tg_id = pr.joined_tg_id
|
|
WHERE pr.partner_tg_id = :tg_id
|
|
ORDER BY pr.created_at DESC
|
|
"""
|
|
)
|
|
|
|
meta_res = await session.execute(meta_sql, {"tg_id": tg_id})
|
|
meta_row = meta_res.fetchone()
|
|
|
|
invited_res = await session.execute(invited_sql, {"tg_id": tg_id})
|
|
invited_rows = invited_res.fetchall()
|
|
|
|
response = {
|
|
"tg_id": tg_id,
|
|
"partner_balance": float(meta_row[0] or 0) if meta_row else 0.0,
|
|
"partner_percent": float(meta_row[1] or 0) if meta_row else 0.0,
|
|
"partner_code": meta_row[2] if meta_row else None,
|
|
"payout_method": meta_row[3] if meta_row else None,
|
|
"invited": [
|
|
{
|
|
"tg_id": row[0],
|
|
"joined_at": row[1].isoformat() if isinstance(row[1], datetime) else None,
|
|
"balance": float(row[2] or 0),
|
|
"subs_count": int(row[3] or 0),
|
|
"payments_count": int(row[4] or 0),
|
|
}
|
|
for row in invited_rows
|
|
],
|
|
}
|
|
|
|
return JSONResponse(content=response)
|
|
|
|
|
|
@router.get("/{tg_id}/invited")
|
|
async def get_partner_invited(
|
|
tg_id: int = Path(..., description="Telegram ID партнёра"),
|
|
admin=Depends(verify_admin_token),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Возвращает список приглашённых пользователей конкретного партнёра.
|
|
|
|
Структура ответа:
|
|
[
|
|
{ "tg_id": int, "joined_at": str | None, "balance": float, "subs_count": int, "payments_count": int }
|
|
]
|
|
"""
|
|
|
|
invited_sql = text(
|
|
"""
|
|
SELECT
|
|
pr.joined_tg_id,
|
|
pr.created_at,
|
|
COALESCE(u.balance, 0) AS user_balance,
|
|
(
|
|
SELECT COUNT(*) FROM keys k
|
|
WHERE k.tg_id = pr.joined_tg_id
|
|
) AS subs_count,
|
|
(
|
|
SELECT COUNT(*) FROM payments pay
|
|
WHERE pay.tg_id = pr.joined_tg_id
|
|
AND lower(pay.status) = 'success'
|
|
) AS payments_count
|
|
FROM partners pr
|
|
LEFT JOIN users u ON u.tg_id = pr.joined_tg_id
|
|
WHERE pr.partner_tg_id = :tg_id
|
|
ORDER BY pr.created_at DESC
|
|
"""
|
|
)
|
|
|
|
invited_res = await session.execute(invited_sql, {"tg_id": tg_id})
|
|
invited_rows = invited_res.fetchall()
|
|
|
|
invited_list = [
|
|
{
|
|
"tg_id": row[0],
|
|
"joined_at": row[1].isoformat() if isinstance(row[1], datetime) else None,
|
|
"balance": float(row[2] or 0),
|
|
"subs_count": int(row[3] or 0),
|
|
"payments_count": int(row[4] or 0),
|
|
}
|
|
for row in invited_rows
|
|
]
|
|
|
|
return JSONResponse(content=invited_list)
|