ForcePilot/backend/server/routers/channels/pairing_router.py
Kris bba1775220 refactor(channel-router): 清理冗余空行并优化代码结构
本次提交包含多类优化:
1.  移除多个路由文件中多余的空导入行,统一代码格式
2.  重构Query参数定义,将长参数拆分为多行提升可读性
3.  新增多个业务端点:
    - 渠道能力画像矩阵查询CAP-03
    - 配对审批计数接口用于待办角标
    - 批量查询对端目录资料接口
    - 向导扫码登录相关端点
    - 会话实时事件SSE推送端点
    - 工作台待办统计接口
4.  完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5.  调整路由导入顺序,修复动态路由路径冲突隐患
6.  更新文档注释与接口清单,修正部分接口描述细节
2026-07-06 20:50:03 +08:00

420 lines
17 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.

"""配对审批域 Router。
提供 DM 安全配对审批的 10 个 HTTP 端点,全部由 ``get_admin_user`` 守门,仅
管理员可访问。Router 不含任何业务逻辑,仅做协议翻译:将 HTTP 请求参数组装
为契约层命令/参数,调用 ``use_cases.pairing_management`` 的类型化方法走控制
面管道,再通过 ``raiseOnControlFailure`` 转译失败、``serialize_control_data``
序列化成功结果(模板 A
端点清单:
- GET /pairings PRG-QUERY-01 列出配对审批记录
- POST /pairings PRG-CREATE-01 发起配对请求
- POST /pairings/batch-approve PRG-BATCH-ACT 批量批准配对
- POST /pairings/batch-reject PRG-BATCH-ACT 批量拒绝配对
- POST /pairings/clean-expired PRG-CLEAN-EXPIRED 清理过期配对
- GET /pairings/stats PRG-STATS 配对统计
- GET /pairings/{pairing_id} PRG-01 查询配对详情
- POST /pairings/{pairing_id}/approve PRG-ACT-01 批准配对FR-33
- POST /pairings/{pairing_id}/reject PRG-ACT-02 拒绝配对
- POST /pairings/{pairing_id}/revoke PRG-ACT-03 撤销配对
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Body, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.pairing import (
BatchPairingCmd,
CleanExpiredPairingsCmd,
PairingQuery,
PairingStatsQuery,
PairingStatus,
)
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
Granularity,
build_operator,
get_channel_use_cases,
parse_datetime,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
pairing_router = APIRouter(tags=["channels-pairing"])
# ---------------- Request Schemas ----------------
class CreatePairingRequest(BaseModel):
"""发起配对请求的请求体。"""
model_config = ConfigDict(frozen=True)
channel_type: ChannelType = Field(..., description="渠道类型(必填)")
account_id: str = Field(..., min_length=1, description="渠道账户 ID")
peer_id: str = Field(..., min_length=1, description="对端 ID")
peer_name: str | None = Field(default=None, max_length=128, description="对端名称")
expires_in_seconds: int = Field(default=604800, ge=1, le=2592000, description="配对有效期默认7天")
class PairingActionRequest(BaseModel):
"""配对审批操作(批准/拒绝)的请求体。"""
model_config = ConfigDict(frozen=True)
approver_id: str | None = Field(default=None, min_length=1, description="审批人 ID缺省取 operator.user_id")
reason: str | None = Field(default=None, max_length=512, description="操作原因(审计用)")
class PairingRevokeRequest(BaseModel):
"""配对撤销操作的请求体。"""
model_config = ConfigDict(frozen=True)
reason: str | None = Field(default=None, max_length=512, description="撤销原因(审计用)")
class BatchPairingActionRequest(BaseModel):
"""批量配对审批操作(批准/拒绝的请求体PRG-BATCH-ACT
``pairing_ids`` 至少 1 条、最多 500 条,避免单次请求过大。
"""
model_config = ConfigDict(frozen=True)
pairing_ids: list[str] = Field(..., min_length=1, max_length=500, description="配对 ID 列表1-500 条)")
reason: str | None = Field(default=None, max_length=512, description="操作原因(审计用)")
class CleanExpiredPairingsRequest(BaseModel):
"""清理过期配对的请求体PRG-CLEAN-EXPIRED
``max_count`` 限定单次清理规模1-1000默认 500``older_than`` 可选
过期时间上界ISO 8601缺省取当前时间
"""
model_config = ConfigDict(frozen=True)
max_count: int = Field(default=500, ge=1, le=1000, description="单次最大清理数1-1000默认 500")
older_than: str | None = Field(default=None, description="过期时间上界ISO 8601缺省取当前时间")
# ---------------- Endpoints ----------------
@pairing_router.get("/pairings", response_model=dict)
async def list_pairings(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
account_id: str | None = Query(default=None, description="按账户 ID 过滤"),
peer_id: str | None = Query(default=None, description="按对端 ID 过滤"),
status: PairingStatus | None = Query(default=None, description="按状态过滤"),
created_after: str | None = Query(default=None, description="申请时间下界ISO 8601按 requested_at 过滤)"),
created_before: str | None = Query(default=None, description="申请时间上界ISO 8601按 requested_at 过滤)"),
limit: int = Query(default=100, ge=1, le=1000, 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]:
"""列出配对审批记录FR-33PRG-QUERY-01
支持按渠道/账户/对端/状态过滤,按 requested_at 倒序返回。
对应控制面操作 ``pairing/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 = PairingQuery(
channel_type=channel_type,
account_id=account_id,
peer_id=peer_id,
status=status,
created_after=created_after_dt,
created_before=created_before_dt,
limit=limit,
offset=offset,
)
result = await use_cases.pairing_management.listPairings(query, operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.get("/pairings/count", response_model=dict)
async def count_pairings(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
account_id: str | None = Query(default=None, description="按账户 ID 过滤"),
peer_id: str | None = Query(default=None, description="按对端 ID 过滤"),
status: PairingStatus | None = Query(default=None, description="按状态过滤"),
created_after: str | None = Query(default=None, description="申请时间下界ISO 8601按 requested_at 过滤)"),
created_before: str | None = Query(default=None, description="申请时间上界ISO 8601按 requested_at 过滤)"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""统计配对审批记录数FR-33 待办角标)。
支持按渠道/账户/对端/状态过滤,返回单个整数计数。
对应控制面操作 ``pairing/count``。
"""
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 = PairingQuery(
channel_type=channel_type,
account_id=account_id,
peer_id=peer_id,
status=status,
created_after=created_after_dt,
created_before=created_before_dt,
limit=1,
offset=0,
)
result = await use_cases.pairing_management.countPairings(query, operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings", response_model=dict)
async def create_pairing(
payload: CreatePairingRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""发起配对请求PRG-CREATE-01
创建 DM 安全配对请求,等待审批流程。
对应控制面操作 ``pairing/create``。
"""
operator = build_operator(current_user, request)
result = await use_cases.pairing_management.createPairing(
channel_type=payload.channel_type,
account_id=payload.account_id,
peer_id=payload.peer_id,
operator=operator,
peer_name=payload.peer_name,
expires_in_seconds=payload.expires_in_seconds,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/batch-approve", response_model=dict)
async def batch_approve_pairings(
payload: BatchPairingActionRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量批准 DM 安全配对PRG-BATCH-ACT
逐条独立事务复用单条 approve 逻辑(含降级检查 FR-36非 PENDING 配对
逐条记入 failed。对应控制面操作 ``pairing/batch_approve``。
响应:``{total, succeeded: [{pairing_id, status}], failed: [{id, error_code, message}]}``。
"""
operator = build_operator(current_user, request)
cmd = BatchPairingCmd(
pairing_ids=tuple(payload.pairing_ids),
operator=operator,
reason=payload.reason,
)
result = await use_cases.pairing_management.batchApprovePairings(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/batch-reject", response_model=dict)
async def batch_reject_pairings(
payload: BatchPairingActionRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量拒绝 DM 安全配对PRG-BATCH-ACT
逐条独立事务复用单条 reject 逻辑(含降级检查 FR-36非 PENDING 配对
逐条记入 failed。对应控制面操作 ``pairing/batch_reject``。
响应:``{total, succeeded: [{pairing_id, status}], failed: [{id, error_code, message}]}``。
"""
operator = build_operator(current_user, request)
cmd = BatchPairingCmd(
pairing_ids=tuple(payload.pairing_ids),
operator=operator,
reason=payload.reason,
)
result = await use_cases.pairing_management.batchRejectPairings(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/clean-expired", response_model=dict)
async def clean_expired_pairings(
payload: CleanExpiredPairingsRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""清理过期 DM 安全配对PRG-CLEAN-EXPIRED
查询过期 PENDING 配对,单一事务内批量更新为 EXPIRED 状态并发布
``PairingExpired`` 事件COOPERATIVE fail-closed单条失败回滚整批
对应控制面操作 ``pairing/clean_expired``。
响应:``{total, cleaned: [pairing_id, ...]}``。
"""
operator = build_operator(current_user, request)
cmd = CleanExpiredPairingsCmd(
operator=operator,
older_than=parse_datetime("older_than", payload.older_than),
max_count=payload.max_count,
)
result = await use_cases.pairing_management.cleanExpiredPairings(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.get("/pairings/stats", response_model=dict)
async def get_pairing_stats(
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",
),
granularity: Granularity = Query(default="day", description="时间粒度"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""配对审批统计PRG-STATS
对应控制面操作 ``pairing/stats``(由 ``ChannelControlService.getPairingStats``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
聚合配对审批记录,计算申请/批准/拒绝/撤销/过期计数、批准率、平均审批
时长与按时间粒度切片的趋势。``start_time`` / ``end_time`` 可选,
同时提供时需满足 ``start_time < end_time``。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 → 协议翻译
``parse_datetime``)→ 构造 ``PairingStatsQuery`` →
``use_cases.pairing_management.getPairingStats`` → ``raiseOnControlFailure``
转译失败 → ``serialize_control_data`` 序列化统计结果返回。
"""
operator = build_operator(current_user, request)
query = PairingStatsQuery(
channel_type=channel_type,
start_time=parse_datetime("start_time", start_time),
end_time=parse_datetime("end_time", end_time),
granularity=granularity,
)
result = await use_cases.pairing_management.getPairingStats(query=query, operator=operator)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.get("/pairings/{pairing_id}", response_model=dict)
async def get_pairing(
pairing_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""按 pairing_id 查询配对详情PRG-01
供管理员审批前核对配对信息。对应控制面操作 ``pairing/get``。
"""
operator = build_operator(current_user, request)
result = await use_cases.pairing_management.getPairing(
pairing_id=pairing_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/{pairing_id}/approve", response_model=dict)
async def approve_pairing(
pairing_id: str,
request: Request,
payload: PairingActionRequest | None = Body(default=None),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批准 DM 安全配对FR-33PRG-ACT-01
仅 PENDING 状态可批准;过期配对先惰性转为 EXPIRED 再抛 RuleViolationError
降级期间渠道的配对审批暂停FR-36返回 503。
对应控制面操作 ``pairing/approve``。
"""
operator = build_operator(current_user, request)
approver_id = payload.approver_id if payload else None
reason = payload.reason if payload else None
result = await use_cases.pairing_management.approvePairing(
pairing_id=pairing_id,
operator=operator,
approver_id=approver_id,
reason=reason,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/{pairing_id}/reject", response_model=dict)
async def reject_pairing(
pairing_id: str,
request: Request,
payload: PairingActionRequest | None = Body(default=None),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""拒绝配对PRG-ACT-02
仅 PENDING 状态可拒绝reason 缺省填充 "rejected by operator"
对应控制面操作 ``pairing/reject``。
"""
operator = build_operator(current_user, request)
approver_id = payload.approver_id if payload else None
reason = payload.reason if payload else None
result = await use_cases.pairing_management.rejectPairing(
pairing_id=pairing_id,
operator=operator,
approver_id=approver_id,
reason=reason,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@pairing_router.post("/pairings/{pairing_id}/revoke", response_model=dict)
async def revoke_pairing(
pairing_id: str,
request: Request,
payload: PairingRevokeRequest | None = Body(default=None),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""撤销已批准配对PRG-ACT-03
仅 APPROVED 状态可撤销reason 缺省填充 "revoked by {operator.user_id}"
对应控制面操作 ``pairing/revoke``。
"""
operator = build_operator(current_user, request)
reason = payload.reason if payload else None
result = await use_cases.pairing_management.revokePairing(
pairing_id=pairing_id,
operator=operator,
reason=reason,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}