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