Админка: журнал «Показы пейвола» — кто и на каком файле упёрся
Таблица 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:
102
app/main.py
102
app/main.py
@@ -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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user