From f30260939a1750eed53d7a8090168e1c88f82488 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 12:20:03 +0800 Subject: [PATCH 1/7] feat: Add feature_billing and feature_user_management fields to tenant model --- api/app/controllers/user_controller.py | 69 +++++++++++++++++++++++++ api/app/repositories/user_repository.py | 30 +++++++---- api/app/schemas/user_schema.py | 3 +- api/app/services/tenant_service.py | 12 +++-- 4 files changed, 98 insertions(+), 16 deletions(-) diff --git a/api/app/controllers/user_controller.py b/api/app/controllers/user_controller.py index 16213690..20e2b974 100644 --- a/api/app/controllers/user_controller.py +++ b/api/app/controllers/user_controller.py @@ -111,6 +111,18 @@ def get_current_user_info( break api_logger.info(f"当前用户信息获取成功: {result.username}, 角色: {result_schema.role}, 工作空间: {result_schema.current_workspace_name}") + + # 设置权限:如果用户来自 SSO Source,则使用该 Source 的 permissions;否则返回全部权限 + if current_user.external_source: + from premium.sso.models import SSOSource + source = db.query(SSOSource).filter(SSOSource.source_code == current_user.external_source).first() + if source and source.permissions: + result_schema.permissions = source.permissions + else: + result_schema.permissions = [] + else: + result_schema.permissions = ["pricing", "user"] + return success(data=result_schema, msg=t("users.info.get_success")) @@ -135,6 +147,63 @@ def get_tenant_superusers( return success(data=superusers_schema, msg=t("users.list.superusers_success")) +@router.get("/tenant/users", response_model=ApiResponse) +def get_tenant_users( + page: int = 1, + size: int = 20, + is_active: bool = None, + is_superuser: bool = None, + search: str = None, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), + t: Callable = Depends(get_translator) +): + """获取当前用户所在租户的用户列表(普通用户可访问)""" + api_logger.info(f"获取租户用户列表请求: tenant_id={current_user.tenant_id}, 操作者: {current_user.username}") + + if not current_user.tenant_id: + raise BusinessException("用户没有租户信息", code=BizCode.TENANT_NOT_FOUND) + + from app.services.tenant_service import TenantService + tenant_service = TenantService(db) + + skip = (page - 1) * size + users = tenant_service.get_tenant_users( + tenant_id=current_user.tenant_id, + skip=skip, + limit=size, + is_active=is_active, + is_superuser=is_superuser, + search=search + ) + total = tenant_service.count_tenant_users( + tenant_id=current_user.tenant_id, + is_active=is_active, + is_superuser=is_superuser, + search=search + ) + + users_schema = [user_schema.User.model_validate(u) for u in users] + for u_schema in users_schema: + user = users[[s.id for s in users_schema].index(u_schema.id)] + if user.external_source: + from premium.sso.models import SSOSource + source = db.query(SSOSource).filter(SSOSource.source_code == user.external_source).first() + u_schema.permissions = source.permissions if source and source.permissions else [] + else: + u_schema.permissions = ["pricing", "user"] + + return success( + data={ + "users": users_schema, + "total": total, + "page": page, + "size": size, + }, + msg=t("users.list.get_success") + ) + + @router.get("/{user_id}", response_model=ApiResponse) def get_user_info_by_id( diff --git a/api/app/repositories/user_repository.py b/api/app/repositories/user_repository.py index 3f8919aa..af4449e5 100644 --- a/api/app/repositories/user_repository.py +++ b/api/app/repositories/user_repository.py @@ -158,22 +158,26 @@ class UserRepository: raise def get_users_by_tenant( - self, - tenant_id: uuid.UUID, - skip: int = 0, + self, + tenant_id: uuid.UUID, + skip: int = 0, limit: int = 100, is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, search: Optional[str] = None ) -> List[User]: """获取租户下的用户列表""" db_logger.debug(f"查询租户用户: tenant_id={tenant_id}") - + try: query = self.db.query(User).options(joinedload(User.tenant)).filter(User.tenant_id == tenant_id) - + if is_active is not None: query = query.filter(User.is_active == is_active) - + + if is_superuser is not None: + query = query.filter(User.is_superuser == is_superuser) + if search: query = query.filter( or_( @@ -181,7 +185,7 @@ class UserRepository: User.email.ilike(f"%{search}%") ) ) - + users = query.offset(skip).limit(limit).all() db_logger.debug(f"租户用户查询成功: tenant_id={tenant_id}, count={len(users)}") return users @@ -190,18 +194,22 @@ class UserRepository: raise def count_users_by_tenant( - self, + self, tenant_id: uuid.UUID, is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, search: Optional[str] = None ) -> int: """统计租户下的用户数量""" try: query = self.db.query(func.count(User.id)).filter(User.tenant_id == tenant_id) - + if is_active is not None: query = query.filter(User.is_active == is_active) - + + if is_superuser is not None: + query = query.filter(User.is_superuser == is_superuser) + if search: query = query.filter( or_( @@ -209,7 +217,7 @@ class UserRepository: User.email.ilike(f"%{search}%") ) ) - + return query.scalar() except Exception as e: db_logger.error(f"统计租户用户失败: tenant_id={tenant_id} - {str(e)}") diff --git a/api/app/schemas/user_schema.py b/api/app/schemas/user_schema.py index 6b880696..f307a5a3 100644 --- a/api/app/schemas/user_schema.py +++ b/api/app/schemas/user_schema.py @@ -1,6 +1,6 @@ from dataclasses import field from pydantic import BaseModel, EmailStr, Field, field_validator, validator, ConfigDict -from typing import Optional +from typing import Optional, List import datetime import uuid @@ -85,6 +85,7 @@ class User(UserBase): current_workspace_name: Optional[str] = None role: Optional[WorkspaceRole] = None preferred_language: Optional[str] = "zh" # 用户语言偏好 + permissions: Optional[List[str]] = None # 用户权限列表,由 external_source 的 permissions 控制 # 将 datetime 转换为毫秒时间戳 @validator("created_at", pre=True) diff --git a/api/app/services/tenant_service.py b/api/app/services/tenant_service.py index 066edf57..b9c5800d 100644 --- a/api/app/services/tenant_service.py +++ b/api/app/services/tenant_service.py @@ -142,11 +142,12 @@ class TenantService: # 租户用户管理 def get_tenant_users( - self, - tenant_id: uuid.UUID, - skip: int = 0, + self, + tenant_id: uuid.UUID, + skip: int = 0, limit: int = 100, is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, search: Optional[str] = None ) -> List[UserModel]: """获取租户下的用户列表""" @@ -155,19 +156,22 @@ class TenantService: skip=skip, limit=limit, is_active=is_active, + is_superuser=is_superuser, search=search ) def count_tenant_users( - self, + self, tenant_id: uuid.UUID, is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, search: Optional[str] = None ) -> int: """统计租户下的用户数量""" return self.user_repo.count_users_by_tenant( tenant_id=tenant_id, is_active=is_active, + is_superuser=is_superuser, search=search ) From 14838dc06400062691dcf9b61029343815fe4926 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 13:58:31 +0800 Subject: [PATCH 2/7] feat: Update user controller --- api/app/controllers/user_controller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/app/controllers/user_controller.py b/api/app/controllers/user_controller.py index 20e2b974..e67a0b76 100644 --- a/api/app/controllers/user_controller.py +++ b/api/app/controllers/user_controller.py @@ -112,7 +112,7 @@ def get_current_user_info( api_logger.info(f"当前用户信息获取成功: {result.username}, 角色: {result_schema.role}, 工作空间: {result_schema.current_workspace_name}") - # 设置权限:如果用户来自 SSO Source,则使用该 Source 的 permissions;否则返回全部权限 + # 设置权限:如果用户来自 SSO Source,则使用该 Source 的 permissions;否则返回 "all" 表示拥有所有权限 if current_user.external_source: from premium.sso.models import SSOSource source = db.query(SSOSource).filter(SSOSource.source_code == current_user.external_source).first() @@ -121,7 +121,8 @@ def get_current_user_info( else: result_schema.permissions = [] else: - result_schema.permissions = ["pricing", "user"] + from premium.sso.base import SSOPermission + result_schema.permissions = [SSOPermission.ALL.value] return success(data=result_schema, msg=t("users.info.get_success")) @@ -191,7 +192,8 @@ def get_tenant_users( source = db.query(SSOSource).filter(SSOSource.source_code == user.external_source).first() u_schema.permissions = source.permissions if source and source.permissions else [] else: - u_schema.permissions = ["pricing", "user"] + from premium.sso.base import SSOPermission + u_schema.permissions = [SSOPermission.ALL.value] return success( data={ From ee6b8ffa628f05cdfeb4136751607c0680f99fef Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 14:07:52 +0800 Subject: [PATCH 3/7] feat: Update user controller. --- api/app/services/tenant_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/app/services/tenant_service.py b/api/app/services/tenant_service.py index b9c5800d..2ff7ff51 100644 --- a/api/app/services/tenant_service.py +++ b/api/app/services/tenant_service.py @@ -142,9 +142,9 @@ class TenantService: # 租户用户管理 def get_tenant_users( - self, - tenant_id: uuid.UUID, - skip: int = 0, + self, + tenant_id: uuid.UUID, + skip: int = 0, limit: int = 100, is_active: Optional[bool] = None, is_superuser: Optional[bool] = None, @@ -161,7 +161,7 @@ class TenantService: ) def count_tenant_users( - self, + self, tenant_id: uuid.UUID, is_active: Optional[bool] = None, is_superuser: Optional[bool] = None, From d0ca5c8b276ae96535daf46b8a294cbb82c0410c Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 14:17:22 +0800 Subject: [PATCH 4/7] feat: Update user controller --- api/app/controllers/user_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/app/controllers/user_controller.py b/api/app/controllers/user_controller.py index e67a0b76..3626f169 100644 --- a/api/app/controllers/user_controller.py +++ b/api/app/controllers/user_controller.py @@ -121,8 +121,7 @@ def get_current_user_info( else: result_schema.permissions = [] else: - from premium.sso.base import SSOPermission - result_schema.permissions = [SSOPermission.ALL.value] + result_schema.permissions = ["all"] return success(data=result_schema, msg=t("users.info.get_success")) @@ -192,8 +191,7 @@ def get_tenant_users( source = db.query(SSOSource).filter(SSOSource.source_code == user.external_source).first() u_schema.permissions = source.permissions if source and source.permissions else [] else: - from premium.sso.base import SSOPermission - u_schema.permissions = [SSOPermission.ALL.value] + u_schema.permissions = ["all"] return success( data={ From 539999131caa6e5a97ac70421318509f77f2a459 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 14:26:46 +0800 Subject: [PATCH 5/7] feat: Update user controller --- api/app/controllers/user_controller.py | 58 ----------------------- api/app/repositories/tenant_repository.py | 24 +--------- api/app/services/tenant_service.py | 37 +-------------- 3 files changed, 2 insertions(+), 117 deletions(-) diff --git a/api/app/controllers/user_controller.py b/api/app/controllers/user_controller.py index 3626f169..cc16a6b4 100644 --- a/api/app/controllers/user_controller.py +++ b/api/app/controllers/user_controller.py @@ -147,64 +147,6 @@ def get_tenant_superusers( return success(data=superusers_schema, msg=t("users.list.superusers_success")) -@router.get("/tenant/users", response_model=ApiResponse) -def get_tenant_users( - page: int = 1, - size: int = 20, - is_active: bool = None, - is_superuser: bool = None, - search: str = None, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), - t: Callable = Depends(get_translator) -): - """获取当前用户所在租户的用户列表(普通用户可访问)""" - api_logger.info(f"获取租户用户列表请求: tenant_id={current_user.tenant_id}, 操作者: {current_user.username}") - - if not current_user.tenant_id: - raise BusinessException("用户没有租户信息", code=BizCode.TENANT_NOT_FOUND) - - from app.services.tenant_service import TenantService - tenant_service = TenantService(db) - - skip = (page - 1) * size - users = tenant_service.get_tenant_users( - tenant_id=current_user.tenant_id, - skip=skip, - limit=size, - is_active=is_active, - is_superuser=is_superuser, - search=search - ) - total = tenant_service.count_tenant_users( - tenant_id=current_user.tenant_id, - is_active=is_active, - is_superuser=is_superuser, - search=search - ) - - users_schema = [user_schema.User.model_validate(u) for u in users] - for u_schema in users_schema: - user = users[[s.id for s in users_schema].index(u_schema.id)] - if user.external_source: - from premium.sso.models import SSOSource - source = db.query(SSOSource).filter(SSOSource.source_code == user.external_source).first() - u_schema.permissions = source.permissions if source and source.permissions else [] - else: - u_schema.permissions = ["all"] - - return success( - data={ - "users": users_schema, - "total": total, - "page": page, - "size": size, - }, - msg=t("users.list.get_success") - ) - - - @router.get("/{user_id}", response_model=ApiResponse) def get_user_info_by_id( user_id: uuid.UUID, diff --git a/api/app/repositories/tenant_repository.py b/api/app/repositories/tenant_repository.py index 2934dda3..462c75e5 100644 --- a/api/app/repositories/tenant_repository.py +++ b/api/app/repositories/tenant_repository.py @@ -100,15 +100,6 @@ class TenantRepository: db_tenant.is_active = False return True - def get_tenant_users(self, tenant_id: uuid.UUID, is_active: Optional[bool] = None) -> List[User]: - """获取租户下的所有用户""" - query = self.db.query(User).filter(User.tenant_id == tenant_id) - - if is_active is not None: - query = query.filter(User.is_active == is_active) - - return query.all() - def get_user_tenant(self, user_id: uuid.UUID) -> Optional[Tenants]: """获取用户所属的租户""" user = self.db.query(User).filter(User.id == user_id).first() @@ -130,15 +121,6 @@ class TenantRepository: user.tenant_id = tenant_id self.db.flush() - return True - - def count_tenant_users(self, tenant_id: uuid.UUID, is_active: Optional[bool] = None) -> int: - """统计租户下的用户数量""" - query = self.db.query(func.count(User.id)).filter(User.tenant_id == tenant_id) - - if is_active is not None: - query = query.filter(User.is_active == is_active) - return query.scalar() @@ -161,8 +143,4 @@ def get_tenants(db: Session, skip: int = 0, limit: int = 100) -> List[Tenants]: def get_user_tenant(db: Session, user_id: uuid.UUID) -> Optional[Tenants]: """获取用户所属的租户""" - return TenantRepository(db).get_user_tenant(user_id) - -def get_tenant_users(db: Session, tenant_id: uuid.UUID) -> List[User]: - """获取租户下的所有用户""" - return TenantRepository(db).get_tenant_users(tenant_id) \ No newline at end of file + return TenantRepository(db).get_user_tenant(user_id) \ No newline at end of file diff --git a/api/app/services/tenant_service.py b/api/app/services/tenant_service.py index 2ff7ff51..369327ba 100644 --- a/api/app/services/tenant_service.py +++ b/api/app/services/tenant_service.py @@ -138,42 +138,7 @@ class TenantService: except Exception as e: business_logger.error(f"删除租户失败: {str(e)}") - raise BusinessException(f"删除租户失败: {str(e)}", code=BizCode.DB_ERROR) - - # 租户用户管理 - def get_tenant_users( - self, - tenant_id: uuid.UUID, - skip: int = 0, - limit: int = 100, - is_active: Optional[bool] = None, - is_superuser: Optional[bool] = None, - search: Optional[str] = None - ) -> List[UserModel]: - """获取租户下的用户列表""" - return self.user_repo.get_users_by_tenant( - tenant_id=tenant_id, - skip=skip, - limit=limit, - is_active=is_active, - is_superuser=is_superuser, - search=search - ) - - def count_tenant_users( - self, - tenant_id: uuid.UUID, - is_active: Optional[bool] = None, - is_superuser: Optional[bool] = None, - search: Optional[str] = None - ) -> int: - """统计租户下的用户数量""" - return self.user_repo.count_users_by_tenant( - tenant_id=tenant_id, - is_active=is_active, - is_superuser=is_superuser, - search=search - ) + raise BusinessException(f"删除租户失败:{str(e)}", code=BizCode.DB_ERROR) def assign_user_to_tenant(self, user_id: uuid.UUID, tenant_id: uuid.UUID) -> bool: """将用户分配给租户""" From 2597a1f5321220411b0a5d31b2791970f7a4a278 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 14:36:19 +0800 Subject: [PATCH 6/7] feat: Update user controller --- api/app/repositories/tenant_repository.py | 24 +++++++++++++++- api/app/services/tenant_service.py | 35 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/api/app/repositories/tenant_repository.py b/api/app/repositories/tenant_repository.py index 462c75e5..2934dda3 100644 --- a/api/app/repositories/tenant_repository.py +++ b/api/app/repositories/tenant_repository.py @@ -100,6 +100,15 @@ class TenantRepository: db_tenant.is_active = False return True + def get_tenant_users(self, tenant_id: uuid.UUID, is_active: Optional[bool] = None) -> List[User]: + """获取租户下的所有用户""" + query = self.db.query(User).filter(User.tenant_id == tenant_id) + + if is_active is not None: + query = query.filter(User.is_active == is_active) + + return query.all() + def get_user_tenant(self, user_id: uuid.UUID) -> Optional[Tenants]: """获取用户所属的租户""" user = self.db.query(User).filter(User.id == user_id).first() @@ -121,6 +130,15 @@ class TenantRepository: user.tenant_id = tenant_id self.db.flush() + return True + + def count_tenant_users(self, tenant_id: uuid.UUID, is_active: Optional[bool] = None) -> int: + """统计租户下的用户数量""" + query = self.db.query(func.count(User.id)).filter(User.tenant_id == tenant_id) + + if is_active is not None: + query = query.filter(User.is_active == is_active) + return query.scalar() @@ -143,4 +161,8 @@ def get_tenants(db: Session, skip: int = 0, limit: int = 100) -> List[Tenants]: def get_user_tenant(db: Session, user_id: uuid.UUID) -> Optional[Tenants]: """获取用户所属的租户""" - return TenantRepository(db).get_user_tenant(user_id) \ No newline at end of file + return TenantRepository(db).get_user_tenant(user_id) + +def get_tenant_users(db: Session, tenant_id: uuid.UUID) -> List[User]: + """获取租户下的所有用户""" + return TenantRepository(db).get_tenant_users(tenant_id) \ No newline at end of file diff --git a/api/app/services/tenant_service.py b/api/app/services/tenant_service.py index 369327ba..36205503 100644 --- a/api/app/services/tenant_service.py +++ b/api/app/services/tenant_service.py @@ -140,6 +140,41 @@ class TenantService: business_logger.error(f"删除租户失败: {str(e)}") raise BusinessException(f"删除租户失败:{str(e)}", code=BizCode.DB_ERROR) + # 租户用户管理 + def get_tenant_users( + self, + tenant_id: uuid.UUID, + skip: int = 0, + limit: int = 100, + is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, + search: Optional[str] = None + ) -> List[UserModel]: + """获取租户下的用户列表""" + return self.user_repo.get_users_by_tenant( + tenant_id=tenant_id, + skip=skip, + limit=limit, + is_active=is_active, + is_superuser=is_superuser, + search=search + ) + + def count_tenant_users( + self, + tenant_id: uuid.UUID, + is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None, + search: Optional[str] = None + ) -> int: + """统计租户下的用户数量""" + return self.user_repo.count_users_by_tenant( + tenant_id=tenant_id, + is_active=is_active, + is_superuser=is_superuser, + search=search + ) + def assign_user_to_tenant(self, user_id: uuid.UUID, tenant_id: uuid.UUID) -> bool: """将用户分配给租户""" # 检查租户是否存在 From 7fbf3e8873817f6120e573bb128cc34216943981 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 27 Mar 2026 14:48:25 +0800 Subject: [PATCH 7/7] feat: Update user controller --- api/app/models/user_model.py | 5 ++++- api/app/schemas/user_schema.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/app/models/user_model.py b/api/app/models/user_model.py index 81319789..c0b17d14 100644 --- a/api/app/models/user_model.py +++ b/api/app/models/user_model.py @@ -19,9 +19,12 @@ class User(Base): last_login_at = Column(DateTime, nullable=True) # 最后登录时间,可为空 # SSO 外部关联字段 - external_id = Column(String(100), nullable=True) # 外部用户ID + external_id = Column(String(100), nullable=True) # 外部用户 ID external_source = Column(String(50), nullable=True) # 来源系统 + # 用户联系方式 + phone = Column(String(50), nullable=True) # 用户电话 + # 用户语言偏好 preferred_language = Column(String(10), server_default=text("'zh'"), default='zh', nullable=False, index=True) # 用户偏好语言,默认中文 diff --git a/api/app/schemas/user_schema.py b/api/app/schemas/user_schema.py index f307a5a3..aa9ac256 100644 --- a/api/app/schemas/user_schema.py +++ b/api/app/schemas/user_schema.py @@ -20,6 +20,7 @@ class UserCreate(UserBase): class UserUpdate(BaseModel): username: Optional[str] = None email: Optional[EmailStr] = None + phone: Optional[str] = None is_active: Optional[bool] = None is_superuser: Optional[bool] = None @@ -85,6 +86,7 @@ class User(UserBase): current_workspace_name: Optional[str] = None role: Optional[WorkspaceRole] = None preferred_language: Optional[str] = "zh" # 用户语言偏好 + phone: Optional[str] = None # 用户电话 permissions: Optional[List[str]] = None # 用户权限列表,由 external_source 的 permissions 控制 # 将 datetime 转换为毫秒时间戳