chore: 完成多批次代码优化与重构

- 重构工作流模块目录结构,迁移代码文件
- 修复类型断言空值安全问题,添加 ! 操作符
- 优化样式类名,替换 flex-cc 为标准 flex 工具类
- 更新路由标签简化文案,移除冗余注释
- 调整 ruff 配置,放宽行长度限制
- 更新 README 与多语言文案,优化项目描述
- 修复表单、图表组件的类型与样式问题
- 简化搜索表单、数据卡片的布局代码
This commit is contained in:
zhangtao
2026-06-20 05:31:46 +08:00
parent 82b742620b
commit d34d4a4c50
378 changed files with 4149 additions and 6375 deletions
@@ -26,7 +26,7 @@ from .schema import (
)
from .service import TenantService
TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["平台管理/租户管理"])
TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["租户管理"])
_TENANT_NS = "tenant"
@@ -39,7 +39,7 @@ _TENANT_NS = "tenant"
@cache(expire=120, namespace=_TENANT_NS)
async def get_obj_detail_controller(
id: Annotated[int, Path(description="租户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:query']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))],
) -> JSONResponse:
"""
获取租户详情
@@ -62,7 +62,7 @@ async def get_obj_detail_controller(
async def get_obj_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[TenantQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:query']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))],
) -> JSONResponse:
"""
查询租户列表
@@ -94,7 +94,7 @@ async def get_obj_list_controller(
)
async def create_obj_controller(
data: TenantCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:create']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))],
) -> JSONResponse:
"""
创建租户
@@ -118,7 +118,7 @@ async def create_obj_controller(
async def update_obj_controller(
data: TenantUpdateSchema,
id: Annotated[int, Path(description="租户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:update']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
"""
修改租户
@@ -142,7 +142,7 @@ async def update_obj_controller(
)
async def delete_obj_controller(
ids: Annotated[list[int], Body(..., description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:delete']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))],
) -> JSONResponse:
"""
删除租户
@@ -165,7 +165,7 @@ async def delete_obj_controller(
)
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:patch']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))],
) -> JSONResponse:
"""
批量修改租户状态
@@ -188,7 +188,7 @@ async def batch_set_available_obj_controller(
)
async def toggle_tenant_status_controller(
id: Annotated[int, Path(description="租户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:patch']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))],
) -> JSONResponse:
"""
启/禁用租户
@@ -212,7 +212,7 @@ async def toggle_tenant_status_controller(
@cache(expire=120, namespace=_TENANT_NS)
async def get_tenant_users_controller(
id: Annotated[int, Path(description="租户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:query']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))],
) -> JSONResponse:
"""
获取租户用户列表
@@ -235,7 +235,7 @@ async def get_tenant_users_controller(
async def add_tenant_user_controller(
id: Annotated[int, Path(description="租户ID")],
data: TenantUserAddSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:create']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))],
) -> JSONResponse:
"""
向租户添加用户
@@ -260,7 +260,7 @@ async def add_tenant_user_controller(
async def remove_tenant_user_controller(
id: Annotated[int, Path(description="租户ID")],
uid: Annotated[int, Path(description="用户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:delete']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))],
) -> JSONResponse:
"""
从租户移除用户
@@ -276,6 +276,7 @@ async def remove_tenant_user_controller(
await FastAPICache.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="移除用户成功")
# ============ P1: 租户配置 ============
@@ -286,7 +287,7 @@ async def remove_tenant_user_controller(
)
async def get_tenant_config_controller(
id: Annotated[int, Path(description="租户ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:query']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))],
) -> JSONResponse:
"""
获取租户配置
@@ -332,7 +333,7 @@ async def update_tenant_config_controller(
id: Annotated[int, Path(description="租户ID")],
data: Annotated[list[TenantConfigItem], Body(..., description="配置项列表")],
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:update']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
"""
更新租户配置
@@ -344,12 +345,11 @@ async def update_tenant_config_controller(
返回:
- JSONResponse: 包含更新后的配置列表的 JSON 响应。
"""
result = await TenantService.update_config_service(
auth=auth, redis=redis, tenant_id=id, config=data
)
result = await TenantService.update_config_service(auth=auth, redis=redis, tenant_id=id, config=data)
await FastAPICache.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result, msg="更新租户配置成功")
# ============ 续期 ============
@@ -361,7 +361,7 @@ async def update_tenant_config_controller(
async def renew_tenant_controller(
id: Annotated[int, Path(description="租户ID")],
data: TenantRenewSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:update']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
"""
租户续期
@@ -377,6 +377,7 @@ async def renew_tenant_controller(
await FastAPICache.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result, msg="租户续期成功")
# ============ 套餐变更预览 ============
@@ -388,7 +389,7 @@ async def renew_tenant_controller(
async def package_change_preview_controller(
id: Annotated[int, Path(description="租户ID")],
new_package_id: Annotated[int, Query(..., description="目标套餐ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_system:tenant:query']))],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))],
) -> JSONResponse:
"""
套餐变更影响预览
@@ -400,7 +401,5 @@ async def package_change_preview_controller(
返回:
- JSONResponse: 包含套餐变更影响预览的 JSON 响应。
"""
result = await TenantService.package_change_preview_service(
auth=auth, tenant_id=id, new_package_id=new_package_id
)
result = await TenantService.package_change_preview_service(auth=auth, tenant_id=id, new_package_id=new_package_id)
return SuccessResponse(data=result, msg="套餐变更预览成功")
@@ -22,69 +22,27 @@ class TenantModel(ModelMixin):
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="租户名称")
code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="租户编码")
contact_name: Mapped[str | None] = mapped_column(
String(64), nullable=True, default=None, comment="联系人姓名"
)
contact_phone: Mapped[str | None] = mapped_column(
String(20), nullable=True, default=None, comment="联系人电话"
)
contact_email: Mapped[str | None] = mapped_column(
String(128), nullable=True, default=None, comment="联系人邮箱"
)
address: Mapped[str | None] = mapped_column(
String(255), nullable=True, default=None, comment="地址"
)
domain: Mapped[str | None] = mapped_column(
String(255), nullable=True, default=None, comment="域名"
)
logo_url: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="Logo URL"
)
contact_name: Mapped[str | None] = mapped_column(String(64), nullable=True, default=None, comment="联系人姓名")
contact_phone: Mapped[str | None] = mapped_column(String(20), nullable=True, default=None, comment="联系人电话")
contact_email: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None, comment="联系人邮箱")
address: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None, comment="地址")
domain: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None, comment="域名")
logo_url: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="Logo URL")
sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="排序")
package_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("platform_package.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
default=None,
index=True,
comment="关联套餐ID",
)
start_time: Mapped[datetime | None] = mapped_column(
DateTime, nullable=True, default=None, comment="开始时间"
)
end_time: Mapped[datetime | None] = mapped_column(
DateTime, nullable=True, default=None, comment="结束时间"
)
# ============ 租户配置字段(从 TenantConfigModel 合并)===========
version: Mapped[str | None] = mapped_column(
String(20), nullable=True, default=None, comment="版本号"
)
favicon: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="favicon地址"
)
login_bg: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="登录背景地址"
)
copyright: Mapped[str | None] = mapped_column(
String(255), nullable=True, default=None, comment="版权信息"
)
keep_record: Mapped[str | None] = mapped_column(
String(100), nullable=True, default=None, comment="备案号"
)
help_doc: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="帮助文档地址"
)
privacy: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="隐私政策地址"
)
clause: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="服务条款地址"
)
git_code: Mapped[str | None] = mapped_column(
String(500), nullable=True, default=None, comment="源码地址"
)
# ── 关系 ═══════════════════════════════════════════
package_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("platform_package.id", ondelete="SET NULL", onupdate="CASCADE"), nullable=True, default=None, index=True, comment="关联套餐ID")
start_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None, comment="开始时间")
end_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None, comment="结束时间")
version: Mapped[str | None] = mapped_column(String(20), nullable=True, default=None, comment="版本号")
favicon: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="favicon地址")
login_bg: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="登录背景地址")
copyright: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None, comment="版权信息")
keep_record: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None, comment="备案号")
help_doc: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="帮助文档地址")
privacy: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="隐私政策地址")
clause: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="服务条款地址")
git_code: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None, comment="源码地址")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
@validates("name")
def validate_name(self, key: str, name: str) -> str:
@@ -116,35 +74,8 @@ class TenantUserModel(MappedBase):
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_user.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
comment="用户ID",
)
tenant_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("platform_tenant.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
comment="租户ID",
)
role: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="member",
comment="租户内角色(owner:拥有者 admin:管理员 member:成员)",
)
is_default: Mapped[int] = mapped_column(
SmallInteger,
nullable=False,
default=0,
comment="是否默认租户(0:否 1:是)",
)
create_time: Mapped[datetime] = mapped_column(
DateTime,
default=datetime.now,
nullable=False,
comment="创建时间",
)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_user.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="用户ID")
tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("platform_tenant.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="租户ID")
role: Mapped[str] = mapped_column(String(20), nullable=False, default="member", comment="租户内角色(owner:拥有者 admin:管理员 member:成员)")
is_default: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0, comment="是否默认租户(0:否 1:是)")
create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False, comment="创建时间")
@@ -1,9 +1,8 @@
from dataclasses import dataclass
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.common.enums import QueueEnum
from app.core.base_params import BaseQueryParam
from app.core.base_schema import BaseSchema
from app.core.validator import DateTimeStr, email_validator, mobile_validator
@@ -13,7 +12,7 @@ class TenantCreateSchema(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="租户名称")
code: str = Field(..., min_length=2, max_length=100, description="租户编码")
status: int = Field(default=0, ge=0, le=1, description="状态(0:正常 1:用)")
status: int = Field(default=0, ge=0, le=1, description="状态(0:启动 1:用)")
description: str | None = Field(default=None, description="描述")
start_time: DateTimeStr | None = Field(default=None, description="开始时间")
end_time: DateTimeStr | None = Field(default=None, description="结束时间")
@@ -84,7 +83,7 @@ class TenantUpdateSchema(TenantCreateSchema):
name: str | None = Field(default=None, max_length=100, description="租户名称") # type: ignore[assignment]
code: str | None = Field(default=None, max_length=100, description="租户编码") # type: ignore[assignment]
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:正常 1:用)")
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:启动 1:用)")
description: str | None = Field(default=None, description="描述")
start_time: DateTimeStr | None = Field(default=None, description="开始时间")
end_time: DateTimeStr | None = Field(default=None, description="结束时间")
@@ -150,29 +149,21 @@ class TenantOutSchema(TenantCreateSchema, BaseSchema):
model_config = ConfigDict(from_attributes=True)
@dataclass
class TenantQueryParam:
class TenantQueryParam(BaseQueryParam):
"""租户查询参数"""
def __init__(
self,
name: str | None = Query(None, description="租户名称"),
code: str | None = Query(None, description="租户编码"),
status: str | None = Query(None, description="状态"),
created_time: list[DateTimeStr] | None = Query(
None,
description="创建时间范围",
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
),
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
if name:
self.name = (QueueEnum.like.value, name)
if code:
self.code = (QueueEnum.like.value, code)
if status:
self.status = (QueueEnum.eq.value, status)
if created_time and len(created_time) == 2:
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
class TenantUserAddSchema(BaseModel):
@@ -96,9 +96,7 @@ class TenantService:
logger.error(f"为租户[{tenant_obj.name}]创建初始管理员失败: {e!s}")
raise CustomException(msg="创建租户初始管理员失败")
logger.info(
f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username},临时密码: {password}"
)
logger.info(f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username},临时密码: {password}")
await auth.db.refresh(tenant_obj)
result = TenantOutSchema.model_validate(tenant_obj)
@@ -158,10 +156,7 @@ class TenantService:
)
)
await auth.db.flush()
logger.info(
f"租户[{id}]套餐变更:已清理角色中不再可用的菜单关联, "
f"available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}"
)
logger.info(f"租户[{id}]套餐变更:已清理角色中不再可用的菜单关联, available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}")
result = TenantOutSchema.model_validate(updated)
return result
@@ -246,9 +241,7 @@ class TenantService:
return users
@classmethod
async def add_tenant_user_service(
cls, auth: AuthSchema, tenant_id: int, data: TenantUserAddSchema
) -> None:
async def add_tenant_user_service(cls, auth: AuthSchema, tenant_id: int, data: TenantUserAddSchema) -> None:
"""向租户添加用户"""
# 验证租户存在
tenant = await TenantCRUD(auth).get(id=tenant_id)
@@ -279,19 +272,10 @@ class TenantService:
# 如果设为默认租户,先取消其他默认
if data.is_default == 1:
await auth.db.execute(
sa
.update(TenantUserModel)
.where(TenantUserModel.user_id == data.user_id)
.values(is_default=0)
)
await auth.db.execute(sa.update(TenantUserModel).where(TenantUserModel.user_id == data.user_id).values(is_default=0))
elif data.is_default == 0:
# 检查是否是该用户的第一个租户关联
count_result = await auth.db.execute(
select(sa.func.count())
.select_from(TenantUserModel)
.where(TenantUserModel.user_id == data.user_id)
)
count_result = await auth.db.execute(select(sa.func.count()).select_from(TenantUserModel).where(TenantUserModel.user_id == data.user_id))
count = count_result.scalar()
if count == 0:
# 第一个租户自动设为默认
@@ -312,9 +296,7 @@ class TenantService:
logger.info(f"向租户[{tenant.name}]添加用户[{user.username}]成功, role={data.role}")
@classmethod
async def remove_tenant_user_service(
cls, auth: AuthSchema, tenant_id: int, user_id: int
) -> None:
async def remove_tenant_user_service(cls, auth: AuthSchema, tenant_id: int, user_id: int) -> None:
"""从租户移除用户"""
from sqlalchemy import select
@@ -380,11 +362,16 @@ class TenantService:
"package_name": "未绑定套餐",
}
from app.api.v1.module_platform.package.crud import PackageCRUD
pkg = await PackageCRUD(auth).get(id=tenant.package_id)
if not pkg:
return {
"tenant_id": tenant.id, "max_users": 0, "max_roles": 0,
"max_storage_mb": 0, "max_depts": 0, "package_name": "套餐已删除",
"tenant_id": tenant.id,
"max_users": 0,
"max_roles": 0,
"max_storage_mb": 0,
"max_depts": 0,
"package_name": "套餐已删除",
}
return {
"tenant_id": tenant.id,
@@ -412,6 +399,7 @@ class TenantService:
return
from app.api.v1.module_platform.package.crud import PackageCRUD
pkg = await PackageCRUD(auth).get(id=tenant.package_id)
if not pkg:
return
@@ -432,21 +420,36 @@ class TenantService:
if resource_type == "user":
from app.api.v1.module_system.user.model import UserModel
count_stmt = select(func.count()).select_from(UserModel).where(
UserModel.tenant_id == tenant_id,
UserModel.is_deleted.is_(False),
count_stmt = (
select(func.count())
.select_from(UserModel)
.where(
UserModel.tenant_id == tenant_id,
UserModel.is_deleted.is_(False),
)
)
elif resource_type == "role":
from app.api.v1.module_system.role.model import RoleModel
count_stmt = select(func.count()).select_from(RoleModel).where(
RoleModel.tenant_id == tenant_id,
RoleModel.is_deleted.is_(False),
count_stmt = (
select(func.count())
.select_from(RoleModel)
.where(
RoleModel.tenant_id == tenant_id,
RoleModel.is_deleted.is_(False),
)
)
elif resource_type == "dept":
from app.api.v1.module_system.dept.model import DeptModel
count_stmt = select(func.count()).select_from(DeptModel).where(
DeptModel.tenant_id == tenant_id,
DeptModel.is_deleted.is_(False),
count_stmt = (
select(func.count())
.select_from(DeptModel)
.where(
DeptModel.tenant_id == tenant_id,
DeptModel.is_deleted.is_(False),
)
)
elif resource_type == "storage":
# storage 的实际容量校验在文件上传时进行,此处仅检查是否有配额
@@ -457,9 +460,7 @@ class TenantService:
if current_count >= max_limit:
resource_labels = {"user": "用户", "role": "角色", "dept": "部门"}
raise CustomException(
msg=f"租户{resource_labels.get(resource_type, resource_type)}数量已达套餐上限({max_limit}),无法继续创建"
)
raise CustomException(msg=f"租户{resource_labels.get(resource_type, resource_type)}数量已达套餐上限({max_limit}),无法继续创建")
# ============ P1: 租户配置(已合并到主表) ============
@@ -470,10 +471,7 @@ class TenantService:
if not tenant:
raise CustomException(msg="租户不存在")
config_fields = [
"name", "description", "version", "logo_url", "favicon", "login_bg",
"copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"
]
config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"]
config = {field: getattr(tenant, field, None) for field in config_fields}
return config
@@ -481,10 +479,7 @@ class TenantService:
async def get_config_items_service(cls, auth: AuthSchema, tenant_id: int) -> list[TenantConfigOutSchema]:
"""获取租户所有配置(对外接口,返回结构化列表)"""
config = await cls.get_config_service(auth, tenant_id)
return [
TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None)
for k, v in config.items()
]
return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in config.items()]
@classmethod
async def get_config_cache_service(cls, redis: Redis, tenant_id: int) -> dict:
@@ -525,10 +520,7 @@ class TenantService:
async def get_config_cache_items_service(cls, redis: Redis, tenant_id: int) -> list[TenantConfigOutSchema]:
"""获取租户缓存配置(对外接口,返回结构化列表)"""
config = await cls.get_config_cache_service(redis, tenant_id)
return [
TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None)
for k, v in config.items()
]
return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in config.items()]
@classmethod
async def _sync_configs_to_redis(cls, redis: Redis, tenant_id: int, config: dict) -> None:
@@ -544,18 +536,13 @@ class TenantService:
await RedisCURD(redis).delete(redis_key)
@classmethod
async def update_config_service(
cls, auth: AuthSchema, redis: Redis, tenant_id: int, config: dict
) -> list[TenantConfigOutSchema]:
async def update_config_service(cls, auth: AuthSchema, redis: Redis, tenant_id: int, config: dict) -> list[TenantConfigOutSchema]:
"""更新租户配置(同步 Redis 缓存)"""
tenant = await TenantCRUD(auth).get(id=tenant_id)
if not tenant:
raise CustomException(msg="租户不存在")
config_fields = [
"name", "description", "version", "logo_url", "favicon", "login_bg",
"copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"
]
config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"]
for field in config_fields:
if field in config:
@@ -567,10 +554,7 @@ class TenantService:
new_config = await cls.get_config_service(auth, tenant_id)
await cls._sync_configs_to_redis(redis, tenant_id, new_config)
logger.info(f"租户[{tenant_id}]配置已更新")
return [
TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None)
for k, v in new_config.items()
]
return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in new_config.items()]
# ============ P1: 租户菜单 ============
# 租户菜单权限现通过 Package → PackageMenuModel 统一控制
@@ -601,10 +585,7 @@ class TenantService:
tenants = result.scalars().all()
for tenant in tenants:
config_fields = [
"name", "description", "version", "logo_url", "favicon", "login_bg",
"copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"
]
config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"]
config = {field: getattr(tenant, field, None) for field in config_fields}
await cls._sync_configs_to_redis(redis, tenant.id, config)
@@ -636,9 +617,7 @@ class TenantService:
if tenant.status not in (0, 1, 2):
status_labels = {0: "正常", 1: "宽限期", 2: "暂停", 3: "冻结", 4: "过期", 5: "归档"}
current_label = status_labels.get(str(tenant.status), str(tenant.status))
raise CustomException(
msg=f"当前租户状态为「{current_label}」,仅正常/宽限期/暂停状态可续期"
)
raise CustomException(msg=f"当前租户状态为「{current_label}」,仅正常/宽限期/暂停状态可续期")
new_end = datetime.fromisoformat(end_time) if isinstance(end_time, str) else end_time
if new_end <= datetime.now():
@@ -654,9 +633,7 @@ class TenantService:
return TenantOutSchema.model_validate(tenant)
@classmethod
async def package_change_preview_service(
cls, auth: AuthSchema, tenant_id: int, new_package_id: int
) -> PackageChangePreviewOut:
async def package_change_preview_service(cls, auth: AuthSchema, tenant_id: int, new_package_id: int) -> PackageChangePreviewOut:
"""套餐变更影响预览
返回受影响角色、菜单清单、配额对比等,供超管确认后再执行变更。
@@ -689,9 +666,7 @@ class TenantService:
current_menu_ids = await PackageService.get_tenant_available_menu_ids(auth, tenant_id)
# 新套餐可用菜单(直接取套餐菜单,不再包含自定义授权)
new_menu_ids = set(
await PackageService.get_package_menu_ids(auth, new_package_id)
)
new_menu_ids = set(await PackageService.get_package_menu_ids(auth, new_package_id))
final_menu_ids = new_menu_ids # 不再合并租户自定义菜单
# 差异计算
@@ -703,17 +678,11 @@ class TenantService:
if removed_ids:
menu_stmt = select(MenuModel).where(MenuModel.id.in_(removed_ids))
menu_result = await auth.db.execute(menu_stmt)
removed_menus = [
{"id": m.id, "name": m.name, "route_path": m.route_path}
for m in menu_result.scalars().all()
]
removed_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()]
if added_ids:
menu_stmt = select(MenuModel).where(MenuModel.id.in_(added_ids))
menu_result = await auth.db.execute(menu_stmt)
added_menus = [
{"id": m.id, "name": m.name, "route_path": m.route_path}
for m in menu_result.scalars().all()
]
added_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()]
# 受影响角色
role_stmt = select(RoleModel).where(RoleModel.tenant_id == tenant_id)
@@ -724,30 +693,25 @@ class TenantService:
total_affected_users = 0
for role in roles:
# 查该角色下有多少菜单会被移除
role_menu_stmt = select(RoleMenusModel.menu_id).where(
RoleMenusModel.role_id == role.id
)
role_menu_stmt = select(RoleMenusModel.menu_id).where(RoleMenusModel.role_id == role.id)
rm_result = await auth.db.execute(role_menu_stmt)
role_menu_ids = {row[0] for row in rm_result.all()}
affected_menu_count = len(role_menu_ids & removed_ids)
# 查该角色下用户数
user_count_stmt = (
select(func.count())
.select_from(UserModel)
.join(UserModel.roles)
.where(RoleModel.id == role.id)
)
user_count_stmt = select(func.count()).select_from(UserModel).join(UserModel.roles).where(RoleModel.id == role.id)
uc_result = await auth.db.execute(user_count_stmt)
user_count = uc_result.scalar() or 0
affected_roles.append({
"id": role.id,
"name": role.name,
"code": role.code,
"affected_menu_count": affected_menu_count,
"user_count": user_count,
})
affected_roles.append(
{
"id": role.id,
"name": role.name,
"code": role.code,
"affected_menu_count": affected_menu_count,
"user_count": user_count,
}
)
total_affected_users += user_count
# 配额对比(从套餐读取)
@@ -802,11 +766,7 @@ class TenantService:
async with async_db_session() as session:
# 获取所有已过期的活跃租户(status=0)
rows = await session.execute(
text(
"SELECT id, name, contact_email, contact_name, "
"end_time, status FROM platform_tenant "
"WHERE status = '0' AND end_time IS NOT NULL AND end_time < :now"
),
text("SELECT id, name, contact_email, contact_name, end_time, status FROM platform_tenant WHERE status = '0' AND end_time IS NOT NULL AND end_time < :now"),
{"now": now},
)
expired_tenants = rows.fetchall()
@@ -831,16 +791,11 @@ class TenantService:
text("UPDATE platform_tenant SET status = :s WHERE id = :tid"),
{"s": new_status, "tid": tenant_id},
)
logger.info(
f"租户状态切换: id={tenant_id} name={tenant_name} "
f"status={cur_status}{new_status} ({label})"
)
logger.info(f"租户状态切换: id={tenant_id} name={tenant_name} status={cur_status}{new_status} ({label})")
# 发送通知邮件
if email:
await TenantService._send_expiry_email(
tenant_name, contact_name, email, end_time, days_past, label
)
await TenantService._send_expiry_email(tenant_name, contact_name, email, end_time, days_past, label)
await session.commit()
@@ -856,13 +811,7 @@ class TenantService:
from app.core.database import async_db_session
async with async_db_session() as session:
rows = await session.execute(
text(
"SELECT id, name, contact_email, contact_name, end_time "
"FROM platform_tenant "
"WHERE status = '1' AND contact_email IS NOT NULL AND contact_email != ''"
)
)
rows = await session.execute(text("SELECT id, name, contact_email, contact_name, end_time FROM platform_tenant WHERE status = '1' AND contact_email IS NOT NULL AND contact_email != ''"))
grace_tenants = rows.fetchall()
sent = 0
@@ -871,9 +820,7 @@ class TenantService:
days_past = (datetime.now() - end_time).days if end_time else 0
try:
ok = await TenantService._send_renew_email(
name, contact_name, email, days_past
)
ok = await TenantService._send_renew_email(name, contact_name, email, days_past)
if ok:
sent += 1
except Exception as e:
@@ -895,19 +842,13 @@ class TenantService:
async with async_db_session() as session:
# 归档过期租户
result = await session.execute(
text(
"SELECT COUNT(*) FROM platform_tenant "
"WHERE status = '4' AND end_time < :cutoff"
),
text("SELECT COUNT(*) FROM platform_tenant WHERE status = '4' AND end_time < :cutoff"),
{"cutoff": cutoff},
)
count = result.scalar() or 0
if count > 0:
await session.execute(
text(
"UPDATE platform_tenant SET status = '5' "
"WHERE status = '4' AND end_time < :cutoff"
),
text("UPDATE platform_tenant SET status = '5' WHERE status = '4' AND end_time < :cutoff"),
{"cutoff": cutoff},
)
await session.commit()
@@ -963,9 +904,7 @@ class TenantService:
logger.warning(f"到期通知邮件发送失败: tenant={tenant_name}, err={e}")
@staticmethod
async def _send_renew_email(
name: str, contact_name: str | None, email: str, days_past: int
) -> bool:
async def _send_renew_email(name: str, contact_name: str | None, email: str, days_past: int) -> bool:
"""发送续费提醒邮件"""
from app.config.setting import settings
from app.utils.email_util import render_template, send_email