diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index f0617090a..c7a91fc8a 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -14,9 +14,20 @@ UpdateRoleParam, UpdateRoleScopeParam, ) +from backend.core.conf import settings from backend.utils.serializers import select_join_serialize from backend.utils.timezone import timezone +if settings.TENANT_ENABLED: + try: + from backend.plugin.tenant.utils import get_tenant_dict as inject_tenant_dict + except ImportError: + raise ImportError('租户插件方法导入失败,请联系系统管理员') +else: + + def inject_tenant_dict(obj: dict[str, Any]) -> dict[str, Any]: + return obj + class CRUDRole(CRUDPlus[Role]): """角色数据库操作类""" @@ -160,9 +171,11 @@ async def update_menus(db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuP await db.execute(role_menu_stmt) if menu_ids.menus: - role_menu_data = [ - CreateRoleMenuParam(role_id=role_id, menu_id=menu_id).model_dump() for menu_id in menu_ids.menus - ] + role_menu_data = [] + for menu_id in menu_ids.menus: + menu_dict = CreateRoleMenuParam(role_id=role_id, menu_id=menu_id).model_dump() + role_menu_data.append(inject_tenant_dict(menu_dict)) + role_menu_stmt = insert(role_menu) await db.execute(role_menu_stmt, role_menu_data) @@ -182,10 +195,11 @@ async def update_scopes(db: AsyncSession, role_id: int, scope_ids: UpdateRoleSco await db.execute(role_scope_stmt) if scope_ids.scopes: - role_scope_data = [ - CreateRoleScopeParam(role_id=role_id, data_scope_id=scope_id).model_dump() - for scope_id in scope_ids.scopes - ] + role_scope_data = [] + for scope_id in scope_ids.scopes: + scope_dict = CreateRoleScopeParam(role_id=role_id, data_scope_id=scope_id).model_dump() + role_scope_data.append(inject_tenant_dict(scope_dict)) + role_scope_stmt = insert(role_data_scope) await db.execute(role_scope_stmt, role_scope_data) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 2ebd158d6..f7af5135b 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -28,10 +28,21 @@ from backend.app.admin.utils.password_security import get_hash_password from backend.common.enums import StatusType from backend.common.exception import errors +from backend.core.conf import settings from backend.plugin.core import check_plugin_installed from backend.utils.serializers import select_join_serialize from backend.utils.timezone import timezone +if settings.TENANT_ENABLED: + try: + from backend.plugin.tenant.utils import get_tenant_dict as inject_tenant_dict + except ImportError: + raise ImportError('租户插件方法导入失败,请联系系统管理员') +else: + + def inject_tenant_dict(obj: dict[str, Any]) -> dict[str, Any]: + return obj + class CRUDUser(CRUDPlus[User]): """用户数据库操作类""" @@ -139,6 +150,7 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) + new_user = self.model(**dict_obj) db.add(new_user) await db.flush() @@ -148,7 +160,11 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: result = await db.execute(role_stmt) roles = result.scalars().all() - user_role_data = [AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump() for role in roles] + user_role_data = [] + for role in roles: + role_dict = AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump() + user_role_data.append(inject_tenant_dict(role_dict)) + user_role_stmt = insert(user_role) await db.execute(user_role_stmt, user_role_data) @@ -162,6 +178,7 @@ async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None """ dict_obj = obj.model_dump() dict_obj.update({'is_staff': True, 'salt': None}) + new_user = self.model(**dict_obj) db.add(new_user) await db.flush() @@ -172,7 +189,8 @@ async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None if role is None: raise errors.NotFoundError(msg='未找到可用角色,请联系系统管理员') - user_role_stmt = insert(user_role).values(AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump()) + user_role_data = inject_tenant_dict(AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump()) + user_role_stmt = insert(user_role).values(user_role_data) await db.execute(user_role_stmt) async def update(self, db: AsyncSession, user_id: int, obj: UpdateUserParam) -> int: @@ -197,7 +215,11 @@ async def update(self, db: AsyncSession, user_id: int, obj: UpdateUserParam) -> result = await db.execute(role_stmt) roles = result.scalars().all() - user_role_data = [AddUserRoleParam(user_id=user_id, role_id=role.id).model_dump() for role in roles] + user_role_data = [] + for role in roles: + role_dict = AddUserRoleParam(user_id=user_id, role_id=role.id).model_dump() + user_role_data.append(inject_tenant_dict(role_dict)) + user_role_stmt = insert(user_role) await db.execute(user_role_stmt, user_role_data) diff --git a/backend/app/admin/model/dept.py b/backend/app/admin/model/dept.py index d7be850ca..5855ab02e 100644 --- a/backend/app/admin/model/dept.py +++ b/backend/app/admin/model/dept.py @@ -2,17 +2,25 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import Base, id_key +from backend.common.model import Base, TenantMixin, id_key +from backend.core.conf import settings -class Dept(Base): +class Dept(Base, TenantMixin): """部门表""" __tablename__ = 'sys_dept' - __table_args__ = ( - sa.UniqueConstraint('name', 'deleted', name='uk_sys_dept_name_deleted'), - {'comment': '部门表'}, - ) + + if settings.TENANT_ENABLED: + __table_args__ = ( + sa.UniqueConstraint('name', 'tenant_id', 'deleted', name='uk_sys_dept_name_tenant_deleted'), + {'comment': '部门表'}, + ) + else: + __table_args__ = ( + sa.UniqueConstraint('name', 'deleted', name='uk_sys_dept_name_deleted'), + {'comment': '部门表'}, + ) id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(sa.String(64), comment='部门名称') diff --git a/backend/app/admin/model/login_log.py b/backend/app/admin/model/login_log.py index 0726b392c..3ef252c6c 100644 --- a/backend/app/admin/model/login_log.py +++ b/backend/app/admin/model/login_log.py @@ -4,11 +4,11 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key +from backend.common.model import DataClassBase, TenantMixin, TimeZone, UniversalText, id_key from backend.utils.timezone import timezone -class LoginLog(DataClassBase): +class LoginLog(DataClassBase, TenantMixin): """登录日志表""" __tablename__ = 'sys_login_log' diff --git a/backend/app/admin/model/m2m.py b/backend/app/admin/model/m2m.py index 330975b31..f023792a5 100644 --- a/backend/app/admin/model/m2m.py +++ b/backend/app/admin/model/m2m.py @@ -1,6 +1,14 @@ import sqlalchemy as sa from backend.common.model import MappedBase +from backend.core.conf import settings + +# 租户列定义(根据配置决定是否添加) +_tenant_columns = ( + (lambda: [sa.Column('tenant_id', sa.BigInteger, nullable=False, index=True, comment='租户ID')]) + if settings.TENANT_ENABLED + else list +) # 用户角色表 user_role = sa.Table( @@ -9,6 +17,7 @@ sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), sa.Column('user_id', sa.BigInteger, primary_key=True, comment='用户ID'), sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'), + *_tenant_columns(), ) # 角色菜单表 @@ -18,6 +27,7 @@ sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'), sa.Column('menu_id', sa.BigInteger, primary_key=True, comment='菜单ID'), + *_tenant_columns(), ) # 角色数据范围表 @@ -27,6 +37,7 @@ sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'), sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色 ID'), sa.Column('data_scope_id', sa.BigInteger, primary_key=True, comment='数据范围 ID'), + *_tenant_columns(), ) # 数据范围规则表 diff --git a/backend/app/admin/model/opera_log.py b/backend/app/admin/model/opera_log.py index 7963d864f..f257c42fa 100644 --- a/backend/app/admin/model/opera_log.py +++ b/backend/app/admin/model/opera_log.py @@ -4,11 +4,11 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key +from backend.common.model import DataClassBase, TenantMixin, TimeZone, UniversalText, id_key from backend.utils.timezone import timezone -class OperaLog(DataClassBase): +class OperaLog(DataClassBase, TenantMixin): """操作日志表""" __tablename__ = 'sys_opera_log' diff --git a/backend/app/admin/model/role.py b/backend/app/admin/model/role.py index 8488d903c..20e570587 100644 --- a/backend/app/admin/model/role.py +++ b/backend/app/admin/model/role.py @@ -2,17 +2,25 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import Base, UniversalText, id_key +from backend.common.model import Base, TenantMixin, UniversalText, id_key +from backend.core.conf import settings -class Role(Base): +class Role(Base, TenantMixin): """角色表""" __tablename__ = 'sys_role' - __table_args__ = ( - sa.UniqueConstraint('name', 'deleted', name='uk_sys_role_name_deleted'), - {'comment': '角色表'}, - ) + + if settings.TENANT_ENABLED: + __table_args__ = ( + sa.UniqueConstraint('name', 'tenant_id', 'deleted', name='uk_sys_role_name_tenant_deleted'), + {'comment': '角色表'}, + ) + else: + __table_args__ = ( + sa.UniqueConstraint('name', 'deleted', name='uk_sys_role_name_deleted'), + {'comment': '角色表'}, + ) id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(sa.String(32), comment='角色名称') diff --git a/backend/app/admin/model/user.py b/backend/app/admin/model/user.py index 712ebb208..14e5b1332 100644 --- a/backend/app/admin/model/user.py +++ b/backend/app/admin/model/user.py @@ -4,20 +4,29 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import Base, TimeZone, id_key +from backend.common.model import Base, TenantMixin, TimeZone, id_key +from backend.core.conf import settings from backend.database.db import uuid4_str from backend.utils.timezone import timezone -class User(Base): +class User(Base, TenantMixin): """用户表""" __tablename__ = 'sys_user' - __table_args__ = ( - sa.UniqueConstraint('username', 'deleted', name='uk_sys_user_username_deleted'), - sa.UniqueConstraint('email', 'deleted', name='uk_sys_user_email_deleted'), - {'comment': '用户表'}, - ) + + if settings.TENANT_ENABLED: + __table_args__ = ( + sa.UniqueConstraint('username', 'tenant_id', 'deleted', name='uk_sys_user_username_tenant_deleted'), + sa.UniqueConstraint('email', 'tenant_id', 'deleted', name='uk_sys_user_email_tenant_deleted'), + {'comment': '用户表'}, + ) + else: + __table_args__ = ( + sa.UniqueConstraint('username', 'deleted', name='uk_sys_user_username_deleted'), + sa.UniqueConstraint('email', 'deleted', name='uk_sys_user_email_deleted'), + {'comment': '用户表'}, + ) id: Mapped[id_key] = mapped_column(init=False) uuid: Mapped[str] = mapped_column(sa.String(64), init=False, default_factory=uuid4_str, unique=True) diff --git a/backend/app/admin/model/user_password_history.py b/backend/app/admin/model/user_password_history.py index 11d076749..3b4d65937 100644 --- a/backend/app/admin/model/user_password_history.py +++ b/backend/app/admin/model/user_password_history.py @@ -4,11 +4,11 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import DataClassBase, TimeZone, id_key +from backend.common.model import DataClassBase, TenantMixin, TimeZone, id_key from backend.utils.timezone import timezone -class UserPasswordHistory(DataClassBase): +class UserPasswordHistory(DataClassBase, TenantMixin): """用户密码历史记录表""" __tablename__ = 'sys_user_password_history' diff --git a/backend/app/admin/schema/user.py b/backend/app/admin/schema/user.py index d0da918f4..601fcae88 100644 --- a/backend/app/admin/schema/user.py +++ b/backend/app/admin/schema/user.py @@ -8,6 +8,7 @@ from backend.app.admin.schema.role import GetRoleWithRelationDetail from backend.common.enums import StatusType from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase, ser_string +from backend.core.conf import settings class AuthSchemaBase(SchemaBase): @@ -20,6 +21,9 @@ class AuthSchemaBase(SchemaBase): class AuthLoginParam(AuthSchemaBase): """用户登录参数""" + if settings.TENANT_ENABLED: + tenant_id: int = Field(description='租户 ID') + uuid: str | None = Field(None, description='验证码 UUID') captcha: str | None = Field(None, description='验证码') @@ -80,6 +84,9 @@ class GetUserInfoDetail(UserInfoSchemaBase): model_config = ConfigDict(from_attributes=True) + if settings.TENANT_ENABLED: + tenant_id: int = Field(description='租户 ID') + dept_id: int | None = Field(None, description='部门 ID') id: int = Field(description='用户 ID') uuid: str = Field(description='用户 UUID') diff --git a/backend/app/admin/service/auth_service.py b/backend/app/admin/service/auth_service.py index f48fa9c25..779cc0622 100644 --- a/backend/app/admin/service/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -18,6 +18,7 @@ from backend.common.log import log from backend.common.response.response_code import CustomErrorCode from backend.common.security.jwt import ( + check_tenant_status, create_access_token, create_new_token, create_refresh_token, @@ -74,13 +75,14 @@ async def swagger_login(self, *, db: AsyncSession, obj: HTTPBasicCredentials) -> await user_dao.update_login_time(db, obj.username) access_token_data = await create_access_token( user.id, + ctx.tenant_id, multi_login=user.is_multi_login, # extra info swagger=True, ) return access_token_data.access_token, user - async def login( + async def login( # noqa: C901 self, *, db: AsyncSession, @@ -98,6 +100,7 @@ async def login( :return: """ user = None + try: await load_login_config(db) if settings.LOGIN_CAPTCHA_ENABLED: @@ -110,11 +113,21 @@ async def login( raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR) await redis_client.delete(f'{settings.LOGIN_CAPTCHA_REDIS_PREFIX}:{obj.uuid}') + if settings.TENANT_ENABLED: + if obj.tenant_id is None: + raise errors.RequestError(msg='租户 ID 不能为空') + ctx.tenant_id = obj.tenant_id + await check_tenant_status(db, ctx.tenant_id) + else: + # 登录前先写入当前租户,供后续登录请求流程使用 + ctx.tenant_id = settings.TENANT_DEFAULT_ID + user, days_remaining = await self.user_verify(db, obj.username, obj.password) await user_dao.update_login_time(db, obj.username) await db.refresh(user) access_token_data = await create_access_token( user.id, + ctx.tenant_id, multi_login=user.is_multi_login, # extra info username=user.username, @@ -128,6 +141,7 @@ async def login( refresh_token_data = await create_refresh_token( access_token_data.session_uuid, user.id, + ctx.tenant_id, multi_login=user.is_multi_login, ) response.set_cookie( @@ -213,11 +227,14 @@ async def refresh_token(*, db: AsyncSession, request: Request, response: Respons raise errors.RequestError(msg='Refresh Token 已过期,请重新登录') token_payload = jwt_decode(refresh_token) + ctx.tenant_id = token_payload.tenant_id user = await user_dao.get(db, token_payload.user_id) if not user: raise errors.NotFoundError(msg='用户不存在') if not user.status: - raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') + raise errors.AuthorizationError(msg='用户已被锁定, 请联系系统管理员') + + await check_tenant_status(db, ctx.tenant_id) token_keys = await redis_client.get_prefix(f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*') if not user.is_multi_login and [ key for key in token_keys if not key.endswith(f':{token_payload.session_uuid}') @@ -227,6 +244,7 @@ async def refresh_token(*, db: AsyncSession, request: Request, response: Respons refresh_token, token_payload.session_uuid, user.id, + ctx.tenant_id, multi_login=user.is_multi_login, # extra info username=user.username, diff --git a/backend/app/admin/service/login_log_service.py b/backend/app/admin/service/login_log_service.py index 5aad1c668..715146358 100644 --- a/backend/app/admin/service/login_log_service.py +++ b/backend/app/admin/service/login_log_service.py @@ -48,22 +48,22 @@ async def create( :return: """ try: - obj = CreateLoginLogParam( - user_uuid=user_uuid, - username=username, - status=status, - ip=ctx.ip, - country=ctx.country, - region=ctx.region, - city=ctx.city, - user_agent=ctx.user_agent, - browser=ctx.browser, - os=ctx.os, - device=ctx.device, - msg=msg, - login_time=login_time, - ) - # 为后台任务创建独立数据库会话 + data = { + 'user_uuid': user_uuid, + 'username': username, + 'status': status, + 'ip': ctx.ip, + 'country': ctx.country, + 'region': ctx.region, + 'city': ctx.city, + 'user_agent': ctx.user_agent, + 'browser': ctx.browser, + 'os': ctx.os, + 'device': ctx.device, + 'msg': msg, + 'login_time': login_time, + } + obj = CreateLoginLogParam(**data) async with async_db_session.begin() as db: await login_log_dao.create(db, obj) except Exception as e: diff --git a/backend/cli.py b/backend/cli.py index 329f2ef08..94d630069 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -487,6 +487,7 @@ async def get_sql_scripts() -> list[str]: 'init', settings.DATABASE_PK_MODE, suffix='test_data', + tenant=settings.TENANT_ENABLED, ) if await anyio.Path(main_sql_file).exists(): diff --git a/backend/common/context.py b/backend/common/context.py index 2b06d0a43..b3401b0f2 100644 --- a/backend/common/context.py +++ b/backend/common/context.py @@ -22,6 +22,7 @@ class TypedContextProtocol(Protocol): language: str user_id: int | None + tenant_id: int class TypedContext(TypedContextProtocol, _Context): diff --git a/backend/common/dataclasses.py b/backend/common/dataclasses.py index a776f6457..9431cc5c9 100644 --- a/backend/common/dataclasses.py +++ b/backend/common/dataclasses.py @@ -58,6 +58,7 @@ class NewToken: @dataclasses.dataclass class TokenPayload: user_id: int + tenant_id: int session_uuid: str expire_time: datetime diff --git a/backend/common/model.py b/backend/common/model.py index 133cbcc7f..d9860a518 100644 --- a/backend/common/model.py +++ b/backend/common/model.py @@ -83,6 +83,20 @@ class UserMixin(MappedAsDataclass): updated_by: Mapped[int | None] = mapped_column(init=False, default=None, sort_order=998, comment='修改者') +class TenantMixin(MappedAsDataclass): + """租户 Mixin 数据类""" + + if settings.TENANT_ENABLED: + tenant_id: Mapped[int] = mapped_column( + BigInteger, + init=False, + nullable=False, + index=True, + sort_order=997, + comment='租户ID', + ) + + class DateTimeMixin(MappedAsDataclass): """日期时间 Mixin 数据类""" diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index 3a7a1cea4..4be8fa691 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -53,7 +53,8 @@ def jwt_decode(token: str) -> TokenPayload: session_uuid = payload.get('session_uuid') user_id = payload.get('sub') expire = payload.get('exp') - if not session_uuid or not user_id or not expire: + tenant_id = payload.get('tenant_id') + if not session_uuid or not user_id or not expire or tenant_id is None: raise errors.TokenError(msg='Token 无效') except ExpiredSignatureError: raise errors.TokenError(msg='Token 已过期') @@ -63,14 +64,22 @@ def jwt_decode(token: str) -> TokenPayload: user_id=int(user_id), session_uuid=session_uuid, expire_time=timezone.from_datetime(timezone.to_utc(expire)), + tenant_id=int(tenant_id), ) -async def create_access_token(user_id: int, *, multi_login: bool, **kwargs) -> AccessToken: +async def create_access_token( + user_id: int, + tenant_id: int, + *, + multi_login: bool, + **kwargs, +) -> AccessToken: """ 生成加密 token :param user_id: 用户 ID + :param tenant_id: 租户 ID :param multi_login: 是否允许多端登录 :param kwargs: token 额外信息 :return: @@ -81,6 +90,7 @@ async def create_access_token(user_id: int, *, multi_login: bool, **kwargs) -> A 'session_uuid': session_uuid, 'exp': timezone.to_utc(expire).timestamp(), 'sub': str(user_id), + 'tenant_id': tenant_id, }) if not multi_login: @@ -103,12 +113,13 @@ async def create_access_token(user_id: int, *, multi_login: bool, **kwargs) -> A return AccessToken(access_token=access_token, access_token_expire_time=expire, session_uuid=session_uuid) -async def create_refresh_token(session_uuid: str, user_id: int, *, multi_login: bool) -> RefreshToken: +async def create_refresh_token(session_uuid: str, user_id: int, tenant_id: int, *, multi_login: bool) -> RefreshToken: """ 生成加密刷新 token,仅用于创建新的 token :param session_uuid: 会话 UUID :param user_id: 用户 ID + :param tenant_id: 租户 ID :param multi_login: 是否允许多端登录 :return: """ @@ -117,6 +128,7 @@ async def create_refresh_token(session_uuid: str, user_id: int, *, multi_login: 'session_uuid': session_uuid, 'exp': timezone.to_utc(expire).timestamp(), 'sub': str(user_id), + 'tenant_id': tenant_id, }) if not multi_login: @@ -134,6 +146,7 @@ async def create_new_token( refresh_token: str, session_uuid: str, user_id: int, + tenant_id: int, *, multi_login: bool, **kwargs, @@ -144,6 +157,7 @@ async def create_new_token( :param refresh_token: 刷新 token :param session_uuid: 会话 UUID :param user_id: 用户 ID + :param tenant_id: 租户 ID :param multi_login: 是否允许多端登录 :param kwargs: token 附加信息 :return: @@ -155,8 +169,18 @@ async def create_new_token( await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}') await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}') - new_access_token = await create_access_token(user_id, multi_login=multi_login, **kwargs) - new_refresh_token = await create_refresh_token(new_access_token.session_uuid, user_id, multi_login=multi_login) + new_access_token = await create_access_token( + user_id, + tenant_id, + multi_login=multi_login, + **kwargs, + ) + new_refresh_token = await create_refresh_token( + new_access_token.session_uuid, + user_id, + tenant_id, + multi_login=multi_login, + ) return NewToken( new_access_token=new_access_token.access_token, new_access_token_expire_time=new_access_token.access_token_expire_time, @@ -192,6 +216,41 @@ def get_token(request: Request) -> str: return token +async def check_tenant_status(db: AsyncSession, tenant_id: int) -> None: + """ + 校验租户状态 + + :param db: 数据库会话 + :param tenant_id: 租户 ID + :return: + """ + if not settings.TENANT_ENABLED: + return + + if tenant_id == settings.TENANT_DEFAULT_ID: + return + + try: + from backend.plugin.tenant.crud.crud_package import tenant_package_dao + from backend.plugin.tenant.crud.crud_tenant import tenant_dao + except ImportError: + raise errors.ServerError(msg='租户插件方法导入失败,请联系系统管理员') + + tenant = await tenant_dao.get(db, tenant_id) + if not tenant: + raise errors.NotFoundError(msg='租户不存在,请联系系统管理员') + + if tenant.status == 0: + raise errors.AuthorizationError(msg='租户已被禁用,请联系系统管理员') + + if tenant.expire_time and tenant.expire_time < timezone.now(): + raise errors.AuthorizationError(msg='租户已过期,请联系系统管理员') + + package = await tenant_package_dao.get(db, tenant.package_id) + if package and package.status == 0: + raise errors.AuthorizationError(msg='租户套餐已被禁用,请联系系统管理员') + + async def get_current_user(db: AsyncSession, pk: int) -> User: """ 获取当前用户 @@ -207,6 +266,10 @@ async def get_current_user(db: AsyncSession, pk: int) -> User: raise errors.TokenError(msg='Token 无效') if not user.status: raise errors.AuthorizationError(msg='用户已被锁定,请联系系统管理员') + + if settings.TENANT_ENABLED: + await check_tenant_status(db, ctx.tenant_id) + if user.dept_id and not user.dept: raise errors.AuthorizationError(msg='用户所属部门不存在或已被删除,请联系系统管理员') if user.dept and not user.dept.status: @@ -251,6 +314,7 @@ async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail: """ token_payload = jwt_decode(token) ctx.user_id = token_payload.user_id + ctx.tenant_id = token_payload.tenant_id redis_token = await redis_client.get(f'{settings.TOKEN_REDIS_PREFIX}:{ctx.user_id}:{token_payload.session_uuid}') if not redis_token: raise errors.TokenError(msg='Token 已过期') diff --git a/backend/common/security/rbac.py b/backend/common/security/rbac.py index 1bee2f9e3..0839188ac 100644 --- a/backend/common/security/rbac.py +++ b/backend/common/security/rbac.py @@ -34,6 +34,8 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None: # 检测用户角色 user_roles = request.user.roles + if not user_roles: + raise errors.AuthorizationError(msg='用户未分配角色,请联系系统管理员') enabled_roles = [role for role in user_roles if role.status == StatusType.enable] if not enabled_roles: raise errors.AuthorizationError(msg='用户所属角色已被锁定,请联系系统管理员') diff --git a/backend/core/conf.py b/backend/core/conf.py index cc01f652f..77e5ebfe8 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -244,6 +244,10 @@ def settings_customise_sources( OPERA_LOG_QUEUE_TIMEOUT: int = 60 # 1 分钟 OPERA_LOG_BODY_MAX_SIZE: int = 10240 # 10 KB + # 租户 + TENANT_ENABLED: bool = True + TENANT_DEFAULT_ID: int = 0 + # Plugin 配置 PLUGIN_REQUIRED: list[str] = ['dict'] PLUGIN_PIP_CHINA: bool = True diff --git a/backend/core/registrar.py b/backend/core/registrar.py index adceac660..972e3caf2 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -64,6 +64,15 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]: # 启动缓存 Pub/Sub 监听器 cache_pubsub_manager.start_listener() + # 注册租户 SQLAlchemy 监听器 + if settings.TENANT_ENABLED: + try: + from backend.plugin.tenant.listener import register_tenant_sqlalchemy_listeners + except ImportError: + raise ImportError('租户插件监听器导入失败,请联系系统管理员') + else: + register_tenant_sqlalchemy_listeners() + try: yield finally: diff --git a/backend/middleware/access_middleware.py b/backend/middleware/access_middleware.py index c486eea01..f8b833ec5 100644 --- a/backend/middleware/access_middleware.py +++ b/backend/middleware/access_middleware.py @@ -47,6 +47,9 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - inc_fastapi_request_in_progress(method=method, path=path) inc_fastapi_request(method=method, path=path) + # 为每个请求上下文注入默认租户 ID,授权接口认证成功后会覆盖为真实值 + ctx.tenant_id = settings.TENANT_DEFAULT_ID + try: response = await call_next(request) except Exception as e: diff --git a/backend/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py index 9406c29e3..150ec4aad 100644 --- a/backend/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -2,12 +2,14 @@ import time from asyncio import Queue +from collections import defaultdict from typing import Any from fastapi import Response from starlette.datastructures import UploadFile from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request +from starlette_context import request_cycle_context from backend.app.admin.schema.opera_log import CreateOperaLogParam from backend.app.admin.service.opera_log_service import opera_log_service @@ -26,7 +28,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware): """操作日志中间件""" opera_log_queue_name = 'opera_log_queue' - opera_log_queue: Queue[CreateOperaLogParam] = Queue(maxsize=settings.OPERA_LOG_QUEUE_MAXSIZE) + opera_log_queue: Queue[tuple[dict[str, Any], CreateOperaLogParam]] = Queue(maxsize=settings.OPERA_LOG_QUEUE_MAXSIZE) async def dispatch(self, request: Request, call_next: Any) -> Response: # noqa: C901 """ @@ -98,28 +100,35 @@ async def dispatch(self, request: Request, call_next: Any) -> Response: # noqa: log.info(f'{ctx.ip: <15} | {method: <8} | {code!s: <6} | {path} | {elapsed:.3f}ms') if should_log_opera and request.method != 'OPTIONS': - opera_log_in = CreateOperaLogParam( - trace_id=get_request_trace_id(), - username=username, - method=method, - title=summary, - path=path, - ip=ctx.ip, - country=ctx.country, - region=ctx.region, - city=ctx.city, - user_agent=ctx.user_agent, - os=ctx.os, - browser=ctx.browser, - device=ctx.device, - args=args, - status=status, - code=str(code), - msg=msg, - cost_time=elapsed, - opera_time=ctx.start_time, - ) - await self.opera_log_queue.put(opera_log_in) + opera_log_data = { + 'trace_id': get_request_trace_id(), + 'username': username, + 'method': method, + 'title': summary, + 'path': path, + 'ip': ctx.ip, + 'country': ctx.country, + 'region': ctx.region, + 'city': ctx.city, + 'user_agent': ctx.user_agent, + 'os': ctx.os, + 'browser': ctx.browser, + 'device': ctx.device, + 'args': args, + 'status': status, + 'code': str(code), + 'msg': msg, + 'cost_time': elapsed, + 'opera_time': ctx.start_time, + } + if settings.TENANT_ENABLED: + tenant_id = ctx.get('tenant_id') + if tenant_id is None: + raise RuntimeError('opera log context is missing tenant_id') + opera_log_data['tenant_id'] = tenant_id + + opera_log_in = CreateOperaLogParam(**opera_log_data) + await self.opera_log_queue.put((ctx.copy(), opera_log_in)) if settings.GRAFANA_METRICS_ENABLE: observe_queue_size(self.opera_log_queue, queue_name=self.opera_log_queue_name) @@ -247,12 +256,21 @@ def desensitization(args: dict[str, Any]) -> dict[str, Any]: async def consumer(cls) -> None: """操作日志消费者""" - async def bulk_create_opera_log(logs: list[CreateOperaLogParam]) -> None: + async def bulk_create_opera_log(logs: list[tuple[dict[str, Any], CreateOperaLogParam]]) -> None: """批量创建操作日志""" if settings.DATABASE_ECHO: log.info('自动执行【操作日志批量创建】任务...') + logs_by_tenant = defaultdict(list) + for context_data, log_in in logs: + tenant_id = context_data.get('tenant_id') + if tenant_id is None: + raise RuntimeError('opera log context is missing tenant_id') + logs_by_tenant[tenant_id].append((context_data, log_in)) async with async_db_session.begin() as db: - await opera_log_service.bulk_create(db=db, objs=logs) + for tenant_logs in logs_by_tenant.values(): + request_context = dict(tenant_logs[0][0]) + with request_cycle_context(request_context): + await opera_log_service.bulk_create(db=db, objs=[log_in for _, log_in in tenant_logs]) await batch_consume( cls.opera_log_queue, diff --git a/backend/plugin/core.py b/backend/plugin/core.py index 020f02dff..29602d5d3 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -38,6 +38,8 @@ def check_plugin_installed(plugin_name: str) -> bool: def get_required_plugins() -> tuple[str, ...]: """获取必需插件列表""" required_plugins = list(settings.PLUGIN_REQUIRED) + if settings.TENANT_ENABLED and 'tenant' not in required_plugins: + required_plugins.append('tenant') if not settings.RBAC_ROLE_MENU_MODE and 'casbin_rbac' not in required_plugins: required_plugins.append('casbin_rbac') return tuple(required_plugins) @@ -423,6 +425,7 @@ def build_sql_filename( pk_type: PrimaryKeyType, *, suffix: str | None = None, + tenant: bool = False, ) -> str: """ 构建插件 SQL 脚本文件名 @@ -437,6 +440,8 @@ def build_sql_filename( parts.append('snowflake') if suffix: parts.append(suffix) + if tenant: + parts.append('tenant') return f'{"_".join(parts)}.sql' @@ -451,6 +456,15 @@ async def get_plugin_sql(plugin: str, db_type: DataBaseType, pk_type: PrimaryKey """ sql_dir = PLUGIN_DIR / plugin / 'sql' / ('mysql' if db_type == DataBaseType.mysql else 'postgresql') default_filename = build_sql_filename('init', pk_type) + if not settings.TENANT_ENABLED: + sql_file = sql_dir / default_filename + return str(sql_file) if await anyio.Path(sql_file).exists() else None + + tenant_filename = build_sql_filename('init', pk_type, tenant=True) + tenant_sql_file = sql_dir / tenant_filename + if await anyio.Path(tenant_sql_file).exists(): + return str(tenant_sql_file) + default_sql_file = sql_dir / default_filename return str(default_sql_file) if await anyio.Path(default_sql_file).exists() else None diff --git a/backend/plugin/notice/model/notice.py b/backend/plugin/notice/model/notice.py index a1ef519f2..c0626ed8a 100644 --- a/backend/plugin/notice/model/notice.py +++ b/backend/plugin/notice/model/notice.py @@ -2,10 +2,10 @@ from sqlalchemy.orm import Mapped, mapped_column -from backend.common.model import Base, UniversalText, id_key +from backend.common.model import Base, TenantMixin, UniversalText, id_key -class Notice(Base): +class Notice(Base, TenantMixin): """系统通知公告表""" __tablename__ = 'sys_notice' diff --git a/backend/plugin/notice/sql/mysql/init_snowflake_tenant.sql b/backend/plugin/notice/sql/mysql/init_snowflake_tenant.sql new file mode 100644 index 000000000..7d0d18313 --- /dev/null +++ b/backend/plugin/notice/sql/mysql/init_snowflake_tenant.sql @@ -0,0 +1,27 @@ +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values (2049629108257816576, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 2049629108245233667, now(), null); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, now(), null), +(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, now(), null), +(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, now(), null); + +insert into sys_notice (id, title, type, status, content, created_time, updated_time) +values (2112248797756129280, 'hahahahahaahahaha', 0, 1, '你好😄 + +``` +print(''fba yyds'') +``` + +⚡⚡⚡ + +| col1 | col2 | col3 | +| ---- | ---- | ---- | +| | | | +| | | | + +* 1 +* 2 +* 3 +', '2025-12-15 15:33:16', null, 0); diff --git a/backend/plugin/notice/sql/mysql/init_tenant.sql b/backend/plugin/notice/sql/mysql/init_tenant.sql new file mode 100644 index 000000000..d9895f0ea --- /dev/null +++ b/backend/plugin/notice/sql/mysql/init_tenant.sql @@ -0,0 +1,31 @@ +set @system_menu_id = (select id from sys_menu where name = 'System'); + +insert into sys_menu (title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values ('notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, @system_menu_id, now(), null); + +set @notice_menu_id = LAST_INSERT_ID(); + +insert into sys_menu (title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +('新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, @notice_menu_id, now(), null), +('修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, @notice_menu_id, now(), null), +('删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, @notice_menu_id, now(), null); + +insert into sys_notice (id, title, type, status, content, created_time, updated_time, tenant_id) +values (1, 'hahahahahaahahaha', 0, 1, '你好😄 + +``` +print(''fba yyds'') +``` + +⚡⚡⚡ + +| col1 | col2 | col3 | +| ---- | ---- | ---- | +| | | | +| | | | + +* 1 +* 2 +* 3 +', '2025-12-15 15:33:16', null, 0); diff --git a/backend/plugin/notice/sql/postgresql/init_snowflake_tenant.sql b/backend/plugin/notice/sql/postgresql/init_snowflake_tenant.sql new file mode 100644 index 000000000..7d0d18313 --- /dev/null +++ b/backend/plugin/notice/sql/postgresql/init_snowflake_tenant.sql @@ -0,0 +1,27 @@ +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values (2049629108257816576, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 2049629108245233667, now(), null); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, now(), null), +(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, now(), null), +(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, now(), null); + +insert into sys_notice (id, title, type, status, content, created_time, updated_time) +values (2112248797756129280, 'hahahahahaahahaha', 0, 1, '你好😄 + +``` +print(''fba yyds'') +``` + +⚡⚡⚡ + +| col1 | col2 | col3 | +| ---- | ---- | ---- | +| | | | +| | | | + +* 1 +* 2 +* 3 +', '2025-12-15 15:33:16', null, 0); diff --git a/backend/plugin/notice/sql/postgresql/init_tenant.sql b/backend/plugin/notice/sql/postgresql/init_tenant.sql new file mode 100644 index 000000000..b9bf1cea0 --- /dev/null +++ b/backend/plugin/notice/sql/postgresql/init_tenant.sql @@ -0,0 +1,37 @@ +do $$ +declare + notice_menu_id bigint; +begin + insert into sys_menu (title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) + values ('notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, (select id from sys_menu where name = 'System'), now(), null) + returning id into notice_menu_id; + + insert into sys_menu (title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) + values + ('新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, notice_menu_id, now(), null), + ('修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, notice_menu_id, now(), null), + ('删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, notice_menu_id, now(), null); +end $$; + +select setval(pg_get_serial_sequence('sys_menu', 'id'), coalesce(max(id), 0) + 1, true) from sys_menu; + +insert into sys_notice (id, title, type, status, content, created_time, updated_time, tenant_id) +values (1, 'hahahahahaahahaha', 0, 1, '你好😄 + +``` +print(''fba yyds'') +``` + +⚡⚡⚡ + +| col1 | col2 | col3 | +| ---- | ---- | ---- | +| | | | +| | | | + +* 1 +* 2 +* 3 +', '2025-12-15 15:33:16', null, 0); + +select setval(pg_get_serial_sequence('sys_notice', 'id'),coalesce(max(id), 0) + 1, true) from sys_notice; diff --git a/backend/plugin/oauth2/api/v1/user_social.py b/backend/plugin/oauth2/api/v1/user_social.py index 7041e5d38..f4216e701 100644 --- a/backend/plugin/oauth2/api/v1/user_social.py +++ b/backend/plugin/oauth2/api/v1/user_social.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Request +from backend.common.context import ctx from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.security.jwt import DependsJwtAuth from backend.database.db import CurrentSession, CurrentSessionTransaction @@ -17,7 +18,11 @@ async def get_user_bindings(db: CurrentSession, request: Request) -> ResponseSch @router.get('/me/binding', summary='获取绑定授权链接', dependencies=[DependsJwtAuth]) async def get_binding_auth_url(request: Request, source: UserSocialType) -> ResponseSchemaModel[str]: - binding_url = await user_social_service.get_binding_auth_url(user_id=request.user.id, source=source) + binding_url = await user_social_service.get_binding_auth_url( + user_id=request.user.id, + tenant_id=ctx.tenant_id, + source=source, + ) return response_base.success(data=binding_url) diff --git a/backend/plugin/oauth2/crud/crud_user_social.py b/backend/plugin/oauth2/crud/crud_user_social.py index 03b434c88..68174696a 100644 --- a/backend/plugin/oauth2/crud/crud_user_social.py +++ b/backend/plugin/oauth2/crud/crud_user_social.py @@ -1,8 +1,11 @@ from collections.abc import Sequence +from sqlalchemy import and_ from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy_crud_plus import CRUDPlus +from sqlalchemy_crud_plus import CRUDPlus, JoinConfig +from backend.app.admin.model import User +from backend.core.conf import settings from backend.plugin.oauth2.model import UserSocial from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam from backend.utils.timezone import timezone @@ -22,16 +25,31 @@ async def check_binding(self, db: AsyncSession, user_id: int, source: str) -> Us """ return await self.select_model_by_column(db, user_id=user_id, source=source, deleted=0) - async def get_by_sid(self, db: AsyncSession, sid: str, source: str) -> UserSocial | None: + async def get_by_sid( + self, + db: AsyncSession, + tenant_id: int, + sid: str, + source: str, + ) -> UserSocial | None: """ - 通过 sid 获取社交用户 + 通过 sid 获取当前租户内的社交用户 :param db: 数据库会话 + :param tenant_id: 租户 ID :param sid: 社交账号唯一编码 :param source: 社交账号类型 :return: """ - return await self.select_model_by_column(db, sid=sid, source=source, deleted=0) + conditions = [User.tenant_id == tenant_id] if settings.TENANT_ENABLED else [] + return await self.select_model_by_column( + db, + *conditions, + sid=sid, + source=source, + deleted=0, + join_conditions=[JoinConfig(model=User, join_on=and_(User.id == self.model.user_id, User.deleted == 0))], + ) async def get_by_user_id(self, db: AsyncSession, user_id: int) -> Sequence[UserSocial]: """ diff --git a/backend/plugin/oauth2/plugin.toml b/backend/plugin/oauth2/plugin.toml index 58e6b16f0..850e55aa3 100644 --- a/backend/plugin/oauth2/plugin.toml +++ b/backend/plugin/oauth2/plugin.toml @@ -1,6 +1,6 @@ [plugin] summary = "OAuth 2.0" -version = "0.0.11" +version = "0.1.0" description = "支持 GitHub、Google 等社交平台登录" author = "wu-clan" tags = ["auth"] diff --git a/backend/plugin/oauth2/service/oauth2_service.py b/backend/plugin/oauth2/service/oauth2_service.py index bcd93df63..dd0c3f480 100644 --- a/backend/plugin/oauth2/service/oauth2_service.py +++ b/backend/plugin/oauth2/service/oauth2_service.py @@ -1,9 +1,11 @@ import json +import uuid from typing import Any +from urllib.parse import urlparse from fast_captcha import text_captcha -from fastapi import BackgroundTasks, Response +from fastapi import BackgroundTasks, Request, Response from sqlalchemy.ext.asyncio import AsyncSession from backend.app.admin.crud.crud_user import user_dao @@ -21,12 +23,57 @@ from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam from backend.plugin.oauth2.service.user_social_service import user_social_service +from backend.plugin.oauth2.utils import get_oauth2_authorization_url from backend.utils.timezone import timezone class OAuth2Service: """OAuth2 认证服务类""" + async def get_login_auth_url(self, *, db: AsyncSession, request: Request, source: UserSocialType) -> str: + """ + 获取 OAuth2 登录授权链接 + + :param db: 数据库会话 + :param request: FastAPI 请求对象 + :param source: 社交平台 + :return: + """ + tenant_id = settings.TENANT_DEFAULT_ID + + if settings.TENANT_ENABLED: + try: + from backend.plugin.tenant.service.tenant_service import tenant_service + except ImportError: + raise errors.ServerError(msg='租户插件方法导入失败,请联系系统管理员') + + tenant_domain = request.headers.get('Origin') or request.headers.get('Referer') + if tenant_domain: + tenant_domain = urlparse(tenant_domain).hostname + else: + tenant_domain = ( + request.headers.get('X-Forwarded-Host') + or request.headers.get('X-Original-Host') + or request.url.hostname + ) + + if tenant_domain: + tenant_domain = tenant_domain.strip().split(',')[0].strip().lower() + tenant = await tenant_service.get_by_domain(db=db, domain=tenant_domain) + if tenant: + tenant_id = tenant.id + if tenant_id == settings.TENANT_DEFAULT_ID: + raise errors.ForbiddenError(msg='OAuth2 登录缺少有效租户上下文') + + state = str(uuid.uuid4()) + await redis_client.setex( + f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}', + settings.OAUTH2_STATE_EXPIRE_SECONDS, + json.dumps({'type': UserSocialAuthType.login.value, 'tenant_id': tenant_id}), + ) + + return await get_oauth2_authorization_url(source=source, state=state) + @staticmethod async def login( *, @@ -54,7 +101,7 @@ async def login( :param avatar: 头像地址 :return: """ - user_social = await user_social_dao.get_by_sid(db, sid, source.value) + user_social = await user_social_dao.get_by_sid(db, ctx.tenant_id, sid, source.value) if user_social: sys_user = await user_dao.get(db, user_social.user_id) # 更新用户头像 @@ -96,6 +143,7 @@ async def login( # 创建 token access_token_data = await jwt.create_access_token( sys_user.id, + ctx.tenant_id, multi_login=sys_user.is_multi_login, # extra info username=sys_user.username, @@ -109,6 +157,7 @@ async def login( refresh_token_data = await jwt.create_refresh_token( access_token_data.session_uuid, sys_user.id, + ctx.tenant_id, multi_login=sys_user.is_multi_login, ) await user_dao.update_login_time(db, sys_user.username) @@ -158,7 +207,6 @@ async def login_or_binding( :param state: OAuth2 state 参数 :return: """ - sid = user.get('uuid') username = user.get('username') nickname = user.get('nickname') @@ -187,35 +235,46 @@ async def login_or_binding( state_info = json.loads(state_data) await redis_client.delete(f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}') + tenant_id = state_info.get('tenant_id') + if tenant_id is None: + raise errors.ForbiddenError(msg='OAuth2 状态信息缺少租户 ID') + tenant_id = int(tenant_id) + current_tenant_id = ctx.tenant_id + ctx.tenant_id = tenant_id - # 绑定流程 - if state_info.get('type') == UserSocialAuthType.binding.value: - user_id = state_info.get('user_id') - if not user_id: - raise errors.ForbiddenError(msg='非法操作,OAuth2 状态信息无效') - await user_social_service.binding_with_oauth2( + try: + await jwt.check_tenant_status(db, tenant_id) + + # 绑定流程 + if state_info.get('type') == UserSocialAuthType.binding.value: + user_id = state_info.get('user_id') + if not user_id: + raise errors.ForbiddenError(msg='非法操作,OAuth2 状态信息无效') + await user_social_service.binding_with_oauth2( + db=db, + user_id=user_id, + sid=str(sid), + source=social, + ) + return None + + # 登录流程 + if state_info.get('type') != UserSocialAuthType.login.value: + raise errors.ForbiddenError(msg='OAuth2 状态信息无效') + + return await self.login( db=db, - user_id=user_id, + response=response, + background_tasks=background_tasks, sid=str(sid), source=social, + username=username, + nickname=nickname, + email=email, + avatar=avatar, ) - return None - - # 登录流程 - if state_info.get('type') != UserSocialAuthType.login.value: - raise errors.ForbiddenError(msg='OAuth2 状态信息无效') - - return await self.login( - db=db, - response=response, - background_tasks=background_tasks, - sid=str(sid), - source=social, - username=username, - nickname=nickname, - email=email, - avatar=avatar, - ) + finally: + ctx.tenant_id = current_tenant_id oauth2_service: OAuth2Service = OAuth2Service() diff --git a/backend/plugin/oauth2/service/user_social_service.py b/backend/plugin/oauth2/service/user_social_service.py index 55ce367bd..4f3cf9555 100644 --- a/backend/plugin/oauth2/service/user_social_service.py +++ b/backend/plugin/oauth2/service/user_social_service.py @@ -3,12 +3,14 @@ from sqlalchemy.ext.asyncio import AsyncSession +from backend.common.context import ctx from backend.common.exception import errors from backend.core.conf import settings from backend.database.redis import redis_client from backend.plugin.oauth2.crud.crud_user_social import user_social_dao from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam +from backend.plugin.oauth2.utils import get_oauth2_authorization_url class UserSocialService: @@ -19,7 +21,7 @@ async def get_bindings(*, db: AsyncSession, user_id: int) -> list[str]: :param db: 数据库会话 :param user_id: 用户 ID - :return: 绑定列表,每个元素包含 sid、source 等信息 + :return: """ bindings = await user_social_dao.get_by_user_id(db, user_id) return [binding.source for binding in bindings] @@ -44,7 +46,7 @@ async def binding_with_oauth2( if await user_social_dao.check_binding(db, user_id, source.value): raise errors.RequestError(msg=f'用户已绑定 {source.value} 账号') - if await user_social_dao.get_by_sid(db, sid, source.value): + if await user_social_dao.get_by_sid(db, ctx.tenant_id, sid, source.value): raise errors.RequestError(msg=f'该 {source.value} 账号已被其他用户绑定') new_user_social = CreateUserSocialParam(sid=sid, source=source.value, user_id=user_id) @@ -66,34 +68,16 @@ async def unbinding(*, db: AsyncSession, user_id: int, source: UserSocialType) - return await user_social_dao.delete(db, user_id, source.value) @staticmethod - async def get_binding_auth_url(*, user_id: int, source: UserSocialType) -> str: + async def get_binding_auth_url(*, user_id: int, tenant_id: int, source: UserSocialType) -> str: state = str(uuid.uuid4()) await redis_client.set( f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}', - json.dumps({'type': UserSocialAuthType.binding.value, 'user_id': user_id}), + json.dumps({'type': UserSocialAuthType.binding.value, 'user_id': user_id, 'tenant_id': tenant_id}), ex=settings.OAUTH2_STATE_EXPIRE_SECONDS, ) - match source: - case UserSocialType.github: - from backend.plugin.oauth2.api.v1.github import github_client - - auth_url = await github_client.get_authorization_url( - redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI, - state=state, - ) - case UserSocialType.google: - from backend.plugin.oauth2.api.v1.google import google_client - - auth_url = await google_client.get_authorization_url( - redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI, - state=state, - ) - case _: - raise errors.ForbiddenError(msg=f'暂不支持 {source} 绑定') - - return auth_url + return await get_oauth2_authorization_url(source=source, state=state) user_social_service: UserSocialService = UserSocialService() diff --git a/backend/plugin/oauth2/utils.py b/backend/plugin/oauth2/utils.py new file mode 100644 index 000000000..8c3e0fe9e --- /dev/null +++ b/backend/plugin/oauth2/utils.py @@ -0,0 +1,32 @@ +from backend.common.exception import errors +from backend.core.conf import settings +from backend.plugin.oauth2.enums import UserSocialType + + +async def get_oauth2_authorization_url(*, source: UserSocialType, state: str) -> str: + """ + 获取 OAuth2 授权链接 + + :param source: 社交平台 + :param state: OAuth2 状态值 + :return: + """ + match source: + case UserSocialType.github: + from backend.plugin.oauth2.api.v1.github import github_client + + auth_url = await github_client.get_authorization_url( + redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI, + state=state, + ) + case UserSocialType.google: + from backend.plugin.oauth2.api.v1.google import google_client + + auth_url = await google_client.get_authorization_url( + redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI, + state=state, + ) + case _: + raise errors.ForbiddenError(msg=f'暂不支持 {source} OAuth2 授权') + + return auth_url diff --git a/backend/sql/mysql/init_snowflake_test_data_tenant.sql b/backend/sql/mysql/init_snowflake_test_data_tenant.sql new file mode 100644 index 000000000..6df41cb09 --- /dev/null +++ b/backend/sql/mysql/init_snowflake_test_data_tenant.sql @@ -0,0 +1,109 @@ +insert into sys_dept (id, name, sort, leader, phone, email, status, deleted, parent_id, created_time, updated_time, tenant_id) +values (2048601258595581952, '测试', 0, null, null, null, 1, 0, null, now(), null, 0); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(2049629108245233664, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108245233665, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null), +(2049629108245233666, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null), +(2049629108245233667, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108245233668, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233672, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null), +(2049629108245233674, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233680, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108249427969, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108249427970, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'), +(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427975, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'), +(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427979, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427983, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108249427984, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null), +(2049629108249427985, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null), +(2049629108249427986, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108249427987, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null), +(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null), +(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null), +(2049629108249427990, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null), +(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null), +(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null), +(2049629108253622273, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108253622274, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622276, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622277, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi-best-architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622282, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null); + +insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time, tenant_id) +values (2048601263515500544, '测试', 1, true, null, now(), null, 0); + +insert into sys_role_menu (id, role_id, menu_id, tenant_id) +values +(2048601263578415104, 2048601263515500544, 2049629108245233664, 0), +(2048601263641329664, 2048601263515500544, 2049629108245233665, 0), +(2048601263708438528, 2048601263515500544, 2049629108245233666, 0), +(2048601263775547392, 2048601263515500544, 2049629108253622282, 0); + +insert into sys_user (id, uuid, username, nickname, password, salt, email, status, is_superuser, is_staff, is_multi_login, avatar, phone, join_time, last_login_time, last_password_changed_time, dept_id, created_time, updated_time, tenant_id) +values +(2048601263834267648, uuid(), 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', unhex('24326224313224387932654E7563583139566A6D5A33745968424C634F'), 'admin@example.com', 1, true, true, true, null, null, now(), now(), now(), 2048601258595581952, now(), null, 0), +(2049946297615646720, uuid(), 'test', '用户66666', '$2b$12$BMiXsNQAgTx7aNc7kVgnwedXGyUxPEHRnJMFbiikbqHgVoT3y14Za', unhex('24326224313224424D6958734E514167547837614E63376B56676E7765'), 'test@example.com', 1, false, false, false, null, null, now(), now(), now(), 2048601258595581952, now(), null, 0); + +insert into sys_user_role (id, user_id, role_id, tenant_id) +values +(2048601263838461952, 2048601263834267648, 2048601263515500544, 0), +(2049946493732913152, 2049946297615646720, 2048601263515500544, 0); + +insert into sys_data_scope (id, name, status, created_time, updated_time) +values +(2048601263901376512, '本部门数据权限', 1, now(), null), +(2048601263968485376, '部门及以下数据权限', 1, now(), null), +(2048601263968485377, '仅本人数据权限', 1, now(), null), +(2048601263968485378, '全模型本部门数据权限', 1, now(), null), +(2048601263968485379, '排除超级管理员数据权限', 1, now(), null); + +insert into sys_data_rule (id, name, model, `column`, operator, expression, `value`, created_time, updated_time) +values +(2048601264035594240, '部门 ID 等于当前用户部门', 'Dept', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2048601264102703104, '部门名称等于测试', 'Dept', 'name', 1, 0, '测试', now(), null), +(2048601264102703105, '父部门 ID 等于测试部门 ID', 'Dept', 'parent_id', 0, 0, '1', now(), null), +(2048601264102703106, '创建者等于当前用户', '__ALL__', '__created_by__', 0, 0, '${user_id}', now(), null), +(2048601264102703107, '全模型部门 ID 等于当前用户部门', '__ALL__', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2048601264102703109, '用户非超级管理员', 'User', 'is_superuser', 0, 1, '1', now(), null); + +insert into sys_role_data_scope (id, role_id, data_scope_id, tenant_id) +values +(2048601264169811968, 2048601263515500544, 2048601263901376512, 0), +(2048601264236920832, 2048601263515500544, 2048601263968485376, 0); + +insert into sys_data_scope_rule (id, data_scope_id, data_rule_id) +values +(2048601264169811968, 2048601263901376512, 2048601264035594240), +(2048601264236920832, 2048601263968485376, 2048601264102703104), +(2048601264299835392, 2048601263968485376, 2048601264102703105), +(2048601264299835393, 2048601263968485377, 2048601264102703106), +(2048601264299835394, 2048601263968485378, 2048601264102703107), +(2048601264299835395, 2048601263968485379, 2048601264102703109); diff --git a/backend/sql/mysql/init_test_data_tenant.sql b/backend/sql/mysql/init_test_data_tenant.sql new file mode 100644 index 000000000..44294983c --- /dev/null +++ b/backend/sql/mysql/init_test_data_tenant.sql @@ -0,0 +1,109 @@ +insert into sys_dept (id, name, sort, leader, phone, email, status, deleted, parent_id, created_time, updated_time, tenant_id) +values (1, '测试', 0, null, null, null, 1, 0, null, now(), null, 0); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(1, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null), +(3, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null), +(4, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(5, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(9, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null), +(11, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(17, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(21, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(22, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'), +(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(27, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'), +(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(31, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(35, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(36, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null), +(37, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null), +(38, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(39, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null), +(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null), +(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null), +(42, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null), +(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null), +(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null), +(45, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(46, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(47, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(48, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(49, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(50, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 49, '2025-06-26 20:29:06', null), +(51, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi-best-architecture', null, 49, '2025-06-26 20:29:06', null), +(52, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 49, '2025-06-26 20:29:06', null), +(53, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null); + +insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time, tenant_id) +values (1, '测试', 1, true, null, now(), null, 0); + +insert into sys_role_menu (id, role_id, menu_id, tenant_id) +values +(1, 1, 1, 0), +(2, 1, 2, 0), +(3, 1, 3, 0), +(4, 1, 53, 0); + +insert into sys_user (id, uuid, username, nickname, password, salt, email, status, is_superuser, is_staff, is_multi_login, avatar, phone, join_time, last_login_time, last_password_changed_time, dept_id, created_time, updated_time, tenant_id) +values +(1, uuid(), 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', unhex('24326224313224387932654E7563583139566A6D5A33745968424C634F'), 'admin@example.com', 1, true, true, true, null, null, now(), now(), now(), 1, now(), null, 0), +(2, uuid(), 'test', '用户66666', '$2b$12$BMiXsNQAgTx7aNc7kVgnwedXGyUxPEHRnJMFbiikbqHgVoT3y14Za', unhex('24326224313224424D6958734E514167547837614E63376B56676E7765'), 'test@example.com', 1, false, false, false, null, null, now(), now(), now(), 1, now(), null, 0); + +insert into sys_user_role (id, user_id, role_id, tenant_id) +values +(1, 1, 1, 0), +(2, 2, 1, 0); + +insert into sys_data_scope (id, name, status, created_time, updated_time) +values +(1, '本部门数据权限', 1, now(), null), +(2, '部门及以下数据权限', 1, now(), null), +(3, '仅本人数据权限', 1, now(), null), +(4, '全模型本部门数据权限', 1, now(), null), +(5, '排除超级管理员数据权限', 1, now(), null); + +insert into sys_data_rule (id, name, model, `column`, operator, expression, `value`, created_time, updated_time) +values +(1, '部门 ID 等于当前用户部门', 'Dept', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2, '部门名称等于测试', 'Dept', 'name', 1, 0, '测试', now(), null), +(3, '父部门 ID 等于测试部门 ID', 'Dept', 'parent_id', 0, 0, '1', now(), null), +(4, '创建者等于当前用户', '__ALL__', '__created_by__', 0, 0, '${user_id}', now(), null), +(5, '全模型部门 ID 等于当前用户部门', '__ALL__', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(6, '用户非超级管理员', 'User', 'is_superuser', 0, 1, '1', now(), null); + +insert into sys_role_data_scope (id, role_id, data_scope_id, tenant_id) +values +(1, 1, 1, 0), +(2, 1, 2, 0); + +insert into sys_data_scope_rule (id, data_scope_id, data_rule_id) +values +(1, 1, 1), +(2, 2, 2), +(3, 2, 3), +(4, 3, 4), +(5, 4, 5), +(6, 5, 6); diff --git a/backend/sql/postgresql/init_snowflake_test_data_tenant.sql b/backend/sql/postgresql/init_snowflake_test_data_tenant.sql new file mode 100644 index 000000000..86d2a124b --- /dev/null +++ b/backend/sql/postgresql/init_snowflake_test_data_tenant.sql @@ -0,0 +1,109 @@ +insert into sys_dept (id, name, sort, leader, phone, email, status, deleted, parent_id, created_time, updated_time, tenant_id) +values (2048601264366944256, '测试', 0, null, null, null, 1, 0, null, now(), null, 0); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(2049629108245233664, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108245233665, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null), +(2049629108245233666, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null), +(2049629108245233667, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108245233668, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null), +(2049629108245233672, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null), +(2049629108245233674, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null), +(2049629108245233680, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null), +(2049629108249427969, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108249427970, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'), +(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null), +(2049629108249427975, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'), +(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null), +(2049629108249427979, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null), +(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null), +(2049629108249427983, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108249427984, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null), +(2049629108249427985, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null), +(2049629108249427986, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108249427987, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null), +(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null), +(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null), +(2049629108249427990, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null), +(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null), +(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null), +(2049629108253622273, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108253622274, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622276, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622277, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null), +(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi-best-architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null), +(2049629108253622282, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null); + +insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time, tenant_id) +values (2048601269345583104, '测试', 1, true, null, now(), null, 0); + +insert into sys_role_menu (id, role_id, menu_id, tenant_id) +values +(2048601269412691968, 2048601269345583104, 2049629108245233664, 0), +(2048601269479800832, 2048601269345583104, 2049629108245233665, 0), +(2048601269546909696, 2048601269345583104, 2049629108245233666, 0), +(2048601269609824256, 2048601269345583104, 2049629108253622282, 0); + +insert into sys_user (id, uuid, username, nickname, password, salt, email, status, is_superuser, is_staff, is_multi_login, avatar, phone, join_time, last_login_time, last_password_changed_time, dept_id, created_time, updated_time, tenant_id) +values +(2048601269672738816, gen_random_uuid(), 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', decode('24326224313224387932654E7563583139566A6D5A33745968424C634F', 'hex'), 'admin@example.com', 1, true, true, true, null, null, now(), now(), now(), 2048601264366944256, now(), null, 0), +(2049946297615646720, gen_random_uuid(), 'test', '用户66666', '$2b$12$BMiXsNQAgTx7aNc7kVgnwedXGyUxPEHRnJMFbiikbqHgVoT3y14Za', decode('24326224313224424D6958734E514167547837614E63376B56676E7765', 'hex'), 'test@example.com', 1, false, false, false, null, null, now(), now(), now(), 2048601264366944256, now(), null, 0); + +insert into sys_user_role (id, user_id, role_id, tenant_id) +values +(2048601269739847680, 2048601269672738816, 2048601269345583104, 0), +(2049946493732913152, 2049946297615646720, 2048601269345583104, 0); + +insert into sys_data_scope (id, name, status, created_time, updated_time) +values +(2048601269806956544, '本部门数据权限', 1, now(), null), +(2048601269869871104, '部门及以下数据权限', 1, now(), null), +(2048601269869871105, '仅本人数据权限', 1, now(), null), +(2048601269869871106, '全模型本部门数据权限', 1, now(), null), +(2048601269869871107, '排除超级管理员数据权限', 1, now(), null); + +insert into sys_data_rule (id, name, model, "column", operator, expression, "value", created_time, updated_time) +values +(2048601269932785664, '部门 ID 等于当前用户部门', 'Dept', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2048601269999894528, '部门名称等于测试', 'Dept', 'name', 1, 0, '测试', now(), null), +(2048601269999894529, '父部门 ID 等于测试部门 ID', 'Dept', 'parent_id', 0, 0, '1', now(), null), +(2048601269999894530, '创建者等于当前用户', '__ALL__', '__created_by__', 0, 0, '${user_id}', now(), null), +(2048601269999894531, '全模型部门 ID 等于当前用户部门', '__ALL__', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2048601269999894533, '用户非超级管理员', 'User', 'is_superuser', 0, 1, '1', now(), null); + +insert into sys_role_data_scope (id, role_id, data_scope_id, tenant_id) +values +(2048601270062809088, 2048601269345583104, 2048601269806956544, 0), +(2048601270125723648, 2048601269345583104, 2048601269869871104, 0); + +insert into sys_data_scope_rule (id, data_scope_id, data_rule_id) +values +(2048601270062809088, 2048601269806956544, 2048601269932785664), +(2048601270125723648, 2048601269869871104, 2048601269999894528), +(2048601270192832512, 2048601269869871104, 2048601269999894529), +(2048601270192832513, 2048601269869871105, 2048601269999894530), +(2048601270192832514, 2048601269869871106, 2048601269999894531), +(2048601270192832515, 2048601269869871107, 2048601269999894533); diff --git a/backend/sql/postgresql/init_test_data_tenant.sql b/backend/sql/postgresql/init_test_data_tenant.sql new file mode 100644 index 000000000..143e952a2 --- /dev/null +++ b/backend/sql/postgresql/init_test_data_tenant.sql @@ -0,0 +1,120 @@ +insert into sys_dept (id, name, sort, leader, phone, email, status, deleted, parent_id, created_time, updated_time, tenant_id) +values (1, '测试', 0, null, null, null, 1, 0, null, now(), null, 0); + +insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time) +values +(1, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(2, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null), +(3, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null), +(4, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(5, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null), +(9, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null), +(11, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null), +(17, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null), +(21, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(22, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'), +(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null), +(27, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'), +(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null), +(31, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null), +(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null), +(35, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(36, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null), +(37, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null), +(38, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(39, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null), +(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null), +(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null), +(42, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null), +(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null), +(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null), +(45, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(46, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(47, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(48, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null), +(49, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null), +(50, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 49, '2025-06-26 20:29:06', null), +(51, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi-best-architecture', null, 49, '2025-06-26 20:29:06', null), +(52, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 49, '2025-06-26 20:29:06', null), +(53, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null); + +insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time, tenant_id) +values (1, '测试', 1, true, null, now(), null, 0); + +insert into sys_role_menu (id, role_id, menu_id, tenant_id) +values +(1, 1, 1, 0), +(2, 1, 2, 0), +(3, 1, 3, 0), +(4, 1, 53, 0); + +insert into sys_user (id, uuid, username, nickname, password, salt, email, status, is_superuser, is_staff, is_multi_login, avatar, phone, join_time, last_login_time, last_password_changed_time, dept_id, created_time, updated_time, tenant_id) +values +(1, gen_random_uuid(), 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', decode('24326224313224387932654E7563583139566A6D5A33745968424C634F', 'hex'), 'admin@example.com', 1, true, true, true, null, null, now(), now(), now(), 1, now(), null, 0), +(2, gen_random_uuid(), 'test', '用户66666', '$2b$12$BMiXsNQAgTx7aNc7kVgnwedXGyUxPEHRnJMFbiikbqHgVoT3y14Za', decode('24326224313224424D6958734E514167547837614E63376B56676E7765', 'hex'), 'test@example.com', 1, false, false, false, null, null, now(), now(), now(), 1, now(), null, 0); + +insert into sys_user_role (id, user_id, role_id, tenant_id) +values +(1, 1, 1, 0), +(2, 2, 1, 0); + +insert into sys_data_scope (id, name, status, created_time, updated_time) +values +(1, '本部门数据权限', 1, now(), null), +(2, '测试部门及以下数据权限', 1, now(), null), +(3, '仅本人数据权限', 1, now(), null), +(4, '全模型本部门数据权限', 1, now(), null), +(5, '排除超级管理员数据权限', 1, now(), null); + +insert into sys_data_rule (id, name, model, "column", operator, expression, "value", created_time, updated_time) +values +(1, '部门 ID 等于当前用户部门', 'Dept', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(2, '部门名称等于测试', 'Dept', 'name', 1, 0, '测试', now(), null), +(3, '父部门 ID 等于测试部门 ID', 'Dept', 'parent_id', 0, 0, '1', now(), null), +(4, '创建者等于当前用户', '__ALL__', '__created_by__', 0, 0, '${user_id}', now(), null), +(5, '全模型部门 ID 等于当前用户部门', '__ALL__', '__dept_id__', 0, 0, '${dept_id}', now(), null), +(6, '用户非超级管理员', 'User', 'is_superuser', 0, 1, '1', now(), null); + +insert into sys_role_data_scope (id, role_id, data_scope_id, tenant_id) +values +(1, 1, 1, 0), +(2, 1, 2, 0); + +insert into sys_data_scope_rule (id, data_scope_id, data_rule_id) +values +(1, 1, 1), +(2, 2, 2), +(3, 2, 3), +(4, 3, 4), +(5, 4, 5), +(6, 5, 6); + +select setval(pg_get_serial_sequence('sys_dept', 'id'),coalesce(max(id), 0) + 1, true) from sys_dept; +select setval(pg_get_serial_sequence('sys_menu', 'id'),coalesce(max(id), 0) + 1, true) from sys_menu; +select setval(pg_get_serial_sequence('sys_role', 'id'),coalesce(max(id), 0) + 1, true) from sys_role; +select setval(pg_get_serial_sequence('sys_role_menu', 'id'),coalesce(max(id), 0) + 1, true) from sys_role_menu; +select setval(pg_get_serial_sequence('sys_user', 'id'),coalesce(max(id), 0) + 1, true) from sys_user; +select setval(pg_get_serial_sequence('sys_user_role', 'id'),coalesce(max(id), 0) + 1, true) from sys_user_role; +select setval(pg_get_serial_sequence('sys_data_scope', 'id'),coalesce(max(id), 0) + 1, true) from sys_data_scope; +select setval(pg_get_serial_sequence('sys_data_rule', 'id'),coalesce(max(id), 0) + 1, true) from sys_data_rule; +select setval(pg_get_serial_sequence('sys_role_data_scope', 'id'),coalesce(max(id), 0) + 1, true) from sys_role_data_scope; +select setval(pg_get_serial_sequence('sys_data_scope_rule', 'id'),coalesce(max(id), 0) + 1, true) from sys_data_scope_rule; diff --git a/backend/utils/dynamic_import.py b/backend/utils/dynamic_import.py index 33b483262..51ebbd549 100644 --- a/backend/utils/dynamic_import.py +++ b/backend/utils/dynamic_import.py @@ -3,12 +3,10 @@ import os.path from functools import lru_cache -from typing import Any, TypeVar +from typing import Any import sqlalchemy as sa -T = TypeVar('T') - @lru_cache(maxsize=128) def import_module_cached(module_path: str) -> Any: