Merge branch 'RM2_develop' into qyqy_develop
This commit is contained in:
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user