Files
replypier/backend/app/api/notification.py
T
WangShen007 83a16ed07d feat(modules): 帮助中心、工单、工作流与多渠道接入
补齐 FAQ、帮助中心、工单、可视化工作流及 Telegram/WhatsApp/Slack 等渠道模块。
2026-10-17 22:05:00 +08:00

280 lines
9.1 KiB
Python

"""
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.models.user import User
from app.core.auth import get_current_user
from app.models.notification import Notification
from app.models.schemas.notification import (
NotificationResponse,
NotificationSettingsOut,
NotificationSettingsUpdate,
)
from app.repositories.notification_settings import UserNotificationSettingsRepository
from app.core.logger import get_logger
from app.services.user import send_fcm_notification
from app.core.params import MAX_DB_INT, DbIntId
router = APIRouter()
logger = get_logger(__name__)
@router.get("", response_model=List[NotificationResponse])
async def list_notifications(
skip: int = Query(0, ge=0, le=MAX_DB_INT),
limit: int = Query(50, ge=1, le=500),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get user's notifications with pagination"""
try:
notifications = db.query(Notification)\
.filter(Notification.user_id == current_user.id)\
.order_by(Notification.created_at.desc())\
.offset(skip)\
.limit(limit)\
.all()
return notifications
except Exception as e:
logger.error(f"Error fetching notifications: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to fetch notifications"
)
@router.patch("/{notification_id}/read")
async def mark_as_read(
notification_id: DbIntId,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Mark a notification as read"""
try:
notification = db.query(Notification)\
.filter(
Notification.id == notification_id,
Notification.user_id == current_user.id
).first()
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
notification.is_read = True
db.commit()
return {"message": "Notification marked as read"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error marking notification as read: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update notification"
)
@router.post("/read-all")
async def mark_all_as_read(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Mark every unread notification as read, in one statement.
The client used to loop over the notifications it had fetched and PATCH each
one. That page is 50 rows, so anyone with more unread than that could never
clear the badge however often they tapped "Mark all read" — and it cost 50
round trips to not fix it.
"""
try:
updated = db.query(Notification)\
.filter(
Notification.user_id == current_user.id,
Notification.is_read == False
).update({Notification.is_read: True}, synchronize_session=False)
db.commit()
return {"message": "Notifications marked as read", "updated": updated}
except Exception as e:
db.rollback()
logger.error(f"Error marking all notifications as read: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update notifications"
)
@router.delete("/{notification_id}")
async def delete_notification(
notification_id: DbIntId,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Delete one notification.
Scoped to the caller: another user's id must 404 rather than delete.
"""
try:
notification = db.query(Notification)\
.filter(
Notification.id == notification_id,
Notification.user_id == current_user.id
).first()
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
db.delete(notification)
db.commit()
return {"message": "Notification deleted"}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Error deleting notification: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete notification"
)
@router.delete("")
async def clear_notifications(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Delete all of the caller's notifications."""
try:
deleted = db.query(Notification)\
.filter(Notification.user_id == current_user.id)\
.delete(synchronize_session=False)
db.commit()
return {"message": "Notifications cleared", "deleted": deleted}
except Exception as e:
db.rollback()
logger.error(f"Error clearing notifications: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to clear notifications"
)
@router.get("/unread-count")
async def get_unread_count(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get count of unread notifications"""
try:
count = db.query(Notification)\
.filter(
Notification.user_id == current_user.id,
Notification.is_read == False
).count()
return {"count": count}
except Exception as e:
logger.error(f"Error getting unread count: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get unread count"
)
@router.get("/settings", response_model=NotificationSettingsOut)
async def get_notification_settings(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get the current user's chat notification preferences"""
try:
return UserNotificationSettingsRepository(db).get_or_create(current_user.id)
except Exception as e:
logger.error(f"Error fetching notification settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to fetch notification settings"
)
@router.put("/settings", response_model=NotificationSettingsOut)
async def update_notification_settings(
payload: NotificationSettingsUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update the current user's chat notification preferences.
Always scoped to the caller — there is no way to address another user's
settings, so no permission check beyond authentication is needed.
"""
try:
settings = UserNotificationSettingsRepository(db).get_or_create(current_user.id)
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(settings, key, value)
db.commit()
db.refresh(settings)
return settings
except Exception as e:
logger.error(f"Error updating notification settings: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update notification settings"
)
@router.post("/test")
async def send_test_notification(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Send a test notification to the current user"""
try:
# Create test notification
notification = Notification(
user_id=current_user.id,
type="CHAT",
title="Test Notification",
message="This is a test notification from ChatterMate",
notification_metadata={"test": True}
)
db.add(notification)
db.commit()
# Send FCM notification
await send_fcm_notification(current_user.id, notification, db)
return {"message": "Test notification sent successfully"}
except Exception as e:
logger.error(f"Error sending test notification: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send test notification"
)