From 00baaf9e9312959600711fc11712f31c33b791b4 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 23 Jul 2026 22:45:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor(#777):=20=E4=BF=AE=E5=A4=8DDomain?= =?UTF-8?q?=E5=B1=82=E4=BE=9D=E8=B5=96Adapters=E7=9A=84DDD=E8=BF=9D?= =?UTF-8?q?=E8=A7=84=EF=BC=884=E5=A4=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 领域层定义端口接口(EmailServicePort/SessionStorePort/JWTServicePort/PasswordHasherPort) 遵循DDD依赖倒置原则:领域层定义端口,外层实现端口 - EmailConfig从adapters上移到domain层(值对象,无基础设施依赖) - Adapter实现类继承领域端口接口: Smtp EmailService(EmailServicePort) Redis SessionStore(SessionStorePort) JWTService(JWTServicePort) PasswordHasher(PasswordHasherPort) - 清理domain/auth/__init__.py,移除对application/adapters的反向import - 测试文件import路径修正,从adapters层导入具体实现 - 领域层零对外依赖(adapters/application),全量单测4383 passed --- packages/adapters/redis/session_store.py | 4 +- packages/adapters/smtp/email_service.py | 18 +---- packages/application/auth/jwt_service.py | 4 +- packages/application/auth/password_hasher.py | 8 ++- packages/domain/auth/__init__.py | 52 +++++++-------- packages/domain/auth/email_service.py | 64 ++++++++++++++++-- packages/domain/auth/jwt_service.py | 44 +++++++++++++ packages/domain/auth/password_hasher.py | 38 +++++++++++ packages/domain/auth/session_store.py | 69 ++++++++++++++++++-- tests/unit/test_email_service.py | 2 +- tests/unit/test_session_store.py | 2 +- 11 files changed, 242 insertions(+), 63 deletions(-) mode change 100644 => 100755 packages/adapters/smtp/email_service.py mode change 100644 => 100755 packages/application/auth/jwt_service.py mode change 100644 => 100755 packages/application/auth/password_hasher.py mode change 100644 => 100755 packages/domain/auth/__init__.py mode change 100644 => 100755 packages/domain/auth/email_service.py create mode 100755 packages/domain/auth/jwt_service.py create mode 100755 packages/domain/auth/password_hasher.py mode change 100644 => 100755 packages/domain/auth/session_store.py mode change 100644 => 100755 tests/unit/test_email_service.py mode change 100644 => 100755 tests/unit/test_session_store.py diff --git a/packages/adapters/redis/session_store.py b/packages/adapters/redis/session_store.py index e252a38a3..9c8815e4c 100755 --- a/packages/adapters/redis/session_store.py +++ b/packages/adapters/redis/session_store.py @@ -10,6 +10,8 @@ from typing import Optional import redis from redis import Redis +from packages.domain.auth.session_store import SessionStorePort + class RedisConfig: """Redis 配置""" @@ -50,7 +52,7 @@ class NoopSessionStore: return False -class SessionStore: +class SessionStore(SessionStorePort): """Session 存储服务""" def __init__(self, redis_client: Optional[Redis] = None, config: Optional[RedisConfig] = None): diff --git a/packages/adapters/smtp/email_service.py b/packages/adapters/smtp/email_service.py old mode 100644 new mode 100755 index df55dca18..bd8804c81 --- a/packages/adapters/smtp/email_service.py +++ b/packages/adapters/smtp/email_service.py @@ -4,11 +4,12 @@ """ import smtplib -from dataclasses import dataclass from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import List, Optional +from packages.domain.auth.email_service import EmailConfig, EmailServicePort + class NoopEmailService: def send_verification_email(self, **kwargs): @@ -18,20 +19,7 @@ class NoopEmailService: return False, "Email delivery is disabled" -@dataclass -class EmailConfig: - """邮件配置""" - - smtp_host: str = "smtp.gmail.com" - smtp_port: int = 587 - smtp_user: str = "" - smtp_password: str = "" - from_email: str = "" - from_name: str = "小虾 SaaS" - use_tls: bool = True - - -class EmailService: +class EmailService(EmailServicePort): """邮件服务类""" def __init__(self, config: Optional[EmailConfig] = None): diff --git a/packages/application/auth/jwt_service.py b/packages/application/auth/jwt_service.py old mode 100644 new mode 100755 index 5feb7dc07..2a08bc450 --- a/packages/application/auth/jwt_service.py +++ b/packages/application/auth/jwt_service.py @@ -6,6 +6,8 @@ from typing import Any, Dict, Optional import jwt from jwt.exceptions import ExpiredSignatureError, InvalidTokenError +from packages.domain.auth.jwt_service import JWTServicePort + class JWTConfig: """JWT 配置""" @@ -57,7 +59,7 @@ class TokenType: REFRESH = "refresh" -class JWTService: +class JWTService(JWTServicePort): """JWT 服务类""" def __init__(self, config: JWTConfig = None): diff --git a/packages/application/auth/password_hasher.py b/packages/application/auth/password_hasher.py old mode 100644 new mode 100755 index 911a6be88..d9ec60c9f --- a/packages/application/auth/password_hasher.py +++ b/packages/application/auth/password_hasher.py @@ -3,12 +3,14 @@ 使用 bcrypt 安全存储密码 """ -from typing import Optional +from typing import Optional, Tuple import bcrypt +from packages.domain.auth.password_hasher import PasswordHasherPort, PasswordValidatorPort -class PasswordHasher: + +class PasswordHasher(PasswordHasherPort): """密码哈希服务""" def __init__(self, rounds: int = 12): @@ -98,7 +100,7 @@ class PasswordHasher: return False -class PasswordValidator: +class PasswordValidator(PasswordValidatorPort): """密码强度验证器""" def __init__( diff --git a/packages/domain/auth/__init__.py b/packages/domain/auth/__init__.py old mode 100644 new mode 100755 index ce8c4b53d..ca8d31202 --- a/packages/domain/auth/__init__.py +++ b/packages/domain/auth/__init__.py @@ -1,36 +1,30 @@ -"""Authentication domain services. +"""领域层认证相关端口接口. -Only pure domain authentication helpers are exported here. Infrastructure-backed -services such as Redis session storage and SMTP email delivery live under -`packages.adapters` and should be injected into use cases. +遵循 DDD 依赖倒置原则:领域层定义端口(抽象接口), +外层(应用层、基础设施层)实现端口。 + +使用方式: + from packages.domain.auth import EmailServicePort, SessionStorePort + from packages.domain.auth import JWTServicePort, PasswordHasherPort + from packages.domain.auth.sms_service import SmsService + +注意:具体实现请从对应的适配器层导入: + - 邮件服务: from packages.adapters.smtp import EmailService + - Session存储: from packages.adapters.redis import SessionStore + - JWT服务: from packages.application.auth.jwt_service import JWTService + - 密码哈希: from packages.application.auth.password_hasher import PasswordHasher """ -from packages.application.auth.jwt_service import ( - JWTConfig, - JWTService, - TokenType, - jwt_service, -) -from packages.application.auth.password_hasher import ( - PasswordHasher, - PasswordValidator, - password_hasher, - password_validator, -) -from packages.domain.auth.email_service import EmailConfig, EmailService -from packages.domain.auth.session_store import RedisConfig, SessionStore +from packages.domain.auth.email_service import EmailConfig, EmailServicePort +from packages.domain.auth.jwt_service import JWTServicePort +from packages.domain.auth.password_hasher import PasswordHasherPort, PasswordValidatorPort +from packages.domain.auth.session_store import SessionStorePort __all__ = [ - "JWTService", - "JWTConfig", - "TokenType", - "jwt_service", - "PasswordHasher", - "PasswordValidator", - "password_hasher", - "password_validator", - "SessionStore", - "RedisConfig", - "EmailService", "EmailConfig", + "EmailServicePort", + "JWTServicePort", + "PasswordHasherPort", + "PasswordValidatorPort", + "SessionStorePort", ] diff --git a/packages/domain/auth/email_service.py b/packages/domain/auth/email_service.py old mode 100644 new mode 100755 index b2e9ad0ae..b1ace6460 --- a/packages/domain/auth/email_service.py +++ b/packages/domain/auth/email_service.py @@ -1,10 +1,62 @@ -"""Compatibility import for SMTP email delivery. +"""邮件服务端口(领域层接口). -Infrastructure implementations live under `packages.adapters`. New code should -import `packages.adapters.smtp.email_service` directly or inject an email-sender -port into the use case. +定义邮件发送服务的抽象接口,具体实现由基础设施层(adapters)提供。 +遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。 """ -from packages.adapters.smtp.email_service import EmailConfig, EmailService +from __future__ import annotations -__all__ = ["EmailConfig", "EmailService"] +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class EmailConfig: + """邮件配置(领域层值对象)""" + + smtp_host: str = "smtp.gmail.com" + smtp_port: int = 587 + smtp_user: str = "" + smtp_password: str = "" + from_email: str = "" + from_name: str = "小虾 SaaS" + use_tls: bool = True + + +class EmailServicePort(ABC): + """邮件服务端口(抽象接口)""" + + @abstractmethod + def send_email( + self, + to_email: str, + subject: str, + html_body: str, + text_body: Optional[str] = None, + cc: Optional[List[str]] = None, + bcc: Optional[List[str]] = None, + ) -> tuple[bool, Optional[str]]: + """发送邮件""" + ... + + @abstractmethod + def send_verification_email( + self, + to_email: str, + verification_code: str, + username: str = "", + ) -> tuple[bool, Optional[str]]: + """发送验证邮件""" + ... + + @abstractmethod + def send_password_reset_email( + self, + to_email: str, + reset_token: str, + reset_url: str = "", + username: str = "", + ) -> tuple[bool, Optional[str]]: + """发送密码重置邮件""" + ... diff --git a/packages/domain/auth/jwt_service.py b/packages/domain/auth/jwt_service.py new file mode 100755 index 000000000..29caf1cb1 --- /dev/null +++ b/packages/domain/auth/jwt_service.py @@ -0,0 +1,44 @@ +"""JWT 服务端口(领域层接口). + +定义 JWT Token 生成、验证、解析的抽象接口,具体实现由应用层或基础设施层提供。 +遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。 +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class JWTServicePort(ABC): + """JWT 服务端口(抽象接口)""" + + @abstractmethod + def create_access_token( + self, + user_id: str, + role: str = "", + additional_claims: Dict[str, Any] | None = None, + ) -> str: + """创建 access_token""" + ... + + @abstractmethod + def create_refresh_token(self, user_id: str, session_id: str) -> str: + """创建 refresh_token""" + ... + + @abstractmethod + def verify_token(self, token: str) -> Dict[str, Any]: + """验证任意 Token""" + ... + + @abstractmethod + def verify_access_token(self, token: str) -> Dict[str, Any]: + """验证 access_token""" + ... + + @abstractmethod + def verify_refresh_token(self, token: str) -> Dict[str, Any]: + """验证 refresh_token""" + ... diff --git a/packages/domain/auth/password_hasher.py b/packages/domain/auth/password_hasher.py new file mode 100755 index 000000000..bc59ff19d --- /dev/null +++ b/packages/domain/auth/password_hasher.py @@ -0,0 +1,38 @@ +"""密码哈希服务端口(领域层接口). + +定义密码哈希与验证的抽象接口,具体实现由应用层或基础设施层提供。 +遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。 +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Optional, Tuple + + +class PasswordHasherPort(ABC): + """密码哈希服务端口(抽象接口)""" + + @abstractmethod + def hash_password(self, password: str) -> str: + """哈希密码""" + ... + + @abstractmethod + def verify_password(self, password: str, hashed_password: str) -> bool: + """验证密码""" + ... + + @abstractmethod + def needs_rehash(self, hashed_password: str) -> bool: + """检查哈希是否需要重新计算""" + ... + + +class PasswordValidatorPort(ABC): + """密码强度验证端口(抽象接口)""" + + @abstractmethod + def validate(self, password: str) -> Tuple[bool, Optional[str]]: + """验证密码强度""" + ... diff --git a/packages/domain/auth/session_store.py b/packages/domain/auth/session_store.py old mode 100644 new mode 100755 index bb65a83f3..852f7ce6a --- a/packages/domain/auth/session_store.py +++ b/packages/domain/auth/session_store.py @@ -1,10 +1,67 @@ -"""Compatibility import for Redis-backed session storage. +"""Session 存储端口(领域层接口). -Infrastructure implementations live under `packages.adapters`. New code should -import `packages.adapters.redis.session_store` directly or inject a session-store -port into the use case. +定义 Session 存储服务的抽象接口,具体实现由基础设施层(adapters)提供。 +遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。 """ -from packages.adapters.redis.session_store import RedisConfig, SessionStore +from __future__ import annotations -__all__ = ["RedisConfig", "SessionStore"] +from abc import ABC, abstractmethod +from typing import Optional + + +class SessionStorePort(ABC): + """Session 存储端口(抽象接口)""" + + @abstractmethod + def save_session( + self, + session_id: str, + user_id: str, + refresh_token: str, + user_agent: str = "", + ip_address: str = "", + expires_in_days: int = 7, + ) -> bool: + """保存 Session""" + ... + + @abstractmethod + def get_session(self, session_id: str) -> Optional[dict]: + """获取 Session""" + ... + + @abstractmethod + def get_session_by_refresh_token(self, refresh_token: str) -> Optional[dict]: + """通过 refresh_token 查找 Session""" + ... + + @abstractmethod + def get_refresh_token(self, session_id: str) -> Optional[str]: + """获取 refresh_token""" + ... + + @abstractmethod + def update_last_active(self, session_id: str) -> bool: + """更新最后活跃时间""" + ... + + @abstractmethod + def delete_session(self, session_id: str) -> bool: + """删除 Session""" + ... + + @abstractmethod + def get_user_sessions(self, user_id: str) -> list[dict]: + """获取用户所有 Session""" + ... + + @abstractmethod + def delete_all_user_sessions(self, user_id: str) -> int: + """删除用户所有 Session""" + ... + + @abstractmethod + def session_exists(self, session_id: str) -> bool: + """检查 Session 是否存在""" + ... diff --git a/tests/unit/test_email_service.py b/tests/unit/test_email_service.py old mode 100644 new mode 100755 index 228d1c3ff..c9002b8a9 --- a/tests/unit/test_email_service.py +++ b/tests/unit/test_email_service.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch import pytest -from packages.domain.auth.email_service import EmailConfig, EmailService +from packages.adapters.smtp.email_service import EmailConfig, EmailService class TestEmailService: diff --git a/tests/unit/test_session_store.py b/tests/unit/test_session_store.py old mode 100644 new mode 100755 index 389aaedd2..0bf0dca32 --- a/tests/unit/test_session_store.py +++ b/tests/unit/test_session_store.py @@ -7,7 +7,7 @@ from unittest.mock import Mock import pytest -from packages.domain.auth.session_store import SessionStore +from packages.adapters.redis.session_store import SessionStore class TestSessionStore: -- 2.54.0