本次提交包含多类优化:
1. 移除多个路由文件中多余的空导入行,统一代码格式
2. 重构Query参数定义,将长参数拆分为多行提升可读性
3. 新增多个业务端点:
- 渠道能力画像矩阵查询CAP-03
- 配对审批计数接口用于待办角标
- 批量查询对端目录资料接口
- 向导扫码登录相关端点
- 会话实时事件SSE推送端点
- 工作台待办统计接口
4. 完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5. 调整路由导入顺序,修复动态路由路径冲突隐患
6. 更新文档注释与接口清单,修正部分接口描述细节
529 lines
22 KiB
Python
529 lines
22 KiB
Python
"""会话资源域 Router。
|
||
|
||
提供渠道会话管理的 12 个 HTTP 端点,全部由 ``get_admin_user`` 守门,仅管理员
|
||
可访问。Router 不含任何业务逻辑,仅做协议翻译:将 HTTP 请求参数组装为契约层
|
||
命令/参数,调用 ``use_cases.session_management`` 的类型化方法走控制面管道,
|
||
再通过 ``raiseOnControlFailure`` 转译失败、``serialize_control_data`` 序列化
|
||
成功结果(模板 A)。P3 绑定相关端点不走控制面管道,直接调用
|
||
``use_cases.user_binding`` 用例方法(模板 B),异常由全局异常处理器统一转译。
|
||
|
||
端点清单:
|
||
- GET /sessions SES-QUERY-01 列出渠道会话
|
||
- POST /sessions/batch-close SES-BATCH-CLOSE-01 批量关闭会话
|
||
- GET /sessions/events SES-EVENTS-01 渠道会话实时事件 SSE
|
||
- GET /sessions/{session_id} SES-QUERY-02 查询会话详情
|
||
- POST /sessions/{session_id}/merge SES-05 合并会话(FR-07)
|
||
- POST /sessions/{session_id}/transfer SES-13 转移会话所有者(FR-26)
|
||
- POST /sessions/{session_id}/close SES-CLOSE-01 关闭会话
|
||
- GET /sessions/{session_id}/messages SES-MSG-01 查询会话消息流
|
||
- GET /sessions/{session_id}/stats SES-STATS-01 查询会话统计指标
|
||
- POST /sessions/{session_id}/bind-user SES-BIND-01 绑定系统用户(P3)
|
||
- DELETE /sessions/{session_id}/bind-user SES-UNBIND-01 解绑系统用户(P3)
|
||
- GET /sessions/{session_id}/identity SES-IDENTITY-01 查询会话身份绑定状态(P3)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
from collections.abc import AsyncIterator
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Query, Request
|
||
from fastapi.responses import StreamingResponse
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from yuxi.channels.application.extension.channel_event_broadcaster import (
|
||
ChannelEventBroadcaster,
|
||
)
|
||
from yuxi.channels.contract.dtos.channel import ChannelType, SessionStatus
|
||
from yuxi.channels.contract.dtos.conversation import MergeConversationCmd
|
||
from yuxi.channels.contract.dtos.plugin import DomainEvent
|
||
from yuxi.channels.contract.dtos.session import (
|
||
BatchCloseSessionsCmd,
|
||
CloseSessionCmd,
|
||
OwnerTransferCmd,
|
||
)
|
||
from yuxi.channels.infrastructure import DependencyInjectionContainer
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.routers.channels import (
|
||
build_operator,
|
||
dataclass_to_dict,
|
||
get_channel_di_container,
|
||
get_channel_use_cases,
|
||
parse_datetime,
|
||
raiseOnControlFailure,
|
||
serialize_control_data,
|
||
)
|
||
from server.utils.auth_middleware import get_admin_user
|
||
|
||
session_router = APIRouter(tags=["channels-session"])
|
||
|
||
|
||
class MergeSessionRequest(BaseModel):
|
||
"""合并会话请求体。
|
||
|
||
路径参数 ``session_id`` 作为目标会话 ID,映射为
|
||
``MergeConversationCmd.target_conversation_id``,不在 body 中。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
source_conversation_id: str = Field(..., min_length=1, description="源会话 ID(被合并方)")
|
||
reason: str = Field(default="", max_length=512, description="合并原因(审计用)")
|
||
|
||
|
||
class TransferOwnerRequest(BaseModel):
|
||
"""转移所有者请求体。
|
||
|
||
路径参数 ``session_id`` 作为目标会话 ID,映射为
|
||
``OwnerTransferCmd.conversation_id``,不在 body 中。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
new_owner_id: str = Field(..., min_length=1, description="新所有者对端 ID")
|
||
|
||
|
||
class CloseSessionRequest(BaseModel):
|
||
"""关闭会话请求体。
|
||
|
||
路径参数 ``session_id`` 映射为 ``CloseSessionCmd.session_id``,不在 body 中。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
reason: str | None = Field(default=None, max_length=512, description="关闭原因(审计用)")
|
||
|
||
|
||
class BatchCloseFilter(BaseModel):
|
||
"""批量关闭会话筛选条件(SES-BATCH-CLOSE-01)。
|
||
|
||
``inactive_before`` 在 Router 层经 ``parse_datetime`` 解析为 UTC datetime
|
||
后传入 ``BatchCloseSessionsCmd.filter``,确保时区一致性与统一错误处理。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
channel_type: ChannelType | None = Field(default=None, description="按渠道类型过滤")
|
||
inactive_before: str | None = Field(
|
||
default=None,
|
||
description="最后活动时间早于该时刻(ISO 8601,视为非活跃)",
|
||
)
|
||
status: str | None = Field(
|
||
default=None,
|
||
description="会话状态过滤(active / closed)",
|
||
)
|
||
|
||
|
||
class BatchCloseSessionsRequest(BaseModel):
|
||
"""批量关闭会话请求体(SES-BATCH-CLOSE-01)。
|
||
|
||
``session_ids`` 与 ``filter`` 二者不可同时为空,由
|
||
``BatchCloseSessionsCmd.__post_init__`` 校验(INV-8)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
session_ids: list[str] | None = Field(
|
||
default=None,
|
||
description="显式会话 ID 列表(与 filter 二选一)",
|
||
)
|
||
filter: BatchCloseFilter | None = Field(
|
||
default=None,
|
||
description="筛选条件(含 channel_type / inactive_before / status)",
|
||
)
|
||
max_count: int = Field(default=100, ge=1, le=1000, description="单次最大关闭数")
|
||
reason: str | None = Field(default=None, max_length=512, description="关闭原因(审计用)")
|
||
|
||
|
||
class BindUserRequest(BaseModel):
|
||
"""绑定系统用户请求体(P3 渐进式绑定)。
|
||
|
||
路径参数 ``session_id`` 作为目标会话 ID,不在 body 中。
|
||
``user_uid`` 对应系统用户的 ``User.uid``(``user_type='user'``)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
user_uid: str = Field(..., min_length=1, description="系统用户 UID")
|
||
remark: str | None = Field(default=None, max_length=512, description="绑定备注(审计用)")
|
||
|
||
|
||
@session_router.get("/sessions", response_model=dict)
|
||
async def list_sessions(
|
||
request: Request,
|
||
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤,留空跨渠道查询"),
|
||
peer_id: str | None = Query(default=None, description="按对端 ID 模糊匹配"),
|
||
created_after: str | None = Query(default=None, description="创建时间下界(ISO 8601)"),
|
||
created_before: str | None = Query(default=None, description="创建时间上界(ISO 8601)"),
|
||
owner_peer_id: str | None = Query(default=None, description="按主会话所有者对端 ID 精确匹配"),
|
||
status: SessionStatus | None = Query(default=None, description="会话状态过滤(active/closed)"),
|
||
last_message_after: str | None = Query(default=None, description="最近消息时间下界(ISO 8601)"),
|
||
last_message_before: str | None = Query(default=None, description="最近消息时间上界(ISO 8601)"),
|
||
abnormal: bool = Query(default=False, description="仅返回异常会话(僵尸/账户不可用/无主)"),
|
||
limit: int = Query(default=100, ge=1, le=1000, description="分页大小"),
|
||
offset: int = Query(default=0, ge=0, description="分页偏移"),
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""列出渠道会话(运维排查用例,SES-QUERY-01)。
|
||
|
||
供管理员排查"用户消息未到达"等运维问题。``channel_type`` 留空时跨渠道查询;
|
||
``peer_id`` 提供时按对端 ID 模糊匹配;``created_after`` / ``created_before``
|
||
提供时按会话创建时间范围过滤。对应控制面操作 ``session/list``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
created_after_dt = parse_datetime("created_after", created_after)
|
||
created_before_dt = parse_datetime("created_before", created_before)
|
||
last_message_after_dt = parse_datetime("last_message_after", last_message_after)
|
||
last_message_before_dt = parse_datetime("last_message_before", last_message_before)
|
||
result = await use_cases.session_management.listSessions(
|
||
channel_type=channel_type,
|
||
limit=limit,
|
||
operator=operator,
|
||
offset=offset,
|
||
peer_id=peer_id,
|
||
created_after=created_after_dt,
|
||
created_before=created_before_dt,
|
||
owner_peer_id=owner_peer_id,
|
||
status=status,
|
||
last_message_after=last_message_after_dt,
|
||
last_message_before=last_message_before_dt,
|
||
abnormal=abnormal,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.post("/sessions/batch-close", response_model=dict)
|
||
async def batch_close_sessions(
|
||
payload: BatchCloseSessionsRequest,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量关闭渠道会话(SES-BATCH-CLOSE-01)。
|
||
|
||
支持 ``session_ids`` 显式列表与 ``filter`` 筛选条件两种模式(二者不可
|
||
同时空,由 ``BatchCloseSessionsCmd.__post_init__`` 校验)。逐条独立事务
|
||
复用单条 ``closeSession`` 逻辑(模式 D,部分成功语义),单条失败不回滚
|
||
已成功条目。对应控制面操作 ``session/batch_close``。
|
||
|
||
``filter.inactive_before`` 在本层经 ``parse_datetime`` 解析为 UTC
|
||
datetime,确保时区一致性与统一错误处理(project 硬约束)。
|
||
|
||
静态路径先于 ``/sessions/{session_id}`` 声明,避免被动态参数捕获。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
# 将 BatchCloseFilter 转换为 dict 并解析 inactive_before 为 UTC datetime,
|
||
# 透传至 BatchCloseSessionsCmd.filter(persistence_port.findSessionsByFilter
|
||
# 接受 datetime 或 ISO 8601 字符串,统一为 datetime 避免歧义)
|
||
filter_param: dict[str, Any] | None = None
|
||
if payload.filter is not None:
|
||
filter_param = {
|
||
"channel_type": payload.filter.channel_type,
|
||
"inactive_before": parse_datetime("inactive_before", payload.filter.inactive_before),
|
||
"status": payload.filter.status,
|
||
}
|
||
cmd = BatchCloseSessionsCmd(
|
||
operator=operator,
|
||
session_ids=tuple(payload.session_ids) if payload.session_ids else (),
|
||
filter=filter_param,
|
||
max_count=payload.max_count,
|
||
reason=payload.reason,
|
||
)
|
||
result = await use_cases.session_management.batchCloseSessions(cmd)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.get("/sessions/events")
|
||
async def stream_session_events(
|
||
request: Request,
|
||
di_container: DependencyInjectionContainer = Depends(get_channel_di_container),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> StreamingResponse:
|
||
"""渠道会话/消息实时事件 SSE 端点(BE-14)。
|
||
|
||
返回 ``text/event-stream`` 长连接,将 ``ChannelSessionUpdated`` /
|
||
``ChannelMessageReceived`` / ``ChannelMessageSent`` 三类事件以 SSE 帧
|
||
形式推送给已认证管理员。每 15 秒无事件时发送心跳注释,客户端断开时
|
||
自动取消订阅。
|
||
|
||
本端点静态路径先于 ``/sessions/{session_id}`` 声明,避免被动态参数捕获。
|
||
"""
|
||
broadcaster = di_container.resolve(ChannelEventBroadcaster)
|
||
|
||
async def event_stream() -> AsyncIterator[str]:
|
||
"""SSE 事件流异步生成器。"""
|
||
queue, subscription_id = await broadcaster.subscribe()
|
||
try:
|
||
while not await request.is_disconnected():
|
||
try:
|
||
event = await asyncio.wait_for(queue.get(), timeout=15.0)
|
||
except TimeoutError:
|
||
if await request.is_disconnected():
|
||
break
|
||
yield ":heartbeat\n\n"
|
||
continue
|
||
if await request.is_disconnected():
|
||
break
|
||
yield _format_sse_event(event)
|
||
finally:
|
||
await broadcaster.unsubscribe(subscription_id)
|
||
|
||
return StreamingResponse(
|
||
event_stream(),
|
||
media_type="text/event-stream; charset=utf-8",
|
||
headers={
|
||
"Cache-Control": "no-cache",
|
||
"Connection": "keep-alive",
|
||
"X-Accel-Buffering": "no",
|
||
},
|
||
)
|
||
|
||
|
||
def _format_sse_event(event: DomainEvent) -> str:
|
||
"""将 ``DomainEvent`` 格式化为 SSE 帧。
|
||
|
||
输出格式:
|
||
event: {event.event_type}\n
|
||
id: {event.event_id}\n
|
||
data: {json.dumps(event_dict)}\n\n
|
||
|
||
其中 ``event_dict`` 经 ``dataclass_to_dict`` 递归序列化为 JSON 兼容结构。
|
||
"""
|
||
event_dict = dataclass_to_dict(event)
|
||
return f"event: {event.event_type}\nid: {event.event_id}\ndata: {json.dumps(event_dict, ensure_ascii=False)}\n\n"
|
||
|
||
|
||
@session_router.get("/sessions/{session_id}", response_model=dict)
|
||
async def get_session(
|
||
session_id: str,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""按 session_id 查询渠道会话详情(SES-QUERY-02)。
|
||
|
||
供管理员审批 mergeSession 前核对源/目标会话信息。软删除的会话返回 404。
|
||
对应控制面操作 ``session/get``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.session_management.getSession(
|
||
session_id=session_id,
|
||
operator=operator,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.post("/sessions/{session_id}/merge", response_model=dict)
|
||
async def merge_session(
|
||
session_id: str,
|
||
payload: MergeSessionRequest,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""合并会话(FR-07,SES-05)。
|
||
|
||
将源会话合并至目标会话(路径 ``session_id``),迁移消息并软删除源会话。
|
||
受 ``merge_strategy_enabled`` 配置策略门控制,falsy 时返回 422。
|
||
对应控制面操作 ``session/merge``。
|
||
|
||
协议翻译:路径参数 ``session_id`` → ``MergeConversationCmd.target_conversation_id``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = MergeConversationCmd(
|
||
source_conversation_id=payload.source_conversation_id,
|
||
target_conversation_id=session_id,
|
||
operator=operator,
|
||
reason=payload.reason,
|
||
)
|
||
result = await use_cases.session_management.mergeSession(cmd)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.post("/sessions/{session_id}/transfer", response_model=dict)
|
||
async def transfer_session_owner(
|
||
session_id: str,
|
||
payload: TransferOwnerRequest,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""转移会话所有者(FR-26,SES-13)。
|
||
|
||
将会话(路径 ``session_id``)所有者从当前对端转移至新对端,纯 DB 写入,
|
||
加入控制面管道事务。审计日志由 AuditStage 在 SHARED 事务中统一写入(fail-closed)。
|
||
对应控制面操作 ``session/transfer_owner``。
|
||
|
||
协议翻译:路径参数 ``session_id`` → ``OwnerTransferCmd.conversation_id``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = OwnerTransferCmd(
|
||
conversation_id=session_id,
|
||
new_owner_id=payload.new_owner_id,
|
||
operator=operator,
|
||
)
|
||
result = await use_cases.session_management.transferSessionOwner(cmd)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.post("/sessions/{session_id}/close", response_model=dict)
|
||
async def close_session(
|
||
session_id: str,
|
||
payload: CloseSessionRequest,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""关闭渠道会话(SES-CLOSE-01)。
|
||
|
||
主动关闭指定会话,停止接收新消息。已关闭会话不可重新激活。
|
||
对应控制面操作 ``session/close``。
|
||
|
||
协议翻译:路径参数 ``session_id`` → ``CloseSessionCmd.session_id``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = CloseSessionCmd(
|
||
session_id=session_id,
|
||
reason=payload.reason,
|
||
operator=operator,
|
||
)
|
||
result = await use_cases.session_management.closeSession(cmd)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.get("/sessions/{session_id}/messages", response_model=dict)
|
||
async def list_session_messages(
|
||
session_id: str,
|
||
request: Request,
|
||
limit: int = Query(default=50, ge=1, le=200, description="分页大小"),
|
||
offset: int = Query(default=0, ge=0, description="分页偏移"),
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""查询会话消息流(SES-MSG-01)。
|
||
|
||
按时间顺序列出会话内的消息,支持分页。
|
||
对应控制面操作 ``session/list_messages``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.session_management.listSessionMessages(
|
||
session_id=session_id,
|
||
limit=limit,
|
||
offset=offset,
|
||
operator=operator,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.get("/sessions/{session_id}/stats", response_model=dict)
|
||
async def get_session_stats(
|
||
session_id: str,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""查询单会话统计指标(SES-STATS-01)。
|
||
|
||
按 ``session_id`` 查询会话的统计指标(消息计数、首响时间、平均响应、
|
||
会话时长等),由 dispatch handler 组合 ConversationPort 消息查询计算。
|
||
对应控制面操作 ``session/stats``。
|
||
|
||
协议翻译:路径参数 ``session_id`` 直接传入
|
||
``getSessionStats(session_id=..., operator=...)``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.session_management.getSessionStats(
|
||
session_id=session_id,
|
||
operator=operator,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.post("/sessions/{session_id}/bind-user", response_model=dict)
|
||
async def bind_session_user(
|
||
session_id: str,
|
||
payload: BindUserRequest,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""绑定系统用户到渠道会话(P3 渐进式绑定,SES-BIND-01)。
|
||
|
||
将渠道会话的统一身份绑定到系统用户,绑定后身份类型升级为 ``manual``,
|
||
并触发跨渠道会话合并(同一统一身份下的兄弟会话合并到当前会话)。
|
||
不走控制面管道,直接调用用例方法,异常由全局异常处理器统一转译。
|
||
|
||
协议翻译:路径参数 ``session_id`` + body ``user_uid`` / ``remark`` →
|
||
``bindUserToSession(session_id, user_uid, operator, remark=...)``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.user_binding.bindUserToSession(
|
||
session_id=session_id,
|
||
user_uid=payload.user_uid,
|
||
operator=operator,
|
||
remark=payload.remark,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.delete("/sessions/{session_id}/bind-user", response_model=dict)
|
||
async def unbind_session_user(
|
||
session_id: str,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""解绑渠道会话的系统用户(P3 渐进式绑定,SES-UNBIND-01)。
|
||
|
||
解除统一身份与系统用户的绑定,身份类型回退为 ``channel_guest``。
|
||
不删除历史消息:解绑仅修改身份关联状态,历史消息保留在原会话中。
|
||
不走控制面管道,直接调用用例方法,异常由全局异常处理器统一转译。
|
||
|
||
协议翻译:路径参数 ``session_id`` →
|
||
``unbindUserFromSession(session_id, operator)``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.user_binding.unbindUserFromSession(
|
||
session_id=session_id,
|
||
operator=operator,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@session_router.get("/sessions/{session_id}/identity", response_model=dict)
|
||
async def get_session_identity(
|
||
session_id: str,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""查询渠道会话身份绑定状态(P3 渐进式绑定,SES-IDENTITY-01)。
|
||
|
||
返回统一身份 ID、绑定用户 UID、身份类型与绑定状态。会话未关联统一
|
||
身份时返回 ``is_bound=False`` 的空状态。不走控制面管道,直接调用
|
||
用例方法,异常由全局异常处理器统一转译。
|
||
|
||
协议翻译:路径参数 ``session_id`` →
|
||
``getSessionIdentity(session_id, operator)``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.user_binding.getSessionIdentity(
|
||
session_id=session_id,
|
||
operator=operator,
|
||
)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|