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