"""风控通知记录 Service。 本阶段只创建通知记录和提供分页查询,不执行 SMTP 外发。 """ from __future__ import annotations from datetime import UTC, datetime from typing import Any from uuid import uuid4 from sqlalchemy.ext.asyncio import AsyncSession from app.api.schemas.risk import RiskNotificationPageQuery from app.core.contracts import RequestContext from app.core.risk_cursor import decode_offset_cursor from app.core.timeutil import from_local from app.model.fund import FundRiskAlert, FundRiskNotification from app.repository.fund_query_repository import PageRequest from app.repository.risk_repository import RiskRepository from app.service.authorization_service import AuthorizationService from app.service.risk_query_service import RiskQueryService, scope_from_context class RiskNotificationService: def __init__(self, session: AsyncSession) -> None: self.session = session async def list_notifications( self, context: RequestContext, query: RiskNotificationPageQuery, ) -> dict[str, Any]: await AuthorizationService.require(context, "risk:alert:read") binding = RiskQueryService._binding(context, query, "/notifications") page = await RiskRepository( self.session, scope=scope_from_context(context), ).list_notifications( keyword=query.keyword, send_status=query.send_status, start_time=from_local(query.start_time) if query.start_time else None, end_time=from_local(query.end_time) if query.end_time else None, page=PageRequest( limit=query.limit, offset=decode_offset_cursor(query.cursor, binding=binding), ), ) return RiskQueryService._page(page, binding=binding) def create_in_app( self, alert: FundRiskAlert, *, receiver_user_id: int | None, title: str, content: str, ) -> FundRiskNotification: notification = self._build( alert=alert, channel="站内提醒", receiver_user_id=receiver_user_id, receiver_email=None, title=title, content=content, send_status="已发送", sent_at=_now(), fail_reason=None, ) self.session.add(notification) return notification def create_mail_record( self, alert: FundRiskAlert, *, receiver_email: str, title: str, content: str, mail_enabled: bool = False, ) -> FundRiskNotification: notification = self._build( alert=alert, channel="邮件", receiver_user_id=None, receiver_email=receiver_email, title=title, content=content, send_status="待发送" if mail_enabled else "未启用", sent_at=None, fail_reason=None if mail_enabled else "邮件发送功能未启用", ) self.session.add(notification) return notification def mark_mail_sent(self, notification: FundRiskNotification) -> None: """邮件发送成功后回写通知状态。""" notification.send_status = "已发送" notification.sent_at = _now() notification.fail_reason = None def mark_mail_failed( self, notification: FundRiskNotification, reason: str, ) -> None: """邮件发送失败后保留通知记录并记录安全失败原因。""" notification.send_status = "发送失败" notification.sent_at = None notification.fail_reason = reason.strip()[:500] or "邮件发送失败" def create_high_risk_records( self, alerts: list[FundRiskAlert], *, receiver_user_id: int | None = None, receiver_email: str | None = None, mail_enabled: bool = False, ) -> list[FundRiskNotification]: notifications: list[FundRiskNotification] = [] for alert in alerts: if alert.alert_level != "高": continue title = f"高风险预警:{alert.alert_type}" notifications.append(self.create_in_app( alert, receiver_user_id=receiver_user_id, title=title, content=alert.evidence_summary, )) if receiver_email: notifications.append(self.create_mail_record( alert, receiver_email=receiver_email, title=title, content=alert.evidence_summary, mail_enabled=mail_enabled, )) return notifications @staticmethod def _build( *, alert: FundRiskAlert, channel: str, receiver_user_id: int | None, receiver_email: str | None, title: str, content: str, send_status: str, sent_at: datetime | None, fail_reason: str | None, ) -> FundRiskNotification: referenced_content = ( content if f"预警编号:{alert.alert_no}" in content else f"预警编号:{alert.alert_no};{content}" ) return FundRiskNotification( id=_new_notification_id(), notification_no=f"N{uuid4().hex[:12].upper()}", alert_id=alert.id, channel=channel, receiver_user_id=receiver_user_id, receiver_email=receiver_email, title=title, content=referenced_content, send_status=send_status, sent_at=sent_at, fail_reason=fail_reason, created_at=_now(), ) def _new_notification_id() -> int: return uuid4().int & ((1 << 63) - 1) or 1 def _now() -> datetime: return datetime.now(UTC).replace(tzinfo=None)