ForcePilot/backend/package/yuxi/channels/adapters/service_account_adapter.py
Kris 8eead29de0 refactor: 批量清理冗余空行,优化部分枚举使用方式
1.  移除所有适配器文件中多余的空导入行
2.  调整ValidationError继承,移除不必要的ValueError继承
3.  修正多处ChannelType使用方式,从.value改为直接使用枚举实例
4.  优化飞书插件部分硬编码渠道类型为枚举实例
5.  更新wechat_ilink插件清单与适配器配置
6.  新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断
7.  优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
2026-07-04 00:14:56 +08:00

548 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ServiceAccountAdapter实现 ServiceAccountPort 与 ServiceAccountRepositoryPort。
- 同时实现领域服务端口(幂等 ensure与仓储端口create/get
- 复用模块级 pg_managertx 非空时加入应用层事务(共享 SqlAlchemyTransactionContext 的 session
- 错误翻译SQLAlchemyError → ServiceAccountCreationError创建/ DependencyError查询
- 事务边界tx 非空时使用 tx 关联的 session不得自主提交tx 为 None 时通过 pg_manager 获取 session单方法提交
依赖边界:只依赖 yuxi.channels.contract端口 + DTO + 错误)、
yuxi.channels.core.modelServiceAccount 聚合根)、
yuxi.storage.postgresORM 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`` 查询,已存在则返回;
不存在则调用 ``createServiceAccount`` 创建。遵循事务边界:``tx`` 非空时
加入应用层事务,``None`` 时单方法提交。
"""
existing = await self.getServiceAccountByChannelAccount(cmd.channel_type, cmd.account_id)
if existing is not None:
return existing
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",
channel_type=cmd.channel_type,
account_id=cmd.account_id,
uid=cmd.uid,
)
return existing
await self._logger.error(
"service account creation failed due to integrity error",
channel_type=cmd.channel_type,
account_id=cmd.account_id,
error=str(exc),
)
raise ServiceAccountCreationError(
channel_type=cmd.channel_type,
account_id=cmd.account_id,
reason=f"integrity constraint violated: {exc}",
cause=exc,
) from exc
except SQLAlchemyError as exc:
raise ServiceAccountCreationError(
channel_type=cmd.channel_type,
account_id=cmd.account_id,
reason=f"database error: {exc}",
cause=exc,
) from exc
except DependencyError:
raise
except Exception as exc:
raise ServiceAccountCreationError(
channel_type=cmd.channel_type,
account_id=cmd.account_id,
reason=f"unexpected error: {exc}",
cause=exc,
) from exc
await self._logger.info(
"service account created",
channel_type=cmd.channel_type,
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(
ChannelAccountORM.channel_type == channel_type,
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