功能(提醒):统一项目提醒中心与桌面通知链路
增加通用提醒状态、数据库迁移和定时同步,覆盖风险时效、文件回执、项目里程碑、访视窗口与协作申请。 新增网页端和桌面端提醒中心、真实投递诊断与固定隐私通知正文,并补齐登录来源聚合、测试和说明文档。
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""Server-side materialization for time and state driven business reminders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.study_member import StudyMember
|
||||
from app.models.user import User, UserStatus
|
||||
from app.services.project_reminder_service import sync_project_reminders
|
||||
|
||||
logger = logging.getLogger("ctms.notifications")
|
||||
NOTIFICATION_SYNC_LOCK_KEY = 0x43544D53 # "CTMS"
|
||||
|
||||
|
||||
async def sync_all_project_reminders_once() -> int:
|
||||
"""Synchronize every active project member once, with a cross-worker advisory lock."""
|
||||
async with SessionLocal() as lock_session:
|
||||
acquired = bool(await lock_session.scalar(
|
||||
text("SELECT pg_try_advisory_lock(:lock_key)"),
|
||||
{"lock_key": NOTIFICATION_SYNC_LOCK_KEY},
|
||||
))
|
||||
if not acquired:
|
||||
return 0
|
||||
try:
|
||||
memberships = (await lock_session.execute(
|
||||
select(StudyMember.study_id, StudyMember.user_id)
|
||||
.join(User, User.id == StudyMember.user_id)
|
||||
.where(
|
||||
StudyMember.is_active.is_(True),
|
||||
User.status == UserStatus.ACTIVE,
|
||||
)
|
||||
)).all()
|
||||
await lock_session.commit()
|
||||
synced = 0
|
||||
for study_id, user_id in memberships:
|
||||
async with SessionLocal() as session:
|
||||
try:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None or not user.is_active:
|
||||
continue
|
||||
await sync_project_reminders(session, study_id, user)
|
||||
synced += 1
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.exception(
|
||||
"Failed to synchronize reminders for study=%s user=%s",
|
||||
study_id,
|
||||
user_id,
|
||||
)
|
||||
return synced
|
||||
finally:
|
||||
await lock_session.execute(
|
||||
text("SELECT pg_advisory_unlock(:lock_key)"),
|
||||
{"lock_key": NOTIFICATION_SYNC_LOCK_KEY},
|
||||
)
|
||||
|
||||
|
||||
async def run_notification_sync_job(stop_event: asyncio.Event) -> None:
|
||||
logger.info("Notification synchronization task started")
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
synced = await sync_all_project_reminders_once()
|
||||
logger.debug("Synchronized reminders for %s project memberships", synced)
|
||||
except Exception:
|
||||
logger.exception("Notification synchronization task failed")
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
stop_event.wait(),
|
||||
timeout=settings.NOTIFICATION_SYNC_INTERVAL_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
logger.info("Notification synchronization task stopped")
|
||||
Reference in New Issue
Block a user