Пейволл больших файлов: 99₽/файл или подписка 299₽/мес (Фаза 1)
- _finalize_submit держит большой файл (awaiting_payment) без активной подписки; account из cookie-сессии (Yandex/VK) или tg user_id - Оплата purpose=file (снимает задание с холда) / sub (+30 дней, снимает холд) - Фронт: карточка пейволла (99₽/подписка), обработка возврата, single+batch - Старый auth-баннер больших файлов заменён пейволлом - Включается флагом PAYWALL_ENABLED (в .env) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
43
app/main.py
43
app/main.py
@@ -847,15 +847,18 @@ def _write_chunk_at(path: str, data: bytes, offset: int) -> None:
|
||||
|
||||
def _finalize_submit(job_id, file_path, original_name, stitching, title,
|
||||
numeration, flip_side, color, pdfa, quality,
|
||||
size_mb, pages, user_id, trusted=False, ocr=0, web_owner_id=None):
|
||||
size_mb, pages, user_id, trusted=False, ocr=0, web_owner_id=None,
|
||||
account_id=None):
|
||||
"""Общий хвост постановки задания: проверка авторизации, запись в БД, _meta.
|
||||
trusted=True (бизнес-API по ключу) — пропускает пейволл.
|
||||
account_id — аккаунт запроса (из сессии/TG) для проверки подписки.
|
||||
web_owner_id — TG id владельца для истории «Мои документы» (по умолчанию = user_id)."""
|
||||
# Пейволл: ≤50МБ и ≤100стр — бесплатно; больше — нужна активная подписка,
|
||||
# иначе задание держим (url_res_f='awaiting_payment') до разовой оплаты 99₽.
|
||||
acc = account_id or _account_id(user_id)
|
||||
held = False
|
||||
if not trusted and (size_mb > AUTH_REQUIRED_MB or pages > AUTH_REQUIRED_PAGES):
|
||||
if _db_sub_active(_account_id(user_id)):
|
||||
if _db_sub_active(acc):
|
||||
pass # активная подписка — бесплатно
|
||||
elif PAYWALL_ENABLED:
|
||||
held = True # держим до оплаты 99₽ или подписки
|
||||
@@ -926,6 +929,7 @@ def _build_zip(items) -> str:
|
||||
|
||||
@app.post("/api/submit")
|
||||
async def submit(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
stitching: str = Form("без сшивки"),
|
||||
title: int = Form(0),
|
||||
@@ -982,7 +986,7 @@ async def submit(
|
||||
return _finalize_submit(
|
||||
job_id, file_path, original_name, stitching, title,
|
||||
numeration, flip_side, color, pdfa, quality, size_mb, pages, user_id,
|
||||
ocr=ocr,
|
||||
ocr=ocr, account_id=_resolve_account(request, user_id),
|
||||
)
|
||||
|
||||
|
||||
@@ -1009,6 +1013,7 @@ async def upload_chunk(
|
||||
|
||||
@app.post("/api/submit-chunked")
|
||||
async def submit_chunked(
|
||||
request: Request,
|
||||
upload_id: str = Form(...),
|
||||
filename: str = Form("document.pdf"),
|
||||
total_size: int = Form(0),
|
||||
@@ -1061,7 +1066,7 @@ async def submit_chunked(
|
||||
return _finalize_submit(
|
||||
job_id, file_path, original_name, stitching, title,
|
||||
numeration, flip_side, color, pdfa, quality, size_mb, pages, user_id,
|
||||
ocr=ocr,
|
||||
ocr=ocr, account_id=_resolve_account(request, user_id),
|
||||
)
|
||||
|
||||
|
||||
@@ -1593,7 +1598,7 @@ def _db_record_payment(payment_id, amount, currency, status, purpose, api_key, e
|
||||
|
||||
|
||||
def _account_id(user_id) -> str | None:
|
||||
"""Внутренний id аккаунта. Пока источник — Telegram; позже yandex:/vk:/email:."""
|
||||
"""Внутренний id аккаунта из Telegram user_id."""
|
||||
try:
|
||||
uid = int(user_id or 0)
|
||||
except (TypeError, ValueError):
|
||||
@@ -1601,6 +1606,17 @@ def _account_id(user_id) -> str | None:
|
||||
return f"tg:{uid}" if uid else None
|
||||
|
||||
|
||||
def _resolve_account(request, user_id=0) -> str | None:
|
||||
"""Аккаунт запроса: сначала cookie-сессия (Yandex/VK), иначе tg:<user_id>."""
|
||||
try:
|
||||
sess = _db_get_session(request.cookies.get(SESSION_COOKIE)) if request else None
|
||||
except Exception:
|
||||
sess = None
|
||||
if sess and sess.get("account_id"):
|
||||
return sess["account_id"]
|
||||
return _account_id(user_id)
|
||||
|
||||
|
||||
def _db_sub_active(account_id: str | None) -> bool:
|
||||
if not account_id:
|
||||
return False
|
||||
@@ -1644,23 +1660,22 @@ def _db_mark_payment_paid(payment_id, purpose, api_key, extra=None):
|
||||
con.execute("UPDATE WEB_API_KEYS SET plan='business', paid_until=? WHERE api_key=?",
|
||||
(until, api_key))
|
||||
con.commit()
|
||||
# Разовая оплата файла → снимаем задание с холда (в очередь бота)
|
||||
if purpose == "file":
|
||||
job_id = extra.get("job_id")
|
||||
if job_id and _db_get_url_res_f(job_id) == "awaiting_payment":
|
||||
_db_set_url_res_f(job_id, "-")
|
||||
# Подписка → продлеваем аккаунт + освобождаем его задания на холде
|
||||
# Подписка → продлеваем аккаунт
|
||||
if purpose == "sub":
|
||||
acc = extra.get("account_id")
|
||||
if acc:
|
||||
_db_activate_sub(acc, SUB_DAYS)
|
||||
# И file, и sub: если платёж привязан к заданию — снимаем его с холда
|
||||
job_id = extra.get("job_id")
|
||||
if job_id and _db_get_url_res_f(job_id) == "awaiting_payment":
|
||||
_db_set_url_res_f(job_id, "-")
|
||||
except Exception as e:
|
||||
print(f"mark paid error: {e}")
|
||||
|
||||
|
||||
@app.post("/api/pay/create")
|
||||
async def pay_create(payload: dict):
|
||||
"""Создаёт платёж ЮKassa. body: {amount, purpose, email?, api_key?} → confirmation_url."""
|
||||
async def pay_create(payload: dict, request: Request):
|
||||
"""Создаёт платёж ЮKassa. body: {amount, purpose, email?, api_key?, job_id?, user_id?} → confirmation_url."""
|
||||
if not YOOKASSA_SHOP_ID or not YOOKASSA_SECRET_KEY:
|
||||
raise HTTPException(status_code=503, detail="Приём платежей не настроен.")
|
||||
if not isinstance(payload, dict):
|
||||
@@ -1669,7 +1684,7 @@ async def pay_create(payload: dict):
|
||||
email = (payload.get("email") or "").strip()[:128]
|
||||
api_key = (payload.get("api_key") or "").strip()[:80] or None
|
||||
job_id = (payload.get("job_id") or "").strip()[:128] or None
|
||||
account_id = _account_id(payload.get("user_id"))
|
||||
account_id = _resolve_account(request, payload.get("user_id"))
|
||||
|
||||
if purpose == "api_pro":
|
||||
amount = API_PRO_PRICE_RUB
|
||||
|
||||
Reference in New Issue
Block a user