稳定性: 签到失败10分钟自动重试+前端显示Cookie剩余天数+每天9点Cookie过期预警
This commit is contained in:
@@ -148,7 +148,26 @@ async def get_account(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
account = await _get_owned_account(account_id, user, db)
|
||||
return success_response(_account_to_dict(account), "Account retrieved")
|
||||
data = _account_to_dict(account)
|
||||
|
||||
# 解析 Cookie 中的 ALF 字段获取过期时间
|
||||
try:
|
||||
key = _encryption_key()
|
||||
cookie_str = decrypt_cookie(account.encrypted_cookies, account.iv, key)
|
||||
for pair in cookie_str.split(";"):
|
||||
pair = pair.strip()
|
||||
if pair.startswith("ALF="):
|
||||
alf = pair.split("=", 1)[1].strip()
|
||||
if alf.isdigit():
|
||||
from datetime import datetime as _dt
|
||||
expire_dt = _dt.fromtimestamp(int(alf))
|
||||
data["cookie_expire_date"] = expire_dt.strftime("%Y-%m-%d")
|
||||
data["cookie_expire_days"] = (expire_dt - _dt.now()).days
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return success_response(data, "Account retrieved")
|
||||
|
||||
|
||||
# ---- UPDATE ----
|
||||
|
||||
@@ -254,16 +254,58 @@ def run_signin(task_id: str, account_id: str, cron_expr: str = ""):
|
||||
elapsed = _time.time() - start
|
||||
result["elapsed_seconds"] = round(elapsed, 1)
|
||||
logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}")
|
||||
# 签到完成后立即推送通知
|
||||
_push_signin_result(account_id, result, elapsed)
|
||||
|
||||
# 签到失败(获取超话失败)时,10 分钟后自动重试一次
|
||||
signed = result.get("signed", 0)
|
||||
total = result.get("total", 0)
|
||||
msg = result.get("message", "")
|
||||
if signed == 0 and total == 0 and "no topics" in msg:
|
||||
_schedule_retry(task_id, account_id)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = _time.time() - start
|
||||
logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}")
|
||||
_push_signin_result(account_id, {"status": "timeout"}, elapsed)
|
||||
_schedule_retry(task_id, account_id)
|
||||
except Exception as e:
|
||||
elapsed = _time.time() - start
|
||||
logger.error(f"❌ 签到失败: task={task_id}, error={e}")
|
||||
_push_signin_result(account_id, {"status": "error", "reason": str(e)}, elapsed)
|
||||
_schedule_retry(task_id, account_id)
|
||||
|
||||
|
||||
def _schedule_retry(task_id: str, account_id: str):
|
||||
"""10 分钟后自动重试一次签到。"""
|
||||
retry_id = f"retry_{task_id}_{int(_time.time())}"
|
||||
try:
|
||||
scheduler.add_job(
|
||||
_run_retry,
|
||||
trigger="date",
|
||||
run_date=datetime.now() + timedelta(minutes=10),
|
||||
id=retry_id,
|
||||
args=[task_id, account_id],
|
||||
misfire_grace_time=300,
|
||||
)
|
||||
logger.info(f"🔄 已安排 10 分钟后重试: task={task_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"安排重试失败: {e}")
|
||||
|
||||
|
||||
def _run_retry(task_id: str, account_id: str):
|
||||
"""重试签到(不再递归重试)。"""
|
||||
logger.info(f"🔄 重试签到: task={task_id}, account={account_id}")
|
||||
start = _time.time()
|
||||
try:
|
||||
result = _run_async(asyncio.wait_for(_async_do_signin(account_id, ""), timeout=300))
|
||||
elapsed = _time.time() - start
|
||||
result["elapsed_seconds"] = round(elapsed, 1)
|
||||
logger.info(f"🔄 重试完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}")
|
||||
_push_signin_result(account_id, result, elapsed)
|
||||
except Exception as e:
|
||||
elapsed = _time.time() - start
|
||||
logger.error(f"🔄 重试失败: task={task_id}, error={e}")
|
||||
_push_signin_result(account_id, {"status": "error", "reason": f"重试失败: {e}"}, elapsed)
|
||||
|
||||
|
||||
def _push_signin_result(account_id: str, result: dict, elapsed: float):
|
||||
@@ -891,6 +933,67 @@ def _send_webhook(content: str):
|
||||
logger.warning(f"Webhook 业务异常: {resp.text[:300]}")
|
||||
|
||||
|
||||
# =============== Cookie 过期预警 ===============
|
||||
|
||||
def check_cookie_expiry():
|
||||
"""检查所有账号的 Cookie 过期时间,剩余 ≤5 天时推送提醒。"""
|
||||
if not WEBHOOK_URL:
|
||||
return
|
||||
try:
|
||||
warnings = _run_async(_check_cookie_expiry_async())
|
||||
if warnings:
|
||||
lines = ["🍪 Cookie 过期预警", ""]
|
||||
for w in warnings:
|
||||
lines.append(f" 🔴 {w['name']}: {w['expire']} (剩 {w['remain']} 天)")
|
||||
lines.append("")
|
||||
lines.append("请尽快重新扫码续期")
|
||||
_send_webhook("\n".join(lines))
|
||||
logger.info(f"🍪 Cookie 过期预警已推送: {len(warnings)} 个账号")
|
||||
except Exception as e:
|
||||
logger.error(f"Cookie 过期检查失败: {e}")
|
||||
|
||||
|
||||
async def _check_cookie_expiry_async() -> list:
|
||||
from sqlalchemy import select
|
||||
from shared.models.account import Account
|
||||
from shared.crypto import decrypt_cookie, derive_key
|
||||
|
||||
SessionFactory, eng = _make_session()
|
||||
warnings = []
|
||||
try:
|
||||
async with SessionFactory() as session:
|
||||
result = await session.execute(
|
||||
select(Account.remark, Account.weibo_user_id,
|
||||
Account.encrypted_cookies, Account.iv)
|
||||
.where(Account.status.in_(["active", "pending"]))
|
||||
)
|
||||
key = derive_key(shared_settings.COOKIE_ENCRYPTION_KEY)
|
||||
now = datetime.now()
|
||||
for remark, uid, enc, iv in result.all():
|
||||
name = remark or uid
|
||||
try:
|
||||
cookie_str = decrypt_cookie(enc, iv, key)
|
||||
for pair in cookie_str.split(";"):
|
||||
pair = pair.strip()
|
||||
if pair.startswith("ALF="):
|
||||
alf = pair.split("=", 1)[1].strip()
|
||||
if alf.isdigit():
|
||||
expire_dt = datetime.fromtimestamp(int(alf))
|
||||
remain = (expire_dt - now).days
|
||||
if remain <= 5:
|
||||
warnings.append({
|
||||
"name": name,
|
||||
"expire": expire_dt.strftime("%m-%d"),
|
||||
"remain": remain,
|
||||
})
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
await eng.dispose()
|
||||
return warnings
|
||||
|
||||
|
||||
# =============== 日志清理 ===============
|
||||
|
||||
def cleanup_old_signin_logs():
|
||||
@@ -1030,6 +1133,15 @@ if __name__ == "__main__":
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
|
||||
# 每天早上 9 点检查 Cookie 过期预警
|
||||
scheduler.add_job(
|
||||
check_cookie_expiry,
|
||||
trigger=CronTrigger(hour=9, minute=0, timezone="Asia/Shanghai"),
|
||||
id="check_cookie_expiry",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
|
||||
# 每天定时推送签到日报到 Webhook(如果 load_config_from_db 已调度则跳过)
|
||||
if WEBHOOK_URL and not scheduler.get_job("daily_report"):
|
||||
scheduler.add_job(
|
||||
|
||||
Reference in New Issue
Block a user