feat: 迁移奶龙风控业务模块与演示文档
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""风控只读查询接口。"""
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import datetime, time
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.database import get_session
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.risk import (
|
||||
RiskAlertEscalationRequest,
|
||||
RiskAlertExclusionRequest,
|
||||
RiskAlertPageQuery,
|
||||
RiskAlertResolutionRequest,
|
||||
RiskDailyReportGenerateRequest,
|
||||
RiskDailyReportMailRequest,
|
||||
RiskEvidencePageQuery,
|
||||
RiskEvidenceSource,
|
||||
RiskNotificationPageQuery,
|
||||
)
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.risk_action_service import RiskActionService
|
||||
from app.service.risk_daily_report_mail_service import RiskDailyReportMailService
|
||||
from app.service.risk_daily_report_service import RiskDailyReportService
|
||||
from app.service.risk_evidence_archive_service import RiskEvidenceArchiveService
|
||||
from app.service.risk_notification_service import RiskNotificationService
|
||||
from app.service.risk_query_service import RiskQueryService
|
||||
from app.service.risk_scan_service import RiskScanService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/risk",
|
||||
tags=["risk"],
|
||||
dependencies=[Depends(enforce_rate_limit)],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
async def risk_overview(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).overview(context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.get("/alerts")
|
||||
async def list_risk_alerts(
|
||||
query: RiskAlertPageQuery = Depends(), # noqa: B008
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).list_alerts(context, query)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/scan")
|
||||
async def scan_risk_alerts(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskScanService(session).scan(context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/acknowledgements")
|
||||
async def acknowledge_risk_alert(
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskActionService(session).acknowledge(alert_no, context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/investigations")
|
||||
async def investigate_risk_alert(
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskActionService(session).investigate(alert_no, context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/exclusions")
|
||||
async def exclude_risk_alert(
|
||||
payload: RiskAlertExclusionRequest,
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskActionService(session).exclude(alert_no, payload.reason, context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/resolutions")
|
||||
async def resolve_risk_alert(
|
||||
payload: RiskAlertResolutionRequest,
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskActionService(session).resolve(alert_no, payload.resolution, context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/escalations")
|
||||
async def escalate_risk_alert(
|
||||
payload: RiskAlertEscalationRequest,
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskActionService(session).escalate(alert_no, payload.reason, context)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_no}/evidence")
|
||||
async def archive_risk_evidence(
|
||||
evidence_file: UploadFile = File(...), # noqa: B008
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
try:
|
||||
data = await RiskEvidenceArchiveService(session).archive(alert_no, evidence_file, context)
|
||||
return _envelope(data, context)
|
||||
finally:
|
||||
await evidence_file.close()
|
||||
|
||||
|
||||
@router.get("/alerts/{alert_no}")
|
||||
async def get_risk_alert(
|
||||
alert_no: str = Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).get_alert_detail(context, alert_no.strip())
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.get("/evidence/{source}")
|
||||
async def list_risk_evidence(
|
||||
source: RiskEvidenceSource = Path(), # noqa: B008
|
||||
query: RiskEvidencePageQuery = Depends(), # noqa: B008
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).list_evidence(context, source, query)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.get("/notifications")
|
||||
async def list_risk_notifications(
|
||||
query: RiskNotificationPageQuery = Depends(), # noqa: B008
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskNotificationService(session).list_notifications(context, query)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/daily-report")
|
||||
async def generate_risk_daily_report(
|
||||
payload: RiskDailyReportGenerateRequest,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
report_time = (
|
||||
datetime.combine(payload.report_date, time.min)
|
||||
if payload.report_date is not None
|
||||
else None
|
||||
)
|
||||
data = await RiskDailyReportService(session).generate(context, report_time)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/daily-report/stream")
|
||||
async def stream_risk_daily_report(
|
||||
payload: RiskDailyReportGenerateRequest,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> StreamingResponse:
|
||||
report_time = (
|
||||
datetime.combine(payload.report_date, time.min)
|
||||
if payload.report_date is not None
|
||||
else None
|
||||
)
|
||||
|
||||
async def events() -> AsyncIterator[str]:
|
||||
async for event in RiskDailyReportService(session).stream(context, report_time):
|
||||
event_type = str(event.get("type", "message"))
|
||||
payload = json.dumps(event, ensure_ascii=False, default=str)
|
||||
yield f"event: {event_type}\ndata: {payload}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
events(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/daily-report/mail")
|
||||
async def send_risk_daily_report_mail(
|
||||
payload: RiskDailyReportMailRequest,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = RiskDailyReportMailService().send(
|
||||
payload.recipients,
|
||||
payload.subject,
|
||||
payload.content,
|
||||
)
|
||||
return _envelope(data, context)
|
||||
|
||||
|
||||
def _envelope(data: object, context: RequestContext) -> dict[str, object]:
|
||||
return {
|
||||
"data": data,
|
||||
"meta": {"trace_id": context.trace_id},
|
||||
}
|
||||
Reference in New Issue
Block a user