Files
vps-manager/app/services/renewal_service.py
T
gouki cbb32ea3b0 fix(expiry): 已过期资产不再从到期统计中消失,总览页改为到期日历
get_expiring/get_overview 去掉 expiry_date>=today 下界(仅排除已注销)并新增 overdue_count;renewal_service 续费提醒纳入已过期、文案区分已过期N天/今天到期/N天后;dashboard 用到期日历逐条显示具体到期日与倒计时;api.js 新增 Fmt.countdownText 统一文案。
2026-09-18 08:41:21 +00:00

91 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""续费检查服务
检查即将到期的资产,生成提醒消息并通过通知渠道发送。
"""
from datetime import date, timedelta
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), ...],按剩余天数升序
到期日不设下界:过了续费日仍未注销/续费的资产,提醒不能就此消失。
性能优化:日期范围与状态过滤均下推到 SQL 层。
"""
today = date.today()
deadline = today + timedelta(days=threshold_days)
assets = session.exec(
select(Asset).where(
Asset.expiry_date.is_not(None),
Asset.expiry_date <= deadline,
Asset.status.notin_(_SKIP_STATUS),
)
).all()
expiring = [(asset, (asset.expiry_date - today).days) for asset in assets]
expiring.sort(key=lambda x: x[1])
return expiring
def build_renewal_message(expiring: list, threshold_days: int) -> str:
"""生成续费提醒消息(纯文本,兼容 Telegram/邮件)"""
if not expiring:
return ""
overdue = sum(1 for _, d in expiring if d < 0)
if overdue:
head = f"⏰ 资产续费提醒(已过期 {overdue} 项 + {threshold_days} 天内到期 {len(expiring) - overdue} 项)"
else:
head = f"⏰ 资产续费提醒({threshold_days} 天内到期,共 {len(expiring)} 项)"
lines = [head, ""]
for asset, days in expiring:
if days < 0:
tag = f"(已过期 {-days} 天)"
elif days == 0:
tag = "(今天到期)"
else:
tag = 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