From 687d54ef7c0a538d373b186b6a09a66df2d172e3 Mon Sep 17 00:00:00 2001 From: berkio_admin_gitea Date: Fri, 10 Jul 2026 11:15:48 +0000 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0:=20?= =?UTF-8?q?=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=20=C2=AB=D0=9F=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=20=D0=BF=D0=B5=D0=B9=D0=B2=D0=BE=D0=BB=D0=B0?= =?UTF-8?q?=C2=BB=20=E2=80=94=20=D0=BA=D1=82=D0=BE=20=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BA=D0=B0=D0=BA=D0=BE=D0=BC=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B5=20=D1=83=D0=BF=D1=91=D1=80=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Таблица WEB_PAYWALL_EVENTS: пишется при показе пейвола (_finalize_submit), статус обновляется при оплате (_db_mark_payment_paid) и освобождении по подписке (/api/held/release). Разовый бэкфилл текущих awaiting_payment (веб+бот). Раздел в админке: дата, пользователь, файл, размер, стр., источник, статус. Co-Authored-By: Claude Opus 4.8 --- app/main.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/app/main.py b/app/main.py index 9e00344..d9e378d 100644 --- a/app/main.py +++ b/app/main.py @@ -343,6 +343,40 @@ def _db_ensure_auth_table(): PRIMARY KEY (day, event) ) """) + # Показы пейвола: по каждому придержанному файлу — кто (account/user) и какой файл. + con.execute(""" + CREATE TABLE IF NOT EXISTS WEB_PAYWALL_EVENTS ( + job_id TEXT PRIMARY KEY, + ts REAL, + account TEXT, + user_id TEXT, + source TEXT, + file_name TEXT, + size_mb REAL, + pages INTEGER, + status TEXT DEFAULT 'shown' + ) + """) + # Разовый бэкфилл: текущие awaiting_payment (веб+бот) → в журнал показов. + try: + for _r in con.execute( + "SELECT name_or_f, date, time, " + "COALESCE(NULLIF(original_name,''), name_or_f) AS nm, source_size_mb, " + "COALESCE(NULLIF(pages_total,0), num_pages, 0) AS pg, user_id " + "FROM FILES WHERE url_res_f='awaiting_payment'").fetchall(): + _src = "сайт" if str(_r[6]) == str(WEB_USER_ID) else "бот" + _acc = "" if _src == "сайт" else f"tg:{_r[6]}" + try: + _ts = datetime.datetime.strptime(f"{_r[1]} {_r[2]}", "%Y.%m.%d %H:%M:%S").timestamp() + except Exception: + _ts = time.time() + con.execute( + "INSERT OR IGNORE INTO WEB_PAYWALL_EVENTS " + "(job_id, ts, account, user_id, source, file_name, size_mb, pages, status) " + "VALUES (?,?,?,?,?,?,?,?, 'shown')", + (_r[0], _ts, _acc, str(_r[6]), _src, _r[3], _r[4], _r[5])) + except Exception as _pmig: + print(f"WEB_PAYWALL_EVENTS backfill error: {_pmig}") # Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub. con.execute(""" CREATE TABLE IF NOT EXISTS WEB_PAYMENTS ( @@ -1074,6 +1108,7 @@ def _finalize_submit(job_id, file_path, original_name, stitching, title, if held: # Задание не идёт в очередь бота, пока не оплатят (файл 99₽ или подписка) _db_set_url_res_f(job_id, "awaiting_payment") + _log_paywall_shown(job_id, acc, user_id, "сайт", original_name, size_mb, pages) return {"job_id": job_id, "requires_payment": True, "file_price": FILE_PRICE_RUB, "sub_price": SUB_PRICE_RUB} return {"job_id": job_id} @@ -1996,6 +2031,7 @@ def _db_mark_payment_paid(payment_id, purpose, api_key, extra=None, payment_meth 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, "-") + _mark_paywall_resolved(job_id, "оплачен") # Воронка: успешная оплата пейвольных покупок (файл/подписка) if purpose in ("file", "sub"): _bump_funnel("pay_success") @@ -2495,6 +2531,8 @@ async def held_release(payload: dict, request: Request): return True ok = await asyncio.to_thread(_release) + if ok: + await asyncio.to_thread(_mark_paywall_resolved, job_id, "по подписке") return {"released": bool(ok)} @@ -3140,6 +3178,12 @@ _ADMIN_HTML = """ +
+

Показы пейвола

+
+
ДатаПользовательФайлРазмерСтр.ИсточникСтатус
+
+

По дням

@@ -3304,6 +3348,18 @@ _ADMIN_HTML = """ ''; }).join('') : ''; + // Показы пейвола (журнал: кто + файл + статус) + var pw = d.paywall_events || []; + document.getElementById('pw-note').textContent = '· ' + pw.length; + var PWST = { 'shown':['pend','ждёт оплаты'], 'оплачен':['ok','оплачен'], 'по подписке':['ok','по подписке'] }; + document.getElementById('t-paywall').innerHTML = pw.length ? pw.map(function(e){ + var who = (e.account && e.account!=='') ? e.account : (e.source||'—'); + var st = PWST[e.status] || ['pend', e.status||'—']; + return ''; + }).join('') : ''; + // По дням (свежие сверху) var daily = (d.daily||[]).slice().reverse(); document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){ @@ -3464,6 +3520,45 @@ def _bump_funnel(event: str): pass +def _log_paywall_shown(job_id, account, user_id, source, file_name, size_mb, pages): + """Журнал показов пейвола: кто (account/user) и какой файл. Идемпотентно по job_id.""" + try: + with sqlite3.connect(DB_PATH, timeout=10) as con: + con.execute( + "INSERT OR IGNORE INTO WEB_PAYWALL_EVENTS " + "(job_id, ts, account, user_id, source, file_name, size_mb, pages, status) " + "VALUES (?,?,?,?,?,?,?,?, 'shown')", + (job_id, time.time(), account or "", str(user_id or ""), source, + file_name, size_mb, pages)) + con.commit() + except Exception as e: + print(f"paywall log error: {e}") + + +def _mark_paywall_resolved(job_id, status): + """Отметить показ пейвола разрешённым (оплачен / по подписке). Если записи нет + (напр. холд пришёл из бота и не логировался) — создаём из строки FILES.""" + try: + with sqlite3.connect(DB_PATH, timeout=10) as con: + cur = con.execute("UPDATE WEB_PAYWALL_EVENTS SET status=? WHERE job_id=?", (status, job_id)) + if cur.rowcount == 0: + r = con.execute( + "SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS nm, " + "source_size_mb, COALESCE(NULLIF(pages_total,0), num_pages, 0) AS pg, user_id " + "FROM FILES WHERE name_or_f=?", (job_id,)).fetchone() + if r: + src = "сайт" if str(r[5]) == str(WEB_USER_ID) else "бот" + acc = "" if src == "сайт" else f"tg:{r[5]}" + con.execute( + "INSERT OR IGNORE INTO WEB_PAYWALL_EVENTS " + "(job_id, ts, account, user_id, source, file_name, size_mb, pages, status) " + "VALUES (?,?,?,?,?,?,?,?,?)", + (job_id, time.time(), acc, str(r[5]), src, r[2], r[3], r[4], status)) + con.commit() + except Exception as e: + print(f"paywall resolve log error: {e}") + + _FUNNEL_EVENTS = {"paywall_shown", "pay_click"} @@ -3602,6 +3697,13 @@ def _admin_collect_stats() -> dict: "SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, " "source_size_mb, user_id FROM FILES WHERE url_res_f='awaiting_payment' " "ORDER BY rowid DESC LIMIT 50")] + # Показы пейвола: кто и какой файл (журнал WEB_PAYWALL_EVENTS) + try: + out["paywall_events"] = [dict(r) for r in con.execute( + "SELECT job_id, ts, account, user_id, source, file_name, size_mb, pages, status " + "FROM WEB_PAYWALL_EVENTS ORDER BY ts DESC LIMIT 100")] + except Exception: + out["paywall_events"] = [] # Документы cols = {r[1] for r in con.execute("PRAGMA table_info(FILES)")} pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
ДатаЗаходыУник.ФайловСтраницСр/стрИсходникРезультат
'+size(h.source_size_mb)+''+esc(who)+'
Нет файлов, ожидающих оплаты
'+dts(e.ts)+''+esc(who)+''+esc(e.file_name)+ + ''+size(e.size_mb)+''+(e.pages ? e.pages : '—')+ + ''+esc(e.source||'—')+''+esc(st[1])+'
Пока не было показов пейвола