Merge branch 'RM2_develop' into qyqy_develop

This commit is contained in:
zhangshy
2026-09-11 11:12:43 +08:00
2 changed files with 80 additions and 9 deletions
+23 -8
View File
@@ -8,9 +8,11 @@ from collections import Counter
from collections.abc import AsyncIterator
from datetime import UTC, datetime, time, timedelta
from typing import Any
from zoneinfo import ZoneInfo
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.core.contracts import RequestContext
from app.model.audit import InteractionAudit
from app.repository.fund_query_repository import FundRecord
@@ -68,7 +70,7 @@ class RiskDailyReportService:
) -> AsyncIterator[dict[str, Any]]:
await AuthorizationService.require(context, "risk:alert:read")
generated_at = _utc_naive(now)
yield {"type": "start", "generated_at": generated_at.isoformat()}
yield {"type": "start", "generated_at": _local_datetime_text(generated_at)}
yield {"type": "progress", "stage": "statistics", "message": "正在统计日报数据"}
report = await self._build(context, generated_at)
report["optimization_suggestions"] = ""
@@ -115,7 +117,7 @@ class RiskDailyReportService:
return {
"type": "风控日报",
"report_date": generated_at.date().isoformat(),
"generated_at": generated_at.isoformat(),
"generated_at": _local_datetime_text(generated_at),
"daily_alert_count": len(daily),
"level_distribution": _distribution(daily, "risk_level", LEVEL_ORDER),
"key_risk_events": key_items,
@@ -163,14 +165,20 @@ class RiskDailyReportService:
}
async def _suggestions(self, report: dict[str, Any]) -> tuple[str, str]:
if self.model_service is None or self.endpoint_resolver is None:
return self._fallback(report), "规则化模板"
model_service = self.model_service
endpoint_resolver = self.endpoint_resolver
if model_service is None or endpoint_resolver is None:
from app.service.agent.bootstrap import get_model_service
from app.service.model_gateway import DatabaseModelEndpointResolver
model_service = model_service or get_model_service()
endpoint_resolver = endpoint_resolver or DatabaseModelEndpointResolver()
try:
endpoints = await self.endpoint_resolver.resolve(
endpoints = await endpoint_resolver.resolve(
agent_type="risk",
task_type="daily_report_suggestion",
)
execution = await self.model_service.generate(
execution = await model_service.generate(
endpoints,
self._suggestion_prompt(report),
max_attempts=2,
@@ -225,8 +233,8 @@ class RiskDailyReportService:
"status": item.get("status"),
"ack_status": item.get("ack_status"),
"handler_id": item.get("handler_id"),
"created_at": created_at.isoformat() if created_at else None,
"due_time": due_at.isoformat() if due_at else None,
"created_at": _local_datetime_text(created_at) if created_at else None,
"due_time": _local_datetime_text(due_at) if due_at else None,
"is_overdue": bool(due_at and due_at <= generated_at),
"is_escalated": bool(item.get("is_escalated")),
"close_reason": item.get("close_reason"),
@@ -402,3 +410,10 @@ def _utc_naive(value: datetime | None) -> datetime:
if value.tzinfo is not None:
return value.astimezone(UTC).replace(tzinfo=None)
return value
def _local_datetime_text(value: datetime) -> str:
"""把 UTC 时间转换为配置时区的常用展示格式。"""
aware = value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
local_value = aware.astimezone(ZoneInfo(get_settings().timezone))
return local_value.strftime("%Y-%m-%d %H:%M:%S")
@@ -1,5 +1,5 @@
from datetime import datetime
from types import MappingProxyType
from types import MappingProxyType, SimpleNamespace
import pytest
@@ -30,6 +30,26 @@ class FakeRepository:
return self.snapshot
class FailingEndpointResolver:
async def resolve(self, **_kwargs):
raise RuntimeError("模型端点不可用")
class FailingModelService:
async def generate(self, *_args, **_kwargs):
raise AssertionError("端点解析失败后不应调用模型")
class StubEndpointResolver:
async def resolve(self, **_kwargs):
return [object()]
class StubModelService:
async def generate(self, *_args, **_kwargs):
return SimpleNamespace(text="1. 根据模型生成日报建议")
def record(
alert_no: str,
*,
@@ -97,6 +117,8 @@ async def test_daily_report_contains_nine_sections_and_historical_items() -> Non
service = RiskDailyReportService(
session,
repository=FakeRepository(snapshot),
model_service=FailingModelService(),
endpoint_resolver=FailingEndpointResolver(),
)
report = await service.generate(
@@ -108,12 +130,46 @@ async def test_daily_report_contains_nine_sections_and_historical_items() -> Non
assert report["unresolved_items"]["total"] == 2
assert report["unresolved_items"]["historical"] == 1
assert report["unresolved_items"]["overdue"] == 1
assert report["generated_at"] == "2026-09-10 18:00:00"
assert report["unresolved_items"]["items"][0]["created_at"] == "2026-09-10 16:00:00"
assert report["source"] == "规则化模板"
assert all(f"{index}." in report["content"] for index in range(1, 10))
assert session.committed is True
assert session.added[0].action_type == "risk_daily_report_generated"
@pytest.mark.asyncio
async def test_daily_report_lazily_initializes_model_service(monkeypatch) -> None:
current = record("ALERT-MODEL")
snapshot = RiskReportSnapshot(
daily=(current,),
unresolved=(current,),
false_positive=(),
dispositions=(current,),
)
session = FakeSession()
monkeypatch.setattr(
"app.service.agent.bootstrap.get_model_service",
lambda: StubModelService(),
)
monkeypatch.setattr(
"app.service.model_gateway.DatabaseModelEndpointResolver",
StubEndpointResolver,
)
service = RiskDailyReportService(
session,
repository=FakeRepository(snapshot),
)
report = await service.generate(
context(),
now=datetime(2026, 9, 10, 10, 0),
)
assert report["source"] == "模型"
assert report["optimization_suggestions"] == "1. 根据模型生成日报建议"
def test_mail_service_is_disabled_by_default() -> None:
result = RiskDailyReportMailService(environment={}).send(
["risk@example.com"],