update
This commit is contained in:
parent
657a06778f
commit
85f6b7e42b
@ -10,7 +10,10 @@
|
|||||||
"Bash(npx shadcn@latest add:*)",
|
"Bash(npx shadcn@latest add:*)",
|
||||||
"Bash(npm --prefix /Users/aaron/source_code/hku-icb-class/frontend run build)",
|
"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(/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
|
# SMTP Email
|
||||||
CH_SMTP_HOST=gz-smtp.qcloudmail.com
|
CH_SMTP_HOST=gz-smtp.qcloudmail.com
|
||||||
CH_SMTP_PORT=465
|
CH_SMTP_PORT=465
|
||||||
CH_SMTP_USER=noreply
|
CH_SMTP_USER=noreply@hkuicb.info
|
||||||
CH_SMTP_PASSWORD=hX2gqEPjaRhULXF69
|
CH_SMTP_PASSWORD=hX2gqEPjaRhULXF69
|
||||||
CH_SMTP_FROM_EMAIL=noreply@hkuicb.info
|
CH_SMTP_FROM_EMAIL=noreply@hkuicb.info
|
||||||
CH_SMTP_FROM_NAME=HKU ICB Class Hub
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.deps import require_role
|
from app.core.deps import require_role
|
||||||
@ -76,11 +76,11 @@ async def get_resources(
|
|||||||
|
|
||||||
@router.post("/", response_model=ResourceOut)
|
@router.post("/", response_model=ResourceOut)
|
||||||
async def upload_new_resource(
|
async def upload_new_resource(
|
||||||
title: str,
|
|
||||||
category: str,
|
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
description: str | None = None,
|
title: str = Form(...),
|
||||||
class_id: int | None = None,
|
category: str = Form(...),
|
||||||
|
description: str | None = Form(None),
|
||||||
|
class_id: int | None = Form(None),
|
||||||
user: User = Depends(require_role("super_admin", "class_admin")),
|
user: User = Depends(require_role("super_admin", "class_admin")),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
|||||||
@ -21,10 +21,15 @@ async def create_announcement(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(announcement)
|
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(
|
await create_notifications_for_class(
|
||||||
db, class_id, "announcement", f"新公告: {data.title}",
|
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
|
return announcement
|
||||||
|
|||||||
@ -57,3 +57,39 @@ async def send_approval_notification(student_email: str, approved: bool):
|
|||||||
await send_email(
|
await send_email(
|
||||||
student_email, f"HKU ICB: Registration {status_text.capitalize()}", html
|
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 import select, func, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.db.models import Notification, User
|
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(
|
async def create_notification(
|
||||||
@ -33,19 +39,21 @@ async def create_notifications_for_class(
|
|||||||
content: str | None = None,
|
content: str | None = None,
|
||||||
related_id: int | None = None,
|
related_id: int | None = None,
|
||||||
exclude_user_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(
|
result = await db.execute(
|
||||||
select(User.id).where(
|
select(User.id, User.email).where(
|
||||||
User.class_id == class_id,
|
User.class_id == class_id,
|
||||||
User.status == "approved",
|
User.status == "approved",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
user_ids = [row[0] for row in result.all()]
|
rows = result.all()
|
||||||
|
|
||||||
for uid in user_ids:
|
emails: list[str] = []
|
||||||
if exclude_user_id and uid == exclude_user_id:
|
for uid, email in rows:
|
||||||
continue
|
|
||||||
notification = Notification(
|
notification = Notification(
|
||||||
user_id=uid,
|
user_id=uid,
|
||||||
type=type,
|
type=type,
|
||||||
@ -54,9 +62,26 @@ async def create_notifications_for_class(
|
|||||||
related_id=related_id,
|
related_id=related_id,
|
||||||
)
|
)
|
||||||
db.add(notification)
|
db.add(notification)
|
||||||
|
emails.append(email)
|
||||||
|
|
||||||
await db.commit()
|
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(
|
async def list_notifications(
|
||||||
db: AsyncSession, user_id: int, page: int = 1, page_size: int = 20
|
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.commit()
|
||||||
await db.refresh(item)
|
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(
|
await create_notifications_for_class(
|
||||||
db, class_id, "schedule", f"新排期: {data.title}",
|
db, class_id, "schedule", f"新排期: {data.title}",
|
||||||
|
content=f"{time_str}{location_info}",
|
||||||
related_id=item.id,
|
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
|
return item
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from app.db.models import Timeline, User
|
from app.db.models import Timeline, User
|
||||||
from app.schemas.timeline import TimelineCreate, TimelineUpdate
|
from app.schemas.timeline import TimelineCreate, TimelineUpdate
|
||||||
|
from app.services.notification_service import create_notifications_for_class
|
||||||
|
|
||||||
|
|
||||||
async def create_timeline(
|
async def create_timeline(
|
||||||
@ -21,6 +22,18 @@ async def create_timeline(
|
|||||||
db.add(post)
|
db.add(post)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(post)
|
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
|
return post
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user