Админка: журнал «Показы пейвола» — кто и на каком файле упёрся

Таблица WEB_PAYWALL_EVENTS: пишется при показе пейвола (_finalize_submit),
статус обновляется при оплате (_db_mark_payment_paid) и освобождении по
подписке (/api/held/release). Разовый бэкфилл текущих awaiting_payment (веб+бот).
Раздел в админке: дата, пользователь, файл, размер, стр., источник, статус.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
berkio_admin_gitea
2026-07-10 11:15:48 +00:00
parent b2fea7854d
commit 687d54ef7c

View File

@@ -343,6 +343,40 @@ def _db_ensure_auth_table():
PRIMARY KEY (day, event) 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. # Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub.
con.execute(""" con.execute("""
CREATE TABLE IF NOT EXISTS WEB_PAYMENTS ( CREATE TABLE IF NOT EXISTS WEB_PAYMENTS (
@@ -1074,6 +1108,7 @@ def _finalize_submit(job_id, file_path, original_name, stitching, title,
if held: if held:
# Задание не идёт в очередь бота, пока не оплатят (файл 99₽ или подписка) # Задание не идёт в очередь бота, пока не оплатят (файл 99₽ или подписка)
_db_set_url_res_f(job_id, "awaiting_payment") _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, return {"job_id": job_id, "requires_payment": True,
"file_price": FILE_PRICE_RUB, "sub_price": SUB_PRICE_RUB} "file_price": FILE_PRICE_RUB, "sub_price": SUB_PRICE_RUB}
return {"job_id": job_id} 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") job_id = extra.get("job_id")
if job_id and _db_get_url_res_f(job_id) == "awaiting_payment": if job_id and _db_get_url_res_f(job_id) == "awaiting_payment":
_db_set_url_res_f(job_id, "-") _db_set_url_res_f(job_id, "-")
_mark_paywall_resolved(job_id, "оплачен")
# Воронка: успешная оплата пейвольных покупок (файл/подписка) # Воронка: успешная оплата пейвольных покупок (файл/подписка)
if purpose in ("file", "sub"): if purpose in ("file", "sub"):
_bump_funnel("pay_success") _bump_funnel("pay_success")
@@ -2495,6 +2531,8 @@ async def held_release(payload: dict, request: Request):
return True return True
ok = await asyncio.to_thread(_release) ok = await asyncio.to_thread(_release)
if ok:
await asyncio.to_thread(_mark_paywall_resolved, job_id, "по подписке")
return {"released": bool(ok)} return {"released": bool(ok)}
@@ -3140,6 +3178,12 @@ _ADMIN_HTML = """<!doctype html>
<tbody id="t-held"></tbody></table></div> <tbody id="t-held"></tbody></table></div>
</section> </section>
<section>
<h2>Показы пейвола <span class="muted" id="pw-note"></span></h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th>Пользователь</th><th>Файл</th><th class="num">Размер</th><th class="num">Стр.</th><th>Источник</th><th>Статус</th></tr></thead>
<tbody id="t-paywall"></tbody></table></div>
</section>
<section> <section>
<h2>По дням</h2> <h2>По дням</h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead> <div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead>
@@ -3304,6 +3348,18 @@ _ADMIN_HTML = """<!doctype html>
'</td><td class="num">'+size(h.source_size_mb)+'</td><td>'+esc(who)+'</td></tr>'; '</td><td class="num">'+size(h.source_size_mb)+'</td><td>'+esc(who)+'</td></tr>';
}).join('') : '<tr><td colspan="4" class="muted">Нет файлов, ожидающих оплаты</td></tr>'; }).join('') : '<tr><td colspan="4" class="muted">Нет файлов, ожидающих оплаты</td></tr>';
// Показы пейвола (журнал: кто + файл + статус)
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 '<tr><td>'+dts(e.ts)+'</td><td>'+esc(who)+'</td><td class="fname" title="'+esc(e.file_name)+'">'+esc(e.file_name)+
'</td><td class="num">'+size(e.size_mb)+'</td><td class="num">'+(e.pages ? e.pages : '')+
'</td><td>'+esc(e.source||'')+'</td><td><span class="pill '+st[0]+'">'+esc(st[1])+'</span></td></tr>';
}).join('') : '<tr><td colspan="7" class="muted">Пока не было показов пейвола</td></tr>';
// По дням (свежие сверху) // По дням (свежие сверху)
var daily = (d.daily||[]).slice().reverse(); var daily = (d.daily||[]).slice().reverse();
document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){ document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){
@@ -3464,6 +3520,45 @@ def _bump_funnel(event: str):
pass 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"} _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, " "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' " "source_size_mb, user_id FROM FILES WHERE url_res_f='awaiting_payment' "
"ORDER BY rowid DESC LIMIT 50")] "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)")} 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)" pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"