修复高风险预警邮件通知并补充测试

This commit is contained in:
zhangshy
2026-09-12 15:56:50 +08:00
parent 164d55a05a
commit eb4e895e4e
12 changed files with 464 additions and 34 deletions
+83 -1
View File
@@ -16,6 +16,7 @@ from uuid import uuid4
from sqlalchemy import Select, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.core.contracts import RequestContext
from app.core.errors import AgentError, ConflictAgentError
from app.core.timeutil import local_date, local_hour
@@ -26,11 +27,17 @@ from app.model.fund import (
FundHolding,
FundProduct,
FundRiskAlert,
FundRiskNotification,
FundTransaction,
)
from app.model.risk import RiskLoginRecord, RiskUser, RiskWorkOrder
from app.service.authorization_service import AuthorizationService
from app.service.risk_notification_service import RiskNotificationService
from app.service.risk_smtp_mail_service import (
RiskSmtpConfigurationError,
RiskSmtpMailService,
RiskSmtpSendError,
)
HIGH_RISK = "高"
MEDIUM_RISK = "中"
@@ -510,6 +517,8 @@ class RiskScanService:
notification_enabled: bool = True,
notification_email: str | None = None,
mail_enabled: bool = False,
mail_dry_run: bool = False,
smtp_mail_service: RiskSmtpMailService | None = None,
) -> None:
self.session = session
self.rule_engine = rule_engine or RiskRuleEngine(session)
@@ -517,6 +526,31 @@ class RiskScanService:
self.notification_enabled = notification_enabled
self.notification_email = notification_email
self.mail_enabled = mail_enabled
self.mail_dry_run = mail_dry_run
self.smtp_mail_service = smtp_mail_service or RiskSmtpMailService()
@classmethod
def from_settings(
cls,
session: AsyncSession,
*,
rule_engine: RiskRuleEngine | None = None,
notification_service: RiskNotificationService | None = None,
) -> RiskScanService:
"""从环境配置创建扫描服务,供手工扫描和定时扫描共用。"""
settings = get_settings()
recipients = _parse_recipients(settings.risk_alert_mail_recipients)
notification_email = recipients[0] if recipients else None
if settings.risk_alert_mail_enabled and not notification_email:
logger.warning("高风险邮件通知已启用,但未配置收件人")
return cls(
session,
rule_engine=rule_engine,
notification_service=notification_service,
notification_email=notification_email,
mail_enabled=settings.risk_alert_mail_enabled and bool(notification_email),
mail_dry_run=settings.risk_alert_mail_dry_run,
)
async def scan(self, context: RequestContext) -> dict[str, int | str]:
await AuthorizationService.require(context, "risk:alert:scan")
@@ -572,7 +606,7 @@ class RiskScanService:
)
count += 1
if self.notification_email:
self.notification_service.create_mail_record(
notification = self.notification_service.create_mail_record(
alert,
receiver_email=self.notification_email,
title=title,
@@ -580,11 +614,48 @@ class RiskScanService:
mail_enabled=self.mail_enabled,
)
count += 1
if self.mail_enabled:
await self._deliver_mail(
alert,
notification,
title,
)
return count, ""
except Exception as error:
logger.exception("高风险通知记录创建失败,预警扫描继续提交")
return 0, f"{type(error).__name__}: {error}"[:200]
async def _deliver_mail(
self,
alert: FundRiskAlert,
notification: FundRiskNotification,
title: str,
) -> None:
"""发送高风险通知邮件,失败只回写通知记录,不影响预警落库。"""
if self.mail_dry_run:
self.notification_service.mark_mail_failed(
notification,
"邮件发送处于 dry-run 模式",
)
return
try:
result = await self.smtp_mail_service.send(
[self.notification_email or ""],
title,
f"预警编号:{alert.alert_no};{alert.evidence_summary}",
)
except (RiskSmtpConfigurationError, RiskSmtpSendError) as error:
logger.warning("高风险预警邮件发送失败:alert_no=%s error=%s", alert.alert_no, error)
self.notification_service.mark_mail_failed(notification, str(error))
except Exception:
logger.exception("高风险预警邮件发送发生未预期异常:alert_no=%s", alert.alert_no)
self.notification_service.mark_mail_failed(notification, "邮件发送发生未预期异常")
else:
if result.get("status") == "sent":
self.notification_service.mark_mail_sent(notification)
else:
self.notification_service.mark_mail_failed(notification, "邮件发送未完成")
def _new_alert_id() -> int:
"""生成非零 63 位正整数;预警表主键不自增。"""
@@ -614,3 +685,14 @@ def _level_value(level: str, prefix: str) -> int | None:
"""
digits = level.strip().upper().removeprefix(prefix.upper())
return int(digits) if digits.isdigit() else None
def _parse_recipients(value: str | None) -> list[str]:
"""解析逗号或分号分隔的邮件收件人配置。"""
if not value:
return []
return [
recipient.strip()
for recipient in value.replace(";", ",").split(",")
if recipient.strip()
]