2026-07-02 03:22:12 +08:00
|
|
|
|
"""目录查询 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
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from typing import Any, Literal
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
|
|
|
|
from yuxi.channels.contract.dtos.common import Operator
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from yuxi.channels.contract.errors import ValidationError
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: 渠道侧元数据(可选)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
type: DirectoryEntryType
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
metadata: dict[str, Any] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DirectoryQuery:
|
|
|
|
|
|
"""目录查询。
|
|
|
|
|
|
|
|
|
|
|
|
描述目录查询请求,包括关键词、游标与分页限制,用于 FR-14 目录
|
|
|
|
|
|
查询的分页检索。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
keyword: 关键词(可选)。
|
|
|
|
|
|
cursor: 游标(可选)。
|
|
|
|
|
|
limit: 分页限制(默认 20)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
keyword: str | None = None
|
|
|
|
|
|
cursor: str | None = None
|
|
|
|
|
|
limit: int = 20
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验 limit 为正整数。
|
|
|
|
|
|
|
|
|
|
|
|
``limit`` 必须为正整数,在构造时即抛出 ``ValidationError``,
|
|
|
|
|
|
adapter 不再做该校验(INV-8)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.limit <= 0:
|
|
|
|
|
|
raise ValidationError("limit", "must be a positive integer")
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DirectorySearchResult:
|
|
|
|
|
|
"""目录搜索结果。
|
|
|
|
|
|
|
|
|
|
|
|
描述目录查询的结果,包括条目列表与可选的下一页游标,用于 FR-14
|
|
|
|
|
|
目录查询的分页返回。集合字段使用 tuple 以保证不可变。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
entries: 条目列表。
|
|
|
|
|
|
next_cursor: 下一页游标(可选)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
entries: tuple[DirectoryEntry, ...]
|
|
|
|
|
|
next_cursor: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-07-03 19:18:13 +08:00
|
|
|
|
role: Literal["owner", "admin", "member"] | None = None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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 ClearDirectoryCacheCmd:
|
|
|
|
|
|
"""清理目录缓存命令(DIR-CACHE-CLEAR)。
|
|
|
|
|
|
|
|
|
|
|
|
由 ``DirectoryPort.clearDirectoryCache`` 引用,清理指定范围的目录缓存,
|
|
|
|
|
|
需记录操作人以满足审计要求。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型。
|
|
|
|
|
|
account_id: 渠道账户 ID。
|
|
|
|
|
|
operator: 操作人(审计用)。
|
|
|
|
|
|
scope: 清理范围(``users`` / ``groups`` / ``all``)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: ChannelType
|
|
|
|
|
|
account_id: str
|
|
|
|
|
|
operator: Operator
|
2026-07-03 19:18:13 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class ClearDirectoryCacheResult:
|
|
|
|
|
|
"""清理目录缓存结果(DIR-CACHE-CLEAR)。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
cleared_keys: 清理的缓存 key 数量。
|
|
|
|
|
|
cleared_at: 清理时间戳。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
cleared_keys: int
|
|
|
|
|
|
cleared_at: datetime
|