ForcePilot/backend/server/routers/channels/login_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

327 lines
14 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.

"""登录流程域 RouterLGN-QR-01 / LGN-QR-02 / LGN-01 / LGN-02 / LGN-OUT-01 / LGN-STATUS / LGN-FORCE-LOGOUT
本 router 实现渠道账户登录流程域的全部 HTTP 端点,覆盖扫码启动、扫码轮询、
取消扫码、刷新二维码、登出、登录态查询、强制下线共 7 个操作。所有端点统一
采用模板 A控制面端口路由通过 ``get_channel_use_cases`` 装配
``ChannelUseCases``,经 ``login`` 端口调用类型化方法,由
``ChannelControlService`` 内部走控制面管道
auth → permission → rate_limit → dispatch → audit
鉴权策略:除 ``LGN-FORCE-LOGOUT`` 外的全部端点使用 ``get_admin_user`` 依赖,
要求管理员或超级管理员角色;``LGN-FORCE-LOGOUT`` 涉及强制下线与可选凭据吊销,
属高风险操作,使用 ``get_superadmin_user`` 依赖,仅允许超级管理员(契约
§LGN-FORCE-LOGOUT @pre。角色校验由依赖函数完成控制面管道
``PermissionStage`` 在 ``OPERATION_PERMISSIONS_EXACT`` 中同步注册
``login/force_logout`` 为 superadmin 要求,形成双重保险(规范 §4
模板选型:模板 A控制面端口路由——端点函数体仅做 ``operator = build_operator``
→ ``use_cases.login.<method>`` → ``raiseOnControlFailure`` →
``return {"success": True, "data": serialize_control_data(result.data)}``
不包含 try/except 吞异常、直接 DB 访问、ORM 模型返回或 ``executeControl``
直接调用。
路径设计:所有端点共享 ``/{channel_type}/accounts/{account_id}/login/`` 前缀,
其下 ``qr/start``、``qr/wait``、``qr/cancel``、``qr/refresh``、``logout``、
``status``、``force-logout`` 均为静态路径段,无静态/动态路径冲突;子 router
不自行设置 prefix根前缀 ``/channels`` 由 ``channels_router`` 聚合 router
统一追加。
端点清单对应《登录流程域设计方案》§2.1
- POST /{channel_type}/accounts/{account_id}/login/qr/start LGN-QR-01 start_qr_login
- GET /{channel_type}/accounts/{account_id}/login/qr/wait LGN-QR-02 wait_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/cancel LGN-01 cancel_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/refresh LGN-02 refresh_qr_login
- POST /{channel_type}/accounts/{account_id}/login/logout LGN-OUT-01 logout
- GET /{channel_type}/accounts/{account_id}/login/status LGN-STATUS get_login_status
- POST /{channel_type}/accounts/{account_id}/login/force-logout LGN-FORCE-LOGOUT force_logout
"""
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.login import ForceLogoutCmd
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
login_router = APIRouter(tags=["channels-login"])
class ForceLogoutRequest(BaseModel):
"""强制下线请求体LGN-FORCE-LOGOUT"""
model_config = ConfigDict(frozen=True)
reason: str = Field(
...,
min_length=1,
max_length=500,
description="强制下线原因安全审计用必填1~500 字符)",
)
revoke_credentials: bool = Field(
default=False,
description="是否同时吊销凭据(默认 False",
)
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/start",
response_model=dict,
)
async def start_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
force: bool = Query(
default=False,
description="是否强制启动新会话(取消既有活跃会话)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启动扫码登录LGN-QR-01
对应控制面操作 ``login/qr_start``(由 ``ChannelControlService.startQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.startQrLogin`` 调用端口方法 → 控制面管道调度插件
``LoginAdapter.loginWithQrStart`` 生成会话与二维码 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
会话与二维码数据返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.startQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
force=force,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/qr/wait",
response_model=dict,
)
async def wait_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="扫码会话 ID"),
current_qr_data_url: str | None = Query(
default=None,
description="客户端当前持有的二维码 URL用于变更检测",
),
timeout_ms: int | None = Query(
default=None,
ge=1000,
le=60000,
description="长轮询超时(毫秒)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""轮询扫码登录状态LGN-QR-02
对应控制面操作 ``login/qr_wait``(由 ``ChannelControlService.waitQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
采用 long-polling 模式:服务端在 ``timeout_ms`` 窗口内阻塞等待状态变化
(已扫码 / 已确认 / 已过期 / 二维码刷新),状态变化或超时立即返回,由
客户端再次发起轮询。``current_qr_data_url`` 用于变更检测,仅当二维码
已刷新时返回新的 URL。编排链路HTTP 入参 → ``build_operator`` →
``use_cases.login.waitQrLogin`` → 控制面管道调度插件
``LoginAdapter.loginWithQrWait`` → ``ControlResult`` 经 ``raiseOnControlFailure``
转译失败 → ``serialize_control_data`` 序列化会话状态返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.waitQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
current_qr_data_url=current_qr_data_url,
timeout_ms=timeout_ms,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/cancel",
response_model=dict,
)
async def cancel_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待取消的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""取消扫码登录LGN-01
对应控制面操作 ``login/qr_cancel``(由 ``ChannelControlService.cancelQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.cancelQrLogin`` 调用端口方法 → 控制面管道调度
``QrLoginService.cancelQrLogin`` 推进本地状态机至 CANCELLED 并清理
内存会话映射(不调用插件 LoginAdapter→ ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
会话终结结果返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.cancelQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/refresh",
response_model=dict,
)
async def refresh_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待刷新的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""刷新扫码二维码LGN-02
对应控制面操作 ``login/qr_refresh``(由 ``ChannelControlService.refreshQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.refreshQrLogin`` 调用端口方法 → 控制面管道调度
``QrLoginService.refreshQrLogin`` 复用插件 ``LoginAdapter.loginWithQrStart``
重新生成二维码并刷新过期时间 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
新二维码数据返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.refreshQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/logout",
response_model=dict,
)
async def logout(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""登出渠道账户LGN-OUT-01
对应控制面操作 ``login/logout``(由 ``ChannelControlService.logout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.logout`` 调用端口方法 → 控制面管道调度插件
``LoginAdapter.logout`` 清理渠道侧会话与凭据 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
登出结果返回。幂等性:未登录或已登出的账户再次调用幂等返回成功。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.logout(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/status",
response_model=dict,
)
async def get_login_status(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询账户登录态LGN-STATUS
对应控制面操作 ``login/status``(由 ``ChannelControlService.getLoginStatus``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
供管理员排查"机器人无响应"时确认账户扫码会话状态,仅返回内存态扫码
会话信息(不查询渠道侧 API轻量查询。编排链路HTTP 入参 →
``build_operator`` 构造操作人 → ``use_cases.login.getLoginStatus`` 调用
端口方法 → 控制面管道调度 ``QrLoginService`` 查询内存态扫码会话 →
``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 →
``serialize_control_data`` 序列化登录态结果返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.getLoginStatus(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/force-logout",
response_model=dict,
)
async def force_logout(
channel_type: ChannelType,
account_id: str,
request: Request,
body: ForceLogoutRequest,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""强制下线渠道账户LGN-FORCE-LOGOUT
对应控制面操作 ``login/force_logout``(由 ``ChannelControlService.forceLogout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
超级管理员发起的强制下线可选吊销凭据。编排链路HTTP 入参 →
``build_operator`` 构造操作人 → 构造 ``ForceLogoutCmd`` →
``use_cases.login.forceLogout`` 调用端口方法 → 控制面管道校验
superadmin 权限与账户存在性 / 登录态 → 调度
``QrLoginService.forceLogout``(内部检查适配器是否实现 ``forceLogout``
+ 调用适配器)→ 发布 ``ChannelAccountOffline`` 事件 →
``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 →
``serialize_control_data`` 序列化强制下线结果返回。
"""
operator = build_operator(current_user, request)
cmd = ForceLogoutCmd(
channel_type=channel_type,
account_id=account_id,
operator=operator,
reason=body.reason,
revoke_credentials=body.revoke_credentials,
)
result = await use_cases.login.forceLogout(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}