Files
debt-manager/backend/app/api/notifications.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

92 lines
2.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, update
from app.core.database import get_db
from app.core.deps import get_current_user
from app.models.models import User, Notification
from app.schemas.schemas import NotificationResponse, NotificationListResponse
router = APIRouter(prefix="/notifications", tags=["通知管理"])
@router.get("", response_model=NotificationListResponse)
async def list_notifications(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
unread_only: bool = False,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
q = select(Notification).where(Notification.user_id == user.id)
if unread_only:
q = q.where(Notification.is_read == 0)
total_q = select(func.count(Notification.id)).where(Notification.user_id == user.id)
total = (await db.execute(total_q)).scalar()
q = q.order_by(Notification.created_at.desc()).offset((page - 1) * size).limit(size)
items = (await db.execute(q)).scalars().all()
unread = (await db.execute(
select(func.count(Notification.id)).where(
Notification.user_id == user.id, Notification.is_read == 0
)
)).scalar()
return NotificationListResponse(
items=[NotificationResponse.model_validate(n) for n in items],
total=total,
unread=unread,
)
@router.put("/{notification_id}/read")
async def mark_read(
notification_id: int,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Notification).where(
Notification.id == notification_id,
Notification.user_id == user.id
)
)
n = result.scalar_one_or_none()
if not n:
raise HTTPException(status_code=404, detail="通知不存在")
n.is_read = 1
await db.commit()
return {"message": "已标记已读"}
@router.put("/read-all")
async def mark_all_read(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
await db.execute(
update(Notification).where(
Notification.user_id == user.id,
Notification.is_read == 0
).values(is_read=1)
)
await db.commit()
return {"message": "全部已读"}
@router.delete("/{notification_id}")
async def delete_notification(
notification_id: int,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Notification).where(
Notification.id == notification_id,
Notification.user_id == user.id
)
)
n = result.scalar_one_or_none()
if not n:
raise HTTPException(status_code=404, detail="通知不存在")
await db.delete(n)
await db.commit()
return {"message": "已删除"}