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相关端点,重构路由路径与校验逻辑
254 lines
11 KiB
Python
254 lines
11 KiB
Python
"""目录查询域 Router(FR-14,DIR-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-14,DIR-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-14,DIR-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-14,DIR-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-14,DIR-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-14,DIR-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)}
|