ForcePilot/backend/server/routers/channels/directory_router.py
Kris e5e9f45411 refactor(channel-routers): 批量优化各渠道路由代码与契约对齐
1. config_router: 为expected_version添加ge=1校验
2. directory_router: 补充scope校验逻辑与注释
3. login_router: 拆分强制下线权限,添加参数校验与注释更新
4. reports_router: 统一时间参数处理,修复分页限制使用契约常量
5. dashboard_router: 更新文档与响应格式,修正参数传递逻辑
6. health_router: 缩减健康检查响应字段,修复响应结构与参数校验
7. plugin_router: 新增插件目录端点,补充枚举校验与注释
8. pairing_router: 新增时间过滤参数,补充参数校验
9. __init__.py: 修复异常映射,更新trace_id获取逻辑与工具类
10. doctor_router: 重构单项检查端点,修正注释与校验逻辑
11. account_router: 新增恢复降级账户端点,补充批量操作校验
12. webhook_router: 优化webhook处理逻辑,修复流式读取与响应逻辑
13. content_review_router: 补充批量审核端点,完善参数校验与注释
14. analytics_router: 修正管道阶段描述,统一参数传递
15. wizard_router: 新增OAuth相关端点,重构路由路径与校验逻辑
2026-07-04 00:16:00 +08:00

254 lines
11 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.

"""目录查询域 RouterFR-14DIR-QUERY-01~04 + DIR-01 + DIR-CACHE-CLEAR
本 router 实现渠道通讯录目录查询的全部 HTTP 端点,覆盖用户搜索 / 群组搜索 /
用户资料 / 群组详情 / 群组成员 / 缓存清理共 6 个操作。所有端点统一采用模板 A
(控制面端口路由),通过 ``get_channel_use_cases`` 装配 ``ChannelUseCases``
经 ``directory`` 端口调用类型化方法,由 ``ChannelControlService._executeControl``
内部走控制面管道auth → permission → rate_limit → dispatch → audit
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
角色校验由依赖函数完成router 内不做角色判断(规范 §4
路径设计:静态后缀路径(``/search``)先于动态单段路径(``/{peer_id}`` /
``/{group_id}``)声明,动态单段先于多段子资源路径(``/{group_id}/members``
声明(规范 §6.5),避免静态路径被动态参数捕获。子 router 不自行设置 prefix
根前缀 ``/channels`` 由 ``channels_router`` 聚合 router 统一追加。
端点清单对应《05-目录查询域设计方案 v1.1》§2.3
- GET /{channel_type}/{account_id}/directory/users/search DIR-QUERY-01 search_directory_users
- GET /{channel_type}/{account_id}/directory/groups/search DIR-QUERY-02 search_directory_groups
- GET /{channel_type}/{account_id}/directory/users/{peer_id} DIR-QUERY-03 get_directory_user_profile
- GET /{channel_type}/{account_id}/directory/groups/{group_id} DIR-01 get_directory_group_detail
- GET /{channel_type}/{account_id}/directory/groups/{group_id}/members DIR-QUERY-04 get_directory_group_members
- DELETE /{channel_type}/{account_id}/directory/cache DIR-CACHE-CLEAR clear_directory_cache
"""
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.directory import ClearDirectoryCacheCmd, DirectoryQuery
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
directory_router = APIRouter(tags=["channels-directory"])
# ---------------- Endpoints ----------------
@directory_router.get(
"/{channel_type}/{account_id}/directory/users/search",
response_model=dict,
)
async def search_directory_users(
channel_type: ChannelType,
account_id: str,
request: Request,
keyword: str | None = Query(default=None, description="搜索关键词"),
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""搜索渠道用户目录FR-14DIR-QUERY-01
5min TTL 缓存。渠道未注册目录适配器或目录开关关闭返回 501。
对应控制面操作 ``directory/users``(由 ``ChannelControlService.searchDirectoryUsers``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
query = DirectoryQuery(keyword=keyword, cursor=cursor, limit=limit)
result = await use_cases.directory.searchDirectoryUsers(
channel_type=channel_type,
account_id=account_id,
query=query,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@directory_router.get(
"/{channel_type}/{account_id}/directory/groups/search",
response_model=dict,
)
async def search_directory_groups(
channel_type: ChannelType,
account_id: str,
request: Request,
keyword: str | None = Query(default=None, description="搜索关键词"),
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""搜索渠道群组目录FR-14DIR-QUERY-02
5min TTL 缓存。对应控制面操作 ``directory/groups``(由
``ChannelControlService.searchDirectoryGroups`` 内部构造 ``ControlCmd``
并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
query = DirectoryQuery(keyword=keyword, cursor=cursor, limit=limit)
result = await use_cases.directory.searchDirectoryGroups(
channel_type=channel_type,
account_id=account_id,
query=query,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@directory_router.get(
"/{channel_type}/{account_id}/directory/users/{peer_id}",
response_model=dict,
)
async def get_directory_user_profile(
channel_type: ChannelType,
account_id: str,
peer_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询渠道用户资料FR-14DIR-QUERY-03
按对端 ID 查询。5min TTL 缓存。对应控制面操作 ``directory/user_profile``
(由 ``ChannelControlService.getDirectoryUserProfile`` 内部构造
``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
协议翻译HTTP 路径参数 ``peer_id`` 映射为用例方法 ``user_id`` 入参,
以对齐 ``DirectoryPort.getDirectoryUserProfile`` 的契约命名。
"""
operator = build_operator(current_user, request)
result = await use_cases.directory.getDirectoryUserProfile(
channel_type=channel_type,
account_id=account_id,
user_id=peer_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@directory_router.get(
"/{channel_type}/{account_id}/directory/groups/{group_id}",
response_model=dict,
)
async def get_directory_group_detail(
channel_type: ChannelType,
account_id: str,
group_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询渠道群组详情FR-14DIR-01
按 group_id 查询。5min TTL 缓存。对应控制面操作 ``directory/group_detail``
(由 ``ChannelControlService.getDirectoryGroupDetail`` 内部构造
``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.directory.getDirectoryGroupDetail(
channel_type=channel_type,
account_id=account_id,
group_id=group_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@directory_router.get(
"/{channel_type}/{account_id}/directory/groups/{group_id}/members",
response_model=dict,
)
async def get_directory_group_members(
channel_type: ChannelType,
account_id: str,
group_id: str,
request: Request,
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询渠道群组成员FR-14DIR-QUERY-04
5min TTL 缓存。对应控制面操作 ``directory/group_members``(由
``ChannelControlService.getDirectoryGroupMembers`` 内部构造 ``ControlCmd``
并委托 ``_executeControl`` 执行控制面管道)。
分页参数仅含 ``cursor`` 与 ``limit``,构造 ``DirectoryQuery`` 时不传
``keyword``(群组成员查询无关键词语义)。
"""
operator = build_operator(current_user, request)
query = DirectoryQuery(cursor=cursor, limit=limit)
result = await use_cases.directory.getDirectoryGroupMembers(
channel_type=channel_type,
account_id=account_id,
group_id=group_id,
query=query,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@directory_router.delete(
"/{channel_type}/{account_id}/directory/cache",
response_model=dict,
)
async def clear_directory_cache(
channel_type: ChannelType,
account_id: str,
request: Request,
scope: str = Query(
default="all",
description="清理范围users / groups / all",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""清理目录缓存DIR-CACHE-CLEAR
对应控制面操作 ``directory/cache_clear``(由
``ChannelControlService.clearDirectoryCache`` 内部构造 ``ControlCmd``
并委托 ``_executeControl`` 执行控制面管道)。清理指定渠道账户的目录
查询缓存(用户 / 群组 / 全部),供管理员在渠道侧数据变更后手动刷新
缓存。编排链路HTTP 入参 → ``build_operator`` 构造操作人 → 构造
``ClearDirectoryCacheCmd`` → ``use_cases.directory.clearDirectoryCache``
调用端口方法 → 控制面管道检查适配器是否实现 ``clearCache`` → 调用
适配器清理 + ``CachePort`` 失效本地缓存 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
清理结果返回。适配器未实现 ``clearCache`` 时返回 501。
scope 枚举校验在 ``ClearDirectoryCacheCmd.__post_init__`` 构造时即执行,
非法取值直接抛 400 ``ValidationError``router 路径,未进入控制面管道);
handler 层 ``_directoryCacheClear`` 同步校验作为非 router 调用方的
防御兜底,与 ``user_id`` / ``group_id`` 等业务参数的分层校验策略一致。
"""
operator = build_operator(current_user, request)
cmd = ClearDirectoryCacheCmd(
channel_type=channel_type,
account_id=account_id,
operator=operator,
scope=scope,
)
result = await use_cases.directory.clearDirectoryCache(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}