2026-09-10 21:03:44 +08:00
|
|
|
|
"""风控通知记录 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.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")
|
2026-09-11 14:05:37 +08:00
|
|
|
|
binding = RiskQueryService._binding(context, query, "/notifications")
|
2026-09-10 21:03:44 +08:00
|
|
|
|
page = await RiskRepository(
|
|
|
|
|
|
self.session,
|
|
|
|
|
|
scope=scope_from_context(context),
|
|
|
|
|
|
).list_notifications(
|
|
|
|
|
|
keyword=query.keyword,
|
|
|
|
|
|
send_status=query.send_status,
|
|
|
|
|
|
start_time=query.start_time,
|
|
|
|
|
|
end_time=query.end_time,
|
|
|
|
|
|
page=PageRequest(
|
|
|
|
|
|
limit=query.limit,
|
2026-09-11 14:05:37 +08:00
|
|
|
|
offset=decode_offset_cursor(query.cursor, binding=binding),
|
2026-09-10 21:03:44 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-09-11 14:05:37 +08:00
|
|
|
|
return RiskQueryService._page(page, binding=binding)
|
2026-09-10 21:03:44 +08:00
|
|
|
|
|
|
|
|
|
|
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 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)
|