ForcePilot/backend/server/routers/channels/directory_router.py

254 lines
11 KiB
Python
Raw Normal View History

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