2026-07-03 19:18:13 +08:00
|
|
|
|
"""ServiceAccountAdapter:实现 ServiceAccountPort 与 ServiceAccountRepositoryPort。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 同时实现领域服务端口(幂等 ensure)与仓储端口(create/get)
|
|
|
|
|
|
|
|
|
|
|
|
- 复用模块级 pg_manager,tx 非空时加入应用层事务(共享 SqlAlchemyTransactionContext 的 session)
|
|
|
|
|
|
|
|
|
|
|
|
- 错误翻译:SQLAlchemyError → ServiceAccountCreationError(创建)/ DependencyError(查询)
|
|
|
|
|
|
|
|
|
|
|
|
- 事务边界:tx 非空时使用 tx 关联的 session,不得自主提交;tx 为 None 时通过 pg_manager 获取 session(单方法提交)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
依赖边界:只依赖 yuxi.channels.contract(端口 + DTO + 错误)、
|
|
|
|
|
|
|
|
|
|
|
|
yuxi.channels.core.model(ServiceAccount 聚合根)、
|
|
|
|
|
|
|
|
|
|
|
|
yuxi.storage.postgres(ORM Model + pg_manager)、
|
|
|
|
|
|
|
|
|
|
|
|
yuxi.utils.auth_utils(密码哈希)、sqlalchemy。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import secrets
|
|
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
|
|
|
|
from yuxi.channels.contract.dtos.service_account import (
|
|
|
|
|
|
CreateServiceAccountCmd,
|
|
|
|
|
|
ServiceAccount,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.contract.errors import (
|
|
|
|
|
|
DependencyError,
|
|
|
|
|
|
ServiceAccountCreationError,
|
|
|
|
|
|
ValidationError,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.contract.errors.base import Error
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.service_account_port import (
|
|
|
|
|
|
ServiceAccountPort,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.service_account_repository_port import (
|
|
|
|
|
|
ServiceAccountRepositoryPort,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.storage.postgres.manager import pg_manager
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import User as UserORM
|
|
|
|
|
|
from yuxi.storage.postgres.models_channels import ChannelAccount as ChannelAccountORM
|
|
|
|
|
|
from yuxi.utils.auth_utils import AuthUtils
|
|
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.transaction_port import (
|
|
|
|
|
|
TransactionContext,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ["ServiceAccountAdapter"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 服务账号 uid 前缀,与 ServiceAccount 聚合根 create() 工厂方法保持一致
|
|
|
|
|
|
|
|
|
|
|
|
_SERVICE_ACCOUNT_UID_PREFIX = "svc:channel:"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
|
|
|
|
|
"""服务账号被驱动适配器实现。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
同时实现 ``ServiceAccountPort``(领域服务编排:幂等 ensure)与
|
|
|
|
|
|
|
|
|
|
|
|
``ServiceAccountRepositoryPort``(持久化原语:create/get)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
仅注入 ``LoggerPort``,``pg_manager`` 通过模块级导入使用。事务边界:
|
|
|
|
|
|
|
|
|
|
|
|
- ``tx`` 非空时使用 ``tx`` 关联的 session(``SqlAlchemyTransactionContext._session``),
|
|
|
|
|
|
|
|
|
|
|
|
适配器 **不得** 自主提交,由应用层统一提交。
|
|
|
|
|
|
|
|
|
|
|
|
- ``tx`` 为 ``None`` 时通过 ``pg_manager.get_async_session_context()`` 获取 session,
|
|
|
|
|
|
|
|
|
|
|
|
上下文退出时自动提交(向后兼容)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
约束:
|
|
|
|
|
|
|
|
|
|
|
|
- INV-8 适配器无业务规则:仅做协议转换与持久化操作。
|
|
|
|
|
|
|
|
|
|
|
|
- 复用现有 pg_manager,不引入新连接池(INV-I3)。
|
|
|
|
|
|
|
|
|
|
|
|
- 异常翻译:``SQLAlchemyError`` → ``ServiceAccountCreationError``(创建)/
|
|
|
|
|
|
|
|
|
|
|
|
``DependencyError``(查询),``ServiceAccountCreationError`` 直接放行。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, logger: LoggerPort) -> None:
|
|
|
|
|
|
"""初始化适配器,注入日志端口。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
|
|
|
|
|
|
logger: 日志被驱动端口,用于记录创建与查询过程中的故障。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
self._logger = logger
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_session(self, tx: TransactionContext | None) -> AsyncSession:
|
|
|
|
|
|
"""从事务上下文中解析共享 session。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
适配器层耦合 ``SqlAlchemyTransactionContext`` 实现:访问其私有的
|
|
|
|
|
|
|
|
|
|
|
|
``_session`` 以加入应用层事务。事务边界由应用层控制,适配器不得
|
|
|
|
|
|
|
|
|
|
|
|
自主提交。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
|
|
|
|
|
|
tx: 事务上下文,``None`` 表示无应用层事务(调用方应走 pg_manager 路径)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
|
|
|
|
|
|
事务上下文关联的 ``AsyncSession``。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
抛出:
|
|
|
|
|
|
|
|
|
|
|
|
DependencyError: ``tx`` 非空但无法获取关联 session。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
session = getattr(tx, "_session", None) if tx is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
if session is None:
|
|
|
|
|
|
raise DependencyError(
|
|
|
|
|
|
"service_account",
|
|
|
|
|
|
Error("transaction context has no session"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_uid(uid: str) -> tuple[ChannelType, str]:
|
|
|
|
|
|
"""从服务账号 uid 解析渠道类型与账户 ID。
|
|
|
|
|
|
|
|
|
|
|
|
uid 格式由 ``ServiceAccountAggregate.create()`` 定义:
|
|
|
|
|
|
``svc:channel:{channel_type_value}:{account_id}``。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
uid: 服务账号唯一标识。
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
(channel_type, account_id) 元组。
|
|
|
|
|
|
|
|
|
|
|
|
抛出:
|
|
|
|
|
|
ValidationError: uid 不符合服务账号格式(实现 UnifiedError 协议)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not uid.startswith(_SERVICE_ACCOUNT_UID_PREFIX):
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
field="uid",
|
|
|
|
|
|
message=f"uid does not follow service account format: {uid}",
|
|
|
|
|
|
)
|
|
|
|
|
|
rest = uid[len(_SERVICE_ACCOUNT_UID_PREFIX) :]
|
|
|
|
|
|
parts = rest.split(":", 1)
|
|
|
|
|
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
field="uid",
|
|
|
|
|
|
message=f"uid missing channel_type or account_id: {uid}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return ChannelType(parts[0]), parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _to_dto(
|
|
|
|
|
|
orm: UserORM,
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
) -> ServiceAccount:
|
|
|
|
|
|
"""User ORM → ServiceAccount DTO 转换。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
|
|
|
|
|
|
orm: User ORM 记录(``user_type='service'``)。
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: 渠道类型(来自查询参数或 uid 解析,非 ORM 字段)。
|
|
|
|
|
|
|
|
|
|
|
|
account_id: 渠道账户 ID(来自查询参数或 uid 解析,非 ORM 字段)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
|
|
|
|
|
|
ServiceAccount DTO。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
return ServiceAccount(
|
|
|
|
|
|
uid=orm.uid,
|
|
|
|
|
|
username=orm.username,
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
department_id=orm.department_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_user(
|
|
|
|
|
|
self,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
data: dict[str, Any],
|
|
|
|
|
|
*,
|
|
|
|
|
|
commit: bool,
|
|
|
|
|
|
) -> UserORM:
|
|
|
|
|
|
"""在指定 session 上创建 User 记录。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
|
|
|
|
|
|
session: SQLAlchemy 异步会话。
|
|
|
|
|
|
|
|
|
|
|
|
data: User 字段字典。
|
|
|
|
|
|
|
|
|
|
|
|
commit: 是否由本方法提交事务(``tx`` 为 ``None`` 时 ``True``,非空时 ``False``)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
|
|
|
|
|
|
已刷新的 User ORM 记录(含服务端默认字段)。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
orm = UserORM(**data)
|
|
|
|
|
|
|
|
|
|
|
|
session.add(orm)
|
|
|
|
|
|
|
|
|
|
|
|
await session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
if commit:
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
await session.refresh(orm)
|
|
|
|
|
|
|
|
|
|
|
|
return orm
|
|
|
|
|
|
|
|
|
|
|
|
async def ensureServiceAccount(
|
|
|
|
|
|
self,
|
|
|
|
|
|
cmd: CreateServiceAccountCmd,
|
|
|
|
|
|
*,
|
|
|
|
|
|
tx: TransactionContext | None = None,
|
|
|
|
|
|
) -> ServiceAccount:
|
|
|
|
|
|
"""幂等创建服务账号。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
先通过 ``getServiceAccountByChannelAccount`` 查询,已存在则返回;
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
不存在则通过 ``uid`` 二次预检,避免在共享事务内触发 ``IntegrityError``
|
2026-07-03 19:18:13 +08:00
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
(一旦触发,SQLAlchemy 会标记当前事务为 aborted,后续 ``saveChannelAccount``
|
|
|
|
|
|
|
|
|
|
|
|
将不可用)。仅当确实不存在时才调用 ``createServiceAccount`` 创建。
|
|
|
|
|
|
|
|
|
|
|
|
遵循事务边界:``tx`` 非空时加入应用层事务,``None`` 时单方法提交。
|
2026-07-03 19:18:13 +08:00
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
existing = await self.getServiceAccountByChannelAccount(cmd.channel_type, cmd.account_id)
|
|
|
|
|
|
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return existing
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
# 前置幂等检查:按 uid 查询是否已有残留服务账号(例如前一次创建服务账号后
|
|
|
|
|
|
# channel_accounts 写入失败/被软删的场景)。在共享事务外查询,避免尝试插入
|
|
|
|
|
|
# 已存在的 uid/username 导致事务被标记为 aborted。
|
|
|
|
|
|
existing_by_uid = await self.getServiceAccountByUid(cmd.uid)
|
|
|
|
|
|
if existing_by_uid is not None:
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"service account already exists by uid, returning existing",
|
|
|
|
|
|
channel_type=cmd.channel_type,
|
|
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
uid=cmd.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
return existing_by_uid
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
return await self.createServiceAccount(cmd, tx=tx)
|
|
|
|
|
|
|
|
|
|
|
|
async def createServiceAccount(
|
|
|
|
|
|
self,
|
|
|
|
|
|
cmd: CreateServiceAccountCmd,
|
|
|
|
|
|
*,
|
|
|
|
|
|
tx: TransactionContext | None = None,
|
|
|
|
|
|
) -> ServiceAccount:
|
|
|
|
|
|
"""创建服务账号记录(纯持久化操作)。
|
|
|
|
|
|
|
|
|
|
|
|
uid/username 已由应用层调 ``ServiceAccount.create()`` 聚合根工厂生成
|
|
|
|
|
|
并通过 ``cmd`` 传入(§4.6 / INV-8,适配器不得 import core.model)。
|
|
|
|
|
|
|
|
|
|
|
|
创建 User ORM 记录(``user_type='service'``、``role='user'``),密码为随机
|
|
|
|
|
|
字符串的 argon2 哈希(服务账号不可登录)。创建失败抛
|
|
|
|
|
|
``ServiceAccountCreationError``。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
random_password = secrets.token_urlsafe(32)
|
|
|
|
|
|
|
|
|
|
|
|
user_data: dict[str, Any] = {
|
|
|
|
|
|
"uid": cmd.uid,
|
|
|
|
|
|
"username": cmd.username,
|
|
|
|
|
|
"password_hash": AuthUtils.hash_password(random_password),
|
|
|
|
|
|
"role": "user",
|
|
|
|
|
|
"user_type": "service",
|
|
|
|
|
|
"department_id": cmd.department_id,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
if tx is not None:
|
|
|
|
|
|
session = self._resolve_session(tx)
|
|
|
|
|
|
|
|
|
|
|
|
orm = await self._create_user(session, user_data, commit=False)
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
orm = await self._create_user(session, user_data, commit=True)
|
|
|
|
|
|
|
|
|
|
|
|
except ServiceAccountCreationError:
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
except IntegrityError as exc:
|
|
|
|
|
|
# 幂等性修复:IntegrityError 时可能是重试场景,User 记录已存在但
|
|
|
|
|
|
|
|
|
|
|
|
# channel_accounts.service_user_uid 尚未设置。通过 uid 重新查询。
|
|
|
|
|
|
|
|
|
|
|
|
existing = await self.getServiceAccountByUid(cmd.uid)
|
|
|
|
|
|
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"service account already exists, returning existing",
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
uid=cmd.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return existing
|
|
|
|
|
|
|
|
|
|
|
|
await self._logger.error(
|
|
|
|
|
|
"service account creation failed due to integrity error",
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
raise ServiceAccountCreationError(
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
reason=f"integrity constraint violated: {exc}",
|
|
|
|
|
|
cause=exc,
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
except SQLAlchemyError as exc:
|
|
|
|
|
|
raise ServiceAccountCreationError(
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
reason=f"database error: {exc}",
|
|
|
|
|
|
cause=exc,
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
except DependencyError:
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
raise ServiceAccountCreationError(
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
reason=f"unexpected error: {exc}",
|
|
|
|
|
|
cause=exc,
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"service account created",
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=cmd.channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
account_id=cmd.account_id,
|
|
|
|
|
|
uid=cmd.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return self._to_dto(orm, cmd.channel_type, cmd.account_id)
|
|
|
|
|
|
|
|
|
|
|
|
async def getServiceAccountByChannelAccount(
|
|
|
|
|
|
self,
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
) -> ServiceAccount | None:
|
|
|
|
|
|
"""按渠道账户查询服务账号。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
通过 ``channel_accounts.service_user_uid`` JOIN ``User`` 表查询,
|
|
|
|
|
|
|
|
|
|
|
|
过滤 ``user_type='service'`` 且 ``is_deleted=0``。不存在返回 ``None``。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
stmt = (
|
|
|
|
|
|
select(UserORM)
|
|
|
|
|
|
.join(
|
|
|
|
|
|
ChannelAccountORM,
|
|
|
|
|
|
ChannelAccountORM.service_user_uid == UserORM.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(
|
2026-07-04 00:14:56 +08:00
|
|
|
|
ChannelAccountORM.channel_type == channel_type,
|
2026-07-03 19:18:13 +08:00
|
|
|
|
ChannelAccountORM.account_id == account_id,
|
|
|
|
|
|
ChannelAccountORM.is_deleted == 0,
|
|
|
|
|
|
UserORM.user_type == "service",
|
|
|
|
|
|
UserORM.is_deleted == 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = await session.execute(stmt)
|
|
|
|
|
|
|
|
|
|
|
|
orm = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if orm is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return self._to_dto(orm, channel_type, account_id)
|
|
|
|
|
|
|
|
|
|
|
|
except SQLAlchemyError as exc:
|
|
|
|
|
|
raise DependencyError("service_account", Error(str(exc))) from exc
|
|
|
|
|
|
|
|
|
|
|
|
async def getServiceAccountByUid(self, uid: str) -> ServiceAccount | None:
|
|
|
|
|
|
"""按 uid 查询服务账号。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
直接查 ``User`` 表,过滤 ``user_type='service'`` 且 ``is_deleted=0``。
|
|
|
|
|
|
|
|
|
|
|
|
不存在返回 ``None``。渠道类型与账户 ID 从 uid 解析得出(格式由
|
|
|
|
|
|
|
|
|
|
|
|
``ServiceAccountAggregate.create()`` 定义)。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
stmt = select(UserORM).where(
|
|
|
|
|
|
UserORM.uid == uid,
|
|
|
|
|
|
UserORM.user_type == "service",
|
|
|
|
|
|
UserORM.is_deleted == 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = await session.execute(stmt)
|
|
|
|
|
|
|
|
|
|
|
|
orm = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if orm is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
channel_type, account_id = self._parse_uid(orm.uid)
|
|
|
|
|
|
|
|
|
|
|
|
return self._to_dto(orm, channel_type, account_id)
|
|
|
|
|
|
|
|
|
|
|
|
except SQLAlchemyError as exc:
|
|
|
|
|
|
raise DependencyError("service_account", Error(str(exc))) from exc
|
|
|
|
|
|
|
|
|
|
|
|
except (ValueError, KeyError) as exc:
|
|
|
|
|
|
raise DependencyError(
|
|
|
|
|
|
"service_account",
|
|
|
|
|
|
Error(f"invalid service account uid: {exc}"),
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
async def getUserDepartmentByUid(self, uid: str) -> int | None:
|
|
|
|
|
|
"""按 uid 查询用户的部门 ID。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
供账户创建场景从操作者 uid 解析 ``department_id``,作为服务账号
|
|
|
|
|
|
|
|
|
|
|
|
``CreateServiceAccountCmd.department_id`` 的来源。操作者可能是任何
|
|
|
|
|
|
|
|
|
|
|
|
类型的非服务账号用户(普通用户或管理员),仅过滤 ``is_deleted=0``。
|
|
|
|
|
|
|
|
|
|
|
|
不存在时返回 ``None``(由调用方决定兜底语义)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
|
|
|
|
|
|
uid: 系统用户 UID(``Operator.user_id``)。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
stmt = select(UserORM.department_id).where(
|
|
|
|
|
|
UserORM.uid == uid,
|
|
|
|
|
|
UserORM.is_deleted == 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = await session.execute(stmt)
|
|
|
|
|
|
|
|
|
|
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
except SQLAlchemyError as exc:
|
|
|
|
|
|
raise DependencyError("service_account", Error(str(exc))) from exc
|
|
|
|
|
|
|
|
|
|
|
|
async def getUserIdByUid(self, uid: str) -> int | None:
|
|
|
|
|
|
"""按 uid 查询系统用户的 id(整型主键)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
供 P3 渐进式绑定场景从 ``User.uid`` 字符串解析 ``User.id`` 整型主键,
|
|
|
|
|
|
|
|
|
|
|
|
作为 ``channel_user_identities.user_id`` 列的写入值。操作者可能是
|
|
|
|
|
|
|
|
|
|
|
|
任何类型的非服务账号用户(普通用户或管理员),仅过滤 ``is_deleted=0``。
|
|
|
|
|
|
|
|
|
|
|
|
不存在时返回 ``None``(由调用方决定兜底语义)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
|
|
|
|
|
|
uid: 系统用户 UID(``User.uid`` 字符串)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
|
|
|
|
|
|
``User.id``(整型主键);不存在返回 ``None``。
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
stmt = select(UserORM.id).where(
|
|
|
|
|
|
UserORM.uid == uid,
|
|
|
|
|
|
UserORM.is_deleted == 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = await session.execute(stmt)
|
|
|
|
|
|
|
|
|
|
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
except SQLAlchemyError as exc:
|
|
|
|
|
|
raise DependencyError("service_account", Error(str(exc))) from exc
|