v2.1: 新增通知提醒功能和安全增强
功能: - 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知 - 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章 - 定时任务: APScheduler 每小时检查即将到期的还款 - 邮件通知: SMTP 发送还款提醒和安全警告 - 异常登录通知: 3/5/10次失败触发网页和邮件通知 安全: - 登录安全告警: 异常登录次数达到阈值时发送通知 - 密码重置链接: 一次性使用, 5分钟有效期 - 登录频率限制: IP级和账户级限制 技术: - 后端: FastAPI + SQLAlchemy + PostgreSQL - 前端: 纯 HTML/CSS/JS - 部署: Docker Compose (新增 build 配置)
This commit is contained in:
105
backend/app/services/scheduler.py
Normal file
105
backend/app/services/scheduler.py
Normal file
@@ -0,0 +1,105 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user