ForcePilot/backend/package/yuxi/channels/contract/dtos/directory.py

317 lines
9.4 KiB
Python
Raw Normal View History

"""目录查询 DTO。
定义渠道通讯录目录查询相关的不可变值对象包括目录条目类型枚举
渠道用户渠道群组群组成员目录条目目录查询与目录搜索结果所有 DTO
均为 ``dataclass(frozen=True)``仅依赖标准库用于渠道通讯录
查询与用户 / 群组检索FR-14集合字段使用 tuple 以保证
frozen dataclass 的不可变语义
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.common import Operator
from yuxi.channels.contract.errors import ValidationError
class DirectoryEntryType(StrEnum):
"""目录条目类型。
标识目录条目的类型用于区分用户与群组继承 ``str, Enum``
支持 JSON 序列化与字符串比较
取值
USER: 用户
GROUP: 群组
"""
USER = "user"
GROUP = "group"
@dataclass(frozen=True)
class ChannelUser:
"""渠道用户。
描述渠道侧用户信息包括对端 ID名称与可选的头像邮箱电话
用于目录查询结果展示与身份关联
字段
peer_id: 对端 ID
name: 名称
avatar_url: 头像 URL可选
email: 邮箱可选
phone: 电话可选
"""
peer_id: str
name: str
avatar_url: str | None = None
email: str | None = None
phone: str | None = None
@dataclass(frozen=True)
class ChannelGroup:
"""渠道群组。
描述渠道侧群组信息包括群组 ID名称与成员数用于目录查询
结果展示
字段
group_id: 群组 ID
name: 名称
member_count: 成员数默认 0
"""
group_id: str
name: str
member_count: int = 0
@dataclass(frozen=True)
class DirectoryEntry:
"""目录条目。
描述目录查询的统一条目包括类型ID名称与可选元数据用于
目录搜索结果的统一展示
字段
type: 条目类型user | group
id: 条目 ID
name: 名称
metadata: 渠道侧元数据可选
avatar_url: 头像 URL可选
alias: 别名可选
remark: 备注可选
tags: 标签列表可选
member_count: 成员数可选
owner_id: 所有者 ID可选
extra_metadata: 扩展元数据可选
"""
type: DirectoryEntryType
id: str
name: str
metadata: dict[str, Any] | None = None
avatar_url: str | None = None
alias: str | None = None
remark: str | None = None
tags: tuple[str, ...] | None = None
member_count: int | None = None
owner_id: str | None = None
extra_metadata: dict[str, Any] | None = None
@dataclass(frozen=True)
class DirectoryQuery:
"""目录查询。
描述目录查询请求包括关键词游标与分页限制用于 FR-14 目录
查询的分页检索
字段
keyword: 关键词可选
cursor: 游标可选
limit: 分页限制默认 20
sort_by: 排序字段可选
sort_order: 排序方向可选asc desc
"""
keyword: str | None = None
cursor: str | None = None
limit: int = 20
sort_by: str | None = None
sort_order: Literal["asc", "desc"] | None = None
def __post_init__(self) -> None:
"""校验 limit 为正整数与 sort_order 取值。
``limit`` 必须为正整数``sort_order`` 必须为 ``asc`` /
``desc`` / ``None`` 之一在构造时即抛出 ``ValidationError``
adapter 不再做该校验INV-8
"""
if self.limit <= 0:
raise ValidationError("limit", "must be a positive integer")
if self.sort_order is not None and self.sort_order not in ("asc", "desc"):
raise ValidationError("sort_order", "must be one of: asc, desc")
@dataclass(frozen=True)
class DirectorySearchResult:
"""目录搜索结果。
描述目录查询的结果包括条目列表与可选的下一页游标用于 FR-14
目录查询的分页返回集合字段使用 tuple 以保证不可变
字段
entries: 条目列表
next_cursor: 下一页游标可选
"""
entries: tuple[DirectoryEntry, ...]
next_cursor: str | None = None
@dataclass(frozen=True)
class DirectorySearchSuggestionQuery:
"""目录搜索建议查询。
描述目录搜索建议请求包括关键词与返回数量限制用于根据关键词
快速返回匹配的目录条目建议
字段
keyword: 关键词
limit: 返回数量限制默认 10
"""
keyword: str
limit: int = 10
def __post_init__(self) -> None:
"""校验 keyword 非空且 limit 为正整数。"""
if not self.keyword:
raise ValidationError("keyword", "must not be empty")
if self.limit <= 0:
raise ValidationError("limit", "must be a positive integer")
@dataclass(frozen=True)
class DirectorySearchSuggestionResult:
"""目录搜索建议结果。
描述目录搜索建议的返回结果包括匹配的目录条目列表集合字段
使用 tuple 以保证不可变
字段
entries: 条目列表
"""
entries: tuple[DirectoryEntry, ...]
@dataclass(frozen=True)
class GroupMember:
"""群组成员。
描述渠道群组中的单个成员信息包括用户 ID群组 ID角色与加入时间
用于 ``DirectoryAdapter.getGroupMembers`` 方法的返回元素FR-14
字段
user_id: 用户 ID
group_id: 群组 ID
role: 成员角色可选 "owner" / "admin" / "member"
joined_at: 加入时间可选
"""
user_id: str
group_id: str
role: Literal["owner", "admin", "member"] | None = None
joined_at: datetime | None = None
@dataclass(frozen=True)
class GroupMemberResult:
"""群组成员查询结果。
描述群组成员查询的分页结果包括成员列表与可选的下一页游标用于
``DirectoryAdapter.getGroupMembers`` 方法的返回值FR-14集合字段
使用 tuple 以保证不可变
字段
members: 成员列表
next_cursor: 下一页游标可选
"""
members: tuple[GroupMember, ...]
next_cursor: str | None = None
@dataclass(frozen=True)
class BatchDirectoryProfilesQuery:
"""批量目录资料查询。
用于路由绑定列表等场景把多个 peer_id可能是用户或群组 ID
一次性翻译成可读的名称头像等资料减少前端并发请求
字段
peer_ids: 待查询的对端 ID 集合去重后传入顺序无关
types: 按条目类型过滤可选
"""
peer_ids: tuple[str, ...]
types: tuple[DirectoryEntryType, ...] | None = None
def __post_init__(self) -> None:
"""校验 peer_ids 非空且元素均为非空字符串。"""
if not self.peer_ids:
raise ValidationError("peer_ids", "must not be empty")
if any(not isinstance(pid, str) or not pid for pid in self.peer_ids):
raise ValidationError("peer_ids", "all peer_ids must be non-empty strings")
@dataclass(frozen=True)
class BatchDirectoryProfilesResult:
"""批量目录资料查询结果。
profiles peer_id keyvalue 为统一目录条目type 区分 user/group
metadata 携带用户/群组的原始字段未命中的 peer_id 不会出现在结果中
"""
profiles: dict[str, DirectoryEntry]
@dataclass(frozen=True)
class ClearDirectoryCacheCmd:
"""清理目录缓存命令DIR-CACHE-CLEAR
``DirectoryPort.clearDirectoryCache`` 引用清理指定范围的目录缓存
需记录操作人以满足审计要求
字段
channel_type: 渠道类型
account_id: 渠道账户 ID
operator: 操作人审计用
scope: 清理范围``users`` / ``groups`` / ``all``
"""
channel_type: ChannelType
account_id: str
operator: Operator
scope: Literal["users", "groups", "all"]
def __post_init__(self) -> None:
"""校验必填字段非空与 scope 取值DIR-CACHE-CLEAR
``account_id`` ``scope`` 必须非空``scope`` 必须为 ``users`` /
``groups`` / ``all`` 之一在构造时即抛出 ``ValidationError``
adapter 不再做该校验INV-8
"""
if not self.account_id:
raise ValidationError("account_id", "must not be empty")
if not self.scope:
raise ValidationError("scope", "must not be empty")
if self.scope not in ("users", "groups", "all"):
raise ValidationError(
"scope",
"must be one of: users, groups, all",
)
@dataclass(frozen=True)
class ClearDirectoryCacheResult:
"""清理目录缓存结果DIR-CACHE-CLEAR
字段
cleared_keys: 清理的缓存 key 数量
cleared_at: 清理时间戳
"""
cleared_keys: int
cleared_at: datetime