feat: 续费提醒告警(Telegram/邮件多渠道+续费检查+每日定时+前端控制)

This commit is contained in:
gouki
2026-08-02 18:55:18 +00:00
parent 3ef84180b4
commit 99349875ae
10 changed files with 327 additions and 2 deletions
+75
View File
@@ -0,0 +1,75 @@
"""续费检查服务
检查即将到期的资产,生成提醒消息并通过通知渠道发送。
"""
from datetime import date
from sqlmodel import Session, select
from app.core.config import settings
from app.models.asset import Asset, AssetStatus
from app.services import notification_service
# 已注销的资产不参与续费提醒
_SKIP_STATUS = {AssetStatus.CANCELLED}
def get_expiring_assets(session: Session, threshold_days: int) -> list:
"""返回 threshold_days 天内到期的资产列表 [(asset, days), ...],按剩余天数升序"""
today = date.today()
assets = session.exec(select(Asset).where(Asset.expiry_date.is_not(None))).all()
expiring = []
for asset in assets:
if asset.status in _SKIP_STATUS:
continue
days = (asset.expiry_date - today).days
if 0 <= days <= threshold_days:
expiring.append((asset, days))
expiring.sort(key=lambda x: x[1])
return expiring
def build_renewal_message(expiring: list, threshold_days: int) -> str:
"""生成续费提醒消息(纯文本,兼容 Telegram/邮件)"""
if not expiring:
return ""
lines = [f"⏰ 资产续费提醒({threshold_days} 天内到期,共 {len(expiring)} 项)", ""]
for asset, days in expiring:
tag = "(已过期)" if days == 0 else f"{days} 天后)"
lines.append(f"{asset.name} [{asset.provider}] {tag} 到期 {asset.expiry_date}")
return "\n".join(lines)
def check_renewals(session: Session, threshold_days: int = None, send: bool = True) -> dict:
"""检查续费并(可选)发送提醒
threshold_days: 提醒阈值天数,默认取配置 RENEWAL_THRESHOLD_DAYS
send: 是否实际发送通知(False 时仅返回预览)
"""
if threshold_days is None:
threshold_days = settings.RENEWAL_THRESHOLD_DAYS
expiring = get_expiring_assets(session, threshold_days)
message = build_renewal_message(expiring, threshold_days)
result = {
"threshold_days": threshold_days,
"expiring_count": len(expiring),
"expiring": [
{
"name": asset.name,
"provider": asset.provider,
"days": days,
"expiry_date": str(asset.expiry_date),
}
for asset, days in expiring
],
"message": message,
}
if send and message:
result["notifications"] = notification_service.notify(message)
else:
result["notifications"] = []
return result