From a13c96d30b19abae7a9be7dcc20aedfc8110ef58 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 24 Aug 2026 04:16:09 +0800 Subject: [PATCH] refactor: unify user chain data port access --- app/chain/_messaging.py | 6 ++--- app/chain/interaction.py | 10 +++++--- app/chain/user.py | 8 +++---- .../backend-architecture-next-stage.md | 12 +++++++++- docs/rules/05-architecture.md | 9 +++---- tests/test_architecture_dependencies.py | 24 +++++++++++++++++++ 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/app/chain/_messaging.py b/app/chain/_messaging.py index 6fcaeea09..64df327ed 100644 --- a/app/chain/_messaging.py +++ b/app/chain/_messaging.py @@ -8,7 +8,7 @@ import copy from datetime import datetime from typing import Any, Dict, List, Optional, Union -from app.application.chain.data import UserPortProxy as UserOper +from app.application.chain.data import get_chain_user_port from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo from app.domain.meta.metabase import MetaBase from app.foundation.identity import normalize_internal_user_id @@ -168,7 +168,7 @@ class NotificationMixin: # 是否已发送管理员标志 admin_sended = False send_orignal = False - useroper = UserOper() + useroper = get_chain_user_port() for action in actions: send_message = copy.deepcopy(dispatch_message) if action == "admin" and not admin_sended: @@ -286,7 +286,7 @@ class NotificationMixin: # 是否已发送管理员标志 admin_sended = False send_orignal = False - useroper = UserOper() + useroper = get_chain_user_port() for action in actions: send_message = copy.deepcopy(dispatch_message) if action == "admin" and not admin_sended: diff --git a/app/chain/interaction.py b/app/chain/interaction.py index 7452f6932..ad4850b80 100644 --- a/app/chain/interaction.py +++ b/app/chain/interaction.py @@ -13,7 +13,7 @@ from app.application.messaging.media import ( media_interaction_manager, ) from app.application.torrent import TorrentHelper -from app.application.chain.data import UserPortProxy as UserOper +from app.application.chain.data import get_chain_user_port from app.domain import episode as episode_rules from app.domain import title as title_rules from app.domain.context import Context, MediaInfo @@ -669,7 +669,9 @@ class MediaInteractionChain(ChainBase): return mp_name = ( - UserOper().get_name(**{f"{channel.name.lower()}_userid": userid}) + get_chain_user_port().get_name( + **{f"{channel.name.lower()}_userid": userid} + ) if channel else None ) @@ -980,7 +982,9 @@ class MediaInteractionChain(ChainBase): note = None mp_name = ( - UserOper().get_name(**{f"{channel.name.lower()}_userid": userid}) + get_chain_user_port().get_name( + **{f"{channel.name.lower()}_userid": userid} + ) if channel else None ) diff --git a/app/chain/user.py b/app/chain/user.py index 0c4b16e72..31dcc1382 100644 --- a/app/chain/user.py +++ b/app/chain/user.py @@ -4,7 +4,7 @@ from typing import Any, Literal, Optional, Tuple, Union from app.chain import ChainBase from app.application.security.token import get_password_hash, verify_password -from app.application.chain.data import UserPortProxy as UserOper +from app.application.chain.data import get_chain_user_port from app.runtime.log import logger from app.schemas.event import AuthCredentials from app.schemas.event import AuthInterceptCredentials @@ -116,7 +116,7 @@ class UserChain(ChainBase): logger.info("密码认证失败,认证类型不匹配") return False, PASSWORD_INVALID_CREDENTIALS_MESSAGE - user = UserOper().get_by_name(name=credentials.username) + user = get_chain_user_port().get_by_name(name=credentials.username) if not user: logger.info(f"密码认证失败,用户 {credentials.username} 不存在") return False, PASSWORD_INVALID_CREDENTIALS_MESSAGE @@ -144,7 +144,7 @@ class UserChain(ChainBase): return False, "认证凭证无效" # 检查是否因为用户被禁用 - useroper = UserOper() + useroper = get_chain_user_port() if credentials.username: user = useroper.get_by_name(name=credentials.username) if user and not user.is_active: @@ -232,7 +232,7 @@ class UserChain(ChainBase): return False # 检查用户是否存在,如果不存在且当前为密码认证时则创建新用户 - useroper = UserOper() + useroper = get_chain_user_port() user = useroper.get_by_name(name=username) if user: # 如果用户存在,但是已经被禁用,则直接响应 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index 6b393dbad..cc6865237 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -6,7 +6,7 @@ > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` -> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移。 +> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口。 ## 当前复核结论(2026-08-24) @@ -150,6 +150,16 @@ - 兼容边界不变:全部 `*PortProxy` 类、`WorkFlowManager` 类路径/Singleton identity、动作类型与参数、 工作流事件和数据库 `WorkflowOper/SubscribeOper/TransferHistoryOper` 均保留,插件无需迁移。 +### 长期整改阶段 13:用户与消息 Chain 数据端口收口(2026-08-24) + +- `UserChain`、`InteractionChain` 和消息发送 mixin 原先都把 `UserPortProxy` 别名为 `UserOper`;现在统一 + 通过 `get_chain_user_port()` 获取组合根登记的数据端口,登录、用户绑定查询和通知收件人解析不再存在 + 第二种伪 Oper 获取路径。 +- 架构门禁覆盖这三个用户身份消费者,禁止重新导入 `UserPortProxy`。站点、媒体服务器、音乐、订阅、 + 下载和整理 Chain 仍保留独立迁移清单,继续按行为风险分阶段推进。 +- 兼容边界不变:数据库 `UserOper`、`UserPortProxy`、User/Interaction/Message Chain 公开方法、消息渠道 + payload 和插件调用方式均未改动。 + ### 总体判断 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 1d6b13958..8af54a3d9 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -131,10 +131,11 @@ Session. `app/db/adapters/` is the concrete persistence-adapter layer: it may depend on Application-owned Protocols, UoW/Session and Oper implementations. This deliberate dependency inversion is the only `DB implementation -> Application contract` direction; Application must remain free of DB imports. -Workflow-domain consumers use the named `get_chain_*_port()` functions from -`app/application/chain/data.py`; they must not alias migration-time `*PortProxy` -classes back to database Oper names. Those proxy classes remain compatibility -boundaries while the other established Chain domains migrate independently. +Migrated workflow, user, interaction and messaging Chain consumers use the named +`get_chain_*_port()` functions from `app/application/chain/data.py`; they must not +alias migration-time `*PortProxy` classes back to database Oper names. Those +proxy classes remain compatibility boundaries while the other established Chain +domains migrate independently. ### Adapter boundaries diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 70b4dcd20..1c8cb897e 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -428,6 +428,30 @@ def test_workflow_domain_uses_explicit_chain_data_port_getters(): assert violations == [] +def test_user_and_messaging_chains_use_explicit_data_port_getters(): + """用户、交互和消息链不得把迁移期 UserPortProxy 伪装成 UserOper。""" + paths = [ + APP_ROOT / "chain" / "user.py", + APP_ROOT / "chain" / "interaction.py", + APP_ROOT / "chain" / "_messaging.py", + ] + violations: list[str] = [] + for path in paths: + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom): + continue + if node.module != "app.application.chain.data": + continue + for alias in node.names: + if alias.name == "UserPortProxy": + violations.append( + f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}" + ) + + assert violations == [] + + def test_plugin_components_do_not_reexport_legacy_abi_names(): """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" violations: list[str] = []