update
This commit is contained in:
parent
657a06778f
commit
85f6b7e42b
@ -10,7 +10,10 @@
|
||||
"Bash(npx shadcn@latest add:*)",
|
||||
"Bash(npm --prefix /Users/aaron/source_code/hku-icb-class/frontend run build)",
|
||||
"Bash(/Users/aaron/source_code/hku-icb-class/frontend/node_modules/.bin/tsc --noEmit --project /Users/aaron/source_code/hku-icb-class/frontend/tsconfig.json)",
|
||||
"Bash(test:*)"
|
||||
"Bash(test:*)",
|
||||
"Bash(/Users/aaron/source_code/hku-icb-class/backend/.venv/bin/python:*)",
|
||||
"Bash(.venv/bin/python:*)",
|
||||
"Bash(backend/.venv/bin/pip show:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ CH_COS_BASE_URL=https://hku-icb-1311994147.cos.ap-guangzhou.myqcloud.com
|
||||
# SMTP Email
|
||||
CH_SMTP_HOST=gz-smtp.qcloudmail.com
|
||||
CH_SMTP_PORT=465
|
||||
CH_SMTP_USER=noreply
|
||||
CH_SMTP_USER=noreply@hkuicb.info
|
||||
CH_SMTP_PASSWORD=hX2gqEPjaRhULXF69
|
||||
CH_SMTP_FROM_EMAIL=noreply@hkuicb.info
|
||||
CH_SMTP_FROM_NAME=HKU ICB Class Hub
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import require_role
|
||||
@ -76,11 +76,11 @@ async def get_resources(
|
||||
|
||||
@router.post("/", response_model=ResourceOut)
|
||||
async def upload_new_resource(
|
||||
title: str,
|
||||
category: str,
|
||||
file: UploadFile = File(...),
|
||||
description: str | None = None,
|
||||
class_id: int | None = None,
|
||||
title: str = Form(...),
|
||||
category: str = Form(...),
|
||||
description: str | None = Form(None),
|
||||
class_id: int | None = Form(None),
|
||||
user: User = Depends(require_role("super_admin", "class_admin")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
|
||||
@ -21,10 +21,15 @@ async def create_announcement(
|
||||
await db.commit()
|
||||
await db.refresh(announcement)
|
||||
|
||||
# Send notifications to class members
|
||||
# Send notifications + email to class members
|
||||
content_preview = (data.content[:100] + "...") if data.content and len(data.content) > 100 else (data.content or "")
|
||||
await create_notifications_for_class(
|
||||
db, class_id, "announcement", f"新公告: {data.title}",
|
||||
related_id=announcement.id, exclude_user_id=author_id,
|
||||
content=content_preview,
|
||||
related_id=announcement.id,
|
||||
email_subject=f"HKU ICB - 新公告: {data.title}",
|
||||
email_body=f"<p>{content_preview}</p>" if content_preview else None,
|
||||
email_action_path="/announcements",
|
||||
)
|
||||
|
||||
return announcement
|
||||
|
||||
@ -57,3 +57,39 @@ async def send_approval_notification(student_email: str, approved: bool):
|
||||
await send_email(
|
||||
student_email, f"HKU ICB: Registration {status_text.capitalize()}", html
|
||||
)
|
||||
|
||||
|
||||
async def send_class_notification_email(
|
||||
emails: list[str],
|
||||
subject: str,
|
||||
title: str,
|
||||
body: str,
|
||||
action_url: str | None = None,
|
||||
):
|
||||
"""Send a styled notification email to class members."""
|
||||
action_html = ""
|
||||
if action_url:
|
||||
action_html = f"""
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="{action_url}" style="display:inline-block;padding:10px 24px;background:#111827;color:#fff;border-radius:6px;text-decoration:none;font-size:14px;">
|
||||
查看详情
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
html = f"""
|
||||
<div style="max-width:600px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#1f2937;">
|
||||
<div style="padding:24px 0;border-bottom:2px solid #111827;">
|
||||
<h1 style="margin:0;font-size:20px;color:#111827;">HKU ICB</h1>
|
||||
</div>
|
||||
<div style="padding:24px 0;">
|
||||
<h2 style="margin:0 0 12px;font-size:18px;">{title}</h2>
|
||||
<div style="font-size:14px;line-height:1.6;color:#4b5563;">{body}</div>
|
||||
{action_html}
|
||||
</div>
|
||||
<div style="padding:16px 0;border-top:1px solid #e5e7eb;font-size:12px;color:#9ca3af;">
|
||||
此邮件由系统自动发送,请勿直接回复。
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
for email in emails:
|
||||
await send_email(email, subject, html)
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
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(
|
||||
@ -33,19 +39,21 @@ async def create_notifications_for_class(
|
||||
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 notifications for all approved users in a class."""
|
||||
"""Create in-app notifications + send email for all approved users in a class."""
|
||||
result = await db.execute(
|
||||
select(User.id).where(
|
||||
select(User.id, User.email).where(
|
||||
User.class_id == class_id,
|
||||
User.status == "approved",
|
||||
)
|
||||
)
|
||||
user_ids = [row[0] for row in result.all()]
|
||||
rows = result.all()
|
||||
|
||||
for uid in user_ids:
|
||||
if exclude_user_id and uid == exclude_user_id:
|
||||
continue
|
||||
emails: list[str] = []
|
||||
for uid, email in rows:
|
||||
notification = Notification(
|
||||
user_id=uid,
|
||||
type=type,
|
||||
@ -54,9 +62,26 @@ async def create_notifications_for_class(
|
||||
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
|
||||
|
||||
@ -19,10 +19,16 @@ async def create_schedule(
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
|
||||
# Send notifications to class members
|
||||
# Send notifications + email to class members
|
||||
time_str = data.start_time.strftime("%Y-%m-%d %H:%M")
|
||||
location_info = f" · {data.location}" if data.location else ""
|
||||
await create_notifications_for_class(
|
||||
db, class_id, "schedule", f"新排期: {data.title}",
|
||||
content=f"{time_str}{location_info}",
|
||||
related_id=item.id,
|
||||
email_subject=f"HKU ICB - 新排期: {data.title}",
|
||||
email_body=f"<p><strong>{data.title}</strong></p><p>时间: {time_str}{location_info}</p>",
|
||||
email_action_path="/schedule",
|
||||
)
|
||||
|
||||
return item
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.models import Timeline, User
|
||||
from app.schemas.timeline import TimelineCreate, TimelineUpdate
|
||||
from app.services.notification_service import create_notifications_for_class
|
||||
|
||||
|
||||
async def create_timeline(
|
||||
@ -21,6 +22,18 @@ async def create_timeline(
|
||||
db.add(post)
|
||||
await db.commit()
|
||||
await db.refresh(post)
|
||||
|
||||
# Send notifications + email to class members
|
||||
content_preview = (data.content[:100] + "...") if data.content and len(data.content) > 100 else (data.content or "")
|
||||
await create_notifications_for_class(
|
||||
db, class_id, "timeline", f"新大事件: {data.title}",
|
||||
content=content_preview,
|
||||
related_id=post.id,
|
||||
email_subject=f"HKU ICB - 新大事件: {data.title}",
|
||||
email_body=f"<p>{content_preview}</p>" if content_preview else None,
|
||||
email_action_path="/timeline",
|
||||
)
|
||||
|
||||
return post
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user