ForcePilot/backend/server/routers/channels/outbox_router.py
Kris ddafd95ff0 refactor(routers): 整理并新增多组渠道相关路由功能
1.  移除多个导出接口的显式response_model声明
2.  调整access_rule和test_case的创建接口位置,修复静态路径冲突
3.  优化适配器配置校验的异常处理逻辑
4.  重构集成路由的查询逻辑,统一使用get_integration_or_raise
5.  新增channels路由组下的capability、reports、dashboard、webhook、wizard、doctor、directory、session共8个子路由模块
6.  注册channels_router到全局路由列表
2026-07-02 03:29:06 +08:00

347 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Outbox 管理路由OBX-01~OBX-08 + P0缺口
统一采用模板 A控制面端口路由鉴权依赖 get_admin_user调用
use_cases.outbox_management.<method>raiseOnControlFailure 转译失败,
serialize_control_data 序列化响应。
路径设计:静态跨渠道查询(/messages / /stats / /dead-letter / /retry-policy
/dead-letter/batch-retry / /dead-letter/batch-delete先于动态路径
/messages/{outbox_id} / /dead-letter/{outbox_id} / /messages/{outbox_id}/retry
声明(规范 §6.5),避免静态路径被动态参数捕获。子 router 自身 prefix 为 ``/outbox``
根前缀 ``/channels`` 由 ``channels_router`` 聚合 router 统一追加。
端点清单对应《07-投递基础设施域设计方案》§2.1 + P0缺口补全
- GET /outbox/messages OBX-01 listOutboxEntries
- GET /outbox/stats OBX-02 getOutboxStats
- GET /outbox/trend OBX-TREND getOutboxTrend
- GET /outbox/dead-letter OBX-04 listDeadLetterOutboxEntries
- GET /outbox/retry-policy OBX-07 getOutboxRetryPolicy
- POST /outbox/dead-letter/batch-retry OBX-DL-BATCH-RETRY 批量重投死信
- POST /outbox/dead-letter/batch-delete OBX-DL-BATCH-DELETE 批量清除死信
- GET /outbox/dead-letter/export OBX-DL-EXPORT export_dead_letter
- PUT /outbox/retry-policy OBX-POLICY-UPDATE 更新重试策略
- GET /outbox/messages/{outbox_id} OBX-03 getOutboxEntry
- DELETE /outbox/dead-letter/{outbox_id} OBX-05 deleteDeadLetterOutboxEntry
- POST /outbox/messages/{outbox_id}/retry OBX-08 retryOutboxEntry
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.outbox import (
DeadLetterExportCmd,
OutboxQueryFilter,
OutboxStatus,
OutboxTrendQuery,
RetryPolicySnapshot,
)
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
outbox_router = APIRouter(prefix="/outbox", tags=["channels-outbox"])
class UpdateRetryPolicyRequest(BaseModel):
"""更新重试策略请求体。"""
model_config = ConfigDict(frozen=True)
max_retry: int = Field(..., ge=1, le=20, description="最大重试次数")
ttl_seconds: int = Field(..., ge=60, le=2592000, description="条目存活时间(秒)")
retry_backoff_schedule: list[int] = Field(
default_factory=list,
description="指数退避序列(秒),为空时使用默认退避策略",
)
# ---------------- 静态路径端点(须先于动态路径声明) ----------------
@outbox_router.get("/messages", response_model=dict)
async def list_outbox_messages(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
channel_account_id: str | None = Query(default=None, description="按渠道账户 ID 过滤"),
status: OutboxStatus | None = Query(default=None, description="按状态过滤"),
message_id: str | None = Query(default=None, description="按消息 ID 过滤"),
channel_msg_id: str | None = Query(default=None, description="按渠道侧消息 ID 过滤"),
created_after: str | None = Query(default=None, description="创建时间下界ISO 8601"),
created_before: str | None = Query(default=None, description="创建时间上界ISO 8601"),
limit: int = Query(default=100, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出 outbox 消息OBX-01。对应控制面操作 outbox/list。"""
operator = build_operator(current_user, request)
created_after_dt = parse_datetime("created_after", created_after)
created_before_dt = parse_datetime("created_before", created_before)
query_filter = OutboxQueryFilter(
channel_type=channel_type,
channel_account_id=channel_account_id,
status=status,
message_id=message_id,
channel_msg_id=channel_msg_id,
created_after=created_after_dt,
created_before=created_before_dt,
)
result = await use_cases.outbox_management.listOutboxEntries(
query_filter,
limit,
offset,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.get("/stats", response_model=dict)
async def get_outbox_stats(
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]:
"""查询 outbox 统计OBX-02。对应控制面操作 outbox/stats。"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.getOutboxStats(
channel_type,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.get("/trend", response_model=dict)
async def get_outbox_trend(
request: Request,
start_time: str = Query(..., description="起始时间ISO 8601"),
end_time: str = Query(..., description="结束时间ISO 8601"),
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
granularity: str = Query(default="hour", description="时间粒度minute / hour / day"),
metric: str = Query(default="queue_depth", description="度量指标queue_depth / retry_count / dead_count"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询 outbox 投递积压趋势OBX-TREND。对应控制面操作 outbox/trend。
按时间粒度聚合 outbox 条目的时间序列数据点,支持 ``queue_depth`` /
``retry_count`` / ``dead_count`` 三种度量指标。
"""
operator = build_operator(current_user, request)
start_time_dt = parse_datetime("start_time", start_time)
end_time_dt = parse_datetime("end_time", end_time)
query = OutboxTrendQuery(
start_time=start_time_dt,
end_time=end_time_dt,
channel_type=channel_type,
granularity=granularity,
metric=metric,
)
result = await use_cases.outbox_management.getOutboxTrend(query, operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.get("/dead-letter", response_model=dict)
async def list_dead_letter_outbox(
request: Request,
limit: int = Query(default=100, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询死信队列OBX-04。对应控制面操作 outbox/dead_letter_list。"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.listDeadLetterOutboxEntries(
limit,
offset,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.get("/retry-policy", response_model=dict)
async def get_outbox_retry_policy(
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询 outbox 重试策略OBX-07。对应控制面操作 outbox/retry_policy。
返回装配期注入的 OutboxConfig 快照,不支持运行时修改。
"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.getOutboxRetryPolicy(operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.post("/dead-letter/batch-retry", response_model=dict)
async def batch_retry_dead_letter(
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量重投死信条目OBX-DL-BATCH-RETRY
对所有死信条目执行批量重投FAILED/SENT_UNCONFIRMED 原地重投,
DEAD 条目新建条目重投INV-3 死信不可复活)。
对应控制面操作 ``outbox/batch_retry``。
"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.batchRetryDeadLetter(operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.post("/dead-letter/batch-delete", response_model=dict)
async def batch_delete_dead_letter(
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量清除死信条目OBX-DL-BATCH-DELETE
逻辑删除所有死信条目(置 is_deleted=1、deleted_at=now()),保留审计痕迹。
对应控制面操作 ``outbox/batch_delete``。
"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.batchDeleteDeadLetter(operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.get("/dead-letter/export", response_model=dict)
async def export_dead_letter(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
created_after: str | None = Query(default=None, description="创建时间下界ISO 8601"),
created_before: str | None = Query(default=None, description="创建时间上界ISO 8601"),
format: str = Query(default="json", description="导出格式json / csv"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""导出死信条目OBX-DL-EXPORT。对应控制面操作 outbox/dead_letter_export。
按过滤条件查询 DEAD 状态 outbox 条目并序列化为 ``json`` / ``csv`` 格式。
``format=json`` 时 ``content`` 为记录数组;``format=csv`` 时 ``content``
为 CSV 字符串(含表头与 CSV injection 防护)。
"""
operator = build_operator(current_user, request)
created_after_dt = parse_datetime("created_after", created_after)
created_before_dt = parse_datetime("created_before", created_before)
cmd = DeadLetterExportCmd(
channel_type=channel_type,
created_after=created_after_dt,
created_before=created_before_dt,
format=format,
)
result = await use_cases.outbox_management.exportDeadLetters(cmd, operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.put("/retry-policy", response_model=dict)
async def update_outbox_retry_policy(
payload: UpdateRetryPolicyRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新 outbox 重试策略OBX-POLICY-UPDATE
运行时调整重试参数,用于紧急故障场景下临时调整重试行为。
对应控制面操作 ``outbox/update_retry_policy``。
"""
operator = build_operator(current_user, request)
policy = RetryPolicySnapshot(
max_retry=payload.max_retry,
ttl_seconds=payload.ttl_seconds,
retry_backoff_schedule=list(payload.retry_backoff_schedule),
)
result = await use_cases.outbox_management.updateRetryPolicy(
policy=policy,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
# ---------------- 动态路径端点 ----------------
@outbox_router.get("/messages/{outbox_id}", response_model=dict)
async def get_outbox_message(
outbox_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询 outbox 消息详情OBX-03。对应控制面操作 outbox/get。"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.getOutboxEntry(
outbox_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@outbox_router.delete("/dead-letter/{outbox_id}", response_model=dict)
async def delete_dead_letter_outbox(
outbox_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""清除死信条目OBX-05逻辑删除。对应控制面操作 outbox/dead_letter_delete。
采用逻辑删除(置 is_deleted=1、deleted_at=now()),保留审计痕迹。
仅允许对 dead 状态条目执行删除,非 dead 状态抛 RuleViolationError。
"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.deleteDeadLetterOutboxEntry(
outbox_id,
operator=operator,
)
raiseOnControlFailure(result)
return {
"success": True,
"data": serialize_control_data(result.data),
}
@outbox_router.post("/messages/{outbox_id}/retry", response_model=dict)
async def retry_outbox_message(
outbox_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""手动重投 outbox 消息OBX-08。对应控制面操作 outbox/retry。
仅允许对 failed/sent_unconfirmed 状态条目重投dead 状态抛
RuleViolationError不可复活死信INV-3 聚合根不变量)。
"""
operator = build_operator(current_user, request)
result = await use_cases.outbox_management.retryOutboxEntry(
outbox_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}