backend optimizations + redundant commits cleanup
This commit is contained in:
+32
-3
@@ -4,6 +4,7 @@ import os
|
||||
from time import perf_counter
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
@@ -28,6 +29,7 @@ app = FastAPI(
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
openapi_url="/api/openapi.json",
|
||||
default_response_class=ORJSONResponse,
|
||||
)
|
||||
|
||||
_cors_origins = API_CORS_ORIGINS if API_CORS_ORIGINS != ["*"] else API_CORS_ORIGINS
|
||||
@@ -43,6 +45,8 @@ app.add_middleware(
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1024, compresslevel=6)
|
||||
|
||||
_ETAG_MAX_BODY_BYTES = 256 * 1024
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def security_and_cache_middleware(request: Request, call_next):
|
||||
@@ -60,9 +64,32 @@ async def security_and_cache_middleware(request: Request, call_next):
|
||||
return response
|
||||
|
||||
if request.method == "GET" and response.status_code == 200 and "application/json" in content_type:
|
||||
body = b""
|
||||
content_length_header = response.headers.get("content-length")
|
||||
try:
|
||||
cl = int(content_length_header) if content_length_header is not None else None
|
||||
except (TypeError, ValueError):
|
||||
cl = None
|
||||
if cl is not None and cl > _ETAG_MAX_BODY_BYTES:
|
||||
response.headers.setdefault("Cache-Control", "no-cache")
|
||||
return response
|
||||
chunks: list[bytes] = []
|
||||
total = 0
|
||||
too_big = False
|
||||
async for chunk in response.body_iterator:
|
||||
body += chunk
|
||||
total += len(chunk)
|
||||
if total > _ETAG_MAX_BODY_BYTES:
|
||||
chunks.append(chunk)
|
||||
too_big = True
|
||||
async for remaining in response.body_iterator:
|
||||
chunks.append(remaining)
|
||||
break
|
||||
chunks.append(chunk)
|
||||
body = b"".join(chunks)
|
||||
if too_big:
|
||||
headers = dict(response.headers)
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
headers.pop("content-length", None)
|
||||
return StarletteResponse(content=body, status_code=200, headers=headers, media_type=response.media_type)
|
||||
etag = '"' + hashlib.md5(body).hexdigest() + '"'
|
||||
if_none_match = request.headers.get("if-none-match", "")
|
||||
client_etags = [t.strip() for t in if_none_match.split(",") if t.strip()]
|
||||
@@ -80,12 +107,13 @@ async def security_and_cache_middleware(request: Request, call_next):
|
||||
@app.middleware("http")
|
||||
async def api_access_log_middleware(request: Request, call_next):
|
||||
context = ensure_api_context(request)
|
||||
started = perf_counter()
|
||||
if not API_LOGGING:
|
||||
response = await call_next(request)
|
||||
response.headers["X-Request-Id"] = context.request_id
|
||||
response.headers["X-Response-Time"] = f"{int((perf_counter() - started) * 1000)}ms"
|
||||
return response
|
||||
|
||||
started = perf_counter()
|
||||
try:
|
||||
response = await call_next(request)
|
||||
except Exception as exc:
|
||||
@@ -110,6 +138,7 @@ async def api_access_log_middleware(request: Request, call_next):
|
||||
|
||||
duration_ms = int((perf_counter() - started) * 1000)
|
||||
response.headers["X-Request-Id"] = context.request_id
|
||||
response.headers["X-Response-Time"] = f"{duration_ms}ms"
|
||||
result = "success" if response.status_code < 400 else "fail"
|
||||
log_api_access(
|
||||
request,
|
||||
|
||||
@@ -155,7 +155,6 @@ def generate_crud_router(
|
||||
data["days"] = None
|
||||
obj = model(**data)
|
||||
session.add(obj)
|
||||
await session.commit()
|
||||
await session.refresh(obj)
|
||||
return to_schema(schema_response, obj)
|
||||
|
||||
@@ -181,7 +180,6 @@ def generate_crud_router(
|
||||
for k, v in validated.model_dump(exclude_unset=True).items():
|
||||
setattr(obj, k, v)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(obj)
|
||||
return to_schema(schema_response, obj)
|
||||
|
||||
@@ -203,7 +201,6 @@ def generate_crud_router(
|
||||
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||
|
||||
await session.delete(obj)
|
||||
await session.commit()
|
||||
return {"detail": f"{model.__name__} deleted"}
|
||||
|
||||
return router
|
||||
|
||||
@@ -65,5 +65,4 @@ async def delete_gift_with_usages(
|
||||
|
||||
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
|
||||
await session.delete(gift)
|
||||
await session.commit()
|
||||
return {"message": "Подарок и связанные использования удалены"}
|
||||
|
||||
@@ -44,7 +44,6 @@ async def delete_key_by_email(
|
||||
cluster_id=db_key.server_id,
|
||||
)
|
||||
await session.delete(db_key)
|
||||
await session.commit()
|
||||
logger.info(f"[API] Ключ удалён: {db_key.client_id}")
|
||||
return {"message": "Ключ успешно удалён"}
|
||||
|
||||
@@ -108,7 +107,6 @@ async def edit_key_by_email(
|
||||
hwid_device_limit=getattr(db_key, "device_limit", None),
|
||||
reset_traffic=True,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"[API] Ключ обновлён: {db_key.client_id}")
|
||||
return db_key
|
||||
|
||||
@@ -195,7 +195,6 @@ async def change_domain(
|
||||
)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
return {"updated": result.rowcount or 0}
|
||||
|
||||
@@ -214,7 +213,6 @@ async def restore_trials(
|
||||
.values(trial=0)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
return {"restored": result.rowcount or 0}
|
||||
|
||||
|
||||
+954
-964
File diff suppressed because it is too large
Load Diff
@@ -42,5 +42,4 @@ async def delete_one_referral(
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Referral not found")
|
||||
await session.delete(obj)
|
||||
await session.commit()
|
||||
return {"status": "deleted_one"}
|
||||
|
||||
@@ -125,7 +125,6 @@ async def upsert_setting(
|
||||
value=payload.value,
|
||||
description=payload.description,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(obj)
|
||||
settings_cache.update(
|
||||
key,
|
||||
@@ -148,6 +147,5 @@ async def delete_setting(
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Setting not found")
|
||||
await session.delete(obj)
|
||||
await session.commit()
|
||||
settings_cache.delete(key)
|
||||
return {"detail": "Setting deleted"}
|
||||
|
||||
@@ -56,7 +56,6 @@ async def apply_coupon(
|
||||
tg_id=tg_id,
|
||||
code=str(body.code or ""),
|
||||
)
|
||||
await session.commit()
|
||||
return CouponApplyResponse(
|
||||
ok=True,
|
||||
message="Купон успешно активирован",
|
||||
|
||||
@@ -76,7 +76,6 @@ async def create_flow(
|
||||
)
|
||||
session.add(flow)
|
||||
await bump_site_revision(session)
|
||||
await session.commit()
|
||||
await session.refresh(flow)
|
||||
return _flow_to_response(flow)
|
||||
|
||||
@@ -101,7 +100,6 @@ async def update_flow(
|
||||
flow.updated_at = datetime.now(UTC)
|
||||
|
||||
await bump_site_revision(session)
|
||||
await session.commit()
|
||||
await session.refresh(flow)
|
||||
return _flow_to_response(flow)
|
||||
|
||||
@@ -117,4 +115,3 @@ async def delete_flow(
|
||||
raise HTTPException(404, "Flow not found")
|
||||
await session.delete(flow)
|
||||
await bump_site_revision(session)
|
||||
await session.commit()
|
||||
|
||||
@@ -244,7 +244,6 @@ async def delete_my_gift(
|
||||
raise HTTPException(status_code=404, detail="Подарок не найден")
|
||||
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
|
||||
await session.delete(gift)
|
||||
await session.commit()
|
||||
return {"ok": True, "message": "Подарок удалён"}
|
||||
|
||||
|
||||
@@ -330,5 +329,4 @@ async def delete_gift_with_usages(
|
||||
raise HTTPException(status_code=404, detail="Gift not found")
|
||||
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
|
||||
await session.delete(gift)
|
||||
await session.commit()
|
||||
return {"message": "Подарок и связанные использования удалены"}
|
||||
|
||||
@@ -21,7 +21,6 @@ async def delete_key_by_email(
|
||||
cluster_id=db_key.server_id,
|
||||
)
|
||||
await session.delete(db_key)
|
||||
await session.commit()
|
||||
logger.info(f"[API] Ключ удалён: {db_key.client_id}")
|
||||
return {"message": "Ключ успешно удалён"}
|
||||
except Exception as e:
|
||||
@@ -82,7 +81,6 @@ async def edit_key_by_email(
|
||||
hwid_device_limit=getattr(db_key, "device_limit", None),
|
||||
reset_traffic=True,
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(f"[API] Ключ обновлён: {db_key.client_id}")
|
||||
return db_key
|
||||
except Exception as e:
|
||||
|
||||
@@ -647,7 +647,6 @@ async def user_key_apply_addons(
|
||||
await update_balance(session, int(billing_user_id), -int(final_extra_price_rub))
|
||||
if coupon_id is not None:
|
||||
await mark_coupon_used(session, int(coupon_id), int(billing_user_id))
|
||||
await session.commit()
|
||||
return AccountKeyApplyAddonsResponse(
|
||||
ok=True,
|
||||
message="Доп. опции применены",
|
||||
|
||||
@@ -195,7 +195,6 @@ async def user_key_update_alias(
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
db_key.alias = alias
|
||||
await session.commit()
|
||||
return AccountKeyResponse(
|
||||
email=str(getattr(db_key, "email", "") or ""),
|
||||
alias=getattr(db_key, "alias", None),
|
||||
@@ -237,5 +236,4 @@ async def user_key_delete(
|
||||
session=session,
|
||||
)
|
||||
await session.delete(db_key)
|
||||
await session.commit()
|
||||
return AccountKeyActionResponse(ok=True, message="Подписка удалена")
|
||||
|
||||
@@ -215,7 +215,6 @@ async def user_key_change_location(
|
||||
db_key.client_id = key_client_id
|
||||
db_key.key = public_link if isinstance(public_link, str) and public_link.strip() else None
|
||||
db_key.remnawave_link = remnawave_link
|
||||
await session.commit()
|
||||
return AccountKeyChangeLocationResponse(
|
||||
ok=True,
|
||||
message="Локация успешно изменена",
|
||||
|
||||
@@ -174,7 +174,6 @@ async def user_key_renew(
|
||||
)
|
||||
except ServiceError as e:
|
||||
raise HTTPException(status_code=400, detail=e.message)
|
||||
await session.commit()
|
||||
return AccountKeyRenewResponse(
|
||||
ok=True,
|
||||
message="Подписка продлена",
|
||||
|
||||
@@ -215,7 +215,6 @@ async def change_domain(
|
||||
)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
return {"updated": result.rowcount or 0}
|
||||
|
||||
|
||||
@@ -235,7 +234,6 @@ async def restore_trials(
|
||||
.values(trial=0)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
return {"restored": result.rowcount or 0}
|
||||
|
||||
|
||||
|
||||
+44
-56
@@ -9,7 +9,7 @@ from urllib.parse import urlsplit
|
||||
import qrcode
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from fastapi.responses import ORJSONResponse, StreamingResponse
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -227,7 +227,6 @@ async def partner_apply(
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
await session.commit()
|
||||
return PartnerApplyResponse(
|
||||
ok=True,
|
||||
message="Партнерский код применен",
|
||||
@@ -516,7 +515,6 @@ async def partner_create_payout_request(
|
||||
text("UPDATE users SET partner_balance = :balance WHERE id = :id"),
|
||||
{"balance": new_balance, "id": int(user_id)},
|
||||
)
|
||||
await session.commit()
|
||||
return PartnerPayoutRequestResponse(
|
||||
ok=True,
|
||||
message="Заявка на вывод создана",
|
||||
@@ -577,7 +575,7 @@ async def get_all_partners(
|
||||
"method": partner[5] or None,
|
||||
"referred_count": int(partner[6] or 0),
|
||||
})
|
||||
return JSONResponse(content={"total": total, "items": partners_list})
|
||||
return ORJSONResponse(content={"total": total, "items": partners_list})
|
||||
|
||||
|
||||
@router.get("/stats/all")
|
||||
@@ -623,7 +621,7 @@ async def get_partners_stats(
|
||||
"top_partner_tg_id": 0,
|
||||
"top_partner_refs": 0,
|
||||
}
|
||||
return JSONResponse(content=stats)
|
||||
return ORJSONResponse(content=stats)
|
||||
|
||||
|
||||
@router.patch("/{tg_id}")
|
||||
@@ -644,15 +642,14 @@ async def update_partner(
|
||||
"""
|
||||
)
|
||||
result = await session.execute(stmt, {"tg_id": tg_id, "balance": balance, "percent": percent})
|
||||
await session.commit()
|
||||
if result.rowcount > 0:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": True, "message": f"Партнёр {tg_id} успешно обновлён"}, status_code=200
|
||||
)
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(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)
|
||||
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/{tg_id}")
|
||||
@@ -706,7 +703,7 @@ async def get_partner_data(
|
||||
for row in invited_rows
|
||||
],
|
||||
}
|
||||
return JSONResponse(content=response)
|
||||
return ORJSONResponse(content=response)
|
||||
|
||||
|
||||
@router.post("/{tg_id}/invited")
|
||||
@@ -718,18 +715,18 @@ async def add_partner_invited(
|
||||
):
|
||||
"""Добавляет приглашённого пользователя партнёру."""
|
||||
if joined_tg_id == tg_id:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Нельзя привязать пользователя к самому себе"}, status_code=400
|
||||
)
|
||||
try:
|
||||
partner_exists = await session.execute(text("SELECT 1 FROM users WHERE tg_id = :tg_id"), {"tg_id": tg_id})
|
||||
if not partner_exists.scalar():
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
invited_exists = await session.execute(
|
||||
text("SELECT 1 FROM users WHERE tg_id = :joined_tg_id"), {"joined_tg_id": joined_tg_id}
|
||||
)
|
||||
if not invited_exists.scalar():
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Приглашённый пользователь не найден"}, status_code=404
|
||||
)
|
||||
existing = await session.execute(
|
||||
@@ -738,7 +735,7 @@ async def add_partner_invited(
|
||||
)
|
||||
existing_partner = existing.scalar()
|
||||
if existing_partner is not None:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": f"Пользователь уже привязан к партнёру {existing_partner}"},
|
||||
status_code=409,
|
||||
)
|
||||
@@ -746,8 +743,7 @@ async def add_partner_invited(
|
||||
text("INSERT INTO partners (partner_tg_id, joined_tg_id) VALUES (:partner_tg_id, :joined_tg_id)"),
|
||||
{"partner_tg_id": tg_id, "joined_tg_id": joined_tg_id},
|
||||
)
|
||||
await session.commit()
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": "Приглашённый добавлен",
|
||||
@@ -758,7 +754,7 @@ async def add_partner_invited(
|
||||
)
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.delete("/{tg_id}/invited/{joined_tg_id}")
|
||||
@@ -774,9 +770,8 @@ async def delete_partner_invited(
|
||||
text("DELETE FROM partners WHERE partner_tg_id = :partner_tg_id AND joined_tg_id = :joined_tg_id"),
|
||||
{"partner_tg_id": tg_id, "joined_tg_id": joined_tg_id},
|
||||
)
|
||||
await session.commit()
|
||||
if result.rowcount > 0:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": "Приглашённый удалён",
|
||||
@@ -785,12 +780,12 @@ async def delete_partner_invited(
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
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)
|
||||
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.patch("/{tg_id}/percent")
|
||||
@@ -803,7 +798,7 @@ async def update_partner_percent(
|
||||
"""Обновляет персональный процент партнёра."""
|
||||
normalized = _parse_percent(percent)
|
||||
if normalized is None:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Неверный процент. Допустимо 0-100 или 0.0-1.0"}, status_code=400
|
||||
)
|
||||
try:
|
||||
@@ -811,15 +806,14 @@ async def update_partner_percent(
|
||||
text("UPDATE users SET partner_percent = :percent, partner_percent_custom = true WHERE tg_id = :tg_id"),
|
||||
{"tg_id": tg_id, "percent": normalized},
|
||||
)
|
||||
await session.commit()
|
||||
if result.rowcount > 0:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": True, "message": "Процент обновлён", "percent": normalized}, status_code=200
|
||||
)
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(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)
|
||||
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.patch("/{tg_id}/balance")
|
||||
@@ -833,22 +827,22 @@ async def update_partner_balance(
|
||||
"""Изменяет баланс партнёрской программы."""
|
||||
mode_normalized = (mode or "set").strip().lower()
|
||||
if mode_normalized not in {"set", "add", "subtract"}:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Неверный режим. Используйте set, add или subtract"}, status_code=400
|
||||
)
|
||||
try:
|
||||
amount_val = float(amount)
|
||||
except (TypeError, ValueError):
|
||||
return JSONResponse(content={"success": False, "message": "Неверная сумма"}, status_code=400)
|
||||
return ORJSONResponse(content={"success": False, "message": "Неверная сумма"}, status_code=400)
|
||||
if amount_val < 0:
|
||||
return JSONResponse(content={"success": False, "message": "Сумма не может быть отрицательной"}, status_code=400)
|
||||
return ORJSONResponse(content={"success": False, "message": "Сумма не может быть отрицательной"}, status_code=400)
|
||||
try:
|
||||
current_res = await session.execute(
|
||||
text("SELECT partner_balance FROM users WHERE tg_id = :tg_id"), {"tg_id": tg_id}
|
||||
)
|
||||
current_balance = current_res.scalar()
|
||||
if current_balance is None:
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
current_balance = float(current_balance or 0.0)
|
||||
if mode_normalized == "set":
|
||||
new_balance = amount_val
|
||||
@@ -856,19 +850,18 @@ async def update_partner_balance(
|
||||
new_balance = current_balance + amount_val
|
||||
else:
|
||||
if current_balance < amount_val:
|
||||
return JSONResponse(content={"success": False, "message": "Недостаточно средств"}, status_code=400)
|
||||
return ORJSONResponse(content={"success": False, "message": "Недостаточно средств"}, status_code=400)
|
||||
new_balance = current_balance - amount_val
|
||||
await session.execute(
|
||||
text("UPDATE users SET partner_balance = :balance WHERE tg_id = :tg_id"),
|
||||
{"tg_id": tg_id, "balance": new_balance},
|
||||
)
|
||||
await session.commit()
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": True, "message": "Баланс обновлён", "balance": new_balance}, status_code=200
|
||||
)
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/{tg_id}/invited")
|
||||
@@ -901,7 +894,7 @@ async def get_partner_invited(
|
||||
}
|
||||
for row in invited_rows
|
||||
]
|
||||
return JSONResponse(content=invited_list)
|
||||
return ORJSONResponse(content=invited_list)
|
||||
|
||||
|
||||
@router.get("/payouts/pending")
|
||||
@@ -944,7 +937,7 @@ async def get_partner_payouts_pending(
|
||||
}
|
||||
for row in result.fetchall()
|
||||
]
|
||||
return JSONResponse(content={"total": int(total), "items": items})
|
||||
return ORJSONResponse(content={"total": int(total), "items": items})
|
||||
|
||||
|
||||
@router.get("/payouts/history")
|
||||
@@ -987,7 +980,7 @@ async def get_partner_payouts_history(
|
||||
}
|
||||
for row in result.fetchall()
|
||||
]
|
||||
return JSONResponse(content={"total": int(total), "items": items})
|
||||
return ORJSONResponse(content={"total": int(total), "items": items})
|
||||
|
||||
|
||||
@router.post("/payouts/{payout_id}/approve")
|
||||
@@ -1003,7 +996,7 @@ async def approve_partner_payout(
|
||||
)
|
||||
req = req_row.fetchone()
|
||||
if not req:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Заявка не найдена или уже обработана"}, status_code=404
|
||||
)
|
||||
user_row = await session.execute(
|
||||
@@ -1019,8 +1012,7 @@ async def approve_partner_payout(
|
||||
),
|
||||
{"id": payout_id, "method": payout_method, "destination": destination},
|
||||
)
|
||||
await session.commit()
|
||||
return JSONResponse(content={"success": True, "message": "Заявка одобрена"}, status_code=200)
|
||||
return ORJSONResponse(content={"success": True, "message": "Заявка одобрена"}, status_code=200)
|
||||
|
||||
|
||||
@router.post("/payouts/{payout_id}/reject")
|
||||
@@ -1036,7 +1028,7 @@ async def reject_partner_payout(
|
||||
)
|
||||
req = req_row.fetchone()
|
||||
if not req:
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Заявка не найдена или уже обработана"}, status_code=404
|
||||
)
|
||||
user_row = await session.execute(
|
||||
@@ -1058,8 +1050,7 @@ async def reject_partner_payout(
|
||||
text("UPDATE users SET partner_balance = :balance WHERE tg_id = :tg_id"),
|
||||
{"balance": current_balance + float(req[2] or 0.0), "tg_id": req[1]},
|
||||
)
|
||||
await session.commit()
|
||||
return JSONResponse(content={"success": True, "message": "Заявка отклонена"}, status_code=200)
|
||||
return ORJSONResponse(content={"success": True, "message": "Заявка отклонена"}, status_code=200)
|
||||
|
||||
|
||||
@router.patch("/{tg_id}/percent/reset")
|
||||
@@ -1073,10 +1064,9 @@ async def reset_partner_percent(
|
||||
text("UPDATE users SET partner_percent = NULL, partner_percent_custom = false WHERE tg_id = :tg_id"),
|
||||
{"tg_id": tg_id},
|
||||
)
|
||||
await session.commit()
|
||||
if result.rowcount > 0:
|
||||
return JSONResponse(content={"success": True, "message": "Процент сброшен"}, status_code=200)
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(content={"success": True, "message": "Процент сброшен"}, status_code=200)
|
||||
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
|
||||
|
||||
@router.patch("/{tg_id}/code")
|
||||
@@ -1089,23 +1079,22 @@ async def update_partner_code(
|
||||
"""Обновляет код партнёрской ссылки."""
|
||||
raw = (code or "").strip().lower()
|
||||
if not raw:
|
||||
return JSONResponse(content={"success": False, "message": "Код не может быть пустым"}, status_code=400)
|
||||
return ORJSONResponse(content={"success": False, "message": "Код не может быть пустым"}, status_code=400)
|
||||
if not re.fullmatch(r"[a-z0-9_]{3,32}", raw):
|
||||
return JSONResponse(
|
||||
return ORJSONResponse(
|
||||
content={"success": False, "message": "Неверный код. Разрешены a-z, 0-9, _ (3-32 символа)"}, status_code=400
|
||||
)
|
||||
exists = await session.execute(
|
||||
text("SELECT 1 FROM users WHERE partner_code = :code AND tg_id != :tg_id"), {"code": raw, "tg_id": tg_id}
|
||||
)
|
||||
if exists.first():
|
||||
return JSONResponse(content={"success": False, "message": "Такой код уже занят"}, status_code=409)
|
||||
return ORJSONResponse(content={"success": False, "message": "Такой код уже занят"}, status_code=409)
|
||||
result = await session.execute(
|
||||
text("UPDATE users SET partner_code = :code WHERE tg_id = :tg_id"), {"code": raw, "tg_id": tg_id}
|
||||
)
|
||||
await session.commit()
|
||||
if result.rowcount > 0:
|
||||
return JSONResponse(content={"success": True, "message": "Код обновлён", "code": raw}, status_code=200)
|
||||
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
return ORJSONResponse(content={"success": True, "message": "Код обновлён", "code": raw}, status_code=200)
|
||||
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
|
||||
|
||||
|
||||
@router.post("/reset-disabled-methods")
|
||||
@@ -1138,12 +1127,11 @@ async def reset_disabled_payout_methods(
|
||||
if not ENABLE_PAYOUT_SBP and B:
|
||||
disabled.append(B.METHOD_SBP)
|
||||
if not disabled:
|
||||
return JSONResponse(content={"success": True, "message": "Отключённых методов нет"}, status_code=200)
|
||||
return ORJSONResponse(content={"success": True, "message": "Отключённых методов нет"}, status_code=200)
|
||||
await session.execute(
|
||||
text("UPDATE users SET card_number = NULL WHERE payout_method = ANY(:methods)"), {"methods": disabled}
|
||||
)
|
||||
await session.commit()
|
||||
return JSONResponse(content={"success": True, "message": "Отключённые методы сброшены"}, status_code=200)
|
||||
return ORJSONResponse(content={"success": True, "message": "Отключённые методы сброшены"}, status_code=200)
|
||||
|
||||
|
||||
@router.get("/{tg_id}/export")
|
||||
@@ -1159,7 +1147,7 @@ async def export_partner_invites_csv(
|
||||
)
|
||||
data = rows.fetchall()
|
||||
if not data:
|
||||
return JSONResponse(content={"success": False, "message": "Нет приглашённых"}, status_code=404)
|
||||
return ORJSONResponse(content={"success": False, "message": "Нет приглашённых"}, status_code=404)
|
||||
buffer = StringIO()
|
||||
writer = csv.writer(buffer, delimiter=";")
|
||||
writer.writerow(["joined_tg_id", "created_at"])
|
||||
|
||||
@@ -120,7 +120,6 @@ async def upsert_setting(
|
||||
value=payload.value,
|
||||
description=payload.description,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(obj)
|
||||
settings_cache.update(
|
||||
key,
|
||||
@@ -144,6 +143,5 @@ async def delete_setting(
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Setting not found")
|
||||
await session.delete(obj)
|
||||
await session.commit()
|
||||
settings_cache.delete(key)
|
||||
return {"detail": "Setting deleted"}
|
||||
|
||||
@@ -302,7 +302,6 @@ async def purchase_tariff_with_balance(
|
||||
)
|
||||
if coupon_id is not None:
|
||||
await mark_coupon_used(session, int(coupon_id), int(tg_id))
|
||||
await session.commit()
|
||||
except Exception:
|
||||
logger.exception("web tariff purchase failed")
|
||||
raise HTTPException(status_code=500, detail="Не удалось оформить подписку") from None
|
||||
|
||||
@@ -40,6 +40,10 @@ from logger import logger
|
||||
UPLOAD_DIR = Path("static/web_uploads")
|
||||
ALLOWED_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4", ".webm"})
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
_IMAGE_RESIZE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".webp"})
|
||||
_IMAGE_MAX_SIDE = 2048
|
||||
_IMAGE_JPEG_QUALITY = 85
|
||||
_IMAGE_WEBP_QUALITY = 85
|
||||
|
||||
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9\-]*$")
|
||||
|
||||
@@ -55,6 +59,39 @@ EXTENSION_CONTENT_TYPES: dict[str, frozenset[str]] = {
|
||||
}
|
||||
|
||||
|
||||
def _optimize_image_bytes(data: bytes, ext: str) -> bytes:
|
||||
"""Уменьшает большие картинки до _IMAGE_MAX_SIDE и пережимает с разумным качеством."""
|
||||
try:
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
with Image.open(BytesIO(data)) as img:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
w, h = img.size
|
||||
if max(w, h) <= _IMAGE_MAX_SIDE and len(data) < 500_000:
|
||||
return data
|
||||
if max(w, h) > _IMAGE_MAX_SIDE:
|
||||
img.thumbnail((_IMAGE_MAX_SIDE, _IMAGE_MAX_SIDE), Image.Resampling.LANCZOS)
|
||||
buffer = BytesIO()
|
||||
save_kwargs: dict = {}
|
||||
if ext in (".jpg", ".jpeg"):
|
||||
if img.mode not in ("RGB", "L"):
|
||||
img = img.convert("RGB")
|
||||
save_kwargs = {"format": "JPEG", "quality": _IMAGE_JPEG_QUALITY, "optimize": True, "progressive": True}
|
||||
elif ext == ".webp":
|
||||
save_kwargs = {"format": "WEBP", "quality": _IMAGE_WEBP_QUALITY, "method": 6}
|
||||
elif ext == ".png":
|
||||
save_kwargs = {"format": "PNG", "optimize": True}
|
||||
else:
|
||||
return data
|
||||
img.save(buffer, **save_kwargs)
|
||||
optimized = buffer.getvalue()
|
||||
return optimized if len(optimized) < len(data) else data
|
||||
except Exception:
|
||||
return data
|
||||
|
||||
|
||||
def _sanitize_svg(data: bytes) -> bytes:
|
||||
import re as _re
|
||||
|
||||
@@ -529,6 +566,10 @@ async def upload_media(
|
||||
file_data = b"".join(chunks)
|
||||
if ext == ".svg":
|
||||
file_data = _sanitize_svg(file_data)
|
||||
elif ext in _IMAGE_RESIZE_EXTENSIONS:
|
||||
from core.executor import run_cpu
|
||||
|
||||
file_data = await run_cpu(_optimize_image_bytes, file_data, ext)
|
||||
with open(path, "wb") as f:
|
||||
f.write(file_data)
|
||||
url = f"/api/web/uploads/{name}"
|
||||
|
||||
Reference in New Issue
Block a user