容错策略:核对失败/无可用通道时标记「未核对」并继续按本地数据提醒, 避免因核对链路故障漏掉真正到期的资产;已停止资产仅在 auto_renew=true 时提醒(停止实例不等于停止扣费)。新增 WHMCS 面板只读适配器, 用于 TotHost 这类没有开放 API 的平台。
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""定时检查续费并发送提醒(供 systemd timer 调用)
|
||
|
||
用法:
|
||
python scripts/check_renewals.py [threshold_days] [--dry-run] [--no-verify]
|
||
|
||
默认发送通知;--dry-run 只打印、不发送,便于核对平台真实状态。
|
||
依赖 .env 中的通知渠道配置与账号登录凭据(TELEGRAM_*/SMTP_*、credentials)。
|
||
"""
|
||
|
||
import argparse
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 确保能导入 app 包(脚本位于 scripts/ 子目录)
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||
|
||
from sqlmodel import Session
|
||
|
||
from app.database import assets_engine, init_db
|
||
from app.services import renewal_service
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="检查资产续费并发送提醒")
|
||
parser.add_argument("threshold_days", nargs="?", type=int, default=None)
|
||
parser.add_argument("--dry-run", action="store_true", help="只打印,不发送通知")
|
||
parser.add_argument("--no-verify", action="store_true", help="跳过平台真实状态核对")
|
||
args = parser.parse_args()
|
||
|
||
init_db()
|
||
with Session(assets_engine) as session:
|
||
result = renewal_service.check_renewals(
|
||
session,
|
||
threshold_days=args.threshold_days,
|
||
send=not args.dry_run,
|
||
verify=not args.no_verify,
|
||
)
|
||
|
||
# days 可为负(已过期),汇总行拆开统计,单项文案复用 day_text(与提醒消息一致)
|
||
overdue = sum(1 for item in result["expiring"] if item["days"] < 0)
|
||
summary = (
|
||
f"[续费检查] 阈值 {result['threshold_days']} 天 | 已过期 {overdue} 项 | "
|
||
f"即将到期 {result['expiring_count'] - overdue} 项"
|
||
)
|
||
verification = result.get("verification")
|
||
if verification:
|
||
summary += (
|
||
f" | 已核对 {verification['verified']} 项"
|
||
f" | 未核对 {verification['unverified']} 项"
|
||
)
|
||
print(summary)
|
||
|
||
for item in result["expiring"]:
|
||
print(
|
||
f" - {item['name']} [{item['provider']}] "
|
||
f"{renewal_service.day_text(item['days'])}(到期 {item['expiry_date']})"
|
||
)
|
||
if item.get("verified") is True:
|
||
print(f" 已核对[{item['verify_source']}]: {item['verify_detail']}")
|
||
elif item.get("verified") is False:
|
||
print(f" 未核对: {item['verify_detail']}")
|
||
|
||
for n in result.get("notifications", []):
|
||
status = "成功" if n["ok"] else f"失败:{n['message']}"
|
||
print(f" 通知[{n['channel']}]: {status}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|