mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
refactor: unify user chain data port access
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+4
-4
@@ -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:
|
||||
# 如果用户存在,但是已经被禁用,则直接响应
|
||||
|
||||
@@ -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 和插件调用方式均未改动。
|
||||
|
||||
### 总体判断
|
||||
|
||||
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
Reference in New Issue
Block a user