Пейволл раньше (предпроверка без загрузки) + пакетная обработка по подписке
- /api/paywall/check: решение сервера до загрузки файла → пейволл показывается сразу для больших файлов (не грузим тяжёлый файл впустую) - Загрузка файла — только при клике оплатить (upload-on-pay) - Пакетная обработка (2+ файла, папка, ZIP) — только по активной подписке Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
27
app/main.py
27
app/main.py
@@ -1806,6 +1806,33 @@ async def pay_status(payment_id: str):
|
||||
return {"status": db_status, "purpose": purpose}
|
||||
|
||||
|
||||
@app.post("/api/paywall/check")
|
||||
async def paywall_check(payload: dict, request: Request):
|
||||
"""Предпроверка пейволла БЕЗ загрузки файла. body: {size_mb?, pages?, user_id?}.
|
||||
Возвращает решение сервера, чтобы фронт показал пейволл сразу."""
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
try:
|
||||
size_mb = float(payload.get("size_mb") or 0)
|
||||
except (TypeError, ValueError):
|
||||
size_mb = 0.0
|
||||
try:
|
||||
pages = int(payload.get("pages") or 0)
|
||||
except (TypeError, ValueError):
|
||||
pages = 0
|
||||
account = _resolve_account(request, payload.get("user_id"))
|
||||
sub_active = _db_sub_active(account)
|
||||
over = size_mb > AUTH_REQUIRED_MB or pages > AUTH_REQUIRED_PAGES
|
||||
requires_payment = bool(PAYWALL_ENABLED and over and not sub_active)
|
||||
return {
|
||||
"paywall_enabled": bool(PAYWALL_ENABLED),
|
||||
"sub_active": sub_active,
|
||||
"requires_payment": requires_payment,
|
||||
"file_price": FILE_PRICE_RUB,
|
||||
"sub_price": SUB_PRICE_RUB,
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Аккаунты, сессии, OAuth-вход (Yandex ID)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1303,6 +1303,9 @@
|
||||
pay_canceled: 'Платёж отменён',
|
||||
paywall_title: 'Файл больше бесплатного лимита',
|
||||
paywall_desc: 'Бесплатно — до 50 МБ и 100 страниц. Для файлов больше — разовая оплата или подписка.',
|
||||
paywall_batch_title: 'Пакетная обработка — по подписке',
|
||||
paywall_batch_desc: 'Обработка нескольких файлов сразу — по подписке. Оформите подписку, затем загрузите файлы.',
|
||||
pay_uploading: 'Загружаем файл…',
|
||||
paywall_pay_file: 'Оплатить файл',
|
||||
paywall_pay_sub: 'Подписка',
|
||||
paywall_month: 'мес',
|
||||
@@ -1430,6 +1433,9 @@
|
||||
pay_canceled: 'Payment canceled',
|
||||
paywall_title: 'File exceeds the free limit',
|
||||
paywall_desc: 'Free up to 50 MB and 100 pages. Larger files — one-time payment or subscription.',
|
||||
paywall_batch_title: 'Batch processing — subscription',
|
||||
paywall_batch_desc: 'Processing multiple files at once requires a subscription. Subscribe, then upload your files.',
|
||||
pay_uploading: 'Uploading file…',
|
||||
paywall_pay_file: 'Pay for file',
|
||||
paywall_pay_sub: 'Subscription',
|
||||
paywall_month: 'mo',
|
||||
@@ -1557,6 +1563,9 @@
|
||||
pay_canceled: 'Zahlung storniert',
|
||||
paywall_title: 'Datei über dem Gratis-Limit',
|
||||
paywall_desc: 'Kostenlos bis 50 MB und 100 Seiten. Größere Dateien — Einmalzahlung oder Abo.',
|
||||
paywall_batch_title: 'Stapelverarbeitung — im Abo',
|
||||
paywall_batch_desc: 'Mehrere Dateien gleichzeitig erfordern ein Abo. Abonnieren Sie und laden Sie dann Ihre Dateien hoch.',
|
||||
pay_uploading: 'Datei wird hochgeladen…',
|
||||
paywall_pay_file: 'Datei bezahlen',
|
||||
paywall_pay_sub: 'Abo',
|
||||
paywall_month: 'Mon',
|
||||
@@ -1684,6 +1693,9 @@
|
||||
pay_canceled: 'Paiement annulé',
|
||||
paywall_title: 'Fichier au-delà de la limite gratuite',
|
||||
paywall_desc: 'Gratuit : 50 Mo et 100 pages max. Au-delà — paiement unique ou abonnement.',
|
||||
paywall_batch_title: 'Traitement par lots — sur abonnement',
|
||||
paywall_batch_desc: 'Traiter plusieurs fichiers à la fois nécessite un abonnement. Abonnez-vous, puis chargez vos fichiers.',
|
||||
pay_uploading: 'Téléversement du fichier…',
|
||||
paywall_pay_file: 'Payer le fichier',
|
||||
paywall_pay_sub: 'Abonnement',
|
||||
paywall_month: 'mois',
|
||||
@@ -1811,6 +1823,9 @@
|
||||
pay_canceled: 'Pagamento annullato',
|
||||
paywall_title: 'File oltre il limite gratuito',
|
||||
paywall_desc: 'Gratis fino a 50 MB e 100 pagine. File più grandi — pagamento singolo o abbonamento.',
|
||||
paywall_batch_title: 'Elaborazione multipla — in abbonamento',
|
||||
paywall_batch_desc: 'Elaborare più file insieme richiede un abbonamento. Abbonati e poi carica i file.',
|
||||
pay_uploading: 'Caricamento file…',
|
||||
paywall_pay_file: 'Paga file',
|
||||
paywall_pay_sub: 'Abbonamento',
|
||||
paywall_month: 'mese',
|
||||
@@ -1938,6 +1953,9 @@
|
||||
pay_canceled: 'Pago cancelado',
|
||||
paywall_title: 'Archivo supera el límite gratis',
|
||||
paywall_desc: 'Gratis hasta 50 MB y 100 páginas. Archivos mayores — pago único o suscripción.',
|
||||
paywall_batch_title: 'Procesamiento por lotes — con suscripción',
|
||||
paywall_batch_desc: 'Procesar varios archivos a la vez requiere suscripción. Suscríbete y luego sube tus archivos.',
|
||||
pay_uploading: 'Subiendo archivo…',
|
||||
paywall_pay_file: 'Pagar archivo',
|
||||
paywall_pay_sub: 'Suscripción',
|
||||
paywall_month: 'mes',
|
||||
@@ -2065,6 +2083,9 @@
|
||||
pay_canceled: 'Pagamento cancelado',
|
||||
paywall_title: 'Arquivo acima do limite grátis',
|
||||
paywall_desc: 'Grátis até 50 MB e 100 páginas. Arquivos maiores — pagamento único ou assinatura.',
|
||||
paywall_batch_title: 'Processamento em lote — por assinatura',
|
||||
paywall_batch_desc: 'Processar vários arquivos de uma vez requer assinatura. Assine e depois envie seus arquivos.',
|
||||
pay_uploading: 'Enviando arquivo…',
|
||||
paywall_pay_file: 'Pagar arquivo',
|
||||
paywall_pay_sub: 'Assinatura',
|
||||
paywall_month: 'mês',
|
||||
@@ -2202,29 +2223,63 @@
|
||||
} catch (e) { msg.textContent = t('pay_error'); btn.disabled = false; }
|
||||
}
|
||||
|
||||
// ── Пейволл больших файлов ────────────────────────────────────────────────
|
||||
let paywallJob = null, paywallFilePrice = 99, paywallSubPrice = 299;
|
||||
function showPaywall(jobId, filePrice, subPrice) {
|
||||
// ── Пейволл больших файлов / батча ────────────────────────────────────────
|
||||
let paywallJob = null, paywallFilePrice = 99, paywallSubPrice = 299, paywallMode = 'file';
|
||||
|
||||
// Предпроверка на сервере БЕЗ загрузки файла
|
||||
async function checkPaywall(sizeMb, pages) {
|
||||
try {
|
||||
const body = { size_mb: sizeMb, pages: pages };
|
||||
if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0;
|
||||
const r = await fetch('/api/paywall/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
return await r.json();
|
||||
} catch (e) { return { requires_payment: false, sub_active: false, paywall_enabled: false, file_price: 99, sub_price: 299 }; }
|
||||
}
|
||||
|
||||
function showPaywall(jobId, filePrice, subPrice, mode) {
|
||||
paywallMode = mode || 'file';
|
||||
paywallJob = jobId;
|
||||
paywallFilePrice = filePrice || 99;
|
||||
paywallSubPrice = subPrice || 299;
|
||||
const isBatch = paywallMode === 'batch';
|
||||
document.getElementById('paywall-file-price').textContent = paywallFilePrice;
|
||||
document.getElementById('paywall-sub-price').textContent = paywallSubPrice;
|
||||
document.getElementById('paywall-file-btn').style.display = isBatch ? 'none' : '';
|
||||
const titleEl = document.querySelector('#paywall-card [data-i18n="paywall_title"]');
|
||||
const descEl = document.querySelector('#paywall-card [data-i18n="paywall_desc"]');
|
||||
if (titleEl) titleEl.textContent = t(isBatch ? 'paywall_batch_title' : 'paywall_title');
|
||||
if (descEl) descEl.textContent = t(isBatch ? 'paywall_batch_desc' : 'paywall_desc');
|
||||
const emailEl = document.getElementById('paywall-email');
|
||||
if (sessionUser && sessionUser.email && !emailEl.value) emailEl.value = sessionUser.email;
|
||||
document.getElementById('paywall-file-btn').disabled = false;
|
||||
document.getElementById('paywall-sub-btn').disabled = false;
|
||||
document.getElementById('paywall-msg').textContent = '';
|
||||
document.getElementById('progress-card').style.display = 'none';
|
||||
document.getElementById('result-card').style.display = 'none';
|
||||
document.getElementById('batch-card').style.display = 'none';
|
||||
submitBtn.style.display = 'none';
|
||||
document.getElementById('paywall-card').style.display = 'block';
|
||||
document.getElementById('paywall-card').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
async function _payViaKassa(purpose) {
|
||||
const email = (document.getElementById('paywall-email').value || '').trim();
|
||||
const msg = document.getElementById('paywall-msg');
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; }
|
||||
const body = { purpose, job_id: paywallJob, email };
|
||||
if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0;
|
||||
const fb = document.getElementById('paywall-file-btn'), sb = document.getElementById('paywall-sub-btn');
|
||||
fb.disabled = true; sb.disabled = true; msg.textContent = t('pay_creating');
|
||||
fb.disabled = true; sb.disabled = true;
|
||||
// Если файл ещё не загружен (пейволл показан ДО загрузки) — грузим сейчас
|
||||
if (!paywallJob && paywallMode !== 'batch' && selectedFile) {
|
||||
msg.textContent = t('pay_uploading');
|
||||
try {
|
||||
const data = await uploadFileChunked(selectedFile, collectSettings(), null);
|
||||
paywallJob = data.job_id;
|
||||
} catch (e) { msg.textContent = (e && e.message) ? e.message : t('err_network'); fb.disabled = false; sb.disabled = false; return; }
|
||||
}
|
||||
const body = { purpose, email };
|
||||
if (paywallJob) body.job_id = paywallJob;
|
||||
if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0;
|
||||
msg.textContent = t('pay_creating');
|
||||
try {
|
||||
const r = await fetch('/api/pay/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const d = await r.json();
|
||||
@@ -2569,14 +2624,23 @@
|
||||
acceptFiles(gatherFiles(folderInput.files));
|
||||
});
|
||||
|
||||
// Пакетная обработка (2+ файла / папка / ZIP) — только по подписке.
|
||||
function requireSubForBatch(proceed) {
|
||||
checkPaywall(0, 0).then(chk => {
|
||||
if (chk.paywall_enabled && !chk.sub_active) {
|
||||
showPaywall(null, chk.file_price, chk.sub_price, 'batch');
|
||||
} else { proceed(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Один файл → классический одиночный поток; несколько → батч со списком и ZIP.
|
||||
function acceptFiles(arr) {
|
||||
if (!arr || !arr.length) return;
|
||||
hideError(); hideInfo(); hideDonateCard();
|
||||
submitBtn.style.display = 'block'; // мог быть скрыт после прошлого батча
|
||||
// ZIP-архив: распаковывается на сервере, каждый вложенный файл — отдельное задание.
|
||||
// ZIP-архив (форма батча) — по подписке.
|
||||
const zips = arr.filter(f => /\.zip$/i.test(f.name));
|
||||
if (zips.length) { handleZip(zips[0]); return; }
|
||||
if (zips.length) { requireSubForBatch(() => handleZip(zips[0])); return; }
|
||||
let files = arr;
|
||||
if (files.length > MAX_BATCH) {
|
||||
showError(t('err_too_many')(MAX_BATCH));
|
||||
@@ -2587,12 +2651,15 @@
|
||||
document.getElementById('batch-card').style.display = 'none';
|
||||
handleFile(files[0]);
|
||||
} else {
|
||||
batchMode = true;
|
||||
selectedFile = null;
|
||||
fileBadge.style.display = 'none';
|
||||
document.getElementById('result-card').style.display = 'none';
|
||||
document.getElementById('progress-card').style.display = 'none';
|
||||
handleFiles(files);
|
||||
// Пакет из нескольких файлов — гейт по подписке
|
||||
requireSubForBatch(() => {
|
||||
batchMode = true;
|
||||
selectedFile = null;
|
||||
fileBadge.style.display = 'none';
|
||||
document.getElementById('result-card').style.display = 'none';
|
||||
document.getElementById('progress-card').style.display = 'none';
|
||||
handleFiles(files);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2999,8 +3066,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function submitJob() {
|
||||
async function submitJob() {
|
||||
if (!selectedFile) { showError(t('err_no_file')); return; }
|
||||
// Пейволл БЕЗ загрузки: только для потенциально больших файлов (мелкие не дёргают сервер)
|
||||
if (fileSizeMb > AUTH_MAX_MB || filePages > AUTH_MAX_PAGES) {
|
||||
submitBtn.disabled = true;
|
||||
const chk = await checkPaywall(fileSizeMb, filePages);
|
||||
submitBtn.disabled = false;
|
||||
if (chk.requires_payment) { showPaywall(null, chk.file_price, chk.sub_price, 'file'); return; }
|
||||
}
|
||||
// Крупные файлы — через чанковую загрузку (туннель не тянет большое тело за раз)
|
||||
if (selectedFile.size > CHUNK_SIZE) { submitJobChunked(); return; }
|
||||
hideError();
|
||||
|
||||
Reference in New Issue
Block a user