style: normalize python formatting gates
This commit is contained in:
@@ -1,15 +1,26 @@
|
||||
"""Application use cases package."""
|
||||
|
||||
from .asset_libraries import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
|
||||
from .asset_libraries import (
|
||||
CreateAssetLibraryCommand,
|
||||
CreateAssetLibraryUseCase,
|
||||
ListAssetLibrariesUseCase,
|
||||
)
|
||||
from .assets import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
|
||||
from .classification_jobs import SubmitClassificationJobCommand, SubmitClassificationJobUseCase
|
||||
from .classification_jobs import (
|
||||
SubmitClassificationJobCommand,
|
||||
SubmitClassificationJobUseCase,
|
||||
)
|
||||
from .generated_videos import (
|
||||
GetGeneratedVideoDownloadUrlUseCase,
|
||||
GetGeneratedVideoUseCase,
|
||||
ListGeneratedVideosByTaskUseCase,
|
||||
ListGeneratedVideosUseCase,
|
||||
)
|
||||
from .generation_tasks import CreateGenerationTaskCommand, CreateGenerationTaskUseCase, GetGenerationTaskUseCase
|
||||
from .generation_tasks import (
|
||||
CreateGenerationTaskCommand,
|
||||
CreateGenerationTaskUseCase,
|
||||
GetGenerationTaskUseCase,
|
||||
)
|
||||
from .ingest_jobs import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from .projects import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
"""认证相关 Use Cases"""
|
||||
from packages.application.auth.register_user_use_case import (
|
||||
RegisterUserUseCase,
|
||||
RegisterUserRequest,
|
||||
RegisterUserResponse,
|
||||
VerifyEmailUseCase,
|
||||
VerifyEmailRequest,
|
||||
)
|
||||
|
||||
from packages.application.auth.login_use_case import (
|
||||
LoginUseCase,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshTokenUseCase,
|
||||
RefreshTokenRequest,
|
||||
LogoutUseCase,
|
||||
LoginUseCase,
|
||||
LogoutRequest,
|
||||
LogoutUseCase,
|
||||
RefreshTokenRequest,
|
||||
RefreshTokenUseCase,
|
||||
)
|
||||
from packages.application.auth.password_reset_use_case import (
|
||||
RequestPasswordResetUseCase,
|
||||
RequestPasswordResetRequest,
|
||||
ResetPasswordUseCase,
|
||||
RequestPasswordResetUseCase,
|
||||
ResetPasswordRequest,
|
||||
ResetPasswordUseCase,
|
||||
)
|
||||
from packages.application.auth.register_user_use_case import (
|
||||
RegisterUserRequest,
|
||||
RegisterUserResponse,
|
||||
RegisterUserUseCase,
|
||||
VerifyEmailRequest,
|
||||
VerifyEmailUseCase,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"""
|
||||
用户登录 Use Case
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from packages.adapters.redis import get_session_store
|
||||
from packages.domain.auth import (
|
||||
password_hasher,
|
||||
jwt_service,
|
||||
)
|
||||
from packages.domain.auth import jwt_service, password_hasher
|
||||
|
||||
|
||||
class LoginRequest:
|
||||
"""登录请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
email: str,
|
||||
@@ -30,7 +28,7 @@ class LoginRequest:
|
||||
|
||||
class LoginResponse:
|
||||
"""登录响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
access_token: str,
|
||||
@@ -52,18 +50,18 @@ class LoginResponse:
|
||||
|
||||
class LoginUseCase:
|
||||
"""用户登录用例"""
|
||||
|
||||
|
||||
def __init__(self, user_repository, session_store=None):
|
||||
self.user_repository = user_repository
|
||||
self.session_store = session_store or get_session_store()
|
||||
|
||||
|
||||
def execute(self, request: LoginRequest) -> tuple[Optional[LoginResponse], Optional[str]]:
|
||||
"""
|
||||
执行登录
|
||||
|
||||
|
||||
Args:
|
||||
request: 登录请求
|
||||
|
||||
|
||||
Returns:
|
||||
(登录响应, 错误信息)
|
||||
"""
|
||||
@@ -71,31 +69,32 @@ class LoginUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.email:
|
||||
return None, "Email is required"
|
||||
|
||||
|
||||
if not request.password:
|
||||
return None, "Password is required"
|
||||
|
||||
|
||||
# 2. 查找用户
|
||||
user = self.user_repository.find_by_email(request.email)
|
||||
if not user:
|
||||
return None, "Invalid email or password"
|
||||
|
||||
|
||||
# 3. 验证密码
|
||||
if not password_hasher.verify_password(request.password, user.password_hash):
|
||||
return None, "Invalid email or password"
|
||||
|
||||
|
||||
# 4. 检查邮箱是否已验证(可选,根据需求决定是否强制)
|
||||
# if not user.email_verified:
|
||||
# return None, "Please verify your email first"
|
||||
|
||||
|
||||
# 5. 创建 session 并生成 refresh_token
|
||||
session_id = secrets.token_urlsafe(16)
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
# 6. 生成基础 JWT token(包含 session_id,不包含 workspace)
|
||||
# 这里使用一个特殊的 "user_token",不包含 workspace 和 role
|
||||
# 用户选择工作空间后,会换取包含 workspace 的 access_token
|
||||
import jwt as pyjwt
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
access_token_payload = {
|
||||
"sub": user.id,
|
||||
@@ -107,7 +106,7 @@ class LoginUseCase:
|
||||
access_token = pyjwt.encode(
|
||||
access_token_payload,
|
||||
jwt_service.config.SECRET_KEY,
|
||||
algorithm=jwt_service.config.ALGORITHM
|
||||
algorithm=jwt_service.config.ALGORITHM,
|
||||
)
|
||||
self.session_store.save_session(
|
||||
session_id=session_id,
|
||||
@@ -117,74 +116,77 @@ class LoginUseCase:
|
||||
ip_address=request.ip_address,
|
||||
expires_in_seconds=30 * 24 * 3600, # 30 天
|
||||
)
|
||||
|
||||
|
||||
# 8. 更新最后登录信息
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
user.last_login_ip = request.ip_address
|
||||
self.user_repository.save(user)
|
||||
|
||||
|
||||
# 9. 返回响应
|
||||
return LoginResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
expires_in=jwt_service.config.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
), None
|
||||
|
||||
return (
|
||||
LoginResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
expires_in=jwt_service.config.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Login failed: {str(e)}"
|
||||
|
||||
|
||||
class RefreshTokenRequest:
|
||||
"""刷新令牌请求"""
|
||||
|
||||
|
||||
def __init__(self, refresh_token: str):
|
||||
self.refresh_token = refresh_token
|
||||
|
||||
|
||||
class RefreshTokenUseCase:
|
||||
"""刷新令牌用例"""
|
||||
|
||||
|
||||
def __init__(self, user_repository):
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: RefreshTokenRequest) -> tuple[Optional[LoginResponse], Optional[str]]:
|
||||
"""
|
||||
执行令牌刷新
|
||||
|
||||
|
||||
Args:
|
||||
request: 刷新请求
|
||||
|
||||
|
||||
Returns:
|
||||
(登录响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
if not request.refresh_token:
|
||||
return None, "Refresh token is required"
|
||||
|
||||
|
||||
# 1. 查找 session(通过遍历所有 session)
|
||||
# 注意:这里为了简化,先用遍历实现,生产环境应该用 refresh_token -> session_id 的索引
|
||||
session = None
|
||||
session_id = None
|
||||
|
||||
|
||||
# 这是一个简化实现,实际应该在 SessionStore 中添加 find_by_refresh_token 方法
|
||||
# 这里我们假设 refresh_token 就是 session_id(简化处理)
|
||||
# 生产环境需要更复杂的映射
|
||||
|
||||
|
||||
# 临时方案:从 Redis 获取(需要在 session_store 中添加方法)
|
||||
# 现在先返回错误,提示需要实现
|
||||
return None, "Refresh token implementation pending (需要完善 session_store)"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Token refresh failed: {str(e)}"
|
||||
|
||||
|
||||
class LogoutRequest:
|
||||
"""登出请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str,
|
||||
@@ -198,17 +200,17 @@ class LogoutRequest:
|
||||
|
||||
class LogoutUseCase:
|
||||
"""用户登出用例"""
|
||||
|
||||
|
||||
def __init__(self, session_store=None):
|
||||
self.session_store = session_store or get_session_store()
|
||||
|
||||
|
||||
def execute(self, request: LogoutRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行登出
|
||||
|
||||
|
||||
Args:
|
||||
request: 登出请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
@@ -221,12 +223,12 @@ class LogoutUseCase:
|
||||
# 删除当前 session
|
||||
if not request.session_id:
|
||||
return False, "Session ID is required"
|
||||
|
||||
|
||||
success = self.session_store.delete_session(request.session_id)
|
||||
if success:
|
||||
return True, None
|
||||
else:
|
||||
return False, "Session not found"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Logout failed: {str(e)}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
密码重置 Use Case
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
@@ -11,14 +12,14 @@ from packages.domain.auth import password_hasher, password_validator
|
||||
|
||||
class RequestPasswordResetRequest:
|
||||
"""请求密码重置"""
|
||||
|
||||
|
||||
def __init__(self, email: str):
|
||||
self.email = email.strip().lower()
|
||||
|
||||
|
||||
class RequestPasswordResetUseCase:
|
||||
"""请求密码重置用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_repository,
|
||||
@@ -30,41 +31,39 @@ class RequestPasswordResetUseCase:
|
||||
self.base_url = base_url
|
||||
self.token_expire_hours = token_expire_hours
|
||||
self.email_service = email_service or get_email_service()
|
||||
|
||||
|
||||
def execute(self, request: RequestPasswordResetRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行密码重置请求
|
||||
|
||||
|
||||
Args:
|
||||
request: 重置请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
if not request.email:
|
||||
return False, "Email is required"
|
||||
|
||||
|
||||
# 查找用户
|
||||
user = self.user_repository.find_by_email(request.email)
|
||||
|
||||
|
||||
# 安全考虑:即使用户不存在,也返回成功(避免暴露用户存在性)
|
||||
if not user:
|
||||
return True, None
|
||||
|
||||
|
||||
# 生成重置令牌
|
||||
reset_token = secrets.token_urlsafe(32)
|
||||
reset_url = f"{self.base_url}/reset-password?token={reset_token}"
|
||||
|
||||
|
||||
# 设置令牌和过期时间
|
||||
user.password_reset_token = reset_token
|
||||
user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(
|
||||
hours=self.token_expire_hours
|
||||
)
|
||||
|
||||
user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=self.token_expire_hours)
|
||||
|
||||
# 保存用户
|
||||
self.user_repository.save(user)
|
||||
|
||||
|
||||
# 发送重置邮件
|
||||
try:
|
||||
success, error = self.email_service.send_password_reset_email(
|
||||
@@ -72,22 +71,22 @@ class RequestPasswordResetUseCase:
|
||||
username=user.username or user.display_name,
|
||||
reset_url=reset_url,
|
||||
)
|
||||
|
||||
|
||||
if not success:
|
||||
print(f"Failed to send password reset email: {error}")
|
||||
# 不返回错误,避免暴露用户存在性
|
||||
except Exception as e:
|
||||
print(f"Email service error: {e}")
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Password reset request failed: {str(e)}"
|
||||
|
||||
|
||||
class ResetPasswordRequest:
|
||||
"""重置密码请求"""
|
||||
|
||||
|
||||
def __init__(self, token: str, new_password: str):
|
||||
self.token = token
|
||||
self.new_password = new_password
|
||||
@@ -95,54 +94,54 @@ class ResetPasswordRequest:
|
||||
|
||||
class ResetPasswordUseCase:
|
||||
"""重置密码用例"""
|
||||
|
||||
|
||||
def __init__(self, user_repository):
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: ResetPasswordRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行密码重置
|
||||
|
||||
|
||||
Args:
|
||||
request: 重置请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
if not request.token:
|
||||
return False, "Reset token is required"
|
||||
|
||||
|
||||
if not request.new_password:
|
||||
return False, "New password is required"
|
||||
|
||||
|
||||
# 验证新密码强度
|
||||
valid, error = password_validator.validate(request.new_password)
|
||||
if not valid:
|
||||
return False, error
|
||||
|
||||
|
||||
# 查找用户
|
||||
user = self.user_repository.find_by_password_reset_token(request.token)
|
||||
if not user:
|
||||
return False, "Invalid or expired reset token"
|
||||
|
||||
|
||||
# 检查令牌是否过期
|
||||
if user.password_reset_expires_at:
|
||||
if datetime.now(timezone.utc) > user.password_reset_expires_at:
|
||||
return False, "Reset token has expired"
|
||||
|
||||
|
||||
# 哈希新密码
|
||||
hashed_password = password_hasher.hash_password(request.new_password)
|
||||
|
||||
|
||||
# 更新用户密码
|
||||
user.password_hash = hashed_password
|
||||
user.password_reset_token = None
|
||||
user.password_reset_expires_at = None
|
||||
|
||||
|
||||
# 保存用户
|
||||
self.user_repository.save(user)
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Password reset failed: {str(e)}"
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"""
|
||||
用户注册 Use Case
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from packages.adapters.smtp import get_email_service
|
||||
from packages.domain.entities import User
|
||||
from packages.domain.auth import password_hasher, password_validator
|
||||
from packages.domain.entities import User
|
||||
|
||||
|
||||
class RegisterUserRequest:
|
||||
"""注册请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
email: str,
|
||||
@@ -29,7 +30,7 @@ class RegisterUserRequest:
|
||||
|
||||
class RegisterUserResponse:
|
||||
"""注册响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str,
|
||||
@@ -47,7 +48,7 @@ class RegisterUserResponse:
|
||||
|
||||
class RegisterUserUseCase:
|
||||
"""用户注册用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_repository,
|
||||
@@ -56,7 +57,7 @@ class RegisterUserUseCase:
|
||||
):
|
||||
"""
|
||||
初始化注册用例
|
||||
|
||||
|
||||
Args:
|
||||
user_repository: 用户仓储
|
||||
base_url: 应用基础 URL(用于生成验证链接)
|
||||
@@ -64,14 +65,14 @@ class RegisterUserUseCase:
|
||||
self.user_repository = user_repository
|
||||
self.base_url = base_url
|
||||
self.email_service = email_service or get_email_service()
|
||||
|
||||
|
||||
def execute(self, request: RegisterUserRequest) -> tuple[Optional[RegisterUserResponse], Optional[str]]:
|
||||
"""
|
||||
执行注册
|
||||
|
||||
|
||||
Args:
|
||||
request: 注册请求
|
||||
|
||||
|
||||
Returns:
|
||||
(注册响应, 错误信息)
|
||||
"""
|
||||
@@ -79,34 +80,34 @@ class RegisterUserUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.email:
|
||||
return None, "Email is required"
|
||||
|
||||
|
||||
if not request.username:
|
||||
return None, "Username is required"
|
||||
|
||||
|
||||
if not request.display_name:
|
||||
return None, "Display name is required"
|
||||
|
||||
|
||||
# 2. 验证密码强度
|
||||
valid, error = password_validator.validate(request.password)
|
||||
if not valid:
|
||||
return None, error
|
||||
|
||||
|
||||
# 3. 检查邮箱是否已存在
|
||||
existing_user = self.user_repository.find_by_email(request.email)
|
||||
if existing_user:
|
||||
return None, "Email already registered"
|
||||
|
||||
|
||||
# 4. 检查用户名是否已存在
|
||||
existing_username = self.user_repository.find_by_username(request.username)
|
||||
if existing_username:
|
||||
return None, "Username already taken"
|
||||
|
||||
|
||||
# 5. 哈希密码
|
||||
hashed_password = password_hasher.hash_password(request.password)
|
||||
|
||||
|
||||
# 6. 生成邮箱验证令牌
|
||||
verification_token = secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
# 7. 创建用户
|
||||
user = User(
|
||||
id=uuid4().hex,
|
||||
@@ -118,14 +119,14 @@ class RegisterUserUseCase:
|
||||
email_verification_token=verification_token,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
# 8. 保存用户
|
||||
self.user_repository.save(user)
|
||||
|
||||
|
||||
# 9. 发送验证邮件
|
||||
verification_url = f"{self.base_url}/verify-email?token={verification_token}"
|
||||
email_sent = False
|
||||
|
||||
|
||||
try:
|
||||
success, error = self.email_service.send_verification_email(
|
||||
to_email=user.email,
|
||||
@@ -133,68 +134,71 @@ class RegisterUserUseCase:
|
||||
verification_url=verification_url,
|
||||
)
|
||||
email_sent = success
|
||||
|
||||
|
||||
if not success:
|
||||
print(f"Failed to send verification email: {error}")
|
||||
except Exception as e:
|
||||
print(f"Email service error: {e}")
|
||||
|
||||
|
||||
# 10. 返回响应(即使邮件发送失败,用户也已创建)
|
||||
return RegisterUserResponse(
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
email_verification_sent=email_sent,
|
||||
), None
|
||||
|
||||
return (
|
||||
RegisterUserResponse(
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
email_verification_sent=email_sent,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Registration failed: {str(e)}"
|
||||
|
||||
|
||||
class VerifyEmailRequest:
|
||||
"""邮箱验证请求"""
|
||||
|
||||
|
||||
def __init__(self, token: str):
|
||||
self.token = token
|
||||
|
||||
|
||||
class VerifyEmailUseCase:
|
||||
"""邮箱验证用例"""
|
||||
|
||||
|
||||
def __init__(self, user_repository):
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: VerifyEmailRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行邮箱验证
|
||||
|
||||
|
||||
Args:
|
||||
request: 验证请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
if not request.token:
|
||||
return False, "Verification token is required"
|
||||
|
||||
|
||||
# 查找用户
|
||||
user = self.user_repository.find_by_verification_token(request.token)
|
||||
if not user:
|
||||
return False, "Invalid or expired verification token"
|
||||
|
||||
|
||||
# 检查是否已验证
|
||||
if user.email_verified:
|
||||
return True, None # 已验证,返回成功
|
||||
|
||||
|
||||
# 更新用户状态
|
||||
user.email_verified = True
|
||||
user.email_verification_token = None # 清空令牌
|
||||
|
||||
|
||||
self.user_repository.save(user)
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Email verification failed: {str(e)}"
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
"""
|
||||
通用分页器
|
||||
"""
|
||||
from typing import Generic, TypeVar, List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from math import ceil
|
||||
|
||||
from math import ceil
|
||||
from typing import Generic, List, Optional, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
"""分页参数"""
|
||||
|
||||
page: int = Field(1, ge=1, description="页码(从 1 开始)")
|
||||
page_size: int = Field(20, ge=1, le=100, description="每页数量(最大 100)")
|
||||
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
"""计算偏移量"""
|
||||
return (self.page - 1) * self.page_size
|
||||
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
"""返回限制数量"""
|
||||
@@ -27,13 +29,14 @@ class PaginationParams(BaseModel):
|
||||
|
||||
class PaginationMeta(BaseModel):
|
||||
"""分页元数据"""
|
||||
|
||||
page: int = Field(..., description="当前页码")
|
||||
page_size: int = Field(..., description="每页数量")
|
||||
total: int = Field(..., description="总记录数")
|
||||
total_pages: int = Field(..., description="总页数")
|
||||
has_next: bool = Field(..., description="是否有下一页")
|
||||
has_prev: bool = Field(..., description="是否有上一页")
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_params(
|
||||
cls,
|
||||
@@ -42,7 +45,7 @@ class PaginationMeta(BaseModel):
|
||||
) -> "PaginationMeta":
|
||||
"""从参数和总数创建元数据"""
|
||||
total_pages = ceil(total / params.page_size) if total > 0 else 0
|
||||
|
||||
|
||||
return cls(
|
||||
page=params.page,
|
||||
page_size=params.page_size,
|
||||
@@ -55,9 +58,10 @@ class PaginationMeta(BaseModel):
|
||||
|
||||
class PaginatedResponse(BaseModel, Generic[T]):
|
||||
"""分页响应"""
|
||||
|
||||
data: List[T] = Field(..., description="数据列表")
|
||||
pagination: PaginationMeta = Field(..., description="分页信息")
|
||||
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
@@ -78,11 +82,11 @@ def paginate(
|
||||
) -> PaginatedResponse[T]:
|
||||
"""
|
||||
内存分页(适用于 InMemory Repository)
|
||||
|
||||
|
||||
Args:
|
||||
items: 完整列表
|
||||
params: 分页参数
|
||||
|
||||
|
||||
Returns:
|
||||
分页响应
|
||||
"""
|
||||
@@ -90,7 +94,7 @@ def paginate(
|
||||
start = params.offset
|
||||
end = start + params.limit
|
||||
page_data = items[start:end]
|
||||
|
||||
|
||||
return PaginatedResponse.create(
|
||||
data=page_data,
|
||||
params=params,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""获取单个任务详情用例"""
|
||||
|
||||
from packages.domain import Task
|
||||
from packages.ports import TaskRepository
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""项目管理 Use Cases"""
|
||||
|
||||
from packages.domain import Milestone, Task, TaskIssue, TaskPriority, TaskStatus
|
||||
from packages.ports import MilestoneRepository, TaskIssueRepository, TaskRepository
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""更新任务基本信息用例"""
|
||||
|
||||
from packages.domain import Task
|
||||
from packages.ports import TaskRepository
|
||||
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
"""Workspace 相关 Use Cases"""
|
||||
from packages.application.workspace.create_workspace_use_case import (
|
||||
CreateWorkspaceUseCase,
|
||||
CreateWorkspaceRequest,
|
||||
CreateWorkspaceResponse,
|
||||
)
|
||||
from packages.application.workspace.invite_member_use_case import (
|
||||
InviteMemberUseCase,
|
||||
InviteMemberRequest,
|
||||
InviteMemberResponse,
|
||||
)
|
||||
|
||||
from packages.application.workspace.accept_invitation_use_case import (
|
||||
AcceptInvitationUseCase,
|
||||
AcceptInvitationRequest,
|
||||
AcceptInvitationResponse,
|
||||
DeclineInvitationUseCase,
|
||||
AcceptInvitationUseCase,
|
||||
DeclineInvitationRequest,
|
||||
DeclineInvitationUseCase,
|
||||
)
|
||||
from packages.application.workspace.remove_member_use_case import (
|
||||
RemoveMemberUseCase,
|
||||
RemoveMemberRequest,
|
||||
LeaveWorkspaceUseCase,
|
||||
LeaveWorkspaceRequest,
|
||||
from packages.application.workspace.create_workspace_use_case import (
|
||||
CreateWorkspaceRequest,
|
||||
CreateWorkspaceResponse,
|
||||
CreateWorkspaceUseCase,
|
||||
)
|
||||
from packages.application.workspace.update_member_role_use_case import (
|
||||
UpdateMemberRoleUseCase,
|
||||
UpdateMemberRoleRequest,
|
||||
UpdateMemberRoleResponse,
|
||||
)
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
ListWorkspacesUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesResponse,
|
||||
GetWorkspaceDetailUseCase,
|
||||
GetWorkspaceDetailRequest,
|
||||
WorkspaceInfo,
|
||||
WorkspaceDetailInfo,
|
||||
from packages.application.workspace.invite_member_use_case import (
|
||||
InviteMemberRequest,
|
||||
InviteMemberResponse,
|
||||
InviteMemberUseCase,
|
||||
)
|
||||
from packages.application.workspace.list_members_use_case import (
|
||||
ListMembersUseCase,
|
||||
ListMembersRequest,
|
||||
ListMembersResponse,
|
||||
ListMembersUseCase,
|
||||
MemberInfo,
|
||||
)
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
GetWorkspaceDetailRequest,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesResponse,
|
||||
ListWorkspacesUseCase,
|
||||
WorkspaceDetailInfo,
|
||||
WorkspaceInfo,
|
||||
)
|
||||
from packages.application.workspace.remove_member_use_case import (
|
||||
LeaveWorkspaceRequest,
|
||||
LeaveWorkspaceUseCase,
|
||||
RemoveMemberRequest,
|
||||
RemoveMemberUseCase,
|
||||
)
|
||||
from packages.application.workspace.subscription_use_case import (
|
||||
UpgradeSubscriptionUseCase,
|
||||
CancelSubscriptionRequest,
|
||||
CancelSubscriptionUseCase,
|
||||
UpgradeSubscriptionRequest,
|
||||
UpgradeSubscriptionResponse,
|
||||
CancelSubscriptionUseCase,
|
||||
CancelSubscriptionRequest,
|
||||
UpgradeSubscriptionUseCase,
|
||||
)
|
||||
from packages.application.workspace.update_member_role_use_case import (
|
||||
UpdateMemberRoleRequest,
|
||||
UpdateMemberRoleResponse,
|
||||
UpdateMemberRoleUseCase,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""
|
||||
接受/拒绝邀请 Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from packages.domain.entities import (
|
||||
WorkspaceMember,
|
||||
InvitationStatus,
|
||||
)
|
||||
from packages.domain.entities import InvitationStatus, WorkspaceMember
|
||||
|
||||
|
||||
class AcceptInvitationRequest:
|
||||
"""接受邀请请求"""
|
||||
|
||||
|
||||
def __init__(self, invitation_token: str, user_id: str):
|
||||
self.invitation_token = invitation_token
|
||||
self.user_id = user_id
|
||||
@@ -21,7 +19,7 @@ class AcceptInvitationRequest:
|
||||
|
||||
class AcceptInvitationResponse:
|
||||
"""接受邀请响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -35,7 +33,7 @@ class AcceptInvitationResponse:
|
||||
|
||||
class AcceptInvitationUseCase:
|
||||
"""接受邀请用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -47,14 +45,14 @@ class AcceptInvitationUseCase:
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.workspace_invitation_repository = workspace_invitation_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: AcceptInvitationRequest) -> tuple[Optional[AcceptInvitationResponse], Optional[str]]:
|
||||
"""
|
||||
执行接受邀请
|
||||
|
||||
|
||||
Args:
|
||||
request: 接受请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -62,40 +60,40 @@ class AcceptInvitationUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.invitation_token:
|
||||
return None, "Invitation token is required"
|
||||
|
||||
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
|
||||
# 2. 查找邀请
|
||||
invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token)
|
||||
if not invitation:
|
||||
return None, "Invalid invitation token"
|
||||
|
||||
|
||||
# 3. 检查邀请状态
|
||||
if invitation.status != InvitationStatus.PENDING:
|
||||
return None, f"Invitation has already been {invitation.status}"
|
||||
|
||||
|
||||
# 4. 检查是否过期
|
||||
if invitation.expires_at and datetime.now(timezone.utc) > invitation.expires_at:
|
||||
# 更新状态为过期
|
||||
invitation.status = InvitationStatus.EXPIRED
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
return None, "Invitation has expired"
|
||||
|
||||
|
||||
# 5. 验证用户存在
|
||||
user = self.user_repository.find_by_id(request.user_id)
|
||||
if not user:
|
||||
return None, "User not found"
|
||||
|
||||
|
||||
# 6. 验证用户邮箱匹配
|
||||
if user.email.lower() != invitation.invitee_email.lower():
|
||||
return None, "This invitation is for a different email address"
|
||||
|
||||
|
||||
# 7. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(invitation.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 8. 检查用户是否已经是成员
|
||||
existing_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
invitation.workspace_id,
|
||||
@@ -106,13 +104,16 @@ class AcceptInvitationUseCase:
|
||||
invitation.status = InvitationStatus.ACCEPTED
|
||||
invitation.accepted_at = datetime.now(timezone.utc)
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
return AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=existing_member.role,
|
||||
), None
|
||||
|
||||
|
||||
return (
|
||||
AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=existing_member.role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
# 9. 创建成员记录
|
||||
member = WorkspaceMember(
|
||||
id=uuid4().hex,
|
||||
@@ -122,45 +123,48 @@ class AcceptInvitationUseCase:
|
||||
invited_by=invitation.inviter_user_id,
|
||||
joined_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
self.workspace_member_repository.save(member)
|
||||
|
||||
|
||||
# 10. 更新邀请状态
|
||||
invitation.status = InvitationStatus.ACCEPTED
|
||||
invitation.accepted_at = datetime.now(timezone.utc)
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
|
||||
# 11. 返回响应
|
||||
return AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=member.role,
|
||||
), None
|
||||
|
||||
return (
|
||||
AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=member.role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to accept invitation: {str(e)}"
|
||||
|
||||
|
||||
class DeclineInvitationRequest:
|
||||
"""拒绝邀请请求"""
|
||||
|
||||
|
||||
def __init__(self, invitation_token: str):
|
||||
self.invitation_token = invitation_token
|
||||
|
||||
|
||||
class DeclineInvitationUseCase:
|
||||
"""拒绝邀请用例"""
|
||||
|
||||
|
||||
def __init__(self, workspace_invitation_repository):
|
||||
self.workspace_invitation_repository = workspace_invitation_repository
|
||||
|
||||
|
||||
def execute(self, request: DeclineInvitationRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行拒绝邀请
|
||||
|
||||
|
||||
Args:
|
||||
request: 拒绝请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
@@ -168,21 +172,21 @@ class DeclineInvitationUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.invitation_token:
|
||||
return False, "Invitation token is required"
|
||||
|
||||
|
||||
# 2. 查找邀请
|
||||
invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token)
|
||||
if not invitation:
|
||||
return False, "Invalid invitation token"
|
||||
|
||||
|
||||
# 3. 检查邀请状态
|
||||
if invitation.status != InvitationStatus.PENDING:
|
||||
return False, f"Invitation has already been {invitation.status}"
|
||||
|
||||
|
||||
# 4. 更新状态为已拒绝
|
||||
invitation.status = InvitationStatus.DECLINED
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to decline invitation: {str(e)}"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"""
|
||||
创建 Workspace Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from packages.domain.entities import Workspace, WorkspaceMember, WorkspaceMemberRole
|
||||
|
||||
|
||||
class CreateWorkspaceRequest:
|
||||
"""创建工作空间请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -24,7 +25,7 @@ class CreateWorkspaceRequest:
|
||||
|
||||
class CreateWorkspaceResponse:
|
||||
"""创建工作空间响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -42,14 +43,14 @@ class CreateWorkspaceResponse:
|
||||
|
||||
class CreateWorkspaceUseCase:
|
||||
"""创建工作空间用例"""
|
||||
|
||||
|
||||
# 订阅计划配额配置
|
||||
PLAN_QUOTAS = {
|
||||
"free": {"max_projects": 3, "max_storage_gb": 10},
|
||||
"pro": {"max_projects": 999999, "max_storage_gb": 100}, # 999999 表示无限
|
||||
"enterprise": {"max_projects": 999999, "max_storage_gb": 1000},
|
||||
}
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -59,14 +60,14 @@ class CreateWorkspaceUseCase:
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: CreateWorkspaceRequest) -> tuple[Optional[CreateWorkspaceResponse], Optional[str]]:
|
||||
"""
|
||||
执行创建工作空间
|
||||
|
||||
|
||||
Args:
|
||||
request: 创建请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -74,25 +75,25 @@ class CreateWorkspaceUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.name:
|
||||
return None, "Workspace name is required"
|
||||
|
||||
|
||||
if len(request.name) > 100:
|
||||
return None, "Workspace name is too long (max 100 characters)"
|
||||
|
||||
|
||||
if not request.owner_user_id:
|
||||
return None, "Owner user ID is required"
|
||||
|
||||
|
||||
# 2. 验证用户存在
|
||||
owner = self.user_repository.find_by_id(request.owner_user_id)
|
||||
if not owner:
|
||||
return None, "Owner user not found"
|
||||
|
||||
|
||||
# 3. 验证订阅计划
|
||||
if request.subscription_plan not in self.PLAN_QUOTAS:
|
||||
return None, f"Invalid subscription plan: {request.subscription_plan}"
|
||||
|
||||
|
||||
# 4. 获取配额
|
||||
quota = self.PLAN_QUOTAS[request.subscription_plan]
|
||||
|
||||
|
||||
# 5. 创建 Workspace
|
||||
workspace = Workspace(
|
||||
id=uuid4().hex,
|
||||
@@ -105,10 +106,10 @@ class CreateWorkspaceUseCase:
|
||||
used_storage_gb=0.0,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
# 6. 保存 Workspace
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
|
||||
# 7. 创建 Owner 成员记录
|
||||
owner_member = WorkspaceMember(
|
||||
id=uuid4().hex,
|
||||
@@ -118,17 +119,20 @@ class CreateWorkspaceUseCase:
|
||||
invited_by=None, # Owner 不需要邀请
|
||||
joined_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
self.workspace_member_repository.save(owner_member)
|
||||
|
||||
|
||||
# 8. 返回响应
|
||||
return CreateWorkspaceResponse(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
), None
|
||||
|
||||
return (
|
||||
CreateWorkspaceResponse(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to create workspace: {str(e)}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
邀请成员到 Workspace Use Case
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
@@ -8,15 +9,15 @@ from uuid import uuid4
|
||||
|
||||
from packages.adapters.smtp import get_email_service
|
||||
from packages.domain.entities import (
|
||||
InvitationStatus,
|
||||
WorkspaceInvitation,
|
||||
WorkspaceMemberRole,
|
||||
InvitationStatus,
|
||||
)
|
||||
|
||||
|
||||
class InviteMemberRequest:
|
||||
"""邀请成员请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -32,7 +33,7 @@ class InviteMemberRequest:
|
||||
|
||||
class InviteMemberResponse:
|
||||
"""邀请成员响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
invitation_id: str,
|
||||
@@ -48,13 +49,13 @@ class InviteMemberResponse:
|
||||
|
||||
class InviteMemberUseCase:
|
||||
"""邀请成员用例"""
|
||||
|
||||
|
||||
VALID_ROLES = [
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
WorkspaceMemberRole.MEMBER,
|
||||
WorkspaceMemberRole.VIEWER,
|
||||
]
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -72,14 +73,14 @@ class InviteMemberUseCase:
|
||||
self.base_url = base_url
|
||||
self.invitation_expire_days = invitation_expire_days
|
||||
self.email_service = email_service or get_email_service()
|
||||
|
||||
|
||||
def execute(self, request: InviteMemberRequest) -> tuple[Optional[InviteMemberResponse], Optional[str]]:
|
||||
"""
|
||||
执行邀请成员
|
||||
|
||||
|
||||
Args:
|
||||
request: 邀请请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -87,25 +88,25 @@ class InviteMemberUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.inviter_user_id:
|
||||
return None, "Inviter user ID is required"
|
||||
|
||||
|
||||
if not request.invitee_email:
|
||||
return None, "Invitee email is required"
|
||||
|
||||
|
||||
if not request.role:
|
||||
return None, "Role is required"
|
||||
|
||||
|
||||
# 2. 验证角色(不能邀请 owner)
|
||||
if request.role not in self.VALID_ROLES:
|
||||
return None, f"Invalid role: {request.role}. Cannot invite as owner."
|
||||
|
||||
|
||||
# 3. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 4. 验证邀请人是成员且有权限(owner 或 admin)
|
||||
inviter_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -113,10 +114,13 @@ class InviteMemberUseCase:
|
||||
)
|
||||
if not inviter_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
if inviter_member.role not in [WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN]:
|
||||
|
||||
if inviter_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return None, "Only owners and admins can invite members"
|
||||
|
||||
|
||||
# 5. 检查被邀请人是否已经是成员
|
||||
invitee_user = self.user_repository.find_by_email(request.invitee_email)
|
||||
if invitee_user:
|
||||
@@ -126,7 +130,7 @@ class InviteMemberUseCase:
|
||||
)
|
||||
if existing_member:
|
||||
return None, "User is already a member of this workspace"
|
||||
|
||||
|
||||
# 6. 检查是否已有待处理的邀请
|
||||
existing_invitation = self.workspace_invitation_repository.find_pending_by_workspace_and_email(
|
||||
request.workspace_id,
|
||||
@@ -134,11 +138,11 @@ class InviteMemberUseCase:
|
||||
)
|
||||
if existing_invitation:
|
||||
return None, "An invitation has already been sent to this email"
|
||||
|
||||
|
||||
# 7. 生成邀请令牌
|
||||
invitation_token = secrets.token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(days=self.invitation_expire_days)
|
||||
|
||||
|
||||
# 8. 创建邀请记录
|
||||
invitation = WorkspaceInvitation(
|
||||
id=uuid4().hex,
|
||||
@@ -151,17 +155,17 @@ class InviteMemberUseCase:
|
||||
expires_at=expires_at,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
# 9. 保存邀请
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
|
||||
# 10. 发送邀请邮件
|
||||
invitation_url = f"{self.base_url}/invitations/{invitation_token}/accept"
|
||||
|
||||
|
||||
try:
|
||||
inviter = self.user_repository.find_by_id(request.inviter_user_id)
|
||||
inviter_name = inviter.display_name if inviter else "Someone"
|
||||
|
||||
|
||||
success, error = self.email_service.send_workspace_invitation_email(
|
||||
to_email=request.invitee_email,
|
||||
inviter_name=inviter_name,
|
||||
@@ -169,19 +173,22 @@ class InviteMemberUseCase:
|
||||
role=request.role,
|
||||
invitation_url=invitation_url,
|
||||
)
|
||||
|
||||
|
||||
if not success:
|
||||
print(f"Failed to send invitation email: {error}")
|
||||
except Exception as e:
|
||||
print(f"Email service error: {e}")
|
||||
|
||||
|
||||
# 11. 返回响应
|
||||
return InviteMemberResponse(
|
||||
invitation_id=invitation.id,
|
||||
invitee_email=invitation.invitee_email,
|
||||
role=invitation.role,
|
||||
expires_at=invitation.expires_at,
|
||||
), None
|
||||
|
||||
return (
|
||||
InviteMemberResponse(
|
||||
invitation_id=invitation.id,
|
||||
invitee_email=invitation.invitee_email,
|
||||
role=invitation.role,
|
||||
expires_at=invitation.expires_at,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to invite member: {str(e)}"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""
|
||||
获取成员列表 Use Case
|
||||
"""
|
||||
from typing import Optional, List
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class MemberInfo:
|
||||
"""成员信息"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
member_id: str,
|
||||
@@ -31,7 +32,7 @@ class MemberInfo:
|
||||
|
||||
class ListMembersRequest:
|
||||
"""获取成员列表请求"""
|
||||
|
||||
|
||||
def __init__(self, workspace_id: str, requester_user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
@@ -39,14 +40,14 @@ class ListMembersRequest:
|
||||
|
||||
class ListMembersResponse:
|
||||
"""获取成员列表响应"""
|
||||
|
||||
|
||||
def __init__(self, members: List[MemberInfo]):
|
||||
self.members = members
|
||||
|
||||
|
||||
class ListMembersUseCase:
|
||||
"""获取成员列表用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -56,14 +57,14 @@ class ListMembersUseCase:
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
|
||||
def execute(self, request: ListMembersRequest) -> tuple[Optional[ListMembersResponse], Optional[str]]:
|
||||
"""
|
||||
执行获取成员列表
|
||||
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -71,15 +72,15 @@ class ListMembersUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 3. 验证请求者是成员(只有成员才能查看成员列表)
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -87,17 +88,17 @@ class ListMembersUseCase:
|
||||
)
|
||||
if not requester_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
|
||||
# 4. 获取所有成员
|
||||
members = self.workspace_member_repository.find_by_workspace(request.workspace_id)
|
||||
|
||||
|
||||
# 5. 获取每个成员的用户信息
|
||||
member_infos = []
|
||||
for member in members:
|
||||
user = self.user_repository.find_by_id(member.user_id)
|
||||
if not user:
|
||||
continue # 跳过不存在的用户
|
||||
|
||||
|
||||
member_info = MemberInfo(
|
||||
member_id=member.id,
|
||||
user_id=user.id,
|
||||
@@ -109,9 +110,9 @@ class ListMembersUseCase:
|
||||
joined_at=member.joined_at,
|
||||
)
|
||||
member_infos.append(member_info)
|
||||
|
||||
|
||||
# 6. 返回响应
|
||||
return ListMembersResponse(members=member_infos), None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to list members: {str(e)}"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""
|
||||
获取工作空间列表和详情 Use Case
|
||||
"""
|
||||
from typing import Optional, List
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class WorkspaceInfo:
|
||||
"""工作空间信息"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -37,21 +38,21 @@ class WorkspaceInfo:
|
||||
|
||||
class ListWorkspacesRequest:
|
||||
"""获取工作空间列表请求"""
|
||||
|
||||
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class ListWorkspacesResponse:
|
||||
"""获取工作空间列表响应"""
|
||||
|
||||
|
||||
def __init__(self, workspaces: List[WorkspaceInfo]):
|
||||
self.workspaces = workspaces
|
||||
|
||||
|
||||
class ListWorkspacesUseCase:
|
||||
"""获取工作空间列表用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -59,14 +60,14 @@ class ListWorkspacesUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: ListWorkspacesRequest) -> tuple[Optional[ListWorkspacesResponse], Optional[str]]:
|
||||
"""
|
||||
执行获取工作空间列表
|
||||
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -74,20 +75,20 @@ class ListWorkspacesUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
|
||||
# 2. 获取用户所有的成员记录
|
||||
memberships = self.workspace_member_repository.find_by_user(request.user_id)
|
||||
|
||||
|
||||
# 3. 获取每个工作空间的信息
|
||||
workspace_infos = []
|
||||
for membership in memberships:
|
||||
workspace = self.workspace_repository.find_by_id(membership.workspace_id)
|
||||
if not workspace:
|
||||
continue
|
||||
|
||||
|
||||
# 获取成员数量
|
||||
member_count = self.workspace_member_repository.count_by_workspace(membership.workspace_id)
|
||||
|
||||
|
||||
workspace_info = WorkspaceInfo(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
@@ -102,17 +103,17 @@ class ListWorkspacesUseCase:
|
||||
created_at=workspace.created_at,
|
||||
)
|
||||
workspace_infos.append(workspace_info)
|
||||
|
||||
|
||||
# 4. 返回响应
|
||||
return ListWorkspacesResponse(workspaces=workspace_infos), None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to list workspaces: {str(e)}"
|
||||
|
||||
|
||||
class GetWorkspaceDetailRequest:
|
||||
"""获取工作空间详情请求"""
|
||||
|
||||
|
||||
def __init__(self, workspace_id: str, user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.user_id = user_id
|
||||
@@ -120,7 +121,7 @@ class GetWorkspaceDetailRequest:
|
||||
|
||||
class WorkspaceDetailInfo:
|
||||
"""工作空间详情信息"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -152,7 +153,7 @@ class WorkspaceDetailInfo:
|
||||
|
||||
class GetWorkspaceDetailUseCase:
|
||||
"""获取工作空间详情用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -160,14 +161,14 @@ class GetWorkspaceDetailUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: GetWorkspaceDetailRequest) -> tuple[Optional[WorkspaceDetailInfo], Optional[str]]:
|
||||
"""
|
||||
执行获取工作空间详情
|
||||
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
|
||||
Returns:
|
||||
(详情信息, 错误信息)
|
||||
"""
|
||||
@@ -175,15 +176,15 @@ class GetWorkspaceDetailUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 3. 验证用户是成员
|
||||
membership = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -191,10 +192,10 @@ class GetWorkspaceDetailUseCase:
|
||||
)
|
||||
if not membership:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
|
||||
# 4. 获取成员数量
|
||||
member_count = self.workspace_member_repository.count_by_workspace(request.workspace_id)
|
||||
|
||||
|
||||
# 5. 构建详情信息
|
||||
detail_info = WorkspaceDetailInfo(
|
||||
workspace_id=workspace.id,
|
||||
@@ -210,8 +211,8 @@ class GetWorkspaceDetailUseCase:
|
||||
user_role=membership.role,
|
||||
created_at=workspace.created_at,
|
||||
)
|
||||
|
||||
|
||||
return detail_info, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to get workspace detail: {str(e)}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
移除成员 Use Case
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
@@ -8,7 +9,7 @@ from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
class RemoveMemberRequest:
|
||||
"""移除成员请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -22,7 +23,7 @@ class RemoveMemberRequest:
|
||||
|
||||
class RemoveMemberUseCase:
|
||||
"""移除成员用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -30,14 +31,14 @@ class RemoveMemberUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: RemoveMemberRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行移除成员
|
||||
|
||||
|
||||
Args:
|
||||
request: 移除请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
@@ -45,18 +46,18 @@ class RemoveMemberUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.requester_user_id:
|
||||
return False, "Requester user ID is required"
|
||||
|
||||
|
||||
if not request.target_user_id:
|
||||
return False, "Target user ID is required"
|
||||
|
||||
|
||||
# 2. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
|
||||
# 3. 验证请求者是成员且有权限
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -64,10 +65,13 @@ class RemoveMemberUseCase:
|
||||
)
|
||||
if not requester_member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
if requester_member.role not in [WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN]:
|
||||
|
||||
if requester_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return False, "Only owners and admins can remove members"
|
||||
|
||||
|
||||
# 4. 验证目标成员存在
|
||||
target_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -75,34 +79,33 @@ class RemoveMemberUseCase:
|
||||
)
|
||||
if not target_member:
|
||||
return False, "Target user is not a member of this workspace"
|
||||
|
||||
|
||||
# 5. 不能移除自己(应该用离开 workspace 的功能)
|
||||
if request.requester_user_id == request.target_user_id:
|
||||
return False, "Cannot remove yourself. Use leave workspace instead."
|
||||
|
||||
|
||||
# 6. 不能移除 owner
|
||||
if target_member.role == WorkspaceMemberRole.OWNER:
|
||||
return False, "Cannot remove the workspace owner"
|
||||
|
||||
|
||||
# 7. Admin 不能移除另一个 Admin(只有 owner 可以)
|
||||
if (requester_member.role == WorkspaceMemberRole.ADMIN and
|
||||
target_member.role == WorkspaceMemberRole.ADMIN):
|
||||
if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN:
|
||||
return False, "Admins cannot remove other admins"
|
||||
|
||||
|
||||
# 8. 删除成员记录
|
||||
success = self.workspace_member_repository.delete(target_member.id)
|
||||
if not success:
|
||||
return False, "Failed to remove member"
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to remove member: {str(e)}"
|
||||
|
||||
|
||||
class LeaveWorkspaceRequest:
|
||||
"""离开 Workspace 请求"""
|
||||
|
||||
|
||||
def __init__(self, workspace_id: str, user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.user_id = user_id
|
||||
@@ -110,7 +113,7 @@ class LeaveWorkspaceRequest:
|
||||
|
||||
class LeaveWorkspaceUseCase:
|
||||
"""离开 Workspace 用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -118,14 +121,14 @@ class LeaveWorkspaceUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: LeaveWorkspaceRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行离开 Workspace
|
||||
|
||||
|
||||
Args:
|
||||
request: 离开请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
@@ -133,15 +136,15 @@ class LeaveWorkspaceUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.user_id:
|
||||
return False, "User ID is required"
|
||||
|
||||
|
||||
# 2. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
|
||||
# 3. 验证用户是成员
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -149,17 +152,20 @@ class LeaveWorkspaceUseCase:
|
||||
)
|
||||
if not member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
|
||||
# 4. Owner 不能离开(需要先转移 ownership 或删除 workspace)
|
||||
if member.role == WorkspaceMemberRole.OWNER:
|
||||
return False, "Owner cannot leave workspace. Transfer ownership or delete workspace first."
|
||||
|
||||
return (
|
||||
False,
|
||||
"Owner cannot leave workspace. Transfer ownership or delete workspace first.",
|
||||
)
|
||||
|
||||
# 5. 删除成员记录
|
||||
success = self.workspace_member_repository.delete(member.id)
|
||||
if not success:
|
||||
return False, "Failed to leave workspace"
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to leave workspace: {str(e)}"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Subscription 管理 Use Case
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
|
||||
class UpgradeSubscriptionRequest:
|
||||
"""升级订阅请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -23,7 +24,7 @@ class UpgradeSubscriptionRequest:
|
||||
|
||||
class UpgradeSubscriptionResponse:
|
||||
"""升级订阅响应"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -41,21 +42,21 @@ class UpgradeSubscriptionResponse:
|
||||
|
||||
class UpgradeSubscriptionUseCase:
|
||||
"""升级订阅用例"""
|
||||
|
||||
|
||||
# 订阅计划配额
|
||||
PLAN_QUOTAS = {
|
||||
"free": {"max_projects": 3, "max_storage_gb": 10, "price": 0},
|
||||
"pro": {"max_projects": 999999, "max_storage_gb": 100, "price": 99},
|
||||
"enterprise": {"max_projects": 999999, "max_storage_gb": 1000, "price": 999},
|
||||
}
|
||||
|
||||
|
||||
# 计划等级
|
||||
PLAN_LEVELS = {
|
||||
"free": 0,
|
||||
"pro": 1,
|
||||
"enterprise": 2,
|
||||
}
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -63,14 +64,16 @@ class UpgradeSubscriptionUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: UpgradeSubscriptionRequest) -> tuple[Optional[UpgradeSubscriptionResponse], Optional[str]]:
|
||||
|
||||
def execute(
|
||||
self, request: UpgradeSubscriptionRequest
|
||||
) -> tuple[Optional[UpgradeSubscriptionResponse], Optional[str]]:
|
||||
"""
|
||||
执行升级订阅
|
||||
|
||||
|
||||
Args:
|
||||
request: 升级请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -78,22 +81,22 @@ class UpgradeSubscriptionUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
|
||||
if not request.new_plan:
|
||||
return None, "New plan is required"
|
||||
|
||||
|
||||
# 2. 验证新计划有效
|
||||
if request.new_plan not in self.PLAN_QUOTAS:
|
||||
return None, f"Invalid plan: {request.new_plan}"
|
||||
|
||||
|
||||
# 3. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 4. 验证权限(只有 Owner 可以管理订阅)
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -101,50 +104,56 @@ class UpgradeSubscriptionUseCase:
|
||||
)
|
||||
if not member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
|
||||
if member.role != WorkspaceMemberRole.OWNER:
|
||||
return None, "Only workspace owner can manage subscription"
|
||||
|
||||
|
||||
# 5. 检查是否是升级(不能降级到免费计划,需要用取消订阅)
|
||||
current_level = self.PLAN_LEVELS.get(workspace.subscription_plan, 0)
|
||||
new_level = self.PLAN_LEVELS.get(request.new_plan, 0)
|
||||
|
||||
|
||||
if new_level < current_level:
|
||||
return None, "Cannot downgrade plan. Use cancel subscription to return to free plan."
|
||||
|
||||
return (
|
||||
None,
|
||||
"Cannot downgrade plan. Use cancel subscription to return to free plan.",
|
||||
)
|
||||
|
||||
if new_level == current_level:
|
||||
return None, f"Workspace is already on {request.new_plan} plan"
|
||||
|
||||
|
||||
# 6. 更新订阅
|
||||
old_plan = workspace.subscription_plan
|
||||
quota = self.PLAN_QUOTAS[request.new_plan]
|
||||
|
||||
|
||||
workspace.subscription_plan = request.new_plan
|
||||
workspace.subscription_status = "active"
|
||||
workspace.max_projects = quota["max_projects"]
|
||||
workspace.max_storage_gb = quota["max_storage_gb"]
|
||||
|
||||
|
||||
# 设置过期时间(假设按月订阅)
|
||||
workspace.subscription_expires_at = datetime.now(timezone.utc) + timedelta(days=30)
|
||||
|
||||
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
|
||||
# 7. 返回响应
|
||||
return UpgradeSubscriptionResponse(
|
||||
workspace_id=workspace.id,
|
||||
old_plan=old_plan,
|
||||
new_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
), None
|
||||
|
||||
return (
|
||||
UpgradeSubscriptionResponse(
|
||||
workspace_id=workspace.id,
|
||||
old_plan=old_plan,
|
||||
new_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to upgrade subscription: {str(e)}"
|
||||
|
||||
|
||||
class CancelSubscriptionRequest:
|
||||
"""取消订阅请求"""
|
||||
|
||||
|
||||
def __init__(self, workspace_id: str, requester_user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
@@ -152,7 +161,7 @@ class CancelSubscriptionRequest:
|
||||
|
||||
class CancelSubscriptionUseCase:
|
||||
"""取消订阅用例"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -160,14 +169,14 @@ class CancelSubscriptionUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: CancelSubscriptionRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行取消订阅
|
||||
|
||||
|
||||
Args:
|
||||
request: 取消请求
|
||||
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
@@ -175,15 +184,15 @@ class CancelSubscriptionUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.requester_user_id:
|
||||
return False, "Requester user ID is required"
|
||||
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
|
||||
# 3. 验证权限(只有 Owner 可以管理订阅)
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -191,24 +200,24 @@ class CancelSubscriptionUseCase:
|
||||
)
|
||||
if not member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
|
||||
if member.role != WorkspaceMemberRole.OWNER:
|
||||
return False, "Only workspace owner can manage subscription"
|
||||
|
||||
|
||||
# 4. 检查当前计划
|
||||
if workspace.subscription_plan == "free":
|
||||
return False, "Workspace is already on free plan"
|
||||
|
||||
|
||||
# 5. 降级到 free 计划
|
||||
workspace.subscription_plan = "free"
|
||||
workspace.subscription_status = "active"
|
||||
workspace.subscription_expires_at = None
|
||||
workspace.max_projects = 3
|
||||
workspace.max_storage_gb = 10
|
||||
|
||||
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to cancel subscription: {str(e)}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
修改成员角色 Use Case
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
@@ -8,7 +9,7 @@ from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
class UpdateMemberRoleRequest:
|
||||
"""修改成员角色请求"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
@@ -24,7 +25,7 @@ class UpdateMemberRoleRequest:
|
||||
|
||||
class UpdateMemberRoleResponse:
|
||||
"""修改成员角色响应"""
|
||||
|
||||
|
||||
def __init__(self, user_id: str, old_role: str, new_role: str):
|
||||
self.user_id = user_id
|
||||
self.old_role = old_role
|
||||
@@ -33,13 +34,13 @@ class UpdateMemberRoleResponse:
|
||||
|
||||
class UpdateMemberRoleUseCase:
|
||||
"""修改成员角色用例"""
|
||||
|
||||
|
||||
VALID_ROLES = [
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
WorkspaceMemberRole.MEMBER,
|
||||
WorkspaceMemberRole.VIEWER,
|
||||
]
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
@@ -47,14 +48,14 @@ class UpdateMemberRoleUseCase:
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
|
||||
def execute(self, request: UpdateMemberRoleRequest) -> tuple[Optional[UpdateMemberRoleResponse], Optional[str]]:
|
||||
"""
|
||||
执行修改成员角色
|
||||
|
||||
|
||||
Args:
|
||||
request: 修改请求
|
||||
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
@@ -62,25 +63,28 @@ class UpdateMemberRoleUseCase:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
|
||||
if not request.target_user_id:
|
||||
return None, "Target user ID is required"
|
||||
|
||||
|
||||
if not request.new_role:
|
||||
return None, "New role is required"
|
||||
|
||||
|
||||
# 2. 验证新角色(不能修改为 owner)
|
||||
if request.new_role not in self.VALID_ROLES:
|
||||
return None, f"Invalid role: {request.new_role}. Cannot change to owner."
|
||||
|
||||
return (
|
||||
None,
|
||||
f"Invalid role: {request.new_role}. Cannot change to owner.",
|
||||
)
|
||||
|
||||
# 3. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
|
||||
# 4. 验证请求者是成员且有权限(只有 owner 和 admin 可以修改角色)
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -88,10 +92,13 @@ class UpdateMemberRoleUseCase:
|
||||
)
|
||||
if not requester_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
if requester_member.role not in [WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN]:
|
||||
|
||||
if requester_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return None, "Only owners and admins can change member roles"
|
||||
|
||||
|
||||
# 5. 验证目标成员存在
|
||||
target_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
@@ -99,35 +106,37 @@ class UpdateMemberRoleUseCase:
|
||||
)
|
||||
if not target_member:
|
||||
return None, "Target user is not a member of this workspace"
|
||||
|
||||
|
||||
# 6. 不能修改自己的角色
|
||||
if request.requester_user_id == request.target_user_id:
|
||||
return None, "Cannot change your own role"
|
||||
|
||||
|
||||
# 7. 不能修改 owner 的角色
|
||||
if target_member.role == WorkspaceMemberRole.OWNER:
|
||||
return None, "Cannot change the owner's role"
|
||||
|
||||
|
||||
# 8. Admin 不能修改另一个 Admin 的角色(只有 owner 可以)
|
||||
if (requester_member.role == WorkspaceMemberRole.ADMIN and
|
||||
target_member.role == WorkspaceMemberRole.ADMIN):
|
||||
if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN:
|
||||
return None, "Admins cannot change other admins' roles"
|
||||
|
||||
|
||||
# 9. 检查角色是否相同
|
||||
if target_member.role == request.new_role:
|
||||
return None, f"User already has the {request.new_role} role"
|
||||
|
||||
|
||||
# 10. 更新角色
|
||||
old_role = target_member.role
|
||||
target_member.role = request.new_role
|
||||
self.workspace_member_repository.save(target_member)
|
||||
|
||||
|
||||
# 11. 返回响应
|
||||
return UpdateMemberRoleResponse(
|
||||
user_id=request.target_user_id,
|
||||
old_role=old_role,
|
||||
new_role=request.new_role,
|
||||
), None
|
||||
|
||||
return (
|
||||
UpdateMemberRoleResponse(
|
||||
user_id=request.target_user_id,
|
||||
old_role=old_role,
|
||||
new_role=request.new_role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to update member role: {str(e)}"
|
||||
|
||||
Reference in New Issue
Block a user