稳定性: 签到失败10分钟自动重试+前端显示Cookie剩余天数+每天9点Cookie过期预警

This commit is contained in:
2026-04-17 10:18:44 +08:00
parent 0068f22737
commit ecb1738d7b
3 changed files with 149 additions and 2 deletions

View File

@@ -148,7 +148,26 @@ async def get_account(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
account = await _get_owned_account(account_id, user, 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 ---- # ---- UPDATE ----

View File

@@ -254,16 +254,58 @@ def run_signin(task_id: str, account_id: str, cron_expr: str = ""):
elapsed = _time.time() - start elapsed = _time.time() - start
result["elapsed_seconds"] = round(elapsed, 1) result["elapsed_seconds"] = round(elapsed, 1)
logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}") logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}")
# 签到完成后立即推送通知
_push_signin_result(account_id, result, elapsed) _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: except asyncio.TimeoutError:
elapsed = _time.time() - start elapsed = _time.time() - start
logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}") logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}")
_push_signin_result(account_id, {"status": "timeout"}, elapsed) _push_signin_result(account_id, {"status": "timeout"}, elapsed)
_schedule_retry(task_id, account_id)
except Exception as e: except Exception as e:
elapsed = _time.time() - start elapsed = _time.time() - start
logger.error(f"❌ 签到失败: task={task_id}, error={e}") logger.error(f"❌ 签到失败: task={task_id}, error={e}")
_push_signin_result(account_id, {"status": "error", "reason": str(e)}, elapsed) _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): 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]}") 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(): def cleanup_old_signin_logs():
@@ -1030,6 +1133,15 @@ if __name__ == "__main__":
misfire_grace_time=3600, 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 已调度则跳过) # 每天定时推送签到日报到 Webhook如果 load_config_from_db 已调度则跳过)
if WEBHOOK_URL and not scheduler.get_job("daily_report"): if WEBHOOK_URL and not scheduler.get_job("daily_report"):
scheduler.add_job( scheduler.add_job(

View File

@@ -74,6 +74,22 @@
<tr><td>备注</td><td>{{ account.remark or '-' }}</td></tr> <tr><td>备注</td><td>{{ account.remark or '-' }}</td></tr>
<tr><td>添加时间</td><td>{{ account.created_at[:10] }}</td></tr> <tr><td>添加时间</td><td>{{ account.created_at[:10] }}</td></tr>
<tr><td>上次检查</td><td>{{ account.last_checked_at[:10] if account.last_checked_at else '-' }}</td></tr> <tr><td>上次检查</td><td>{{ account.last_checked_at[:10] if account.last_checked_at else '-' }}</td></tr>
<tr>
<td>Cookie 有效期</td>
<td>
{% if account.cookie_expire_days is defined and account.cookie_expire_days is not none %}
{% if account.cookie_expire_days <= 3 %}
<span class="badge badge-danger">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
{% elif account.cookie_expire_days <= 7 %}
<span class="badge badge-warning">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
{% else %}
<span class="badge badge-success">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
{% endif %}
{% else %}
<span style="color:#94a3b8;">未知</span>
{% endif %}
</td>
</tr>
</table> </table>
</div> </div>
<div class="card"> <div class="card">