Files
debt-manager/backend/app/services/scheduler.py
OP ffca168dbe v2.1: 新增通知提醒功能和安全增强
功能:
- 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知
- 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章
- 定时任务: APScheduler 每小时检查即将到期的还款
- 邮件通知: SMTP 发送还款提醒和安全警告
- 异常登录通知: 3/5/10次失败触发网页和邮件通知

安全:
- 登录安全告警: 异常登录次数达到阈值时发送通知
- 密码重置链接: 一次性使用, 5分钟有效期
- 登录频率限制: IP级和账户级限制

技术:
- 后端: FastAPI + SQLAlchemy + PostgreSQL
- 前端: 纯 HTML/CSS/JS
- 部署: Docker Compose (新增 build 配置)
2026-06-28 18:06:56 +08:00

106 lines
3.8 KiB
Python

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from sqlalchemy import select, and_
from datetime import datetime, timedelta
from app.core.database import async_session
from app.core.config import get_settings
from app.models.models import (
Debt, Notification, NotificationType, User, DebtStatus
)
from app.services.notification_service import create_notification
from app.services.email_service import send_email, build_repayment_reminder_html
settings = get_settings()
scheduler = AsyncIOScheduler()
async def check_upcoming_repayments():
async with async_session() as db:
now = datetime.utcnow()
reminder_days = settings.REMINDER_DAYS_BEFORE
check_window = now + timedelta(days=reminder_days)
result = await db.execute(
select(Debt).where(
and_(
Debt.status == DebtStatus.ACTIVE.value,
Debt.schedule_data.isnot(None),
)
)
)
debts = result.scalars().all()
for debt in debts:
schedule = debt.schedule_data or []
paid_periods = debt.paid_periods or []
user_result = await db.execute(select(User).where(User.id == debt.user_id))
user = user_result.scalar_one_or_none()
if not user:
continue
for item in schedule:
p = item.get("p")
if p in paid_periods:
continue
try:
due_date = datetime.strptime(item["date"], "%Y-%m-%d")
except (ValueError, KeyError):
continue
if due_date > check_window:
continue
is_overdue = due_date < now
amount_yuan = item.get("pay", 0)
days_left = (due_date - now).days
if is_overdue:
ntype = NotificationType.REPAYMENT_OVERDUE.value
title = f"逾期提醒:{debt.creditor_name}{p}期还款已逾期"
msg = f"您在 {debt.creditor_name} 的第{p}期 ¥{amount_yuan:,.2f} 还款已于 {item['date']} 逾期,请尽快处理。"
else:
ntype = NotificationType.REPAYMENT_DUE.value
title = f"还款提醒:{debt.creditor_name}{p}期将在 {days_left} 天后到期"
msg = f"您在 {debt.creditor_name} 的第{p}期 ¥{amount_yuan:,.2f} 将于 {item['date']} 到期,请按时还款。"
existing = await db.execute(
select(Notification).where(
and_(
Notification.user_id == user.id,
Notification.related_debt_id == debt.id,
Notification.title == title,
)
)
)
if existing.scalar_one_or_none():
continue
await create_notification(
db, user.id, title, msg, ntype,
related_debt_id=debt.id,
)
if user.email and "@" in user.email:
html = build_repayment_reminder_html(
user.nickname or user.email,
debt.creditor_name,
amount_yuan,
item["date"],
days_left,
is_overdue,
)
await send_email(user.email, title, html)
await db.commit()
def start_scheduler():
scheduler.add_job(
check_upcoming_repayments,
"interval",
minutes=settings.NOTIFICATION_CHECK_INTERVAL_MINUTES,
id="repayment_check",
replace_existing=True,
)
scheduler.start()