refactor: 统一状态字段类型为字符串并移除租户配额功能

- 将多个模型中的status字段从布尔值改为字符串类型
- 移除租户配额相关字段和功能
- 优化任务调度器中的租户上下文处理
- 清理无用代码和注释
This commit is contained in:
zhangtao
2025-11-28 00:59:38 +08:00
parent 0c86069efa
commit 42fa1944e0
30 changed files with 86 additions and 346 deletions
@@ -211,7 +211,7 @@ async def get_job_log_controller():
"coalesce": i.coalesce,
"max_instances": i.max_instances,
"next_run_time": i.next_run_time,
"state": SchedulerUtil.get_single_job_status(job_id=i.id)
"state": SchedulerUtil.get_single_job_status(job_id=i.id, tenant_id=None)
}
for i in SchedulerUtil.get_all_jobs()
]
@@ -25,7 +25,7 @@ class JobCreateSchema(BaseModel):
start_date: Optional[str] = Field(default=None, description='开始时间')
end_date: Optional[str] = Field(default=None, description='结束时间')
description: Optional[str] = Field(default=None, max_length=255, description='描述')
status: Optional[str] = Field(default=False, description='任务状态:启动,停止')
status: Optional[str] = Field(default='0', description='任务状态:启动,停止')
@field_validator('trigger')
@classmethod
@@ -77,7 +77,7 @@ class JobLogCreateSchema(BaseModel):
job_trigger: Optional[str] = Field(default=None, description='任务触发器')
job_message: Optional[str] = Field(default=None, description='日志信息')
exception_info: Optional[str] = Field(default=None, description='异常信息')
status: Optional[str] = Field(default=False, description='任务状态:正常,失败')
status: Optional[str] = Field(default='0', description='任务状态:正常,失败')
create_time: Optional[DateTimeStr] = Field(default=None, description='创建时间')
@@ -153,7 +153,7 @@ class JobService:
SchedulerUtil().pause_job(job_id=id)
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False)
elif option == 2:
SchedulerUtil().resume_job(job_id=id)
SchedulerUtil().resume_job(job_id=id, tenant_id=obj.tenant_id)
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
elif option == 3:
# 重启任务:先移除再添加,确保使用最新的任务配置
@@ -24,7 +24,7 @@ from app.core.exceptions import CustomException
from app.core.logger import log
from app.utils.cron_util import CronUtil
from app.api.v1.module_application.job.model import JobModel
from app.api.v1.module_application.job.model import JobLogModel, JobModel
# 租户上下文管理器
class TenantContext:
@@ -36,7 +36,7 @@ class TenantContext:
_current_user_id = None
@classmethod
def set(cls, tenant_id: int = None, user_id: int = None):
def set(cls, tenant_id: int | None = None, user_id: int | None = None):
"""设置租户上下文"""
cls._current_tenant_id = tenant_id
cls._current_user_id = user_id
@@ -89,42 +89,6 @@ class SchedulerUtil:
"""
定时任务相关方法
"""
@classmethod
def _save_job_log_async_wrapper(cls, job_log: 'JobLogModel') -> None:
"""
异步保存任务日志的包装器函数
参数:
- job_log (JobLogModel): 任务日志模型对象
"""
import asyncio
from app.core.database import async_db_session
from app.core.logger import log
async def _save_log():
try:
async with async_db_session() as session:
async with session.begin():
# 设置日志的租户ID(如果未设置)
if not job_log.tenant_id and hasattr(job_log, 'job_id'):
# 尝试从job_id中提取租户信息
job_id_str = str(job_log.job_id)
tenant_info = cls._extract_tenant_info(job_id_str)
job_log.tenant_id = tenant_info.get('tenant_id')
session.add(job_log)
await session.commit()
log.info(f"任务日志保存成功: 任务ID={job_log.job_id}, 租户ID={job_log.tenant_id}")
except Exception as e:
log.error(f"任务日志保存失败: {str(e)}")
# 运行异步函数
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(_save_log())
finally:
loop.close()
@classmethod
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
@@ -154,10 +118,9 @@ class SchedulerUtil:
# 从任务ID中提取租户信息
tenant_info = cls._extract_tenant_info(job_id)
tenant_id = tenant_info.get('tenant_id')
original_job_id = tenant_info.get('original_job_id')
# 使用原始任务ID查询任务信息
query_job = cls.get_job(job_id=original_job_id if tenant_id else job_id, tenant_id=tenant_id)
query_job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
if query_job:
query_job_info = query_job.__getstate__()
# 获取任务名称
@@ -175,7 +138,7 @@ class SchedulerUtil:
# 获取任务触发器
job_trigger = str(query_job_info.get('trigger'))
# 构造日志消息
job_message = f"事件类型: {event_type}, 任务ID: {original_job_id if tenant_id else job_id}, 租户ID: {tenant_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 租户ID: {tenant_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
# 创建ORM对象
job_log = JobLogModel(
@@ -190,13 +153,13 @@ class SchedulerUtil:
status=status,
exception_info=exception_info,
create_time=datetime.now(),
job_id=original_job_id if tenant_id else job_id,
job_id=job_id,
tenant_id=tenant_id # 添加租户ID
)
# 使用线程池执行操作以避免阻塞调度器和数据库锁定问题
executor = ThreadPoolExecutor(max_workers=1)
executor.submit(cls._save_job_log_async_wrapper, job_log)
executor.submit(cls._save_job_log_async_wrapper, job_log, tenant_id)
executor.shutdown(wait=False)
log.info(f"任务执行事件: {event_type}, 租户ID: {tenant_id}, 任务ID: {job_id}")
@@ -245,7 +208,7 @@ class SchedulerUtil:
return f"tenant_{tenant_id}_{job_id}"
@classmethod
def _extract_tenant_info(cls, formatted_job_id: str) -> Dict[str, Optional[Union[int, str]]]:
def _extract_tenant_info(cls, formatted_job_id: str) -> Dict[str, Optional[int]]:
"""
从格式化的任务ID中提取租户信息
@@ -260,17 +223,15 @@ class SchedulerUtil:
try:
return {
'tenant_id': int(parts[1]),
'original_job_id': parts[2]
}
except ValueError:
pass
return {
'tenant_id': None,
'original_job_id': formatted_job_id
}
@classmethod
def _wrap_function_with_context(cls, func: Callable, tenant_id: int, user_id: int = None) -> Callable:
def _wrap_function_with_context(cls, func: Callable, tenant_id: int | None = None, user_id: int | None = None) -> Callable:
"""
包装函数在执行前设置租户上下文
@@ -322,19 +283,17 @@ class SchedulerUtil:
auth = AuthSchema(db=session)
job_list = await JobCRUD(auth).get_obj_list_crud()
for item in job_list:
# 获取任务的租户ID(如果存在)
tenant_id = getattr(item, 'tenant_id', None)
# 删除旧任务(使用租户ID进行格式化)
cls.remove_job(job_id=item.id, tenant_id=tenant_id)
cls.remove_job(job_id=item.id, tenant_id=item.tenant_id)
# 添加任务,传入租户ID
cls.add_job(item, tenant_id=tenant_id)
cls.add_job(item, tenant_id=item.tenant_id)
# 根据数据库中保存的状态来设置任务状态
if item.status is False:
# 如果任务状态为暂停,则立即暂停刚添加的任务
cls.pause_job(job_id=item.id, tenant_id=tenant_id)
cls.pause_job(job_id=item.id, tenant_id=item.tenant_id)
# 添加租户隔离的事件监听器,只监听任务执行相关事件
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, EVENT_JOB_MISSED, EVENT_JOB_ADDED, EVENT_JOB_REMOVED
@@ -359,7 +318,7 @@ class SchedulerUtil:
log.error(f'关闭定时任务失败: {str(e)}')
@classmethod
def get_job(cls, job_id: Union[str, int], tenant_id: Optional[int] = None) -> Optional[Job]:
def get_job(cls, job_id: int, tenant_id: Optional[int] = None) -> Optional[Job]:
"""
根据任务ID获取任务对象
@@ -385,7 +344,7 @@ class SchedulerUtil:
return scheduler.get_jobs()
@classmethod
def add_job(cls, job_info: JobModel, tenant_id: Optional[int] = None) -> Job:
def add_job(cls, job_info: JobModel, tenant_id: int | None = None) -> Job:
"""
根据任务配置创建并添加调度任务
@@ -554,7 +513,7 @@ class SchedulerUtil:
scheduler.remove_all_jobs()
@classmethod
def modify_job(cls, job_id: Union[str, int]) -> Job:
def modify_job(cls, job_id: int) -> Job:
"""
更新指定任务的配置运行中的任务下次执行生效
@@ -567,13 +526,13 @@ class SchedulerUtil:
异常:
- CustomException: 当任务不存在时抛出
"""
query_job = cls.get_job(job_id=str(job_id))
query_job = cls.get_job(job_id=job_id)
if not query_job:
raise CustomException(msg=f"未找到该任务:{job_id}")
return scheduler.modify_job(job_id=str(job_id))
@classmethod
def pause_job(cls, job_id: Union[str, int], tenant_id: Optional[int] = None):
def pause_job(cls, job_id: int, tenant_id: int | None = None):
"""
暂停指定任务仅运行中可暂停已终止不可
@@ -595,7 +554,7 @@ class SchedulerUtil:
log.info(f"暂停任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
@classmethod
def resume_job(cls, job_id: Union[str, int], tenant_id: Optional[int] = None):
def resume_job(cls, job_id: int, tenant_id: int | None = None):
"""
恢复指定任务仅暂停中可恢复已终止不可
@@ -617,7 +576,7 @@ class SchedulerUtil:
log.info(f"恢复任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
@classmethod
def reschedule_job(cls, job_id: Union[str, int], tenant_id: Optional[int] = None, trigger=None, **trigger_args) -> Optional[Job]:
def reschedule_job(cls, job_id: int, tenant_id: int, trigger=None, **trigger_args) -> Optional[Job]:
"""
重启指定任务的触发器
@@ -655,7 +614,7 @@ class SchedulerUtil:
return result
@classmethod
def get_single_job_status(cls, job_id: Union[str, int], tenant_id: Optional[int] = None) -> str:
def get_single_job_status(cls, job_id: int, tenant_id: int | None = None) -> str:
"""
获取单个任务的当前状态
@@ -706,7 +665,8 @@ class SchedulerUtil:
return tenant_jobs
# 获取当前租户上下文的辅助函数
def get_current_tenant() -> Dict[str, Optional[int]]:
@classmethod
def get_current_tenant(cls) -> Dict[str, Optional[int]]:
"""
获取当前任务执行的租户上下文
@@ -11,7 +11,7 @@ from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, Custome
class DemoCreateSchema(BaseModel):
"""新增模型"""
name: str = Field(..., min_length=2, max_length=50, description='名称')
status: str = Field(True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@@ -5,14 +5,14 @@ from datetime import datetime
from pydantic import ConfigDict, Field, BaseModel, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
from ..user.schema import UserOutSchema
from ..user.model import UserModel
class AuthSchema(BaseModel):
"""权限认证模型"""
model_config = ConfigDict(arbitrary_types_allowed=True)
user: Optional[UserOutSchema] = Field(default=None, description='用户信息')
user: UserModel | None = Field(default=None, description='用户信息')
check_data_scope: bool = Field(default=True, description='是否检查数据权限')
db: AsyncSession = Field(description='数据库会话')
@@ -220,6 +220,8 @@ class LoginService:
# 用户认证
auth = AuthSchema(db=db)
user = await UserCRUD(auth).get_by_id_crud(id=user_id)
if not user:
raise CustomException(msg="刷新token失败,用户不存在")
# 记录刷新令牌时的租户信息
log.info(f"用户ID: {user.id}, 用户名: {user.username}, 租户ID: {user.tenant_id} 正在刷新JWT令牌")
@@ -12,7 +12,7 @@ class CustomerCreateSchema(BaseModel):
"""新增模型"""
name: str = Field(..., max_length=64, description='客户名称')
code: Optional[str] = Field(default=None, max_length=20, description='客户编码')
status: str = Field(True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@@ -5,6 +5,7 @@ from typing import Any, List, Dict, Optional
from fastapi import UploadFile
import pandas as pd
from app.api.v1.module_system.tenant.crud import TenantCRUD
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
@@ -17,7 +17,7 @@ class DeptCreateSchema(BaseModel):
phone: Optional[str] = Field(default=None, max_length=11, description="手机")
email: Optional[str] = Field(default=None, max_length=64, description="邮箱")
parent_id: Optional[int] = Field(default=None, ge=0, description="父部门ID")
status: str = Field(default=True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="备注说明")
@field_validator('name')
+4 -54
View File
@@ -104,31 +104,6 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS
"""
return await self.set(ids=ids, status=status)
async def batch_create_obj_crud(self, data_list: List[DictTypeCreateSchema]) -> List[DictTypeModel]:
"""
批量创建数据字典类型
参数:
- data_list (List[DictTypeCreateSchema]): 数据字典类型创建模型列表
返回:
- List[DictTypeModel]: 创建的数据字典类型模型列表
"""
return await self.batch_create(data_list=data_list)
async def batch_update_obj_crud(self, ids: List[int], data: DictTypeUpdateSchema) -> int:
"""
批量更新数据字典类型
参数:
- ids (List[int]): 数据字典类型ID列表
- data (DictTypeUpdateSchema): 数据字典类型更新模型
返回:
- int: 更新的记录数量
"""
return await self.batch_update(ids=ids, data=data)
async def batch_delete_obj_crud(self, ids: List[int]) -> int:
"""
批量删除数据字典类型
@@ -239,31 +214,6 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
"""
return await self.set(ids=ids, status=status)
async def batch_create_obj_crud(self, data_list: List[DictDataCreateSchema]) -> List[DictDataModel]:
"""
批量创建数据字典数据
参数:
- data_list (List[DictDataCreateSchema]): 数据字典数据创建模型列表
返回:
- List[DictDataModel]: 创建的数据字典数据模型列表
"""
return await self.batch_create(data_list=data_list)
async def batch_update_obj_crud(self, ids: List[int], data: DictDataUpdateSchema) -> int:
"""
批量更新数据字典数据
参数:
- ids (List[int]): 数据字典数据ID列表
- data (DictDataUpdateSchema): 数据字典数据更新模型
返回:
- int: 更新的记录数量
"""
return await self.batch_update(ids=ids, data=data)
async def batch_delete_obj_crud(self, ids: List[int], exclude_system: bool = True) -> int:
"""
批量删除数据字典数据
@@ -289,19 +239,19 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
await self.delete(ids=ids)
return len(ids)
async def get_obj_list_by_dict_type_crud(self, dict_type: str, status: Optional[str] = True) -> List[DictDataModel]:
async def get_obj_list_by_dict_type_crud(self, dict_type: str, status: Optional[str] = "0") -> Sequence[DictDataModel]:
"""
根据字典类型获取字典数据列表
参数:
- dict_type (str): 字典类型
- status (Optional[bool]): 状态过滤None表示不过滤
- status (Optional[str]): 状态过滤None表示不过滤
返回:
- List[DictDataModel]: 数据字典数据模型列
- Sequence[DictDataModel]: 数据字典数据模型
"""
search = {"dict_type": dict_type}
if status is not None:
search["status"] = status
order_by = [{"field": "sort", "direction": "asc"}]
order_by = [{"id": "asc"}]
return await self.list(search=search, order_by=order_by)
+14 -14
View File
@@ -14,10 +14,10 @@ class DictTypeCreateSchema(BaseModel):
字典类型表对应pydantic模型
"""
dict_name: str = Field(..., min_length=1, max_length=100, description='字典名称', example='用户状态')
dict_type: str = Field(..., min_length=1, max_length=100, description='字典类型', example='sys_user_status')
status: Optional[str] = Field(default=None, description='状态(1正常 0停用)', example=True)
description: Optional[str] = Field(default=None, max_length=255, description="描述", example="用户账号状态管理")
dict_name: str = Field(..., min_length=1, max_length=100, description='字典名称')
dict_type: str = Field(..., min_length=1, max_length=100, description='字典类型')
status: Optional[str] = Field(default=None, description='状态(1正常 0停用)')
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('dict_name')
def validate_dict_name(cls, value: str):
@@ -73,16 +73,16 @@ class DictDataCreateSchema(BaseModel):
"""
字典数据表对应pydantic模型
"""
dict_sort: int = Field(..., ge=1, le=999, description='字典排序', example=1)
dict_label: str = Field(..., max_length=100, description='字典标签', example='正常')
dict_value: str = Field(..., max_length=100, description='字典键值', example='0')
dict_type: str = Field(..., max_length=100, description='字典类型', example='sys_user_status')
dict_type_id: int = Field(..., description='字典类型ID', example=1)
css_class: Optional[str] = Field(default=None, max_length=100, description='样式属性(其他样式扩展)', example='label-success')
list_class: Optional[str] = Field(default=None, description='表格回显样式', example='success')
is_default: Optional[bool] = Field(default=None, description='是否默认(Y是 N否)', example=True)
status: Optional[str] = Field(default=None, description='状态(1正常 0停用)', example=True)
description: Optional[str] = Field(default=None, max_length=255, description="描述", example="正常状态的用户")
dict_sort: int = Field(..., ge=1, le=999, description='字典排序')
dict_label: str = Field(..., max_length=100, description='字典标签')
dict_value: str = Field(..., max_length=100, description='字典键值')
dict_type: str = Field(..., max_length=100, description='字典类型')
dict_type_id: int = Field(..., description='字典类型ID')
css_class: Optional[str] = Field(default=None, max_length=100, description='样式属性(其他样式扩展)')
list_class: Optional[str] = Field(default=None, description='表格回显样式')
is_default: Optional[str] = Field(default=None, description='是否默认(Y是 N否)')
status: Optional[str] = Field(default=None, description='状态(1正常 0停用)')
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@model_validator(mode='after')
def validate_after(self):
@@ -22,7 +22,7 @@ class OperationLogCreateSchema(BaseModel):
response_code: Optional[int] = Field(default=None, description="响应状态码")
response_json: Optional[str] = Field(default=None, description="响应 JSON 数据")
process_time: Optional[str] = Field(default=None, description="处理时间")
status: Optional[str] = Field(default=True, description="是否成功")
status: Optional[str] = Field(default="0", description="是否成功")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
created_id: Optional[int] = Field(default=None, description="创建人ID")
@@ -27,7 +27,7 @@ class MenuCreateSchema(BaseModel):
params: Optional[list[dict[str, str]]] = Field(default=None, description="路由参数,格式为[{key: string, value: string}]")
affix: bool = Field(default=False, description="是否固定标签页(True:是 False:否)")
parent_id: Optional[int] = Field(default=None, ge=1, description="父菜单ID")
status: str = Field(default=True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@model_validator(mode='before')
@@ -14,7 +14,7 @@ class NoticeCreateSchema(BaseModel):
notice_title: str = Field(..., max_length=50, description='公告标题')
notice_type: str = Field(..., description='公告类型(1通知 2公告)')
notice_content: str = Field(..., description='公告内容')
status: str = Field(default=True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator("notice_type")
@@ -30,10 +30,6 @@ class NoticeCreateSchema(BaseModel):
raise ValueError("公告标题不能为空")
if not self.notice_content.strip():
raise ValueError("公告内容不能为空")
# 验证时间范围
if self.start_time and self.end_time:
if self.start_time >= self.end_time:
raise ValueError("开始时间必须早于结束时间")
return self
@@ -14,7 +14,7 @@ class ParamsCreateSchema(BaseModel):
config_key: str = Field(..., max_length=500, description="参数键名")
config_value: Optional[str] = Field(default=None, description="参数键值")
config_type: bool = Field(default=False, description="系统内置(True:是 False:否)")
status: str = Field(default=True, description="状态(True:正常 False:停用)")
status: str = Field(default="0", description="状态(True:正常 False:停用)")
description: Optional[str] = Field(default=None, max_length=500, description="描述")
@@ -12,7 +12,7 @@ class PositionCreateSchema(BaseModel):
"""岗位创建模型"""
name: str = Field(..., max_length=40, description="岗位名称")
order: Optional[int] = Field(default=1, ge=1, description='显示排序')
status: str = Field(default=True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@@ -18,7 +18,7 @@ class RoleCreateSchema(BaseModel):
code: Optional[str] = Field(default=None, max_length=40, description="角色编码")
order: Optional[int] = Field(default=1, ge=1, description='显示排序')
data_scope: Optional[int] = Field(default=1, ge=1, le=5, description='数据权限范围')
status: str = Field(default=True, description="是否启用")
status: str = Field(default="0", description="是否启用")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator("code")
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from sqlalchemy import DateTime, String, Integer, Boolean
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, mapped_column, validates
from app.core.base_model import ModelMixin
@@ -29,10 +29,6 @@ class TenantModel(ModelMixin):
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='结束时间')
# 租户配额相关字段 - 只保留用户数量限制
max_user_count: Mapped[int] = mapped_column(Integer, nullable=False, default=100, comment='最大用户数量限制')
enable_quota_limit: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment='是否启用配额限制')
@validates('name')
def validate_name(self, key: str, name: str) -> str:
"""验证名称不为空"""
@@ -13,15 +13,11 @@ class TenantCreateSchema(BaseModel):
"""新增模型"""
name: str = Field(..., max_length=64, description='租户名称')
code: Optional[str] = Field(default=None, max_length=20, description='租户编码')
status: str = Field(True, description="是否启用(True:启用 False:禁用)")
status: str = Field(default="0", description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述")
start_time: Optional[DateTimeStr] = Field(default=None, description="开始时间")
end_time: Optional[DateTimeStr] = Field(default=None, description="结束时间")
# 租户配额设置 - 只保留用户数量限制
max_user_count: Optional[int] = Field(default=100, ge=0, description='最大用户数量限制')
enable_quota_limit: bool = Field(True, description='是否启用配额限制')
@field_validator('name')
@classmethod
def _validate_name(cls, v: str) -> str:
@@ -48,15 +44,11 @@ class TenantCreateSchema(BaseModel):
class TenantUpdateSchema(TenantCreateSchema):
"""更新模型"""
name: Optional[str] = Field(None, max_length=64, description='租户名称')
code: Optional[str] = Field(None, max_length=20, description='租户编码')
class TenantOutSchema(TenantCreateSchema, BaseSchema):
"""响应模型"""
model_config = ConfigDict(from_attributes=True)
# 租户使用统计字段 - 只保留用户数量统计
current_user_count: Optional[int] = Field(default=0, description='当前用户数量')
class TenantQueryParam:
@@ -8,7 +8,6 @@ from typing import Any, List, Dict, Optional
from fastapi import UploadFile
import pandas as pd
from sqlalchemy import func
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
@@ -26,105 +25,6 @@ class TenantService:
租户管理模块服务层
"""
@classmethod
async def _check_quota_limit(cls, auth: AuthSchema, tenant_id: int, resource_type: str, increment: int = 1) -> bool:
"""
检查租户资源配额限制 - 只检查用户配额
参数:
- auth (AuthSchema): 认证信息
- tenant_id (int): 租户ID
- resource_type (str): 资源类型当前仅支持 'user'
- increment (int): 要增加的资源数量默认为1
返回:
- bool: 是否允许创建
异常:
- CustomException: 当配额不足时抛出
"""
# 系统租户不受配额限制
if tenant_id == 1:
return True
# 只处理用户类型的配额检查
if resource_type != 'user':
return True
# 获取租户信息
tenant = await TenantCRUD(auth).get_by_id_crud(id=tenant_id)
if not tenant:
raise CustomException(msg="租户不存在")
# 如果未启用配额限制,直接返回允许
if not tenant.enable_quota_limit:
return True
# 计算当前用户数量
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.api.v1.module_system.user.model import UserModel
session: AsyncSession = AsyncSession.get(auth.db)
stmt = select(func.count(UserModel.id)).where(
UserModel.tenant_id == tenant_id,
UserModel.is_deleted == False
)
result = await session.execute(stmt)
current_count = result.scalar() or 0
max_limit = tenant.max_user_count
# 检查配额是否足够
if current_count + increment > max_limit:
raise CustomException(
msg=f"租户配额不足!当前用户数量: {current_count}, 最大限制: {max_limit}"
)
return True
@classmethod
async def get_tenant_quota_info(cls, auth: AuthSchema, tenant_id: int) -> Dict:
"""
获取租户配额信息和使用统计 - 只返回用户配额信息
参数:
- auth (AuthSchema): 认证信息
- tenant_id (int): 租户ID
返回:
- Dict: 租户配额信息字典
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.api.v1.module_system.user.model import UserModel
tenant = await TenantCRUD(auth).get_by_id_crud(id=tenant_id)
if not tenant:
raise CustomException(msg="租户不存在")
session: AsyncSession = AsyncSession.get(auth.db)
# 获取用户数量统计
user_stmt = select(func.count(UserModel.id)).where(
UserModel.tenant_id == tenant_id,
UserModel.is_deleted == False
)
user_result = await session.execute(user_stmt)
current_user_count = user_result.scalar() or 0
return {
"tenant_id": tenant.id,
"tenant_name": tenant.name,
"enable_quota_limit": tenant.enable_quota_limit,
"quotas": {
"user": {
"current": current_user_count,
"max": tenant.max_user_count,
"usage_rate": round(current_user_count / tenant.max_user_count * 100, 2) if tenant.max_user_count > 0 else 0
}
}
}
@classmethod
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""
@@ -144,12 +44,6 @@ class TenantService:
# 获取租户详情基础数据
result = TenantOutSchema.model_validate(obj).model_dump()
# 获取租户配额使用统计 - 只更新用户数量
quota_info = await cls.get_tenant_quota_info(auth, id)
result.update({
"current_user_count": quota_info['quotas']['user']['current']
})
return result
@classmethod
@@ -214,19 +108,9 @@ class TenantService:
obj = await TenantCRUD(auth).get(code=data.code)
if obj:
raise CustomException(msg='创建失败,编码已存在')
# 设置默认配额值(如果未提供) - 只设置用户相关配额
create_data = data.model_dump()
default_quotas = {
'max_user_count': 100,
'enable_quota_limit': True
}
for field, default_value in default_quotas.items():
if field not in create_data:
create_data[field] = default_value
# 创建租户
tenant_obj = await TenantCRUD(auth).create_crud(data=create_data)
tenant_obj = await TenantCRUD(auth).create_crud(data=data)
# 自动创建租户初始管理员用户
await cls._create_tenant_admin_user(auth, tenant_obj)
@@ -265,11 +149,8 @@ class TenantService:
"created_id": auth.user.id if auth.user else None
}
# 检查用户配额
await cls._check_quota_limit(auth, tenant_obj.id, 'user', 1)
# 创建用户
new_user = await UserCRUD(auth).create_crud(data=admin_user_data)
new_user = await UserCRUD(auth).create(data=admin_user_data)
# 记录日志,包含临时密码信息(仅开发环境记录,生产环境应避免)
log.info(f"为租户[{tenant_obj.name}]创建初始管理员用户成功,用户名: {username},临时密码: {password}")
@@ -294,17 +175,9 @@ class TenantService:
"""
# 系统租户特殊处理
if id == 1:
# 系统租户只允许修改配额相关设置,不允许修改核心信息
update_data = data.model_dump(exclude_unset=True)
allowed_fields = ['max_user_count', 'enable_quota_limit', 'start_time', 'end_time', 'description']
# 过滤出允许修改的字段
update_data = {k: v for k, v in update_data.items() if k in allowed_fields}
obj = await TenantCRUD(auth).update_crud(id=id, data=data)
log.info(f"系统租户配额设置已更新")
if update_data:
obj = await TenantCRUD(auth).update_crud(id=id, data=update_data)
log.info(f"系统租户配额设置已更新")
else:
obj = await TenantCRUD(auth).get_by_id_crud(id=id)
return TenantOutSchema.model_validate(obj).model_dump()
# 检查数据是否存在
@@ -503,7 +376,7 @@ class TenantService:
processed_names.add(name)
# 处理编码
code = str(row['code']).strip() if pd.notna(row['code']) else f"T{int(time.time())}{random.randint(100, 999)}"
code = str(row['code']).strip()
if code in processed_codes:
error_msgs.append(f"{count}行: 租户编码 '{code}' 在文件中重复")
continue
@@ -514,14 +387,9 @@ class TenantService:
"name": name,
"code": code,
"status": status,
"description": str(row['description']).strip() if pd.notna(row['description']) else "",
"description": str(row['description']).strip(),
}
# 处理时间字段
if pd.notna(row.get('start_time')):
data['start_time'] = row['start_time']
if pd.notna(row.get('end_time')):
data['end_time'] = row['end_time']
# 检查时间有效性
if 'start_time' in data and 'end_time' in data and data['start_time'] > data['end_time']:
@@ -608,7 +476,5 @@ class TenantService:
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list,
sample_data=sample_data,
description=description
option_list=option_list
)
@@ -94,10 +94,11 @@ class UserCreateSchema(CurrentUserUpdateSchema):
username: Optional[str] = Field(default=None, max_length=32, description="用户名")
password: Optional[str] = Field(default=None, max_length=128, description="密码哈希值")
status: str = Field(default=True, description="是否可用")
status: str = Field(default="0", description="是否可用")
description: Optional[str] = Field(default=None, max_length=255, description="备注")
user_type: Optional[str] = Field(default="0", max_length=32, description="用户类型")
is_superuser: Optional[bool] = Field(default=False, description="是否超管")
tenant_id: Optional[int] = Field(default=None, description='租户ID')
dept_id: Optional[int] = Field(default=None, description='部门ID')
role_ids: Optional[List[int]] = Field(default=[], description='角色ID')
position_ids: Optional[List[int]] = Field(default=[], description='岗位ID')
@@ -131,19 +131,6 @@ class UserService:
# 限制用户类型,非系统管理员只能创建普通用户
user_dict["user_type"] = "0"
# 获取要创建用户的租户ID
tenant_id = user_dict.get("tenant_id") or (auth.user.tenant_id if auth.user else None)
# 执行用户配额检查
if tenant_id:
try:
# 检查该租户的用户配额是否足够
await TenantService._check_quota_limit(auth, tenant_id, 'user', 1)
except CustomException as e:
raise e
except Exception as e:
log.error(f"用户配额检查失败: {str(e)}")
# 创建用户
new_user = await UserCRUD(auth).create(data=user_dict)