"""Dashboard 聚合视图域 Router(DSB-01~DSB-04 + DSB-DELIVERY + DSB-REALTIME)。 统一采用模板 A(控制面端口路由),鉴权依赖 get_admin_user,调用 use_cases.dashboard_view.,raiseOnControlFailure 转译失败, serialize_control_data 序列化响应。 路径设计:6 个端点均为静态路径(/dashboard/overview / /dashboard/accounts /dashboard/messages / /dashboard/sessions / /dashboard/delivery /dashboard/realtime)。子 router 自身不设置 prefix,根前缀 ``/channels`` 由 ``channels_router`` 聚合 router 统一追加。 端点清单: - GET /dashboard/overview DSB-01 getDashboardOverview - GET /dashboard/accounts DSB-02 getAccountStats - GET /dashboard/messages DSB-03 getMessageStats - GET /dashboard/sessions DSB-04 getDashboardSessionStats DSB-01~04 对应《16-聚合视图域-dashboard-router-设计方案》(仅覆盖 4 个 KPI 快照端点)。DSB-DELIVERY / DSB-REALTIME 为后续扩展端点,分别提供 投递 KPI 总览与实时监控指标,不在上述设计方案范围内。 - GET /dashboard/delivery DSB-DELIVERY getDashboardDelivery - GET /dashboard/realtime DSB-REALTIME getRealtimeMetrics """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Query, Request from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.dashboard import RealtimeQuery from yuxi.storage.postgres.models_business import User from server.routers.channels import ( build_operator, get_channel_use_cases, parse_datetime, raiseOnControlFailure, serialize_control_data, ) from server.utils.auth_middleware import get_admin_user dashboard_router = APIRouter(tags=["channels-dashboard"]) @dashboard_router.get("/dashboard/overview", response_model=dict) async def get_dashboard_overview( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), start_time: str | None = Query(default=None, description="消息统计起始时间(ISO 8601);仅 messages 子域受影响"), end_time: str | None = Query(default=None, description="消息统计截止时间(ISO 8601);仅 messages 子域受影响"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询全局总览(DSB-01)。对应控制面操作 dashboard/overview。 聚合账户 / 消息 / 会话 / 投递四类子域 KPI 计数。 ``start_time`` / ``end_time`` 仅影响消息统计;账户、会话、投递为当前 快照,不受时间范围影响。 """ operator = build_operator(current_user, request) start_time_dt = parse_datetime("start_time", start_time) end_time_dt = parse_datetime("end_time", end_time) result = await use_cases.dashboard_view.getDashboardOverview( channel_type=channel_type, start_time=start_time_dt.isoformat() if start_time_dt else None, end_time=end_time_dt.isoformat() if end_time_dt else None, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/accounts", response_model=dict) async def get_dashboard_accounts( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询账户总览(DSB-02)。对应控制面操作 dashboard/accounts。 返回账户按渠道 / 状态分组的计数快照。 """ operator = build_operator(current_user, request) result = await use_cases.dashboard_view.getAccountStats( channel_type=channel_type, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/messages", response_model=dict) async def get_dashboard_messages( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), start_time: str | None = Query(default=None, description="起始时间(ISO 8601)"), end_time: str | None = Query(default=None, description="截止时间(ISO 8601)"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询消息总览(DSB-03)。对应控制面操作 dashboard/messages。 返回指定时间范围内的消息按渠道 / 角色 / 投递状态分组的计数快照。 时间参数由 parse_datetime 在 Router 层翻译为 UTC datetime,再转回 ISO 8601 字符串传给端口(端口签名为 str)。非法格式抛 ValidationError(400)。 """ operator = build_operator(current_user, request) start_time_dt = parse_datetime("start_time", start_time) end_time_dt = parse_datetime("end_time", end_time) result = await use_cases.dashboard_view.getMessageStats( channel_type=channel_type, start_time=start_time_dt.isoformat() if start_time_dt else None, end_time=end_time_dt.isoformat() if end_time_dt else None, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/sessions", response_model=dict) async def get_dashboard_sessions( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询会话总览(DSB-04)。对应控制面操作 dashboard/sessions。 返回未软删除会话按渠道 / 是否临时会话分组的计数快照。 """ operator = build_operator(current_user, request) result = await use_cases.dashboard_view.getDashboardSessionStats( channel_type=channel_type, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/delivery", response_model=dict) async def get_dashboard_delivery( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), start_time: str | None = Query(default=None, description="起始时间(ISO 8601)"), end_time: str | None = Query(default=None, description="截止时间(ISO 8601)"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询投递总览(DSB-DELIVERY)。对应控制面操作 dashboard/delivery。 聚合投递 KPI 指标(总数、成功率、延迟分位数、死信数、按渠道切片)。 时间参数由 parse_datetime 在 Router 层翻译为 UTC datetime,再转回 ISO 8601 字符串传给端口(端口签名为 str)。非法格式抛 ValidationError (400)。 """ operator = build_operator(current_user, request) start_time_dt = parse_datetime("start_time", start_time) end_time_dt = parse_datetime("end_time", end_time) result = await use_cases.dashboard_view.getDashboardDelivery( channel_type=channel_type, start_time=start_time_dt.isoformat() if start_time_dt else None, end_time=end_time_dt.isoformat() if end_time_dt else None, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/todos", response_model=dict) async def get_dashboard_todos( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询工作台待办计数(DSB-TODOS)。对应控制面操作 dashboard/todos。 聚合待审批配对数、待复核内容审核数与当前死信积压数,按渠道类型过滤。 """ operator = build_operator(current_user, request) result = await use_cases.dashboard_view.getDashboardTodos( channel_type=channel_type, operator=operator, ) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @dashboard_router.get("/dashboard/realtime", response_model=dict) async def get_dashboard_realtime( request: Request, channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"), window_seconds: int = Query(default=60, ge=10, le=300, description="滑动窗口秒数(10-300,默认 60)"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """查询实时监控指标(DSB-REALTIME)。对应控制面操作 dashboard/realtime。 纯内存态聚合,业务不开启 DB 事务(``db_tx_required=False``),高频查询 场景使用。审计走 INDEPENDENT 独立事务 best-effort 写入,每请求一次 审计记录,不阻断响应。 ``channel_type`` 为空(全局查询)时,``by_channel`` 返回全部已注册 渠道的切片;非空时仅返回该渠道一条。 """ operator = build_operator(current_user, request) query = RealtimeQuery( channel_type=channel_type, window_seconds=window_seconds, ) result = await use_cases.dashboard_view.getRealtimeMetrics(query, operator=operator) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)}