"""目录查询域 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)}