hku-class/backend/app/services/notification_service.py
2026-04-12 18:15:38 +08:00

133 lines
3.8 KiB
Python

import asyncio
import logging
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import Notification, User
from app.services.email_service import send_class_notification_email
logger = logging.getLogger(__name__)
async def create_notification(
db: AsyncSession,
user_id: int,
type: str,
title: str,
content: str | None = None,
related_id: int | None = None,
) -> Notification:
notification = Notification(
user_id=user_id,
type=type,
title=title,
content=content,
related_id=related_id,
)
db.add(notification)
await db.commit()
await db.refresh(notification)
return notification
async def create_notifications_for_class(
db: AsyncSession,
class_id: int,
type: str,
title: str,
content: str | None = None,
related_id: int | None = None,
exclude_user_id: int | None = None,
email_subject: str | None = None,
email_body: str | None = None,
email_action_path: str | None = None,
):
"""Create in-app notifications + send email for all approved users in a class."""
result = await db.execute(
select(User.id, User.email).where(
User.class_id == class_id,
User.status == "approved",
)
)
rows = result.all()
emails: list[str] = []
for uid, email in rows:
notification = Notification(
user_id=uid,
type=type,
title=title,
content=content,
related_id=related_id,
)
db.add(notification)
emails.append(email)
await db.commit()
# Send email notification in background (fire-and-forget)
if email_subject and emails:
from app.config import settings
action_url = f"{settings.frontend_url}{email_action_path}" if email_action_path else None
asyncio.create_task(_safe_send_emails(emails, email_subject, title, email_body or content or "", action_url))
async def _safe_send_emails(
emails: list[str], subject: str, title: str, body: str, action_url: str | None
):
"""Fire-and-forget email sending with error logging."""
try:
await send_class_notification_email(emails, subject, title, body, action_url)
except Exception as e:
logger.error(f"Failed to send class notification emails: {e}")
async def list_notifications(
db: AsyncSession, user_id: int, page: int = 1, page_size: int = 20
) -> tuple[list[Notification], int]:
total_result = await db.execute(
select(func.count(Notification.id)).where(Notification.user_id == user_id)
)
total = total_result.scalar() or 0
result = await db.execute(
select(Notification)
.where(Notification.user_id == user_id)
.order_by(Notification.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
notifications = list(result.scalars().all())
return notifications, total
async def get_unread_count(db: AsyncSession, user_id: int) -> int:
result = await db.execute(
select(func.count(Notification.id)).where(
Notification.user_id == user_id,
Notification.is_read == False,
)
)
return result.scalar() or 0
async def mark_as_read(db: AsyncSession, notification_id: int, user_id: int) -> bool:
result = await db.execute(
update(Notification)
.where(Notification.id == notification_id, Notification.user_id == user_id)
.values(is_read=True)
)
await db.commit()
return result.rowcount > 0
async def mark_all_as_read(db: AsyncSession, user_id: int) -> int:
result = await db.execute(
update(Notification)
.where(Notification.user_id == user_id, Notification.is_read == False)
.values(is_read=True)
)
await db.commit()
return result.rowcount