1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
542 lines
22 KiB
Python
542 lines
22 KiB
Python
"""会话资源域 Router。
|
||
|
||
提供渠道会话管理的 12 个 HTTP 端点,全部由 ``get_admin_user`` 守门,仅管理员
|
||
可访问。Router 不含任何业务逻辑,仅做协议翻译:将 HTTP 请求参数组装为契约层
|
||
命令/参数,调用 ``use_cases.session_management`` 的类型化方法走控制面管道,
|
||
再通过 ``raiseOnControlFailure`` 转译失败、``serialize_control_data`` 序列化
|
||
成功结果。P3 绑定相关端点不走五阶段控制面管道(用例方法直接调用仓储 + 领域
|
||
服务),但仍返回 ``ControlResult``,失败转译与成功序列化复用同一套
|
||
``raiseOnControlFailure`` / ``serialize_control_data``;前置校验失败(如用户
|
||
不存在)由用例直接抛契约异常,经全局异常处理器统一映射。
|
||
|
||
端点清单:
|
||
- 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 (
|
||
LARGE_LIMIT,
|
||
MESSAGE_LIMIT,
|
||
OFFSET,
|
||
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_session_id``,不在 body 中。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
source_session_id: str = Field(..., min_length=1, description="源渠道会话 ID(被合并方)")
|
||
reason: str = Field(default="", max_length=512, description="合并原因(审计用)")
|
||
|
||
|
||
class TransferOwnerRequest(BaseModel):
|
||
"""转移所有者请求体。
|
||
|
||
路径参数 ``session_id`` 作为目标渠道会话 ID,映射为
|
||
``OwnerTransferCmd.session_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: SessionStatus | 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 = LARGE_LIMIT,
|
||
offset: int = OFFSET,
|
||
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_session_id``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = MergeConversationCmd(
|
||
source_session_id=payload.source_session_id,
|
||
target_session_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.session_id``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = OwnerTransferCmd(
|
||
session_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 = MESSAGE_LIMIT,
|
||
offset: int = OFFSET,
|
||
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``,
|
||
并触发跨渠道会话合并(同一统一身份下的兄弟会话合并到当前会话)。用例
|
||
方法不走五阶段控制面管道(直接调用仓储 + 领域服务),但仍返回
|
||
``ControlResult``,失败转译与成功序列化复用 ``raiseOnControlFailure`` /
|
||
``serialize_control_data``;前置校验失败(如用户不存在、配置禁用)由
|
||
用例直接抛契约异常,经全局异常处理器统一映射。
|
||
|
||
协议翻译:路径参数 ``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``。
|
||
不删除历史消息:解绑仅修改身份关联状态,历史消息保留在原会话中。用例
|
||
方法不走五阶段控制面管道(直接调用仓储 + 领域服务),但仍返回
|
||
``ControlResult``,失败转译与成功序列化复用 ``raiseOnControlFailure`` /
|
||
``serialize_control_data``;前置校验失败(如会话不存在)由用例直接抛
|
||
契约异常,经全局异常处理器统一映射。
|
||
|
||
协议翻译:路径参数 ``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`` 的空状态。用例方法不走五阶段控制面管道
|
||
(直接调用仓储),但仍返回 ``ControlResult``,失败转译与成功序列化复用
|
||
``raiseOnControlFailure`` / ``serialize_control_data``;会话不存在时由
|
||
用例直接抛契约异常,经全局异常处理器统一映射。
|
||
|
||
协议翻译:路径参数 ``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)}
|