mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
chore: 批量优化项目代码,修复多处细节问题
本次提交包含多项优化和修复: 1. 修复CRUD初始化参数传递、搜索参数处理逻辑 2. 更新环境配置中的大模型相关参数 3. 重构部分服务方法命名,统一代码风格 4. 新增多个枚举类型,补充模型关联关系和加载选项 5. 优化查询参数类实现,完善字段校验逻辑 6. 调整Pydantic模型字段注释和类型定义 7. 简化并移除冗余的CRUD方法实现 8. 新增超级管理员权限装饰器 9. 修复邮件日志模型的租户关联和字段定义
This commit is contained in:
@@ -425,7 +425,7 @@ async def tenant_register_controller(
|
||||
返回:
|
||||
- TenantRegisterOutSchema: 注册结果,含 tenant_id/user_id/试用到期日
|
||||
"""
|
||||
result = await TenantRegisterService.register(
|
||||
result = await TenantRegisterService.register_service(
|
||||
db=db,
|
||||
username=data.username,
|
||||
password=data.password,
|
||||
|
||||
@@ -323,7 +323,7 @@ async def ensure_oauth_user(
|
||||
role_ids=list(settings.OAUTH_DEFAULT_ROLE_IDS),
|
||||
)
|
||||
try:
|
||||
await UserService.register_user_service(auth=auth, data=reg)
|
||||
await UserService.register_service(auth=auth, data=reg)
|
||||
except Exception:
|
||||
# 并发创建可能触发唯一约束冲突,回退到再次查询
|
||||
existing = await UserCRUD(auth).get(username=username)
|
||||
|
||||
@@ -865,7 +865,7 @@ class TenantRegisterService:
|
||||
DEFAULT_TRIAL_DAYS = 7
|
||||
|
||||
@classmethod
|
||||
async def register(
|
||||
async def register_service(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
username: str,
|
||||
@@ -873,6 +873,22 @@ class TenantRegisterService:
|
||||
email: str,
|
||||
tenant_name: str | None = None,
|
||||
) -> TenantRegisterOutSchema:
|
||||
"""
|
||||
租户自助注册:一次性创建租户 + 管理员 + owner 角色 + 菜单分配。
|
||||
|
||||
参数:
|
||||
- db (AsyncSession): 数据库会话对象。
|
||||
- username (str): 登录账号。
|
||||
- password (str): 登录密码。
|
||||
- email (str): 邮箱。
|
||||
- tenant_name (str | None): 企业/团队名称。
|
||||
|
||||
返回:
|
||||
- TenantRegisterOutSchema: 注册结果。
|
||||
|
||||
异常:
|
||||
- CustomException: 用户名或邮箱已被占用时抛出。
|
||||
"""
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ async def get_dept_tree_controller(
|
||||
- CustomException: 查询部门树失败时抛出异常。
|
||||
"""
|
||||
order_by = [{"order": "asc"}]
|
||||
result_dict_list = await DeptService.get_dept_tree_service(search=search, auth=auth, order_by=order_by)
|
||||
result_dict_list = await DeptService.tree_service(search=search, auth=auth, order_by=order_by)
|
||||
return SuccessResponse(data=result_dict_list, msg="查询部门树成功")
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ async def get_obj_detail_controller(
|
||||
异常:
|
||||
- CustomException: 查询部门详情失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DeptService.get_dept_detail_service(id=id, auth=auth)
|
||||
result_dict = await DeptService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="查询部门详情成功")
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ async def create_obj_controller(
|
||||
异常:
|
||||
- CustomException: 创建部门失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DeptService.create_dept_service(data=data, auth=auth)
|
||||
result_dict = await DeptService.create_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_DEPT_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建部门成功")
|
||||
|
||||
@@ -123,7 +123,7 @@ async def update_obj_controller(
|
||||
异常:
|
||||
- CustomException: 修改部门失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DeptService.update_dept_service(auth=auth, id=id, data=data)
|
||||
result_dict = await DeptService.update_service(auth=auth, id=id, data=data)
|
||||
await FastAPICache.clear(namespace=_DEPT_NS)
|
||||
return SuccessResponse(data=result_dict, msg="修改部门成功")
|
||||
|
||||
@@ -150,7 +150,7 @@ async def delete_obj_controller(
|
||||
异常:
|
||||
- CustomException: 删除部门失败时抛出异常。
|
||||
"""
|
||||
await DeptService.delete_dept_service(ids=ids, auth=auth)
|
||||
await DeptService.delete_service(ids=ids, auth=auth)
|
||||
await FastAPICache.clear(namespace=_DEPT_NS)
|
||||
return SuccessResponse(msg="删除部门成功")
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
@@ -11,37 +9,4 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
|
||||
"""部门模块数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化部门数据层。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(model=DeptModel, auth=auth)
|
||||
|
||||
async def get_tree_list(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[DeptModel]:
|
||||
"""
|
||||
获取部门树形列表。
|
||||
|
||||
参数:
|
||||
- search (dict | None): 搜索条件。
|
||||
- order_by (list[dict] | None): 排序字段列表。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[DeptModel]: 部门树形列表。
|
||||
"""
|
||||
return await self.tree_list(
|
||||
search=search,
|
||||
order_by=order_by,
|
||||
children_attr="children",
|
||||
preload=preload,
|
||||
)
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.common.enums import PermissionFilterStrategy
|
||||
from app.core.base_model import ModelMixin, TenantMixin
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.api.v1.module_system.role.model import RoleModel
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
|
||||
class DeptModel(ModelMixin, TenantMixin):
|
||||
class DeptModel(ModelMixin, TenantMixin, UserMixin):
|
||||
"""
|
||||
部门模型
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_dept"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "code"), {"comment": "部门表"})
|
||||
__loader_options__: list[str] = ["children"]
|
||||
__tree_children_attr__: str = "children"
|
||||
__loader_options__: list[str] = [
|
||||
"children",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_BASED
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
|
||||
|
||||
@@ -47,7 +47,7 @@ class DeptUpdateSchema(DeptCreateSchema):
|
||||
"""部门更新模型"""
|
||||
|
||||
|
||||
class DeptDetailOutSchema(DeptCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
class DeptOutSchema(DeptCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""部门详情响应模型(不含 children,用于详情和更新)"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -55,16 +55,12 @@ class DeptDetailOutSchema(DeptCreateSchema, BaseSchema, UserBySchema, TenantBySc
|
||||
parent_name: str | None = Field(default=None, max_length=64, description="父部门名称")
|
||||
|
||||
|
||||
class DeptTreeOutSchema(DeptDetailOutSchema):
|
||||
class DeptTreeOutSchema(DeptOutSchema):
|
||||
"""部门树形响应模型(含 children,用于树形列表)"""
|
||||
|
||||
children: list["DeptTreeOutSchema"] | None = Field(default=None, description="子部门列表")
|
||||
|
||||
|
||||
# 兼容旧代码的别名(后续可逐步移除)
|
||||
DeptOutSchema = DeptDetailOutSchema
|
||||
|
||||
|
||||
class DeptQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""部门管理查询参数"""
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.utils.common_util import (
|
||||
from .crud import DeptCRUD
|
||||
from .schema import (
|
||||
DeptCreateSchema,
|
||||
DeptDetailOutSchema,
|
||||
DeptOutSchema,
|
||||
DeptQueryParam,
|
||||
DeptTreeOutSchema,
|
||||
DeptUpdateSchema,
|
||||
@@ -19,25 +19,25 @@ from .schema import (
|
||||
|
||||
class DeptService:
|
||||
"""
|
||||
部门管理模块服务层
|
||||
部门管理服务
|
||||
|
||||
提供部门 CRUD、树形结构查询、级联启/禁用、租户配额检查等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptDetailOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> DeptOutSchema:
|
||||
"""
|
||||
获取部门详情。
|
||||
获取部门详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证对象。
|
||||
- id (int): 部门 ID。
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 部门 ID
|
||||
|
||||
返回:
|
||||
- DeptDetailOutSchema: 部门详情对象。
|
||||
- DeptOutSchema: 部门详情响应模型
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get(id=id)
|
||||
if not dept:
|
||||
raise CustomException(msg="部门不存在")
|
||||
dept_out = DeptDetailOutSchema.model_validate(dept)
|
||||
dept = await DeptCRUD(auth).get_or_404(id=id)
|
||||
dept_out = DeptOutSchema.model_validate(dept)
|
||||
if dept.parent_id:
|
||||
parent = await DeptCRUD(auth).get(id=dept.parent_id)
|
||||
if parent:
|
||||
@@ -45,7 +45,7 @@ class DeptService:
|
||||
return dept_out
|
||||
|
||||
@classmethod
|
||||
async def get_dept_tree_service(
|
||||
async def tree_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DeptQueryParam | None = None,
|
||||
@@ -63,14 +63,14 @@ class DeptService:
|
||||
- list[dict]: 部门树形列表对象。
|
||||
"""
|
||||
# 使用树形结构查询,预加载children关系
|
||||
dept_list = await DeptCRUD(auth).get_tree_list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
dept_list = await DeptCRUD(auth).tree_list(search=vars(search) if search else None, order_by=order_by)
|
||||
# 转换为字典列表(使用树形 Schema),tree_list 已通过 selectin 预加载 children
|
||||
dept_dict_list = [DeptTreeOutSchema.model_validate(dept).model_dump() for dept in dept_list]
|
||||
# 仅保留根节点,子树已在 model_dump 中递归序列化
|
||||
return [d for d in dept_dict_list if d.get("parent_id") is None]
|
||||
|
||||
@classmethod
|
||||
async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptDetailOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptOutSchema:
|
||||
"""
|
||||
创建部门。
|
||||
|
||||
@@ -79,14 +79,14 @@ class DeptService:
|
||||
- data (DeptCreateSchema): 部门创建对象。
|
||||
|
||||
返回:
|
||||
- DeptDetailOutSchema: 新创建的部门对象。
|
||||
- DeptOutSchema: 新创建的部门对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当部门已存在时抛出。
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get(name=data.name)
|
||||
if dept:
|
||||
raise CustomException(msg="创建失败,该部门已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
obj = await DeptCRUD(auth).get(code=data.code)
|
||||
if obj:
|
||||
raise CustomException(msg="创建失败,编码已存在")
|
||||
@@ -97,10 +97,10 @@ class DeptService:
|
||||
await TenantService.check_quota_service(auth, auth.tenant_id, "dept")
|
||||
|
||||
dept = await DeptCRUD(auth).create(data=data)
|
||||
return DeptDetailOutSchema.model_validate(dept)
|
||||
return DeptOutSchema.model_validate(dept)
|
||||
|
||||
@classmethod
|
||||
async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptDetailOutSchema:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptOutSchema:
|
||||
"""
|
||||
更新部门。
|
||||
|
||||
@@ -110,23 +110,21 @@ class DeptService:
|
||||
- data (DeptUpdateSchema): 部门更新对象。
|
||||
|
||||
返回:
|
||||
- DeptDetailOutSchema: 更新后的部门对象。
|
||||
- DeptOutSchema: 更新后的部门对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当部门不存在或名称重复时抛出。
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get(id=id)
|
||||
if not dept:
|
||||
raise CustomException(msg="更新失败,该部门不存在")
|
||||
dept = await DeptCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
exist_dept = await DeptCRUD(auth).get(name=data.name)
|
||||
if exist_dept and exist_dept.id != id:
|
||||
raise CustomException(msg="更新失败,部门名称重复")
|
||||
raise CustomException(msg="更新失败,名称已存在")
|
||||
exist_code = await DeptCRUD(auth).get(code=data.code)
|
||||
if exist_code and exist_code.id != id:
|
||||
raise CustomException(msg="更新失败,部门编码已存在")
|
||||
raise CustomException(msg="更新失败,编码已存在")
|
||||
|
||||
dept = await DeptCRUD(auth).update(id=id, data=data)
|
||||
dept_out = DeptDetailOutSchema.model_validate(dept)
|
||||
dept_out = DeptOutSchema.model_validate(dept)
|
||||
if dept_out.parent_id:
|
||||
parent = await DeptCRUD(auth).get(id=dept_out.parent_id)
|
||||
if parent:
|
||||
@@ -134,7 +132,7 @@ class DeptService:
|
||||
return dept_out
|
||||
|
||||
@classmethod
|
||||
async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除部门。
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ async def get_type_detail_controller(
|
||||
异常:
|
||||
- CustomException: 获取字典类型详情失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictTypeService.get_obj_detail_service(id=id, auth=auth)
|
||||
result_dict = await DictTypeService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取字典类型详情成功")
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ async def get_type_list_controller(
|
||||
异常:
|
||||
- CustomException: 查询字典类型失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictTypeService.get_obj_page_service(
|
||||
result_dict = await DictTypeService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -111,7 +111,7 @@ async def get_type_optionselect_controller(
|
||||
异常:
|
||||
- CustomException: 获取字典类型列表失败时抛出异常。
|
||||
"""
|
||||
result_dict_list = await DictTypeService.get_obj_list_service(auth=auth)
|
||||
result_dict_list = await DictTypeService.list_service(auth=auth)
|
||||
return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功")
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ async def create_type_controller(
|
||||
异常:
|
||||
- CustomException: 创建字典类型失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictTypeService.create_obj_service(auth=auth, redis=redis, data=data)
|
||||
result_dict = await DictTypeService.create_service(auth=auth, redis=redis, data=data)
|
||||
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建字典类型成功")
|
||||
|
||||
@@ -170,7 +170,7 @@ async def update_type_controller(
|
||||
异常:
|
||||
- CustomException: 修改字典类型失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictTypeService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
|
||||
result_dict = await DictTypeService.update_service(auth=auth, redis=redis, id=id, data=data)
|
||||
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="修改字典类型成功")
|
||||
|
||||
@@ -199,7 +199,7 @@ async def delete_type_controller(
|
||||
异常:
|
||||
- CustomException: 删除字典类型失败时抛出异常。
|
||||
"""
|
||||
await DictTypeService.delete_obj_service(auth=auth, redis=redis, ids=ids)
|
||||
await DictTypeService.delete_service(auth=auth, redis=redis, ids=ids)
|
||||
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
|
||||
return SuccessResponse(msg="删除字典类型成功")
|
||||
|
||||
@@ -226,7 +226,7 @@ async def batch_set_available_dict_type_controller(
|
||||
异常:
|
||||
- CustomException: 批量修改字典类型状态失败时抛出异常。
|
||||
"""
|
||||
await DictTypeService.set_obj_available_service(auth=auth, data=data)
|
||||
await DictTypeService.set_available_service(auth=auth, data=data)
|
||||
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
|
||||
return SuccessResponse(msg="批量修改字典类型状态成功")
|
||||
|
||||
@@ -254,9 +254,9 @@ async def export_type_list_controller(
|
||||
- CustomException: 导出字典类型失败时抛出异常。
|
||||
"""
|
||||
# 获取全量数据并转为dict列表
|
||||
result_dict_list = await DictTypeService.get_obj_list_service(search=search, auth=auth)
|
||||
result_dict_list = await DictTypeService.list_service(search=search, auth=auth)
|
||||
export_data = [item.model_dump() for item in result_dict_list]
|
||||
export_result = await DictTypeService.export_obj_service(data_list=export_data)
|
||||
export_result = await DictTypeService.export_service(data_list=export_data)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
@@ -287,7 +287,7 @@ async def get_data_detail_controller(
|
||||
异常:
|
||||
- CustomException: 获取字典数据详情失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictDataService.get_obj_detail_service(id=id, auth=auth)
|
||||
result_dict = await DictDataService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取字典数据详情成功")
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ async def get_data_list_controller(
|
||||
order_by = [{"order": "asc"}]
|
||||
if page.order_by:
|
||||
order_by = page.order_by
|
||||
result_dict = await DictDataService.get_obj_page_service(
|
||||
result_dict = await DictDataService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -352,7 +352,7 @@ async def create_data_controller(
|
||||
异常:
|
||||
- CustomException: 创建字典数据失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictDataService.create_obj_service(auth=auth, redis=redis, data=data)
|
||||
result_dict = await DictDataService.create_service(auth=auth, redis=redis, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建字典数据成功")
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ async def update_data_controller(
|
||||
异常:
|
||||
- CustomException: 修改字典数据失败时抛出异常。
|
||||
"""
|
||||
result_dict = await DictDataService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
|
||||
result_dict = await DictDataService.update_service(auth=auth, redis=redis, id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="修改字典数据成功")
|
||||
|
||||
|
||||
@@ -410,7 +410,7 @@ async def delete_data_controller(
|
||||
异常:
|
||||
- CustomException: 删除字典数据失败时抛出异常。
|
||||
"""
|
||||
await DictDataService.delete_obj_service(auth=auth, redis=redis, ids=ids)
|
||||
await DictDataService.delete_service(auth=auth, redis=redis, ids=ids)
|
||||
return SuccessResponse(msg="删除字典数据成功")
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ async def batch_set_available_dict_data_controller(
|
||||
异常:
|
||||
- CustomException: 批量修改字典数据状态失败时抛出异常。
|
||||
"""
|
||||
await DictDataService.set_obj_available_service(auth=auth, data=data)
|
||||
await DictDataService.set_available_service(auth=auth, data=data)
|
||||
return SuccessResponse(msg="批量修改字典数据状态成功")
|
||||
|
||||
|
||||
@@ -464,9 +464,9 @@ async def export_data_list_controller(
|
||||
异常:
|
||||
- CustomException: 导出字典数据失败时抛出异常。
|
||||
"""
|
||||
result_dict_list = await DictDataService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict_list = await DictDataService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
export_data = [item.model_dump() for item in result_dict_list]
|
||||
export_result = await DictDataService.export_obj_service(data_list=export_data)
|
||||
export_result = await DictDataService.export_service(data_list=export_data)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
@@ -494,6 +494,6 @@ async def get_init_dict_data_controller(dict_type: str, redis: Annotated[Redis,
|
||||
异常:
|
||||
- CustomException: 根据字典类型获取数据失败时抛出异常。
|
||||
"""
|
||||
dict_data_query_result = await DictDataService.get_init_dict_service(redis=redis, dict_type=dict_type, tenant_id=1)
|
||||
dict_data_query_result = await DictDataService.get_init_cache_service(redis=redis, dict_type=dict_type, tenant_id=1)
|
||||
|
||||
return SuccessResponse(data=dict_data_query_result, msg="获取初始化字典数据成功")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin
|
||||
@@ -14,14 +14,13 @@ class DictTypeModel(ModelMixin, TenantMixin):
|
||||
|
||||
__tablename__: str = "sys_dict_type"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "dict_type"), {"comment": "字典类型表"})
|
||||
__loader_options__: list[str] = []
|
||||
__loader_options__: list[str] = ["dict_data_list"]
|
||||
__platform_data_shared__: bool = True
|
||||
|
||||
dict_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="字典名称")
|
||||
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, 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="备注")
|
||||
# 关系定义
|
||||
dict_data_list: Mapped[list["DictDataModel"]] = relationship(
|
||||
"DictDataModel",
|
||||
back_populates="dict_type_obj",
|
||||
@@ -42,7 +41,7 @@ class DictDataModel(ModelMixin, TenantMixin):
|
||||
UniqueConstraint("tenant_id", "dict_type_id", "dict_value", name="uq_dict_data_value"),
|
||||
{"comment": "字典数据表"},
|
||||
)
|
||||
__loader_options__: list[str] = []
|
||||
__loader_options__: list[str] = ["dict_type_obj"]
|
||||
__platform_data_shared__: bool = True
|
||||
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import (
|
||||
@@ -83,6 +84,7 @@ class DictTypeOutSchema(DictTypeCreateSchema, BaseSchema, UserBySchema, TenantBy
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DictTypeQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""字典类型查询参数"""
|
||||
|
||||
@@ -94,8 +96,10 @@ class DictTypeQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.dict_name = (QueueEnum.like.value, dict_name)
|
||||
self.dict_type = (QueueEnum.eq.value, dict_type)
|
||||
if dict_name:
|
||||
self.dict_name = (QueueEnum.like.value, dict_name)
|
||||
if dict_type:
|
||||
self.dict_type = (QueueEnum.eq.value, dict_type)
|
||||
|
||||
|
||||
class DictDataCreateSchema(BaseModel):
|
||||
@@ -159,6 +163,7 @@ class DictDataOutSchema(DictDataCreateSchema, BaseSchema, UserBySchema, TenantBy
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DictDataQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""字典数据查询参数"""
|
||||
|
||||
@@ -171,6 +176,9 @@ class DictDataQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.dict_label = (QueueEnum.like.value, dict_label)
|
||||
self.dict_type = (QueueEnum.eq.value, dict_type)
|
||||
self.dict_type_id = (QueueEnum.eq.value, dict_type_id)
|
||||
if dict_label:
|
||||
self.dict_label = (QueueEnum.like.value, dict_label)
|
||||
if dict_type:
|
||||
self.dict_type = (QueueEnum.eq.value, dict_type)
|
||||
if dict_type_id is not None:
|
||||
self.dict_type_id = (QueueEnum.eq.value, dict_type_id)
|
||||
|
||||
@@ -25,11 +25,13 @@ from .schema import (
|
||||
|
||||
class DictTypeService:
|
||||
"""
|
||||
字典类型管理模块服务层
|
||||
字典类型管理服务
|
||||
|
||||
提供字典类型 CRUD、Redis 缓存同步、字典数据联动更新、批量启/禁用、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> DictTypeOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> DictTypeOutSchema:
|
||||
"""
|
||||
获取数据字典类型详情
|
||||
|
||||
@@ -38,15 +40,12 @@ class DictTypeService:
|
||||
- id (int): 数据字典类型ID
|
||||
|
||||
返回:
|
||||
- dict: 数据字典类型详情字典
|
||||
- DictTypeOutSchema: 字典类型响应模型
|
||||
"""
|
||||
obj = await DictTypeCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="字典类型不存在")
|
||||
return DictTypeOutSchema.model_validate(obj)
|
||||
return await DictTypeCRUD(auth).get_or_404(id=id, out_schema=DictTypeOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_obj_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DictTypeQueryParam | None = None,
|
||||
@@ -61,13 +60,13 @@ class DictTypeService:
|
||||
- order_by (list[dict] | None): 排序字段列表
|
||||
|
||||
返回:
|
||||
- list[DictTypeOutSchema]: 数据字典类型
|
||||
- list[DictTypeOutSchema]: 字典类型响应模型列表
|
||||
"""
|
||||
obj_list = await DictTypeCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
obj_list = await DictTypeCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [DictTypeOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def get_obj_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -93,12 +92,12 @@ class DictTypeService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=DictTypeOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: DictTypeCreateSchema) -> DictTypeOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, redis: Redis, data: DictTypeCreateSchema) -> DictTypeOutSchema:
|
||||
"""
|
||||
创建数据字典类型
|
||||
|
||||
@@ -108,11 +107,11 @@ class DictTypeService:
|
||||
- data (DictTypeCreateSchema): 数据字典类型创建模型
|
||||
|
||||
返回:
|
||||
- dict: 数据字典类型详情字典
|
||||
- DictTypeOutSchema: 字典类型响应模型
|
||||
"""
|
||||
exist_obj = await DictTypeCRUD(auth).get(dict_name=data.dict_name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg="创建失败,该数据字典类型已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
obj = await DictTypeCRUD(auth).create(data=data)
|
||||
|
||||
new_obj_dict = DictTypeOutSchema.model_validate(obj)
|
||||
@@ -128,12 +127,12 @@ class DictTypeService:
|
||||
logger.info(f"创建字典类型成功: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建字典类型失败: {e}")
|
||||
raise CustomException(msg=f"创建字典类型失败 {e}")
|
||||
raise CustomException(msg="同步字典类型缓存失败") from e
|
||||
|
||||
return new_obj_dict
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(
|
||||
async def update_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
redis: Redis,
|
||||
@@ -150,11 +149,9 @@ class DictTypeService:
|
||||
- data (DictTypeUpdateSchema): 数据字典类型更新模型
|
||||
|
||||
返回:
|
||||
- dict: 数据字典类型详情字典
|
||||
- DictTypeOutSchema: 字典类型响应模型
|
||||
"""
|
||||
exist_obj = await DictTypeCRUD(auth).get(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该数据字典类型不存在")
|
||||
exist_obj = await DictTypeCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
if exist_obj.dict_name != data.dict_name:
|
||||
raise CustomException(msg="更新失败,数据字典类型名称不可以修改")
|
||||
|
||||
@@ -197,12 +194,12 @@ class DictTypeService:
|
||||
logger.info(f"更新字典类型成功并刷新缓存: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新字典类型缓存失败: {e}")
|
||||
raise CustomException(msg=f"更新字典类型缓存失败 {e}")
|
||||
raise CustomException(msg="同步字典类型缓存失败") from e
|
||||
|
||||
return new_obj_dict
|
||||
|
||||
@classmethod
|
||||
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
"""
|
||||
删除数据字典类型
|
||||
|
||||
@@ -220,7 +217,7 @@ class DictTypeService:
|
||||
existing_map = {obj.id: obj for obj in existing}
|
||||
for nid in ids:
|
||||
if nid not in existing_map:
|
||||
raise CustomException(msg="删除失败,该数据字典类型不存在")
|
||||
raise CustomException(msg="删除失败,该数据不存在")
|
||||
exist_obj = existing_map[nid]
|
||||
# 检查是否有字典数据
|
||||
exist_obj_type_list = await DictDataCRUD(auth).list(search={"dict_type": exist_obj.dict_type})
|
||||
@@ -234,11 +231,11 @@ class DictTypeService:
|
||||
logger.info(f"删除字典类型成功: {nid}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除字典类型失败: {e}")
|
||||
raise CustomException(msg="删除字典类型失败")
|
||||
raise CustomException(msg="同步删除字典缓存失败") from e
|
||||
await DictTypeCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_obj_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
设置数据字典类型状态
|
||||
|
||||
@@ -252,7 +249,7 @@ class DictTypeService:
|
||||
await DictTypeCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
|
||||
async def export_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出数据字典类型列表
|
||||
|
||||
@@ -285,11 +282,13 @@ class DictTypeService:
|
||||
|
||||
class DictDataService:
|
||||
"""
|
||||
字典数据管理模块服务层
|
||||
字典数据管理服务
|
||||
|
||||
提供字典数据 CRUD、Redis 缓存同步、初始化字典、批量启/禁用、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> DictDataOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> DictDataOutSchema:
|
||||
"""
|
||||
获取数据字典数据详情
|
||||
|
||||
@@ -298,15 +297,12 @@ class DictDataService:
|
||||
- id (int): 数据字典数据ID
|
||||
|
||||
返回:
|
||||
- dict: 数据字典数据详情字典
|
||||
- DictDataOutSchema: 字典数据响应模型
|
||||
"""
|
||||
obj = await DictDataCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="字典数据不存在")
|
||||
return DictDataOutSchema.model_validate(obj)
|
||||
return await DictDataCRUD(auth).get_or_404(id=id, out_schema=DictDataOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_obj_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DictDataQueryParam | None = None,
|
||||
@@ -321,13 +317,13 @@ class DictDataService:
|
||||
- order_by (list[dict] | None): 排序字段列表
|
||||
|
||||
返回:
|
||||
- list[DictDataOutSchema]: 数据字典数据
|
||||
- list[DictDataOutSchema]: 字典数据响应模型列表
|
||||
"""
|
||||
obj_list = await DictDataCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
obj_list = await DictDataCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [DictDataOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def get_obj_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -353,12 +349,12 @@ class DictDataService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=DictDataOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def init_dict_service(cls, redis: Redis) -> None:
|
||||
async def init_cache_service(cls, redis: Redis) -> None:
|
||||
"""
|
||||
应用初始化: 获取所有字典类型对应的字典数据信息并按租户缓存。
|
||||
|
||||
@@ -395,10 +391,10 @@ class DictDataService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"字典初始化过程发生错误: {e}")
|
||||
raise CustomException(msg=f"字典数据初始化失败: {e!s}")
|
||||
raise CustomException(msg="字典数据初始化失败") from e
|
||||
|
||||
@classmethod
|
||||
async def get_init_dict_service(cls, redis: Redis, dict_type: str, tenant_id: int = 1) -> list[dict]:
|
||||
async def get_init_cache_service(cls, redis: Redis, dict_type: str, tenant_id: int = 1) -> list[dict]:
|
||||
"""
|
||||
从缓存获取字典数据列表信息
|
||||
|
||||
@@ -423,26 +419,26 @@ class DictDataService:
|
||||
elif isinstance(obj_list_dict, list):
|
||||
return obj_list_dict
|
||||
|
||||
await cls.init_dict_service(redis)
|
||||
await cls.init_cache_service(redis)
|
||||
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{tenant_id}:{dict_type}"
|
||||
obj_list_dict = await RedisCURD(redis).get(redis_key)
|
||||
if not obj_list_dict:
|
||||
raise CustomException(msg="数据字典不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
|
||||
if isinstance(obj_list_dict, str):
|
||||
try:
|
||||
return json.loads(obj_list_dict)
|
||||
except json.JSONDecodeError:
|
||||
raise CustomException(msg="字典数据格式错误")
|
||||
raise CustomException(msg="字典数据格式错误") from None
|
||||
return obj_list_dict
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取字典缓存失败: {e!s}")
|
||||
raise CustomException(msg=f"获取字典数据失败: {e!s}")
|
||||
raise CustomException(msg="获取字典数据失败") from e
|
||||
|
||||
@classmethod
|
||||
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema) -> DictDataOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema) -> DictDataOutSchema:
|
||||
"""
|
||||
创建数据字典数据
|
||||
|
||||
@@ -452,7 +448,7 @@ class DictDataService:
|
||||
- data (DictDataCreateSchema): 数据字典数据创建模型
|
||||
|
||||
返回:
|
||||
- dict: 数据字典数据详情字典
|
||||
- DictDataOutSchema: 字典数据响应模型
|
||||
"""
|
||||
# 检查相同字典类型下dict_label是否已存在
|
||||
exist_label_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_label=data.dict_label)
|
||||
@@ -481,12 +477,12 @@ class DictDataService:
|
||||
logger.info(f"创建字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建字典数据写入缓存失败: {e}")
|
||||
raise CustomException(msg=f"创建字典数据失败 {e}")
|
||||
raise CustomException(msg="同步字典数据缓存失败") from e
|
||||
|
||||
return DictDataOutSchema.model_validate(obj)
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(
|
||||
async def update_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
redis: Redis,
|
||||
@@ -503,11 +499,9 @@ class DictDataService:
|
||||
- data (DictDataUpdateSchema): 数据字典数据更新模型
|
||||
|
||||
返回:
|
||||
- Dict: 数据字典数据详情字典
|
||||
- DictDataOutSchema: 字典数据响应模型
|
||||
"""
|
||||
exist_obj = await DictDataCRUD(auth).get(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该字典数据不存在")
|
||||
exist_obj = await DictDataCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
|
||||
# 检查相同字典类型下dict_label是否已存在(排除当前记录)
|
||||
if exist_obj.dict_label != data.dict_label:
|
||||
@@ -554,12 +548,12 @@ class DictDataService:
|
||||
logger.info(f"更新字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新字典数据写入缓存失败: {e}")
|
||||
raise CustomException(msg=f"更新字典数据失败 {e}")
|
||||
raise CustomException(msg="同步字典数据缓存失败") from e
|
||||
|
||||
return DictDataOutSchema.model_validate(obj)
|
||||
|
||||
@classmethod
|
||||
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
"""
|
||||
删除数据字典数据
|
||||
|
||||
@@ -608,10 +602,10 @@ class DictDataService:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"删除字典数据失败: {e!s}")
|
||||
raise CustomException(msg=f"删除字典数据失败: {e!s}")
|
||||
raise CustomException(msg="删除字典数据失败") from e
|
||||
|
||||
@classmethod
|
||||
async def set_obj_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量修改数据字典数据状态
|
||||
|
||||
@@ -625,7 +619,7 @@ class DictDataService:
|
||||
await DictDataCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
|
||||
async def export_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出数据字典数据列表
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["日
|
||||
summary="获取登录日志详情",
|
||||
response_model=ResponseSchema[LoginLogDetailOutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
async def get_log_detail_controller(
|
||||
id: Annotated[int, Path(description="登录日志ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:query"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -52,7 +52,7 @@ async def get_obj_detail_controller(
|
||||
summary="查询登录日志列表",
|
||||
response_model=ResponseSchema[PageResultSchema[LoginLogOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
async def get_log_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[LoginLogQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:query"]))],
|
||||
@@ -83,7 +83,7 @@ async def get_obj_list_controller(
|
||||
summary="创建登录日志",
|
||||
response_model=ResponseSchema[LoginLogDetailOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
async def create_log_controller(
|
||||
data: LoginLogCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
@@ -106,7 +106,7 @@ async def create_obj_controller(
|
||||
summary="删除登录日志",
|
||||
response_model=ResponseSchema,
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
async def delete_log_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:delete"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -130,7 +130,7 @@ async def delete_obj_controller(
|
||||
response_model=ResponseSchema[OperationLogDetailOutSchema],
|
||||
dependencies=[Depends(AuthPermission(["module_system:log:query"]))],
|
||||
)
|
||||
async def detail(
|
||||
async def get_operation_log_detail_controller(
|
||||
*,
|
||||
id: Annotated[int, Path(gt=0)],
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
@@ -145,7 +145,7 @@ async def detail(
|
||||
返回:
|
||||
- JSONResponse: 包含操作日志详情的 JSON 响应。
|
||||
"""
|
||||
result_dict = await OperationLogService.detail_service(auth, id)
|
||||
result_dict = await OperationLogService.detail_service(auth=auth, id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取操作日志详情成功")
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ async def list(
|
||||
summary="创建操作日志",
|
||||
response_model=ResponseSchema[OperationLogDetailOutSchema],
|
||||
)
|
||||
async def create(
|
||||
async def create_operation_log_controller(
|
||||
*,
|
||||
data: OperationLogCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
@@ -202,7 +202,7 @@ async def create(
|
||||
返回:
|
||||
- JSONResponse: 包含创建后的操作日志详情的 JSON 响应。
|
||||
"""
|
||||
result_dict = await OperationLogService.create_service(auth, data)
|
||||
result_dict = await OperationLogService.create_service(auth=auth, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建操作日志成功")
|
||||
|
||||
|
||||
@@ -227,5 +227,5 @@ async def delete(
|
||||
返回:
|
||||
- JSONResponse: 删除结果。
|
||||
"""
|
||||
await OperationLogService.delete_service(auth, data.ids)
|
||||
await OperationLogService.delete_service(auth=auth, ids=data.ids)
|
||||
return SuccessResponse(msg="删除操作日志成功")
|
||||
|
||||
@@ -16,4 +16,4 @@ class OperationLogCRUD(CRUDBase[OperationLogModel, None, None]):
|
||||
"""操作日志 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema):
|
||||
super().__init__(OperationLogModel, auth)
|
||||
super().__init__(model=OperationLogModel, auth=auth)
|
||||
|
||||
@@ -29,6 +29,7 @@ class LoginLogModel(ModelMixin, TenantMixin, UserMixin):
|
||||
|
||||
__tablename__: str = "sys_login_log"
|
||||
__table_args__: dict[str, str] = {"comment": "登录日志表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by", "tenant_by"]
|
||||
|
||||
status: Mapped[int] = mapped_column(Integer, default=1, comment="登录状态(1成功 2失败)", index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
@@ -47,7 +48,7 @@ class OperationLogModel(ModelMixin, TenantMixin, UserMixin):
|
||||
|
||||
__tablename__: str = "sys_operation_log"
|
||||
__table_args__: dict[str, str] = {"comment": "操作日志表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by"]
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by", "tenant_by"]
|
||||
|
||||
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="备注")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@@ -47,6 +49,7 @@ class LoginLogDetailOutSchema(LoginLogOutSchema):
|
||||
"""登录日志详情响应"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginLogQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""登录日志查询参数"""
|
||||
|
||||
@@ -61,33 +64,45 @@ class LoginLogQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
self.username = (QueueEnum.like.value, username)
|
||||
|
||||
|
||||
class OperationLogQueryParam(BaseModel):
|
||||
request_path: str | None = Field(None, max_length=255, description="请求路径")
|
||||
request_method: str | None = Field(None, description="请求方式")
|
||||
username: str | None = Field(None, max_length=64, description="用户名")
|
||||
@dataclass
|
||||
class OperationLogQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""操作日志查询参数"""
|
||||
|
||||
@field_validator("request_method")
|
||||
@classmethod
|
||||
def validate_request_method(cls, value: str | None) -> str | None:
|
||||
if value and value.upper() not in ALLOWED_REQUEST_METHODS:
|
||||
raise ValueError(f"请求方式必须是: {', '.join(ALLOWED_REQUEST_METHODS)}")
|
||||
return value.upper() if value else None
|
||||
def __init__(
|
||||
self,
|
||||
request_path: str | None = Query(None, description="请求路径"),
|
||||
request_method: str | None = Query(None, description="请求方式"),
|
||||
username: str | None = Query(None, description="用户名"),
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if request_path:
|
||||
self.request_path = (QueueEnum.like.value, request_path)
|
||||
if request_method:
|
||||
self.request_method = (QueueEnum.eq.value, request_method)
|
||||
if username:
|
||||
self.username = (QueueEnum.like.value, username)
|
||||
|
||||
|
||||
class OperationLogOutSchema(BaseSchema):
|
||||
class OperationLogOutSchema(BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""操作日志响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
tenant_id: int
|
||||
request_path: str
|
||||
request_method: str
|
||||
response_code: int
|
||||
process_time: str | None = None
|
||||
status: int | None = Field(default=None, description="状态(0:启动 1:停用)")
|
||||
description: str | None = Field(default=None, description="描述")
|
||||
request_path: str = Field(..., description="请求路径")
|
||||
request_method: str = Field(..., description="请求方式")
|
||||
response_code: int = Field(..., description="响应状态码")
|
||||
process_time: str | None = Field(default=None, description="处理时间")
|
||||
|
||||
|
||||
class OperationLogDetailOutSchema(OperationLogOutSchema):
|
||||
request_payload: str | None = None
|
||||
response_json: str | None = None
|
||||
"""操作日志详情响应模型"""
|
||||
|
||||
request_payload: str | None = Field(default=None, description="请求体")
|
||||
response_json: str | None = Field(default=None, description="响应体")
|
||||
|
||||
|
||||
class OperationLogCreateSchema(BaseModel):
|
||||
@@ -97,6 +112,9 @@ class OperationLogCreateSchema(BaseModel):
|
||||
response_code: int = Field(200, ge=100, le=599, description="响应状态码")
|
||||
response_json: str | None = Field(None, description="响应体")
|
||||
process_time: str | None = Field(None, max_length=20, description="处理时间")
|
||||
created_id: int | None = Field(None, description="创建人ID")
|
||||
updated_id: int | None = Field(None, description="更新人ID")
|
||||
description: str | None = Field(None, description="备注")
|
||||
|
||||
@field_validator("request_method")
|
||||
@classmethod
|
||||
|
||||
@@ -11,18 +11,20 @@ from .schema import (
|
||||
OperationLogCreateSchema,
|
||||
OperationLogDetailOutSchema,
|
||||
OperationLogOutSchema,
|
||||
OperationLogQueryParam,
|
||||
)
|
||||
|
||||
|
||||
class LoginLogService:
|
||||
"""登录日志管理模块服务层"""
|
||||
"""
|
||||
登录日志管理服务
|
||||
|
||||
提供登录日志 CRUD、清理过期日志等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> LoginLogDetailOutSchema:
|
||||
obj = await LoginLogCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return LoginLogDetailOutSchema.model_validate(obj)
|
||||
return await LoginLogCRUD(auth).get_or_404(id=id, out_schema=LoginLogDetailOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def page_service(
|
||||
@@ -33,7 +35,7 @@ class LoginLogService:
|
||||
search: LoginLogQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> dict:
|
||||
search_dict = search.__dict__ if search else {}
|
||||
search_dict = vars(search) if search else None
|
||||
order_by_list = order_by or [{"updated_time": "desc"}]
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
@@ -50,7 +52,7 @@ class LoginLogService:
|
||||
async def create_service(cls, auth: AuthSchema, data: LoginLogCreateSchema) -> LoginLogDetailOutSchema:
|
||||
obj = await LoginLogCRUD(auth).create(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建登录日志失败")
|
||||
raise CustomException(msg="创建失败")
|
||||
return LoginLogDetailOutSchema.model_validate(obj)
|
||||
|
||||
@classmethod
|
||||
@@ -68,12 +70,22 @@ class LoginLogService:
|
||||
|
||||
|
||||
class OperationLogService:
|
||||
"""
|
||||
操作日志管理服务
|
||||
|
||||
提供操作日志记录、分页查询、清理过期日志等业务能力。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def cleanup_operation_log() -> None:
|
||||
"""定时任务:清理超过保留期的操作日志和登录日志(PRD §14.5)
|
||||
"""
|
||||
定时任务:清理超过保留期的操作日志和登录日志。
|
||||
|
||||
清理 create_time < now - retention_days 的记录。
|
||||
清理 created_time < now - retention_days 的记录。
|
||||
保留期从全局参数 `operation_log_retention_days` 读取,默认 90 天。
|
||||
|
||||
返回:
|
||||
- bool: 清理完成返回 True。
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -110,56 +122,95 @@ class OperationLogService:
|
||||
logger.info(f"操作日志清理完成: 操作日志 {op_result.rowcount} 条, 登录日志 {login_result.rowcount} 条")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def create_service(auth: AuthSchema, data: OperationLogCreateSchema) -> OperationLogDetailOutSchema:
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: OperationLogCreateSchema) -> OperationLogDetailOutSchema:
|
||||
"""
|
||||
创建操作日志。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型。
|
||||
- data (OperationLogCreateSchema): 操作日志创建模型。
|
||||
|
||||
返回:
|
||||
- OperationLogDetailOutSchema: 新创建的操作日志。
|
||||
|
||||
异常:
|
||||
- CustomException: 创建失败时抛出。
|
||||
"""
|
||||
crud = OperationLogCRUD(auth)
|
||||
obj = await crud.create(data)
|
||||
obj = await crud.create(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建操作日志失败")
|
||||
raise CustomException(msg="创建失败")
|
||||
return OperationLogDetailOutSchema.model_validate(obj)
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page: int,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: dict | None = None,
|
||||
search: OperationLogQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> dict:
|
||||
from app.common.enums import QueueEnum
|
||||
"""
|
||||
分页查询操作日志。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型。
|
||||
- page_no (int): 页码。
|
||||
- page_size (int): 每页数量。
|
||||
- search (OperationLogQueryParam | None): 查询参数。
|
||||
- order_by (list[dict[str, str]] | None): 排序参数。
|
||||
|
||||
返回:
|
||||
- dict: 分页数据。
|
||||
"""
|
||||
crud = OperationLogCRUD(auth)
|
||||
|
||||
# 构建过滤条件
|
||||
filters = {}
|
||||
if search:
|
||||
if search.get("request_path"):
|
||||
filters["request_path"] = (QueueEnum.like.value, search["request_path"])
|
||||
if search.get("request_method"):
|
||||
filters["request_method"] = (QueueEnum.eq.value, search["request_method"])
|
||||
if search.get("username"):
|
||||
filters["username"] = (QueueEnum.like.value, search["username"])
|
||||
|
||||
result = await crud.page(
|
||||
offset=(page - 1) * page_size,
|
||||
return await crud.page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "desc"}],
|
||||
search=filters,
|
||||
search=vars(search) if search else None,
|
||||
out_schema=OperationLogOutSchema,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def detail_service(auth: AuthSchema, id: int) -> OperationLogDetailOutSchema:
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> OperationLogDetailOutSchema:
|
||||
"""
|
||||
获取操作日志详情。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型。
|
||||
- id (int): 操作日志ID。
|
||||
|
||||
返回:
|
||||
- OperationLogDetailOutSchema: 操作日志详情。
|
||||
"""
|
||||
|
||||
crud = OperationLogCRUD(auth)
|
||||
obj = await crud.get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该操作日志不存在")
|
||||
return OperationLogDetailOutSchema.model_validate(obj)
|
||||
return await crud.get_or_404(id=id, out_schema=OperationLogDetailOutSchema)
|
||||
|
||||
@staticmethod
|
||||
async def delete_service(auth: AuthSchema, ids: list[int]) -> None:
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除操作日志。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型。
|
||||
- ids (list[int]): 操作日志ID列表。
|
||||
|
||||
异常:
|
||||
- CustomException: 删除失败时抛出。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
existing = await OperationLogCRUD(auth).list(search={"id": ("in", ids)})
|
||||
existing_map = {obj.id for obj in existing}
|
||||
for nid in ids:
|
||||
if nid not in existing_map:
|
||||
raise CustomException(msg="删除失败,该数据不存在")
|
||||
crud = OperationLogCRUD(auth)
|
||||
await crud.delete(ids)
|
||||
|
||||
@@ -32,7 +32,7 @@ _NOTICE_NS = "notice"
|
||||
summary="获取公告详情",
|
||||
response_model=ResponseSchema[NoticeOutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
async def get_notice_detail_controller(
|
||||
id: Annotated[int, Path(description="公告ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:detail"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -46,7 +46,7 @@ async def get_obj_detail_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含公告详情的响应模型。
|
||||
"""
|
||||
result_dict = await NoticeService.get_notice_detail_service(id=id, auth=auth)
|
||||
result_dict = await NoticeService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取公告详情成功")
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ async def get_obj_detail_controller(
|
||||
summary="查询公告",
|
||||
response_model=ResponseSchema[PageResultSchema[NoticeOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
async def get_notice_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[NoticeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:query"]))],
|
||||
@@ -71,7 +71,7 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含分页公告详情的响应模型。
|
||||
"""
|
||||
result_dict = await NoticeService.get_notice_page_service(
|
||||
result_dict = await NoticeService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -86,7 +86,7 @@ async def get_obj_list_controller(
|
||||
summary="创建公告",
|
||||
response_model=ResponseSchema[NoticeOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
async def create_notice_controller(
|
||||
data: NoticeCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:create"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -100,7 +100,7 @@ async def create_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含创建公告结果的响应模型。
|
||||
"""
|
||||
result_dict = await NoticeService.create_notice_service(auth=auth, data=data)
|
||||
result_dict = await NoticeService.create_service(auth=auth, data=data)
|
||||
await FastAPICache.clear(namespace=_NOTICE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建公告成功")
|
||||
|
||||
@@ -110,7 +110,7 @@ async def create_obj_controller(
|
||||
summary="修改公告",
|
||||
response_model=ResponseSchema[NoticeOutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
async def update_notice_controller(
|
||||
data: NoticeUpdateSchema,
|
||||
id: Annotated[int, Path(description="公告ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:update"]))],
|
||||
@@ -126,7 +126,7 @@ async def update_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含修改公告结果的响应模型。
|
||||
"""
|
||||
result_dict = await NoticeService.update_notice_service(auth=auth, id=id, data=data)
|
||||
result_dict = await NoticeService.update_service(auth=auth, id=id, data=data)
|
||||
await FastAPICache.clear(namespace=_NOTICE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="修改公告成功")
|
||||
|
||||
@@ -136,7 +136,7 @@ async def update_obj_controller(
|
||||
summary="删除公告",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
async def delete_notice_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:delete"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -150,7 +150,7 @@ async def delete_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含删除公告结果的响应模型。
|
||||
"""
|
||||
await NoticeService.delete_notice_service(auth=auth, ids=ids)
|
||||
await NoticeService.delete_service(auth=auth, ids=ids)
|
||||
await FastAPICache.clear(namespace=_NOTICE_NS)
|
||||
return SuccessResponse(msg="删除公告成功")
|
||||
|
||||
@@ -160,7 +160,7 @@ async def delete_obj_controller(
|
||||
summary="批量修改公告状态",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
async def batch_set_available_notice_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:patch"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -174,17 +174,16 @@ async def batch_set_available_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含批量修改公告状态结果的响应模型。
|
||||
"""
|
||||
await NoticeService.set_notice_available_service(auth=auth, data=data)
|
||||
await NoticeService.set_available_service(auth=auth, data=data)
|
||||
await FastAPICache.clear(namespace=_NOTICE_NS)
|
||||
return SuccessResponse(msg="批量修改公告状态成功")
|
||||
|
||||
|
||||
@NoticeRouter.get(
|
||||
@NoticeRouter.post(
|
||||
"/export",
|
||||
summary="导出公告",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
async def export_notice_list_controller(
|
||||
search: Annotated[NoticeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:export"]))],
|
||||
) -> StreamingResponse:
|
||||
@@ -198,7 +197,7 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 包含导出公告的流式响应模型。
|
||||
"""
|
||||
result_dict_list = await NoticeService.get_notice_list_service(search=search, auth=auth)
|
||||
result_dict_list = await NoticeService.list_service(search=search, auth=auth)
|
||||
export_data = [item.model_dump() for item in result_dict_list]
|
||||
export_result = await NoticeService.export_notice_service(notice_list=export_data)
|
||||
|
||||
@@ -215,7 +214,7 @@ async def export_obj_list_controller(
|
||||
response_model=ResponseSchema[list[NoticeOutSchema]],
|
||||
)
|
||||
@cache(expire=120, namespace=_NOTICE_NS)
|
||||
async def get_obj_list_available_controller(
|
||||
async def get_notice_list_available_controller(
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
@@ -227,7 +226,7 @@ async def get_obj_list_available_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含分页已启用公告详情的响应模型。
|
||||
"""
|
||||
result_dict = await NoticeService.get_notice_available_page_service(auth=auth)
|
||||
result_dict = await NoticeService.available_page_service(auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功")
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class NoticeModel(ModelMixin, TenantMixin, UserMixin):
|
||||
|
||||
__tablename__: str = "sys_notice"
|
||||
__table_args__: dict[str, str] = {"comment": "通知公告表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by"]
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by", "tenant_by"]
|
||||
|
||||
notice_title: Mapped[str] = mapped_column(String(64), nullable=False, comment="公告标题")
|
||||
notice_type: Mapped[str] = mapped_column(String(1), nullable=False, comment="公告类型(1通知 2公告)")
|
||||
@@ -21,6 +21,7 @@ class NoticeModel(ModelMixin, TenantMixin, UserMixin):
|
||||
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="备注")
|
||||
|
||||
|
||||
class NoticeReadModel(MappedBase):
|
||||
"""
|
||||
通知已读记录表 — 记录用户对公告的已读状态。
|
||||
@@ -38,9 +39,8 @@ class NoticeReadModel(MappedBase):
|
||||
UniqueConstraint("user_id", "notice_id", name="uq_user_notice_read"),
|
||||
{"comment": "通知已读记录表"},
|
||||
)
|
||||
__loader_options__: list[str] = ["notice"]
|
||||
|
||||
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="备注")
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True, comment="用户ID")
|
||||
notice_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_notice.id", ondelete="CASCADE"), primary_key=True, comment="通知ID")
|
||||
read_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.now, comment="已读时间")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -62,6 +64,7 @@ class NoticeOutSchema(NoticeCreateSchema, BaseSchema, UserBySchema, TenantBySche
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NoticeQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""公告通知查询参数"""
|
||||
|
||||
@@ -73,26 +76,25 @@ class NoticeQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.notice_title = (QueueEnum.like.value, notice_title)
|
||||
self.notice_type = (QueueEnum.eq.value, notice_type)
|
||||
|
||||
|
||||
# ─── 通知面板 ───
|
||||
if notice_title:
|
||||
self.notice_title = (QueueEnum.like.value, notice_title)
|
||||
if notice_type:
|
||||
self.notice_type = (QueueEnum.eq.value, notice_type)
|
||||
|
||||
|
||||
class PanelMessageItem(BaseModel):
|
||||
"""面板-消息项"""
|
||||
|
||||
id: int
|
||||
title: str
|
||||
content: str
|
||||
time: str
|
||||
type: str
|
||||
id: int = Field(..., description="消息ID")
|
||||
title: str = Field(..., description="标题")
|
||||
content: str = Field(..., description="内容")
|
||||
time: str = Field(..., description="时间")
|
||||
type: str = Field(..., description="类型")
|
||||
|
||||
|
||||
class PanelDataOut(BaseModel):
|
||||
"""通知面板聚合数据"""
|
||||
|
||||
notices: list[NoticeOutSchema] = []
|
||||
messages: list[PanelMessageItem] = []
|
||||
pendings: list[dict] = []
|
||||
notices: list[NoticeOutSchema] = Field(default_factory=list, description="通知列表")
|
||||
messages: list[PanelMessageItem] = Field(default_factory=list, description="消息列表")
|
||||
pendings: list[dict] = Field(default_factory=list, description="待办列表")
|
||||
|
||||
@@ -17,11 +17,13 @@ from .schema import (
|
||||
|
||||
class NoticeService:
|
||||
"""
|
||||
公告管理模块服务层
|
||||
公告管理服务
|
||||
|
||||
提供公告 CRUD、状态切换、已启用公告分页查询、消息面板、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_notice_detail_service(cls, auth: AuthSchema, id: int) -> NoticeOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> NoticeOutSchema:
|
||||
"""
|
||||
获取公告详情。
|
||||
|
||||
@@ -30,15 +32,12 @@ class NoticeService:
|
||||
- id (int): 公告ID。
|
||||
|
||||
返回:
|
||||
- Dict: 公告详情字典。
|
||||
- NoticeOutSchema: 公告响应模型。
|
||||
"""
|
||||
notice_obj = await NoticeCRUD(auth).get(id=id)
|
||||
if not notice_obj:
|
||||
raise CustomException(msg="公告不存在")
|
||||
return NoticeOutSchema.model_validate(notice_obj)
|
||||
return await NoticeCRUD(auth).get_or_404(id=id, out_schema=NoticeOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_notice_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: NoticeQueryParam | None = None,
|
||||
@@ -53,13 +52,13 @@ class NoticeService:
|
||||
- order_by (list[dict] | None): 排序参数列表。
|
||||
|
||||
返回:
|
||||
- list[dict]: 公告详情字典列表。
|
||||
- list[NoticeOutSchema]: 公告响应模型列表。
|
||||
"""
|
||||
notice_obj_list = await NoticeCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
notice_obj_list = await NoticeCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [NoticeOutSchema.model_validate(notice_obj) for notice_obj in notice_obj_list]
|
||||
|
||||
@classmethod
|
||||
async def get_notice_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -85,12 +84,12 @@ class NoticeService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=NoticeOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_notice_available_page_service(cls, auth: AuthSchema) -> dict:
|
||||
async def available_page_service(cls, auth: AuthSchema) -> dict:
|
||||
"""
|
||||
已启用公告分页(与历史行为一致:固定第 1 页、每页 10 条)。
|
||||
|
||||
@@ -109,7 +108,7 @@ class NoticeService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_notice_service(cls, auth: AuthSchema, data: NoticeCreateSchema) -> NoticeOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, data: NoticeCreateSchema) -> NoticeOutSchema:
|
||||
"""
|
||||
创建公告。
|
||||
|
||||
@@ -118,19 +117,19 @@ class NoticeService:
|
||||
- data (NoticeCreateSchema): 创建公告负载模型。
|
||||
|
||||
返回:
|
||||
- dict: 创建的公告详情字典。
|
||||
- NoticeOutSchema: 创建的公告响应模型。
|
||||
|
||||
异常:
|
||||
- CustomException: 创建失败,该公告通知已存在。
|
||||
"""
|
||||
notice = await NoticeCRUD(auth).get(notice_title=data.notice_title)
|
||||
if notice:
|
||||
raise CustomException(msg="创建失败,该公告通知已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
notice_obj = await NoticeCRUD(auth).create(data=data)
|
||||
return NoticeOutSchema.model_validate(notice_obj)
|
||||
|
||||
@classmethod
|
||||
async def update_notice_service(cls, auth: AuthSchema, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema:
|
||||
"""
|
||||
更新公告。
|
||||
|
||||
@@ -140,22 +139,20 @@ class NoticeService:
|
||||
- data (NoticeUpdateSchema): 更新公告负载模型。
|
||||
|
||||
返回:
|
||||
- dict: 更新的公告详情字典。
|
||||
- NoticeOutSchema: 更新的公告响应模型。
|
||||
|
||||
异常:
|
||||
- CustomException: 更新失败,该公告通知不存在或公告通知标题重复。
|
||||
"""
|
||||
notice = await NoticeCRUD(auth).get(id=id)
|
||||
if not notice:
|
||||
raise CustomException(msg="更新失败,该公告通知不存在")
|
||||
_ = await NoticeCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
exist_notice = await NoticeCRUD(auth).get(notice_title=data.notice_title)
|
||||
if exist_notice and exist_notice.id != id:
|
||||
raise CustomException(msg="更新失败,公告通知标题重复")
|
||||
raise CustomException(msg="更新失败,标题已存在")
|
||||
notice_obj = await NoticeCRUD(auth).update(id=id, data=data)
|
||||
return NoticeOutSchema.model_validate(notice_obj)
|
||||
|
||||
@classmethod
|
||||
async def delete_notice_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除公告。
|
||||
|
||||
@@ -175,11 +172,11 @@ class NoticeService:
|
||||
notice_map = {n.id: n for n in notices}
|
||||
for nid in ids:
|
||||
if nid not in notice_map:
|
||||
raise CustomException(msg="删除失败,该公告通知不存在")
|
||||
raise CustomException(msg="删除失败,该数据不存在")
|
||||
await NoticeCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_notice_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量设置公告状态。
|
||||
|
||||
@@ -196,7 +193,7 @@ class NoticeService:
|
||||
await NoticeCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def export_notice_service(cls, notice_list: list[dict]) -> bytes:
|
||||
async def export_service(cls, notice_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出公告列表。
|
||||
|
||||
@@ -230,7 +227,7 @@ class NoticeService:
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def get_latest_notices_service(cls, auth: AuthSchema, limit: int = 5) -> list[NoticeOutSchema]:
|
||||
async def latest_service(cls, auth: AuthSchema, limit: int = 5) -> list[NoticeOutSchema]:
|
||||
"""获取最新 N 条已启用公告"""
|
||||
from sqlalchemy import desc, select
|
||||
|
||||
@@ -322,12 +319,12 @@ class NoticeService:
|
||||
return max(0, total_count - read_count)
|
||||
|
||||
@classmethod
|
||||
async def get_panel_data_service(cls, auth: AuthSchema) -> PanelDataOut:
|
||||
async def panel_data_service(cls, auth: AuthSchema) -> PanelDataOut:
|
||||
"""聚合通知面板数据:通知 + 消息 + 待办"""
|
||||
from sqlalchemy import desc, select
|
||||
|
||||
# 1. 通知:最新 5 条已启用公告
|
||||
notices = await cls.get_latest_notices_service(auth, limit=5)
|
||||
notices = await cls.latest_service(auth, limit=5)
|
||||
|
||||
# 2. 消息:最近的操作日志(作为系统消息)
|
||||
messages = []
|
||||
|
||||
@@ -22,7 +22,7 @@ ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=["
|
||||
summary="获取参数详情",
|
||||
response_model=ResponseSchema[ParamsOutSchema],
|
||||
)
|
||||
async def get_type_detail_controller(
|
||||
async def get_param_detail_controller(
|
||||
id: Annotated[int, Path(description="参数ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:detail"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -36,7 +36,7 @@ async def get_type_detail_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含参数详情的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.get_obj_detail_service(id=id, auth=auth)
|
||||
result_dict = await ParamsService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取参数详情成功")
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ async def get_type_detail_controller(
|
||||
summary="根据配置键获取参数详情",
|
||||
response_model=ResponseSchema[ParamsOutSchema],
|
||||
)
|
||||
async def get_obj_by_key_controller(
|
||||
async def get_param_by_key_controller(
|
||||
config_key: Annotated[str, Path(description="配置键")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -59,7 +59,7 @@ async def get_obj_by_key_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含参数详情的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.get_obj_by_key_service(config_key=config_key, auth=auth)
|
||||
result_dict = await ParamsService.get_by_key_service(config_key=config_key, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="根据配置键获取参数详情成功")
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ async def get_config_value_by_key_controller(
|
||||
summary="获取参数列表",
|
||||
response_model=ResponseSchema[PageResultSchema[ParamsOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
async def get_param_list_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[ParamsQueryParam, Depends()],
|
||||
@@ -107,7 +107,7 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含参数列表的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.get_obj_page_service(
|
||||
result_dict = await ParamsService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -122,7 +122,7 @@ async def get_obj_list_controller(
|
||||
summary="创建参数",
|
||||
response_model=ResponseSchema[ParamsOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
async def create_param_controller(
|
||||
data: ParamsCreateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:create"]))],
|
||||
@@ -138,7 +138,7 @@ async def create_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含创建参数结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.create_obj_service(auth=auth, redis=redis, data=data)
|
||||
result_dict = await ParamsService.create_service(auth=auth, redis=redis, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建参数成功")
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ async def create_obj_controller(
|
||||
summary="修改参数",
|
||||
response_model=ResponseSchema[ParamsOutSchema],
|
||||
)
|
||||
async def update_objs_controller(
|
||||
async def update_param_controller(
|
||||
data: ParamsUpdateSchema,
|
||||
id: Annotated[int, Path(description="参数ID")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
@@ -165,7 +165,7 @@ async def update_objs_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含修改参数结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
|
||||
result_dict = await ParamsService.update_service(auth=auth, redis=redis, id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="更新参数成功")
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ async def update_objs_controller(
|
||||
summary="删除参数",
|
||||
response_model=ResponseSchema[ParamsOutSchema],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
async def delete_param_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:delete"]))],
|
||||
@@ -190,7 +190,7 @@ async def delete_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含删除参数结果的 JSON 响应
|
||||
"""
|
||||
await ParamsService.delete_obj_service(auth=auth, redis=redis, ids=ids)
|
||||
await ParamsService.delete_service(auth=auth, redis=redis, ids=ids)
|
||||
return SuccessResponse(msg="删除参数成功")
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ async def batch_set_status_controller(
|
||||
summary="导出参数",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
async def export_param_list_controller(
|
||||
search: Annotated[ParamsQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:export"]))],
|
||||
) -> StreamingResponse:
|
||||
@@ -238,9 +238,9 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 包含导出参数的 Excel 文件流响应
|
||||
"""
|
||||
result_dict_list = await ParamsService.get_obj_list_service(search=search, auth=auth)
|
||||
result_dict_list = await ParamsService.list_service(search=search, auth=auth)
|
||||
export_data = [item.model_dump() for item in result_dict_list]
|
||||
export_result = await ParamsService.export_obj_service(data_list=export_data)
|
||||
export_result = await ParamsService.export_service(data_list=export_data)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
@@ -254,7 +254,7 @@ async def export_obj_list_controller(
|
||||
summary="获取初始化缓存参数",
|
||||
response_model=ResponseSchema[list[ParamsOutSchema]],
|
||||
)
|
||||
async def get_init_obj_controller(
|
||||
async def get_init_config_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
@@ -266,5 +266,5 @@ async def get_init_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 获取初始化缓存参数的 JSON 响应
|
||||
"""
|
||||
result_dict = await ParamsService.get_init_config_service(redis=redis, tenant_id=1)
|
||||
result_dict = await ParamsService.get_init_cache_service(redis=redis, tenant_id=1)
|
||||
return SuccessResponse(data=result_dict, msg="获取初始化缓存参数成功")
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
|
||||
|
||||
class ParamsModel(ModelMixin, TenantMixin):
|
||||
class ParamsModel(ModelMixin, TenantMixin, UserMixin):
|
||||
"""
|
||||
参数配置表
|
||||
系统参数表
|
||||
|
||||
用于存储全局系统配置(如 retention_days、smtp 主机等)。
|
||||
平台参数(tenant_id=1)对所有租户共享;租户级参数仅本租户可见。
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_param"
|
||||
__table_args__: dict[str, str] = {"comment": "系统参数表"}
|
||||
__loader_options__: list[str] = []
|
||||
__loader_options__: list[str] = [
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
|
||||
config_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="参数名称")
|
||||
config_key: Mapped[str] = mapped_column(String(500), nullable=False, comment="参数键名")
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@@ -7,21 +10,22 @@ from app.core.base_schema import BaseSchema, TenantBySchema, UserBySchema
|
||||
|
||||
|
||||
class ParamsCreateSchema(BaseModel):
|
||||
"""配置创建模型"""
|
||||
"""
|
||||
参数创建模型
|
||||
"""
|
||||
|
||||
config_name: str = Field(..., min_length=1, max_length=64, description="参数名称")
|
||||
config_key: str = Field(..., min_length=1, max_length=500, description="参数键名")
|
||||
config_key: str = Field(..., min_length=1, max_length=500, description="参数键名(小写字母开头,仅允许字母数字_.-)")
|
||||
config_value: str | None = Field(default=None, max_length=500, description="参数键值")
|
||||
config_type: bool = Field(default=False, description="是否系统内置")
|
||||
config_type: bool = Field(default=False, description="是否系统内置(True:是 False:否)")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态(0:正常 1:停用)")
|
||||
description: str | None = Field(default=None, max_length=500, description="描述")
|
||||
description: str | None = Field(default=None, max_length=500, description="参数描述")
|
||||
|
||||
@field_validator("config_key")
|
||||
@classmethod
|
||||
def _validate_config_key(cls, v: str) -> str:
|
||||
"""校验参数键名:小写字母开头,仅含字母/数字/_ . -"""
|
||||
v = v.strip().lower()
|
||||
import re
|
||||
|
||||
if not re.match(r"^[a-z][a-z0-9_.-]*$", v):
|
||||
raise ValueError("参数键名必须以小写字母开头,仅允许小写字母、数字、_ . -")
|
||||
return v
|
||||
@@ -29,33 +33,50 @@ class ParamsCreateSchema(BaseModel):
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: int) -> int:
|
||||
"""校验状态:仅支持 0(正常) 或 1(停用)"""
|
||||
if v not in {0, 1}:
|
||||
raise ValueError("状态仅支持 0(正常) 或 1(停用)")
|
||||
return v
|
||||
|
||||
|
||||
class ParamsUpdateSchema(ParamsCreateSchema):
|
||||
"""配置更新模型"""
|
||||
"""
|
||||
参数更新模型
|
||||
"""
|
||||
|
||||
|
||||
class ParamsOutSchema(ParamsCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""配置响应模型"""
|
||||
"""
|
||||
参数响应模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParamsQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""配置管理查询参数"""
|
||||
"""
|
||||
参数管理查询参数
|
||||
|
||||
支持:
|
||||
- 时间范围(BaseQueryParam)
|
||||
- 创建人/更新人筛选(UserByQueryParam)
|
||||
- 租户筛选(TenantByQueryParam)
|
||||
- 业务字段:参数名称、参数键名、是否系统内置
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str | None = Query(None, description="配置名称"),
|
||||
config_key: str | None = Query(None, description="配置键名"),
|
||||
config_type: bool | None = Query(None, description="系统内置((True:是 False:否))"),
|
||||
config_name: str | None = Query(None, description="参数名称"),
|
||||
config_key: str | None = Query(None, description="参数键名"),
|
||||
config_type: bool | None = Query(None, description="是否系统内置(True:是 False:否)"),
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.config_name = (QueueEnum.like.value, config_name)
|
||||
self.config_key = (QueueEnum.like.value, config_key)
|
||||
self.config_type = (QueueEnum.eq.value, config_type)
|
||||
if config_name:
|
||||
self.config_name = (QueueEnum.like.value, config_name)
|
||||
if config_key:
|
||||
self.config_key = (QueueEnum.like.value, config_key)
|
||||
if config_type is not None:
|
||||
self.config_type = (QueueEnum.eq.value, config_type)
|
||||
|
||||
@@ -26,62 +26,61 @@ _mid_config_cache: dict = {"ts": 0.0, "data": None}
|
||||
|
||||
class ParamsService:
|
||||
"""
|
||||
配置管理模块服务层
|
||||
参数管理服务
|
||||
|
||||
提供参数 CRUD、Redis 缓存同步、初始化配置、批量启/禁用、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> ParamsOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> ParamsOutSchema:
|
||||
"""
|
||||
获取配置详情
|
||||
获取参数详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 配置管理型ID
|
||||
- id (int): 参数ID
|
||||
|
||||
返回:
|
||||
- dict: 配置管理型模型实例字典表示
|
||||
- ParamsOutSchema: 参数响应模型
|
||||
"""
|
||||
obj = await ParamsCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="参数不存在")
|
||||
return ParamsOutSchema.model_validate(obj)
|
||||
return await ParamsCRUD(auth).get_or_404(id=id, out_schema=ParamsOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_obj_by_key_service(cls, auth: AuthSchema, config_key: str) -> ParamsOutSchema:
|
||||
async def get_by_key_service(cls, auth: AuthSchema, config_key: str) -> ParamsOutSchema:
|
||||
"""
|
||||
根据配置键获取配置详情
|
||||
根据配置键获取参数详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- config_key (str): 配置管理型key
|
||||
- config_key (str): 参数键名
|
||||
|
||||
返回:
|
||||
- Dict: 配置管理型模型实例字典表示
|
||||
- ParamsOutSchema: 参数响应模型
|
||||
"""
|
||||
obj = await ParamsCRUD(auth).get(config_key=config_key)
|
||||
if not obj:
|
||||
raise CustomException(msg=f"配置键 {config_key} 不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return ParamsOutSchema.model_validate(obj)
|
||||
|
||||
@classmethod
|
||||
async def get_config_value_by_key_service(cls, auth: AuthSchema, config_key: str) -> str | None:
|
||||
"""
|
||||
根据配置键获取配置值
|
||||
根据配置键获取参数值
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- config_key (str): 配置管理型key
|
||||
- config_key (str): 参数键名
|
||||
|
||||
返回:
|
||||
- str | None: 配置值字符串或None
|
||||
- str | None: 参数键值字符串或 None
|
||||
"""
|
||||
obj = await ParamsCRUD(auth).get(config_key=config_key)
|
||||
if not obj:
|
||||
raise CustomException(msg=f"配置键 {config_key} 不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return obj.config_value
|
||||
|
||||
@classmethod
|
||||
async def get_obj_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: ParamsQueryParam | None = None,
|
||||
@@ -96,13 +95,13 @@ class ParamsService:
|
||||
- order_by (list[dict] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- list[ParamsOutSchema]: 配置管理型模型实例
|
||||
- list[ParamsOutSchema]: 参数响应模型列表
|
||||
"""
|
||||
obj_list = await ParamsCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
obj_list = await ParamsCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [ParamsOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def get_obj_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -128,12 +127,12 @@ class ParamsService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=ParamsOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: ParamsCreateSchema) -> ParamsOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, redis: Redis, data: ParamsCreateSchema) -> ParamsOutSchema:
|
||||
"""
|
||||
创建配置管理型
|
||||
|
||||
@@ -143,11 +142,11 @@ class ParamsService:
|
||||
- data (ParamsCreateSchema): 配置管理型创建模型
|
||||
|
||||
返回:
|
||||
- dict: 新创建的配置管理型模型实例字典表示
|
||||
- ParamsOutSchema: 新创建的参数响应模型
|
||||
"""
|
||||
exist_obj = await ParamsCRUD(auth).get(config_key=data.config_key)
|
||||
if exist_obj:
|
||||
raise CustomException(msg="创建失败,该配置key已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
obj = await ParamsCRUD(auth).create(data=data)
|
||||
|
||||
out = ParamsOutSchema.model_validate(obj)
|
||||
@@ -167,27 +166,25 @@ class ParamsService:
|
||||
raise CustomException(msg="同步配置到缓存失败")
|
||||
except Exception as e:
|
||||
logger.error(f"创建字典类型失败: {e}")
|
||||
raise CustomException(msg=f"创建字典类型失败 {e}")
|
||||
raise CustomException(msg="同步配置到缓存失败") from e
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id: int, data: ParamsUpdateSchema) -> ParamsOutSchema:
|
||||
async def update_service(cls, auth: AuthSchema, redis: Redis, id: int, data: ParamsUpdateSchema) -> ParamsOutSchema:
|
||||
"""
|
||||
更新配置管理型
|
||||
更新参数
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- redis (Redis): Redis 客户端实例
|
||||
- id (int): 配置管理型ID
|
||||
- data (ParamsUpdateSchema): 配置管理型更新模型
|
||||
- id (int): 参数ID
|
||||
- data (ParamsUpdateSchema): 参数更新模型
|
||||
|
||||
返回:
|
||||
- Dict: 更新后的配置管理型模型实例字典表示
|
||||
- ParamsOutSchema: 更新后的参数响应模型
|
||||
"""
|
||||
exist_obj = await ParamsCRUD(auth).get(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该数系统配置不存在")
|
||||
exist_obj = await ParamsCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
if exist_obj.config_key != data.config_key:
|
||||
raise CustomException(msg="更新失败,系统配置key不允许修改")
|
||||
|
||||
@@ -211,12 +208,12 @@ class ParamsService:
|
||||
raise CustomException(msg="同步配置到缓存失败")
|
||||
except Exception as e:
|
||||
logger.error(f"更新系统配置失败: {e}")
|
||||
raise CustomException(msg="更新系统配置失败")
|
||||
raise CustomException(msg="同步配置到缓存失败") from e
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
|
||||
"""
|
||||
删除配置管理型
|
||||
|
||||
@@ -236,7 +233,7 @@ class ParamsService:
|
||||
for pid in ids:
|
||||
obj = obj_map.get(pid)
|
||||
if not obj:
|
||||
raise CustomException(msg="删除失败,该数据字典类型不存在")
|
||||
raise CustomException(msg="删除失败,该数据不存在")
|
||||
if obj.config_type:
|
||||
raise CustomException(msg=f"{obj.config_name} 删除失败,系统初始化配置不可以删除")
|
||||
|
||||
@@ -249,7 +246,7 @@ class ParamsService:
|
||||
await RedisCURD(redis).delete(redis_key)
|
||||
except Exception as e:
|
||||
logger.error(f"删除系统配置失败: {e}")
|
||||
raise CustomException(msg="删除字典类型失败")
|
||||
raise CustomException(msg="同步删除缓存失败") from e
|
||||
|
||||
@classmethod
|
||||
async def batch_set_status_service(cls, auth: AuthSchema, ids: list[int], status: str) -> None:
|
||||
@@ -270,15 +267,15 @@ class ParamsService:
|
||||
await ParamsCRUD(auth).set(ids=ids, status=status)
|
||||
|
||||
@classmethod
|
||||
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
|
||||
async def export_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出系统配置列表
|
||||
导出参数列表
|
||||
|
||||
参数:
|
||||
- data_list (list[dict]): 系统配置模型实例字典列表表示
|
||||
- data_list (list[dict]): 参数字典列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件二进制数据
|
||||
- bytes: Excel 文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
"id": "编号",
|
||||
@@ -302,7 +299,7 @@ class ParamsService:
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def init_config_service(cls, redis: Redis) -> None:
|
||||
async def init_cache_service(cls, redis: Redis) -> None:
|
||||
"""
|
||||
初始化系统配置并按租户缓存。
|
||||
|
||||
@@ -317,7 +314,7 @@ class ParamsService:
|
||||
auth = AuthSchema(db=session, check_data_scope=False)
|
||||
config_obj = await ParamsCRUD(auth).list()
|
||||
if not config_obj:
|
||||
raise CustomException(msg="系统配置不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
try:
|
||||
for config in config_obj:
|
||||
tenant_id = config.tenant_id
|
||||
@@ -335,10 +332,10 @@ class ParamsService:
|
||||
raise CustomException(msg="初始化系统配置失败")
|
||||
except Exception as e:
|
||||
logger.error(f"❌️ 初始化系统配置失败: {e}")
|
||||
raise CustomException(msg="初始化系统配置失败")
|
||||
raise CustomException(msg="初始化系统配置失败") from e
|
||||
|
||||
@classmethod
|
||||
async def get_init_config_service(cls, redis: Redis, tenant_id: int = 1) -> list[dict]:
|
||||
async def get_init_cache_service(cls, redis: Redis, tenant_id: int = 1) -> list[dict]:
|
||||
"""
|
||||
获取系统配置
|
||||
|
||||
@@ -347,7 +344,7 @@ class ParamsService:
|
||||
- tenant_id (int): 租户ID
|
||||
|
||||
返回:
|
||||
- list[dict]: 系统配置模型实例字典列表表示
|
||||
- list[dict]: 系统配置字典列表
|
||||
"""
|
||||
redis_keys = await RedisCURD(redis).get_keys(f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{tenant_id}:*")
|
||||
redis_configs = await RedisCURD(redis).mget(redis_keys)
|
||||
|
||||
@@ -50,7 +50,7 @@ async def get_obj_list_controller(
|
||||
order_by = [{"order": "asc"}]
|
||||
if page.order_by:
|
||||
order_by = page.order_by
|
||||
result_dict = await PositionService.get_position_page_service(
|
||||
result_dict = await PositionService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -79,7 +79,7 @@ async def get_obj_detail_controller(
|
||||
返回:
|
||||
- JSONResponse: 岗位详情对象
|
||||
"""
|
||||
result_dict = await PositionService.get_position_detail_service(id=id, auth=auth)
|
||||
result_dict = await PositionService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取岗位详情成功")
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ async def create_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 岗位详情对象
|
||||
"""
|
||||
result_dict = await PositionService.create_position_service(data=data, auth=auth)
|
||||
result_dict = await PositionService.create_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_POS_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建岗位成功")
|
||||
|
||||
@@ -128,7 +128,7 @@ async def update_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 岗位详情对象
|
||||
"""
|
||||
result_dict = await PositionService.update_position_service(id=id, data=data, auth=auth)
|
||||
result_dict = await PositionService.update_service(id=id, data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_POS_NS)
|
||||
return SuccessResponse(data=result_dict, msg="修改岗位成功")
|
||||
|
||||
@@ -152,7 +152,7 @@ async def delete_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 成功消息
|
||||
"""
|
||||
await PositionService.delete_position_service(ids=ids, auth=auth)
|
||||
await PositionService.delete_service(ids=ids, auth=auth)
|
||||
await FastAPICache.clear(namespace=_POS_NS)
|
||||
return SuccessResponse(msg="删除岗位成功")
|
||||
|
||||
@@ -176,7 +176,7 @@ async def batch_set_available_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 成功消息
|
||||
"""
|
||||
await PositionService.set_position_available_service(data=data, auth=auth)
|
||||
await PositionService.set_available_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_POS_NS)
|
||||
return SuccessResponse(msg="批量修改岗位状态成功")
|
||||
|
||||
@@ -200,8 +200,8 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 岗位Excel文件流
|
||||
"""
|
||||
position_query_result = await PositionService.get_position_list_service(search=search, auth=auth)
|
||||
position_export_result = await PositionService.export_position_list_service(position_list=position_query_result)
|
||||
position_query_result = await PositionService.list_service(search=search, auth=auth)
|
||||
position_export_result = await PositionService.export_list_service(position_list=position_query_result)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(position_export_result),
|
||||
|
||||
@@ -16,7 +16,13 @@ class PositionModel(ModelMixin, TenantMixin, UserMixin):
|
||||
|
||||
__tablename__: str = "sys_position"
|
||||
__table_args__: dict[str, str] = {"comment": "岗位表"}
|
||||
__loader_options__: list[str] = ["users", "created_by", "updated_by", "deleted_by"]
|
||||
__loader_options__: list[str] = [
|
||||
"users",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位名称")
|
||||
code: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位编码")
|
||||
|
||||
@@ -12,10 +12,14 @@ from .schema import (
|
||||
|
||||
|
||||
class PositionService:
|
||||
"""岗位模块服务层"""
|
||||
"""
|
||||
岗位管理服务
|
||||
|
||||
提供岗位 CRUD、批量启/禁用、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_position_detail_service(cls, auth: AuthSchema, id: int) -> PositionOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> PositionOutSchema:
|
||||
"""
|
||||
获取岗位详情
|
||||
|
||||
@@ -24,15 +28,12 @@ class PositionService:
|
||||
- id (int): 岗位ID
|
||||
|
||||
返回:
|
||||
- Dict: 岗位详情对象
|
||||
- PositionOutSchema: 岗位详情响应模型
|
||||
"""
|
||||
position = await PositionCRUD(auth).get(id=id)
|
||||
if not position:
|
||||
raise CustomException(msg="岗位不存在")
|
||||
return PositionOutSchema.model_validate(position)
|
||||
return await PositionCRUD(auth).get_or_404(id=id, out_schema=PositionOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_position_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: PositionQueryParam | None = None,
|
||||
@@ -49,11 +50,11 @@ class PositionService:
|
||||
返回:
|
||||
- list[PositionOutSchema]: 岗位列表
|
||||
"""
|
||||
position_list = await PositionCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
position_list = await PositionCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [PositionOutSchema.model_validate(position) for position in position_list]
|
||||
|
||||
@classmethod
|
||||
async def get_position_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -79,12 +80,12 @@ class PositionService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=PositionOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_position_service(cls, auth: AuthSchema, data: PositionCreateSchema) -> PositionOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, data: PositionCreateSchema) -> PositionOutSchema:
|
||||
"""
|
||||
创建岗位
|
||||
|
||||
@@ -93,16 +94,16 @@ class PositionService:
|
||||
- data (PositionCreateSchema): 岗位创建模型
|
||||
|
||||
返回:
|
||||
- dict: 创建的岗位详情字典
|
||||
- PositionOutSchema: 新创建的岗位响应模型
|
||||
"""
|
||||
position = await PositionCRUD(auth).get(name=data.name)
|
||||
if position:
|
||||
raise CustomException(msg="创建失败,该岗位已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
new_position = await PositionCRUD(auth).create(data=data)
|
||||
return PositionOutSchema.model_validate(new_position)
|
||||
|
||||
@classmethod
|
||||
async def update_position_service(cls, auth: AuthSchema, id: int, data: PositionUpdateSchema) -> PositionOutSchema:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: PositionUpdateSchema) -> PositionOutSchema:
|
||||
"""
|
||||
更新岗位
|
||||
|
||||
@@ -112,19 +113,17 @@ class PositionService:
|
||||
- data (PositionUpdateSchema): 岗位更新模型
|
||||
|
||||
返回:
|
||||
- dict: 更新的岗位对象
|
||||
- PositionOutSchema: 更新后的岗位响应模型
|
||||
"""
|
||||
position = await PositionCRUD(auth).get(id=id)
|
||||
if not position:
|
||||
raise CustomException(msg="更新失败,该岗位不存在")
|
||||
_ = await PositionCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
exist_position = await PositionCRUD(auth).get(name=data.name)
|
||||
if exist_position and exist_position.id != id:
|
||||
raise CustomException(msg="更新失败,岗位名称重复")
|
||||
raise CustomException(msg="更新失败,名称已存在")
|
||||
updated_position = await PositionCRUD(auth).update(id=id, data=data)
|
||||
return PositionOutSchema.model_validate(updated_position)
|
||||
|
||||
@classmethod
|
||||
async def delete_position_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除岗位
|
||||
|
||||
@@ -142,11 +141,11 @@ class PositionService:
|
||||
position_map = {p.id: p for p in positions}
|
||||
for pid in ids:
|
||||
if pid not in position_map:
|
||||
raise CustomException(msg="删除失败,该岗位不存在")
|
||||
raise CustomException(msg="删除失败,该数据不存在")
|
||||
await PositionCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_position_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
设置岗位状态
|
||||
|
||||
@@ -161,11 +160,11 @@ class PositionService:
|
||||
position_map = {p.id: p for p in positions}
|
||||
for pid in data.ids:
|
||||
if pid not in position_map:
|
||||
raise CustomException(msg=f"岗位ID {pid} 不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
await PositionCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def export_position_list_service(cls, position_list: list[dict]) -> bytes:
|
||||
async def export_list_service(cls, position_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出岗位列表
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ _ROLE_NS = "role"
|
||||
response_model=ResponseSchema[PageResultSchema[RoleOutSchema]],
|
||||
)
|
||||
@cache(expire=300, namespace=_ROLE_NS)
|
||||
async def get_obj_list_controller(
|
||||
async def get_role_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[RoleQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:query"]))],
|
||||
@@ -51,7 +51,7 @@ async def get_obj_list_controller(
|
||||
order_by = [{"order": "asc"}]
|
||||
if page.order_by:
|
||||
order_by = page.order_by
|
||||
result_dict = await RoleService.get_role_page_service(
|
||||
result_dict = await RoleService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -66,7 +66,7 @@ async def get_obj_list_controller(
|
||||
summary="查询角色详情",
|
||||
response_model=ResponseSchema[RoleOutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
async def get_role_detail_controller(
|
||||
id: Annotated[int, Path(description="角色ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:detail"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -80,7 +80,7 @@ async def get_obj_detail_controller(
|
||||
返回:
|
||||
- JSONResponse: 角色详情JSON响应
|
||||
"""
|
||||
result_dict = await RoleService.get_role_detail_service(id=id, auth=auth)
|
||||
result_dict = await RoleService.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取角色详情成功")
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ async def get_obj_detail_controller(
|
||||
summary="创建角色",
|
||||
response_model=ResponseSchema[RoleOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
async def create_role_controller(
|
||||
data: RoleCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:create"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -103,7 +103,7 @@ async def create_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 创建角色JSON响应
|
||||
"""
|
||||
result_dict = await RoleService.create_role_service(data=data, auth=auth)
|
||||
result_dict = await RoleService.create_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_ROLE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建角色成功")
|
||||
|
||||
@@ -113,7 +113,7 @@ async def create_obj_controller(
|
||||
summary="修改角色",
|
||||
response_model=ResponseSchema[RoleOutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
async def update_role_controller(
|
||||
data: RoleUpdateSchema,
|
||||
id: Annotated[int, Path(description="角色ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:update"]))],
|
||||
@@ -129,7 +129,7 @@ async def update_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 修改角色JSON响应
|
||||
"""
|
||||
result_dict = await RoleService.update_role_service(id=id, data=data, auth=auth)
|
||||
result_dict = await RoleService.update_service(id=id, data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_ROLE_NS)
|
||||
return SuccessResponse(data=result_dict, msg="修改角色成功")
|
||||
|
||||
@@ -139,7 +139,7 @@ async def update_obj_controller(
|
||||
summary="删除角色",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
async def delete_role_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:delete"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -153,7 +153,7 @@ async def delete_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 删除角色JSON响应
|
||||
"""
|
||||
await RoleService.delete_role_service(ids=ids, auth=auth)
|
||||
await RoleService.delete_service(ids=ids, auth=auth)
|
||||
await FastAPICache.clear(namespace=_ROLE_NS)
|
||||
return SuccessResponse(msg="删除角色成功")
|
||||
|
||||
@@ -163,7 +163,7 @@ async def delete_obj_controller(
|
||||
summary="批量修改角色状态",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
async def batch_set_available_role_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:patch"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -177,7 +177,7 @@ async def batch_set_available_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 批量修改角色状态JSON响应
|
||||
"""
|
||||
await RoleService.set_role_available_service(data=data, auth=auth)
|
||||
await RoleService.set_available_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_ROLE_NS)
|
||||
return SuccessResponse(msg="批量修改角色状态成功")
|
||||
|
||||
@@ -201,7 +201,7 @@ async def set_role_permission_controller(
|
||||
返回:
|
||||
- JSONResponse: 角色授权JSON响应
|
||||
"""
|
||||
await RoleService.set_role_permission_service(data=data, auth=auth)
|
||||
await RoleService.set_permission_service(data=data, auth=auth)
|
||||
await FastAPICache.clear(namespace=_ROLE_NS)
|
||||
return SuccessResponse(msg="授权角色成功")
|
||||
|
||||
@@ -211,7 +211,7 @@ async def set_role_permission_controller(
|
||||
summary="导出角色",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
async def export_role_list_controller(
|
||||
search: Annotated[RoleQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:export"]))],
|
||||
) -> StreamingResponse:
|
||||
@@ -225,8 +225,8 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 导出角色流响应
|
||||
"""
|
||||
role_query_result = await RoleService.get_role_list_service(search=search, auth=auth)
|
||||
role_export_result = await RoleService.export_role_list_service(role_list=role_query_result)
|
||||
role_query_result = await RoleService.list_service(search=search, auth=auth)
|
||||
role_export_result = await RoleService.export_list_service(role_list=role_query_result)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(role_export_result),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.common.enums import PermissionFilterStrategy
|
||||
from app.core.base_model import MappedBase, ModelMixin, TenantMixin
|
||||
from app.core.base_model import MappedBase, ModelMixin, TenantMixin, UserMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
@@ -61,7 +61,7 @@ class RoleDeptsModel(MappedBase):
|
||||
)
|
||||
|
||||
|
||||
class RoleModel(ModelMixin, TenantMixin):
|
||||
class RoleModel(ModelMixin, TenantMixin, UserMixin):
|
||||
"""
|
||||
角色模型
|
||||
|
||||
@@ -70,7 +70,14 @@ class RoleModel(ModelMixin, TenantMixin):
|
||||
|
||||
__tablename__: str = "sys_role"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "code"), {"comment": "角色表"})
|
||||
__loader_options__: list[str] = ["menus", "depts"]
|
||||
__loader_options__: list[str] = [
|
||||
"menus",
|
||||
"depts",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_ROLE
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称")
|
||||
@@ -80,6 +87,11 @@ class RoleModel(ModelMixin, TenantMixin):
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
data_scope: Mapped[int] = mapped_column(Integer, default=1, nullable=False, comment="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)")
|
||||
|
||||
menus: Mapped[list["MenuModel"]] = relationship(secondary="sys_role_menus", back_populates="roles", lazy="selectin", order_by="MenuModel.order",)
|
||||
menus: Mapped[list["MenuModel"]] = relationship(
|
||||
secondary="sys_role_menus",
|
||||
back_populates="roles",
|
||||
lazy="selectin",
|
||||
order_by="MenuModel.order",
|
||||
)
|
||||
depts: Mapped[list["DeptModel"]] = relationship(secondary="sys_role_depts", back_populates="roles", lazy="selectin")
|
||||
users: Mapped[list["UserModel"]] = relationship(secondary="sys_user_roles", back_populates="roles", lazy="selectin")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -10,16 +12,18 @@ from pydantic import (
|
||||
from app.api.v1.module_platform.menu.schema import MenuOutSchema
|
||||
from app.api.v1.module_system.dept.schema import DeptOutSchema
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
|
||||
from app.core.base_schema import BaseSchema, TenantBySchema, UserBySchema
|
||||
from app.core.validator import (
|
||||
DateTimeStr,
|
||||
role_permission_request_validator,
|
||||
validate_required_code,
|
||||
)
|
||||
|
||||
|
||||
class RoleCreateSchema(BaseModel):
|
||||
"""角色创建模型"""
|
||||
"""
|
||||
角色创建模型
|
||||
"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=64, description="角色名称")
|
||||
code: str = Field(..., min_length=2, max_length=64, description="角色编码")
|
||||
@@ -58,10 +62,14 @@ class RoleCreateSchema(BaseModel):
|
||||
|
||||
|
||||
class RolePermissionSettingSchema(BaseModel):
|
||||
"""角色权限配置模型"""
|
||||
"""
|
||||
角色权限配置模型
|
||||
"""
|
||||
|
||||
data_scope: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
le=5,
|
||||
description="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)",
|
||||
)
|
||||
role_ids: list[int] = Field(default_factory=list, description="角色ID列表")
|
||||
@@ -73,24 +81,22 @@ class RolePermissionSettingSchema(BaseModel):
|
||||
"""
|
||||
校验角色权限配置字段(数据范围与关联 ID 等)。
|
||||
|
||||
参数:
|
||||
- self: 当前模型实例(校验后状态)。
|
||||
|
||||
返回:
|
||||
- RolePermissionSettingSchema: 通过 `role_permission_request_validator` 校验后的同一实例。
|
||||
|
||||
异常:
|
||||
- CustomException: 不满足权限配置约束时抛出。
|
||||
"""
|
||||
return role_permission_request_validator(self)
|
||||
|
||||
|
||||
class RoleUpdateSchema(RoleCreateSchema):
|
||||
"""角色更新模型"""
|
||||
"""
|
||||
角色更新模型
|
||||
"""
|
||||
|
||||
|
||||
class RoleOutSchema(RoleCreateSchema, BaseSchema):
|
||||
"""角色信息响应模型"""
|
||||
class RoleOutSchema(RoleCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""
|
||||
角色信息响应模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -98,36 +104,23 @@ class RoleOutSchema(RoleCreateSchema, BaseSchema):
|
||||
depts: list[DeptOutSchema] = Field(default_factory=list, description="角色部门列表")
|
||||
|
||||
|
||||
class RoleQueryParam:
|
||||
"""角色管理查询参数"""
|
||||
@dataclass
|
||||
class RoleQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""
|
||||
角色管理查询参数
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="角色名称"),
|
||||
description: 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"],
|
||||
),
|
||||
updated_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="更新时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
code: str | None = Query(None, description="角色编码"),
|
||||
status: int | None = Query(None, description="状态(0:启动 1:停用)"),
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
super().__init__(*args, **kwargs)
|
||||
self.name = (QueueEnum.like.value, name)
|
||||
if description:
|
||||
self.description = (QueueEnum.like.value, description)
|
||||
|
||||
# 精确查询字段
|
||||
if status:
|
||||
if code:
|
||||
self.code = (QueueEnum.like.value, code)
|
||||
if status is not None:
|
||||
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]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from app.api.v1.module_platform.tenant.service import TenantService
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
@@ -15,10 +16,14 @@ from .schema import (
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""角色模块服务层"""
|
||||
"""
|
||||
角色管理服务
|
||||
|
||||
提供角色 CRUD、权限配置、数据权限范围设置、批量启/禁用、Excel 导出等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_role_detail_service(cls, auth: AuthSchema, id: int) -> RoleOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> RoleOutSchema:
|
||||
"""
|
||||
获取角色详情
|
||||
|
||||
@@ -27,15 +32,12 @@ class RoleService:
|
||||
- id (int): 角色ID
|
||||
|
||||
返回:
|
||||
- dict: 角色详情字典
|
||||
- RoleOutSchema: 角色详情响应模型
|
||||
"""
|
||||
role = await RoleCRUD(auth).get(id=id)
|
||||
if not role:
|
||||
raise CustomException(msg="角色不存在")
|
||||
return RoleOutSchema.model_validate(role)
|
||||
return await RoleCRUD(auth).get_or_404(id=id, out_schema=RoleOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def get_role_list_service(
|
||||
async def list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: RoleQueryParam | None = None,
|
||||
@@ -50,13 +52,13 @@ class RoleService:
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- list[RoleOutSchema]: 角色详情字典列表
|
||||
- list[RoleOutSchema]: 角色响应模型列表
|
||||
"""
|
||||
role_list = await RoleCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
role_list = await RoleCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
return [RoleOutSchema.model_validate(role) for role in role_list]
|
||||
|
||||
@classmethod
|
||||
async def get_role_page_service(
|
||||
async def page_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
@@ -82,12 +84,12 @@ class RoleService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=RoleOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_role_service(cls, auth: AuthSchema, data: RoleCreateSchema) -> RoleOutSchema:
|
||||
async def create_service(cls, auth: AuthSchema, data: RoleCreateSchema) -> RoleOutSchema:
|
||||
"""
|
||||
创建角色
|
||||
|
||||
@@ -96,25 +98,23 @@ class RoleService:
|
||||
- data (RoleCreateSchema): 创建角色模型
|
||||
|
||||
返回:
|
||||
- dict: 新创建的角色详情字典
|
||||
- RoleOutSchema: 新创建的角色响应模型
|
||||
"""
|
||||
role = await RoleCRUD(auth).get(name=data.name)
|
||||
if role:
|
||||
raise CustomException(msg="创建失败,该角色已存在")
|
||||
raise CustomException(msg="创建失败,该数据已存在")
|
||||
obj = await RoleCRUD(auth).get(code=data.code)
|
||||
if obj:
|
||||
raise CustomException(msg="创建失败,编码已存在")
|
||||
|
||||
# 检查租户配额
|
||||
from app.api.v1.module_platform.tenant.service import TenantService
|
||||
|
||||
await TenantService.check_quota_service(auth, auth.tenant_id, "role")
|
||||
|
||||
new_role = await RoleCRUD(auth).create(data=data)
|
||||
return RoleOutSchema.model_validate(new_role)
|
||||
|
||||
@classmethod
|
||||
async def update_role_service(cls, auth: AuthSchema, id: int, data: RoleUpdateSchema) -> RoleOutSchema:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: RoleUpdateSchema) -> RoleOutSchema:
|
||||
"""
|
||||
更新角色
|
||||
|
||||
@@ -124,14 +124,12 @@ class RoleService:
|
||||
- data (RoleUpdateSchema): 更新角色模型
|
||||
|
||||
返回:
|
||||
- dict: 更新后的角色详情字典
|
||||
- RoleOutSchema: 更新后的角色响应模型
|
||||
"""
|
||||
role = await RoleCRUD(auth).get(id=id)
|
||||
if not role:
|
||||
raise CustomException(msg="更新失败,该角色不存在")
|
||||
_ = await RoleCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在")
|
||||
exist_role = await RoleCRUD(auth).get(name=data.name)
|
||||
if exist_role and exist_role.id != id:
|
||||
raise CustomException(msg="更新失败,角色名称重复")
|
||||
raise CustomException(msg="更新失败,名称已存在")
|
||||
exist_code = await RoleCRUD(auth).get(code=data.code)
|
||||
if exist_code and exist_code.id != id:
|
||||
raise CustomException(msg="更新失败,角色编码已存在")
|
||||
@@ -139,7 +137,7 @@ class RoleService:
|
||||
return RoleOutSchema.model_validate(updated_role)
|
||||
|
||||
@classmethod
|
||||
async def delete_role_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除角色
|
||||
|
||||
@@ -156,14 +154,12 @@ class RoleService:
|
||||
# 批量校验角色存在性
|
||||
roles = await RoleCRUD(auth).list(search={"id": ("in", ids)})
|
||||
if len(roles) != len(ids):
|
||||
found = {r.id for r in roles}
|
||||
missing = [rid for rid in ids if rid not in found]
|
||||
raise CustomException(msg=f"角色 ID {missing} 不存在")
|
||||
raise CustomException(msg="删除失败,部分ID不存在")
|
||||
|
||||
await RoleCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_role_permission_service(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None:
|
||||
async def set_permission_service(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None:
|
||||
"""
|
||||
设置角色权限
|
||||
|
||||
@@ -187,7 +183,7 @@ class RoleService:
|
||||
await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
|
||||
|
||||
@classmethod
|
||||
async def set_role_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
设置角色可用状态
|
||||
|
||||
@@ -202,11 +198,11 @@ class RoleService:
|
||||
role_map = {r.id: r for r in roles}
|
||||
for rid in data.ids:
|
||||
if rid not in role_map:
|
||||
raise CustomException(msg=f"角色ID {rid} 不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
await RoleCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def export_role_list_service(cls, role_list: list[dict[str, Any]]) -> bytes:
|
||||
async def export_list_service(cls, role_list: list[dict[str, Any]]) -> bytes:
|
||||
"""
|
||||
导出角色列表
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=[
|
||||
|
||||
|
||||
@TicketRouter.get("/list", summary="工单列表", response_model=ResponseSchema[PageResultSchema[TicketOutSchema]])
|
||||
async def ticket_list(
|
||||
async def ticket_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[TicketQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:query"]))],
|
||||
@@ -49,8 +49,8 @@ async def ticket_list(
|
||||
|
||||
|
||||
@TicketRouter.get("/detail/{id}", summary="工单详情", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_detail(
|
||||
id: Annotated[int, Path()],
|
||||
async def ticket_detail_controller(
|
||||
id: Annotated[int, Path(description="ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
@@ -68,7 +68,7 @@ async def ticket_detail(
|
||||
|
||||
|
||||
@TicketRouter.post("/create", summary="创建工单", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_create(
|
||||
async def ticket_create_controller(
|
||||
data: TicketCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:create"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -87,8 +87,8 @@ async def ticket_create(
|
||||
|
||||
|
||||
@TicketRouter.put("/update/{id}", summary="更新工单", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_update(
|
||||
id: Annotated[int, Path()],
|
||||
async def ticket_update_controller(
|
||||
id: Annotated[int, Path(description="ID")],
|
||||
data: TicketUpdateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:update"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -108,7 +108,7 @@ async def ticket_update(
|
||||
|
||||
|
||||
@TicketRouter.put("/batch", summary="批量更新工单", response_model=ResponseSchema)
|
||||
async def ticket_batch_update(
|
||||
async def ticket_batch_update_controller(
|
||||
data: TicketBatchSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:update"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -127,7 +127,7 @@ async def ticket_batch_update(
|
||||
|
||||
|
||||
@TicketRouter.delete("/delete", summary="删除工单", response_model=ResponseSchema[None])
|
||||
async def ticket_delete(
|
||||
async def ticket_delete_controller(
|
||||
ids: Annotated[list[int], Body()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:delete"]))],
|
||||
) -> JSONResponse:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
@@ -16,10 +16,16 @@ class TicketModel(ModelMixin, TenantMixin, UserMixin):
|
||||
|
||||
__tablename__: str = "sys_ticket"
|
||||
__table_args__: dict[str, str] = {"comment": "工单表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "assigned_by"]
|
||||
__loader_options__: list[str] = [
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"assigned_by",
|
||||
"tenant_by",
|
||||
]
|
||||
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="工单标题")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:待处理 1:处理中 2:已完成 3:已关闭)", index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
ticket_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment="工单内容(富文本)")
|
||||
summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="工单内容(纯文本摘要)")
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.common.enums import QueueEnum, TicketTypeEnum
|
||||
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
|
||||
from app.core.base_schema import BaseSchema, CommonSchema, TenantBySchema, UserBySchema
|
||||
|
||||
@@ -13,18 +13,10 @@ class TicketCreateSchema(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=200, description="工单标题")
|
||||
ticket_content: str = Field(default="", description="工单内容(富文本)")
|
||||
summary: str | None = Field(default=None, description="工单内容(纯文本摘要)")
|
||||
ticket_type: str = Field(default="suggestion", max_length=20, description="工单类型(suggestion/bug/optimize/other)")
|
||||
ticket_type: TicketTypeEnum = Field(default=TicketTypeEnum.SUGGESTION, description="工单类型(suggestion/bug/optimize/other)")
|
||||
images: str | None = Field(default=None, description="图片URL列表(JSON数组)")
|
||||
description: str | None = Field(default=None, max_length=255, description="工单描述")
|
||||
|
||||
@field_validator("ticket_type")
|
||||
@classmethod
|
||||
def _validate_ticket_type(cls, v: str) -> str:
|
||||
allowed = {"suggestion", "bug", "optimize", "other"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"工单类型仅支持 suggestion、bug、optimize、other,当前值: {v}")
|
||||
return v
|
||||
|
||||
@field_validator("title")
|
||||
@classmethod
|
||||
def _validate_title(cls, v: str) -> str:
|
||||
@@ -40,22 +32,12 @@ class TicketUpdateSchema(BaseModel):
|
||||
title: str | None = Field(default=None, max_length=200, description="工单标题")
|
||||
ticket_content: str | None = Field(default=None, description="工单内容(富文本)")
|
||||
summary: str | None = Field(default=None, description="工单内容(纯文本摘要)")
|
||||
ticket_type: str | None = Field(default=None, max_length=20, description="工单类型")
|
||||
ticket_type: TicketTypeEnum | None = Field(default=None, description="工单类型")
|
||||
status: int | None = Field(default=None, ge=0, le=3, description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
reply: str | None = Field(default=None, description="回复内容")
|
||||
assigned_id: int | None = Field(default=None, gt=0, description="处理人ID")
|
||||
description: str | None = Field(default=None, max_length=255, description="工单描述")
|
||||
|
||||
@field_validator("ticket_type")
|
||||
@classmethod
|
||||
def _validate_ticket_type(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {"suggestion", "bug", "optimize", "other"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"工单类型仅支持 suggestion、bug、optimize、other,当前值: {v}")
|
||||
return v
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: int | None) -> int | None:
|
||||
@@ -71,18 +53,15 @@ class TicketOutSchema(BaseSchema, UserBySchema, TenantBySchema):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
title: str
|
||||
ticket_content: str | None = None
|
||||
summary: str | None = None
|
||||
ticket_type: str
|
||||
status: int
|
||||
images: str | None = None
|
||||
reply: str | None = None
|
||||
assigned_id: int | None = None
|
||||
created_by: CommonSchema | None = None
|
||||
updated_by: CommonSchema | None = None
|
||||
assigned_by: CommonSchema | None = None
|
||||
title: str = Field(..., description="工单标题")
|
||||
ticket_content: str | None = Field(default=None, description="工单内容")
|
||||
summary: str | None = Field(default=None, description="摘要")
|
||||
ticket_type: TicketTypeEnum = Field(..., description="工单类型")
|
||||
status: int = Field(..., description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
images: str | None = Field(default=None, description="图片")
|
||||
reply: str | None = Field(default=None, description="回复内容")
|
||||
assigned_id: int | None = Field(default=None, description="指派人ID")
|
||||
assigned_by: CommonSchema | None = Field(default=None, description="指派人")
|
||||
|
||||
|
||||
class TicketBatchSchema(BaseModel):
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
@@ -26,7 +29,11 @@ _TICKET_STATUS_LABELS = {
|
||||
|
||||
|
||||
class TicketService:
|
||||
"""工单管理服务层"""
|
||||
"""
|
||||
工单管理服务
|
||||
|
||||
提供工单 CRUD、状态流转校验、批量更新、分配处理人等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _validate_status_transition(
|
||||
@@ -35,13 +42,23 @@ class TicketService:
|
||||
ticket,
|
||||
new_status: int,
|
||||
) -> None:
|
||||
"""校验工单状态流转是否合法"""
|
||||
"""
|
||||
校验工单状态流转是否合法
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ticket: 工单对象
|
||||
- new_status (int): 新状态
|
||||
|
||||
异常:
|
||||
- CustomException: 状态流转不合法或权限不足
|
||||
"""
|
||||
old_status = ticket.status if ticket.status is not None else 0
|
||||
old_label = _TICKET_STATUS_LABELS.get(old_status, str(old_status))
|
||||
new_label = _TICKET_STATUS_LABELS.get(new_status, str(new_status))
|
||||
|
||||
if new_status not in _TICKET_STATUS_TRANSITIONS.get(old_status, set()):
|
||||
raise CustomException(msg=f"不允许从“{old_label}”转换为“{new_label}”")
|
||||
raise CustomException(msg=f"不允许从{old_label}转换为{new_label}")
|
||||
|
||||
is_super = auth.user and auth.user.is_superuser
|
||||
is_creator = auth.user and ticket.created_id == auth.user.id
|
||||
@@ -75,23 +92,53 @@ class TicketService:
|
||||
search: TicketQueryParam | None = None,
|
||||
order_by: list | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
分页查询工单
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页条数
|
||||
- search (TicketQueryParam | None): 查询参数
|
||||
- order_by (list | None): 排序参数
|
||||
|
||||
返回:
|
||||
- dict: 分页结果
|
||||
"""
|
||||
return await TicketCRUD(auth).page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"created_time": "desc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=TicketOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> TicketOutSchema:
|
||||
obj = await TicketCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="工单不存在")
|
||||
return TicketOutSchema.model_validate(obj)
|
||||
"""
|
||||
获取工单详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 工单ID
|
||||
|
||||
返回:
|
||||
- TicketOutSchema: 工单详情响应模型
|
||||
"""
|
||||
return await TicketCRUD(auth).get_or_404(id=id, out_schema=TicketOutSchema)
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: TicketCreateSchema) -> TicketOutSchema:
|
||||
"""
|
||||
创建工单
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (TicketCreateSchema): 工单创建数据
|
||||
|
||||
返回:
|
||||
- TicketOutSchema: 创建后的工单响应模型
|
||||
"""
|
||||
obj = await TicketCRUD(auth).create(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建工单失败")
|
||||
@@ -99,19 +146,24 @@ class TicketService:
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: TicketUpdateSchema) -> TicketOutSchema:
|
||||
obj = await TicketCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="工单不存在")
|
||||
"""
|
||||
更新工单
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 工单ID
|
||||
- data (TicketUpdateSchema): 工单更新数据
|
||||
|
||||
返回:
|
||||
- TicketOutSchema: 更新后的工单响应模型
|
||||
"""
|
||||
obj = await TicketCRUD(auth).get_or_404(id=id, msg="工单不存在")
|
||||
|
||||
if data.status is not None:
|
||||
cls._validate_status_transition(auth, obj, data.status)
|
||||
|
||||
# 校验 assigned_id:分配处理人时验证用户是否存在且属于同一租户
|
||||
if data.assigned_id is not None:
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
user_stmt = select(UserModel).where(
|
||||
UserModel.id == data.assigned_id,
|
||||
UserModel.is_deleted.is_(False),
|
||||
@@ -125,18 +177,31 @@ class TicketService:
|
||||
|
||||
updated = await TicketCRUD(auth).update(id=id, data=data)
|
||||
if not updated:
|
||||
raise CustomException(msg="更新失败")
|
||||
raise CustomException(msg="工单不存在")
|
||||
return TicketOutSchema.model_validate(updated)
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除工单
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 工单ID列表
|
||||
"""
|
||||
if not ids:
|
||||
raise CustomException(msg="删除对象不能为空")
|
||||
await TicketCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def batch_service(cls, auth: AuthSchema, data: TicketBatchSchema) -> None:
|
||||
"""批量更新工单状态"""
|
||||
"""
|
||||
批量更新工单状态
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (TicketBatchSchema): 批量更新参数
|
||||
"""
|
||||
if not data.ids:
|
||||
raise CustomException(msg="请选择要操作的工单")
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ async def get_current_user_info_controller(
|
||||
返回:
|
||||
- JSONResponse: 当前用户信息JSON响应
|
||||
"""
|
||||
result_dict = await UserService.get_current_user_info_service(auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取当前用户信息成功")
|
||||
user_dict = await UserService.current_info_service(auth=auth)
|
||||
return SuccessResponse(data=user_dict, msg="获取当前用户信息成功")
|
||||
|
||||
|
||||
@UserRouter.put(
|
||||
@@ -69,7 +69,7 @@ async def update_current_user_info_controller(
|
||||
返回:
|
||||
- JSONResponse: 更新当前用户基本信息JSON响应
|
||||
"""
|
||||
result_dict = await UserService.update_current_user_info_service(data=data, auth=auth)
|
||||
result_dict = await UserService.update_current_info_service(auth=auth, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="更新当前用户基本信息成功")
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ async def change_current_user_password_controller(
|
||||
返回:
|
||||
- JSONResponse: 修改密码JSON响应
|
||||
"""
|
||||
result_dict = await UserService.change_user_password_service(data=data, auth=auth)
|
||||
result_dict = await UserService.change_password_service(auth=auth, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录")
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ async def reset_password_controller(
|
||||
- JSONResponse: 重置密码JSON响应
|
||||
"""
|
||||
data.id = id
|
||||
result_dict = await UserService.reset_user_password_service(data=data, auth=auth)
|
||||
result_dict = await UserService.reset_password_service(auth=auth, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ async def register_user_controller(
|
||||
- JSONResponse: 注册用户JSON响应
|
||||
"""
|
||||
auth = AuthSchema(db=db)
|
||||
user_register_result = await UserService.register_user_service(data=data, auth=auth)
|
||||
user_register_result = await UserService.register_service(data=data, auth=auth)
|
||||
logger.info(f"{data.username} 注册用户成功: {user_register_result}")
|
||||
return SuccessResponse(data=user_register_result, msg="注册用户成功")
|
||||
|
||||
@@ -177,7 +177,7 @@ async def forget_password_controller(
|
||||
summary="查询用户",
|
||||
response_model=ResponseSchema[PageResultSchema[UserOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
async def get_user_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[UserQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:query"]))],
|
||||
@@ -193,7 +193,7 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- JSONResponse: 分页查询结果JSON响应
|
||||
"""
|
||||
result_dict = await UserService.get_user_page_service(
|
||||
result_dict = await UserService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
@@ -208,7 +208,7 @@ async def get_obj_list_controller(
|
||||
summary="查询用户详情",
|
||||
response_model=ResponseSchema[UserOutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
async def get_user_detail_controller(
|
||||
id: Annotated[int, Path(description="用户ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:detail"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -222,7 +222,7 @@ async def get_obj_detail_controller(
|
||||
返回:
|
||||
- JSONResponse: 用户详情JSON响应
|
||||
"""
|
||||
result_dict = await UserService.get_detail_by_id_service(id=id, auth=auth)
|
||||
result_dict = await UserService.detail_service(auth=auth, id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取用户详情成功")
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ async def get_obj_detail_controller(
|
||||
summary="创建用户",
|
||||
response_model=ResponseSchema[UserOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
async def create_user_controller(
|
||||
data: UserCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:create"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -249,7 +249,7 @@ async def create_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 创建用户JSON响应
|
||||
"""
|
||||
result_dict = await UserService.create_user_service(data=data, auth=auth)
|
||||
result_dict = await UserService.create_service(data=data, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="创建用户成功")
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ async def create_obj_controller(
|
||||
summary="修改用户",
|
||||
response_model=ResponseSchema[UserOutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
async def update_user_controller(
|
||||
data: UserUpdateSchema,
|
||||
id: Annotated[int, Path(description="用户ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:update"]))],
|
||||
@@ -274,7 +274,7 @@ async def update_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 修改用户JSON响应
|
||||
"""
|
||||
result_dict = await UserService.update_user_service(id=id, data=data, auth=auth)
|
||||
result_dict = await UserService.update_service(auth=auth, id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="修改用户成功")
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ async def update_obj_controller(
|
||||
summary="删除用户",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
async def delete_user_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:delete"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -297,7 +297,7 @@ async def delete_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 删除用户JSON响应
|
||||
"""
|
||||
await UserService.delete_user_service(ids=ids, auth=auth)
|
||||
await UserService.delete_service(auth=auth, ids=ids)
|
||||
return SuccessResponse(msg="删除用户成功")
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ async def delete_obj_controller(
|
||||
summary="批量修改用户状态",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
async def batch_set_available_user_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:patch"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -320,7 +320,7 @@ async def batch_set_available_obj_controller(
|
||||
返回:
|
||||
- JSONResponse: 批量修改用户状态JSON响应
|
||||
"""
|
||||
await UserService.set_user_available_service(data=data, auth=auth)
|
||||
await UserService.set_available_service(auth=auth, data=data)
|
||||
return SuccessResponse(msg="批量修改用户状态成功")
|
||||
|
||||
|
||||
@@ -330,14 +330,14 @@ async def batch_set_available_obj_controller(
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_system:user:download"]))],
|
||||
)
|
||||
async def export_obj_template_controller() -> StreamingResponse:
|
||||
async def export_user_import_template_controller() -> StreamingResponse:
|
||||
"""
|
||||
获取用户导入模板
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 用户导入模板流响应
|
||||
"""
|
||||
user_import_template_result = await UserService.get_import_template_user_service()
|
||||
user_import_template_result = await UserService.get_import_template_service()
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(user_import_template_result),
|
||||
@@ -354,7 +354,7 @@ async def export_obj_template_controller() -> StreamingResponse:
|
||||
summary="导出用户",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
async def export_user_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[UserQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:export"]))],
|
||||
@@ -370,8 +370,8 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 用户导出模板流响应
|
||||
"""
|
||||
user_list = await UserService.get_user_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
user_export_result = await UserService.export_user_list_service(user_list)
|
||||
user_list = await UserService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
user_export_result = await UserService.export_list_service(user_list=user_list)
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(user_export_result),
|
||||
@@ -385,7 +385,7 @@ async def export_obj_list_controller(
|
||||
summary="导入用户",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def import_obj_list_controller(
|
||||
async def import_user_list_controller(
|
||||
file: UploadFile,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:import"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -399,5 +399,5 @@ async def import_obj_list_controller(
|
||||
返回:
|
||||
- JSONResponse: 导入用户JSON响应
|
||||
"""
|
||||
batch_import_result = await UserService.batch_import_user_service(file=file, auth=auth, update_support=True)
|
||||
batch_import_result = await UserService.batch_import_service(auth=auth, file=file, update_support=True)
|
||||
return SuccessResponse(data=batch_import_result, msg="导入用户成功")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import MappedBase, ModelMixin, TenantMixin, UserMixin
|
||||
@@ -87,10 +87,14 @@ class UserModel(ModelMixin, TenantMixin, UserMixin):
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
|
||||
dept_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("sys_dept.id", ondelete="SET NULL", onupdate="CASCADE"), nullable=True, index=True, comment="部门ID")
|
||||
tenant: Mapped["TenantModel | None"] = relationship("TenantModel", foreign_keys="UserModel.tenant_id", lazy="selectin", viewonly=True,)
|
||||
tenant: Mapped["TenantModel | None"] = relationship(
|
||||
"TenantModel",
|
||||
foreign_keys="UserModel.tenant_id",
|
||||
lazy="selectin",
|
||||
viewonly=True,
|
||||
)
|
||||
dept: Mapped["DeptModel | None"] = relationship(back_populates="users", foreign_keys=[dept_id], lazy="selectin")
|
||||
roles: Mapped[list["RoleModel"]] = relationship(secondary="sys_user_roles", back_populates="users", lazy="selectin")
|
||||
positions: Mapped[list["PositionModel"]] = relationship(secondary="sys_user_positions", back_populates="users", lazy="selectin")
|
||||
created_by: Mapped["UserModel | None"] = relationship("UserModel", foreign_keys="UserModel.created_id", remote_side="UserModel.id", lazy="selectin", uselist=False, viewonly=True)
|
||||
updated_by: Mapped["UserModel | None"] = relationship("UserModel", foreign_keys="UserModel.updated_id", remote_side="UserModel.id", lazy="selectin", uselist=False, viewonly=True)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Query
|
||||
@@ -13,8 +14,9 @@ from pydantic import (
|
||||
from app.api.v1.module_platform.menu.schema import MenuOutSchema
|
||||
from app.api.v1.module_system.role.schema import RoleOutSchema
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
|
||||
from app.core.base_schema import BaseSchema, CommonSchema, TenantBySchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, email_validator, mobile_validator
|
||||
from app.core.validator import email_validator, mobile_validator
|
||||
|
||||
|
||||
class CurrentUserUpdateSchema(BaseModel):
|
||||
@@ -92,6 +94,7 @@ class UserRegisterSchema(BaseModel):
|
||||
if not v:
|
||||
raise ValueError("账号不能为空")
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
|
||||
raise ValueError("账号需以字母开头,3-32 位,仅允许字母、数字、_ . -")
|
||||
return v
|
||||
@@ -133,6 +136,7 @@ class UserForgetPasswordSchema(BaseModel):
|
||||
if not v:
|
||||
raise ValueError("账号不能为空")
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
|
||||
raise ValueError("账号需以字母开头,3-32 位,仅允许字母、数字、_ . -")
|
||||
return v
|
||||
@@ -189,9 +193,9 @@ class ResetPasswordSchema(BaseModel):
|
||||
|
||||
|
||||
class UserCreateSchema(CurrentUserUpdateSchema):
|
||||
"""新增"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
"""
|
||||
新增用户
|
||||
"""
|
||||
|
||||
username: str | None = Field(default=None, max_length=32, description="用户名")
|
||||
password: str | None = Field(default=None, min_length=6, max_length=128, description="密码")
|
||||
@@ -219,6 +223,7 @@ class UserCreateSchema(CurrentUserUpdateSchema):
|
||||
return value
|
||||
v = value.strip()
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,31}$", v):
|
||||
raise ValueError("账号需以字母开头,2-32 位,仅允许字母、数字、_ . -")
|
||||
return v
|
||||
@@ -262,6 +267,7 @@ class UserUpdateSchema(CurrentUserUpdateSchema):
|
||||
return value
|
||||
v = value.strip()
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,31}$", v):
|
||||
raise ValueError("账号需以字母开头,2-32 位,仅允许字母、数字、_ . -")
|
||||
return v
|
||||
@@ -290,8 +296,17 @@ class UserOutSchema(UserUpdateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
menus: list[MenuOutSchema] | None = Field(default=[], description="菜单")
|
||||
|
||||
|
||||
class UserQueryParam:
|
||||
"""用户管理查询参数"""
|
||||
@dataclass
|
||||
class UserQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""
|
||||
用户管理查询参数(继承标准 Mixin)
|
||||
|
||||
支持:
|
||||
- 时间范围(BaseQueryParam)
|
||||
- 创建人/更新人筛选(UserByQueryParam)
|
||||
- 租户筛选(TenantByQueryParam)
|
||||
- 业务字段:用户名、名称、手机号、邮箱、部门、状态
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -304,37 +319,18 @@ class UserQueryParam:
|
||||
pattern=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||
),
|
||||
dept_id: int | None = Query(None, description="部门ID"),
|
||||
tenant_id: int | None = Query(None, description="租户ID(仅平台管理员可筛选)"),
|
||||
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"],
|
||||
),
|
||||
updated_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="更新时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
super().__init__(*args, **kwargs)
|
||||
self.username = (QueueEnum.like.value, username)
|
||||
self.name = (QueueEnum.like.value, name)
|
||||
self.mobile = (QueueEnum.like.value, mobile)
|
||||
self.email = (QueueEnum.like.value, email)
|
||||
|
||||
# 精确查询字段
|
||||
self.dept_id = (QueueEnum.eq.value, dept_id)
|
||||
self.tenant_id = (QueueEnum.eq.value, tenant_id)
|
||||
self.created_id = (QueueEnum.eq.value, created_id)
|
||||
self.updated_id = (QueueEnum.eq.value, updated_id)
|
||||
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]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
if mobile:
|
||||
self.mobile = (QueueEnum.like.value, mobile)
|
||||
if email:
|
||||
self.email = (QueueEnum.like.value, email)
|
||||
if dept_id:
|
||||
self.dept_id = (QueueEnum.eq.value, dept_id)
|
||||
if status:
|
||||
self.status = (QueueEnum.eq.value, status)
|
||||
|
||||
@@ -6,6 +6,8 @@ from fastapi import UploadFile
|
||||
|
||||
from app.api.v1.module_platform.menu.crud import MenuCRUD
|
||||
from app.api.v1.module_platform.menu.schema import MenuOutSchema
|
||||
from app.api.v1.module_platform.package.service import PackageService
|
||||
from app.api.v1.module_platform.tenant.service import TenantService
|
||||
from app.api.v1.module_system.dept.crud import DeptCRUD
|
||||
from app.api.v1.module_system.position.crud import PositionCRUD
|
||||
from app.api.v1.module_system.role.crud import RoleCRUD
|
||||
@@ -31,10 +33,14 @@ from .schema import (
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户模块服务层"""
|
||||
"""
|
||||
用户管理服务
|
||||
|
||||
提供用户 CRUD、密码管理、状态切换、批量导入/导出、当前用户信息获取/更新、忘记密码/注册等业务能力。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_detail_by_id_service(cls, auth: AuthSchema, id: int) -> UserOutSchema:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> UserOutSchema:
|
||||
"""
|
||||
根据ID获取用户详情
|
||||
|
||||
@@ -45,9 +51,7 @@ class UserService:
|
||||
返回:
|
||||
- dict: 用户详情字典
|
||||
"""
|
||||
user = await UserCRUD(auth).get(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
user = await UserCRUD(auth).get_or_404(id=id)
|
||||
|
||||
result = UserOutSchema.model_validate(user)
|
||||
# 如果用户绑定了部门,则获取部门名称
|
||||
@@ -58,7 +62,7 @@ class UserService:
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def get_user_list_service(cls, auth: AuthSchema, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[UserOutSchema]:
|
||||
async def list_service(cls, auth: AuthSchema, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[UserOutSchema]:
|
||||
"""
|
||||
获取用户列表
|
||||
|
||||
@@ -70,7 +74,7 @@ class UserService:
|
||||
返回:
|
||||
- list[dict]: 用户详情字典列表
|
||||
"""
|
||||
user_list = await UserCRUD(auth).list(search=search.__dict__ if search else {}, order_by=order_by)
|
||||
user_list = await UserCRUD(auth).list(search=vars(search) if search else None, order_by=order_by)
|
||||
user_dict_list = []
|
||||
for user in user_list:
|
||||
user_dict = UserOutSchema.model_validate(user)
|
||||
@@ -79,7 +83,7 @@ class UserService:
|
||||
return user_dict_list
|
||||
|
||||
@classmethod
|
||||
async def get_user_page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> dict:
|
||||
async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> dict:
|
||||
"""
|
||||
分页查询用户(数据库 OFFSET/LIMIT)。
|
||||
|
||||
@@ -98,12 +102,12 @@ class UserService:
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search.__dict__ if search else {},
|
||||
search=vars(search) if search else None,
|
||||
out_schema=UserOutSchema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_user_service(cls, data: UserCreateSchema, auth: AuthSchema) -> UserOutSchema:
|
||||
async def create_service(cls, data: UserCreateSchema, auth: AuthSchema) -> UserOutSchema:
|
||||
"""
|
||||
创建用户
|
||||
|
||||
@@ -128,11 +132,9 @@ class UserService:
|
||||
if data.dept_id:
|
||||
dept = await DeptCRUD(auth).get(id=data.dept_id)
|
||||
if not dept:
|
||||
raise CustomException(msg="部门不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
|
||||
# 检查租户配额
|
||||
from app.api.v1.module_platform.tenant.service import TenantService
|
||||
|
||||
await TenantService.check_quota_service(auth, auth.tenant_id, "user")
|
||||
|
||||
# 创建用户
|
||||
@@ -152,7 +154,7 @@ class UserService:
|
||||
return new_user_dict
|
||||
|
||||
@classmethod
|
||||
async def update_user_service(cls, id: int, data: UserUpdateSchema, auth: AuthSchema) -> UserOutSchema:
|
||||
async def update_service(cls, id: int, data: UserUpdateSchema, auth: AuthSchema) -> UserOutSchema:
|
||||
"""
|
||||
更新用户
|
||||
|
||||
@@ -168,9 +170,7 @@ class UserService:
|
||||
raise CustomException(msg="账号不能为空")
|
||||
|
||||
# 检查用户是否存在
|
||||
user = await UserCRUD(auth).get(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
user = await UserCRUD(auth).get_or_404(id=id)
|
||||
|
||||
# 检查是否尝试修改超级管理员
|
||||
if user.is_superuser:
|
||||
@@ -179,22 +179,22 @@ class UserService:
|
||||
# 检查用户名是否重复
|
||||
exist_user = await UserCRUD(auth).get(username=data.username)
|
||||
if exist_user and exist_user.id != id:
|
||||
raise CustomException(msg="已存在相同的账号")
|
||||
raise CustomException(msg="更新失败,账号已存在")
|
||||
# 新增:检查手机号是否重复
|
||||
if data.mobile:
|
||||
exist_mobile_user = await UserCRUD(auth).get(mobile=data.mobile)
|
||||
if exist_mobile_user and exist_mobile_user.id != id:
|
||||
raise CustomException(msg="更新失败,手机号已存在")
|
||||
raise CustomException(msg="该数据已存在")
|
||||
# 新增:检查邮箱是否重复
|
||||
if data.email:
|
||||
exist_email_user = await UserCRUD(auth).get(email=data.email)
|
||||
if exist_email_user and exist_email_user.id != id:
|
||||
raise CustomException(msg="更新失败,邮箱已存在")
|
||||
raise CustomException(msg="该数据已存在")
|
||||
# 检查部门是否存在且可用
|
||||
if data.dept_id:
|
||||
dept = await DeptCRUD(auth).get(id=data.dept_id)
|
||||
if not dept:
|
||||
raise CustomException(msg="部门不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if dept.status == 1:
|
||||
raise CustomException(msg="部门已被禁用")
|
||||
|
||||
@@ -206,25 +206,25 @@ class UserService:
|
||||
# 检查角色是否都存在且可用
|
||||
roles = await RoleCRUD(auth).list(search={"id": ("in", data.role_ids)})
|
||||
if len(roles) != len(data.role_ids):
|
||||
raise CustomException(msg="部分角色不存在")
|
||||
raise CustomException(msg="更新失败,部分角色不存在")
|
||||
if not all(role.status == 0 for role in roles):
|
||||
raise CustomException(msg="部分角色已被禁用")
|
||||
raise CustomException(msg="更新失败,部分角色已被禁用")
|
||||
await UserCRUD(auth).set_user_roles(user_ids=[id], role_ids=data.role_ids)
|
||||
|
||||
if data.position_ids and len(data.position_ids) > 0:
|
||||
# 检查岗位是否都存在且可用
|
||||
positions = await PositionCRUD(auth).list(search={"id": ("in", data.position_ids)})
|
||||
if len(positions) != len(data.position_ids):
|
||||
raise CustomException(msg="部分岗位不存在")
|
||||
raise CustomException(msg="更新失败,部分岗位不存在")
|
||||
if not all(position.status == 0 for position in positions):
|
||||
raise CustomException(msg="部分岗位已被禁用")
|
||||
raise CustomException(msg="更新失败,部分岗位已被禁用")
|
||||
await UserCRUD(auth).set_user_positions(user_ids=[id], position_ids=data.position_ids)
|
||||
|
||||
user_dict = UserOutSchema.model_validate(new_user)
|
||||
return user_dict
|
||||
|
||||
@classmethod
|
||||
async def delete_user_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除用户
|
||||
|
||||
@@ -243,7 +243,7 @@ class UserService:
|
||||
for uid in ids:
|
||||
user = user_map.get(uid)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员不能删除")
|
||||
if user.status == 0:
|
||||
@@ -260,7 +260,7 @@ class UserService:
|
||||
await UserCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def get_current_user_info_service(cls, auth: AuthSchema) -> UserOutSchema:
|
||||
async def current_info_service(cls, auth: AuthSchema) -> UserOutSchema:
|
||||
"""
|
||||
获取当前用户信息
|
||||
|
||||
@@ -272,7 +272,7 @@ class UserService:
|
||||
"""
|
||||
# 获取用户基本信息
|
||||
if not auth.user or not auth.user.id:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
user = await UserCRUD(auth).get(id=auth.user.id)
|
||||
user_dict = UserOutSchema.model_validate(user)
|
||||
# 获取部门名称
|
||||
@@ -283,7 +283,7 @@ class UserService:
|
||||
_pc_only = {"client": "pc"}
|
||||
if auth.user and auth.user.is_superuser:
|
||||
# 使用树形结构查询,预加载children关系(含 type=3 按钮,供前端权限列表使用)
|
||||
menu_all = await MenuCRUD(auth).get_tree_list(
|
||||
menu_all = await MenuCRUD(auth).tree_list(
|
||||
search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only},
|
||||
order_by=[{"order": "asc"}],
|
||||
)
|
||||
@@ -295,8 +295,6 @@ class UserService:
|
||||
|
||||
# 租户菜单约束:非超管用户只能看到租户菜单权限内的菜单
|
||||
if menu_ids and auth.tenant_id:
|
||||
from app.api.v1.module_platform.package.service import PackageService
|
||||
|
||||
allowed_ids = await PackageService.get_tenant_available_menu_ids(auth, auth.tenant_id)
|
||||
allowed_set = set(allowed_ids)
|
||||
menu_ids = menu_ids & allowed_set
|
||||
@@ -305,7 +303,7 @@ class UserService:
|
||||
menus = (
|
||||
[
|
||||
MenuOutSchema.model_validate(menu)
|
||||
for menu in await MenuCRUD(auth).get_tree_list(
|
||||
for menu in await MenuCRUD(auth).tree_list(
|
||||
search={"id": ("in", list(menu_ids)), **_pc_only},
|
||||
order_by=[{"order": "asc"}],
|
||||
)
|
||||
@@ -317,7 +315,7 @@ class UserService:
|
||||
return user_dict
|
||||
|
||||
@classmethod
|
||||
async def update_current_user_info_service(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> UserOutSchema:
|
||||
async def update_current_info_service(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> UserOutSchema:
|
||||
"""
|
||||
更新当前用户信息
|
||||
|
||||
@@ -329,28 +327,28 @@ class UserService:
|
||||
- Dict: 更新后的当前用户详情字典
|
||||
"""
|
||||
if not auth.user or not auth.user.id:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
user = await UserCRUD(auth).get(id=auth.user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员不能修改个人信息")
|
||||
# 新增:检查手机号是否重复
|
||||
if data.mobile:
|
||||
exist_mobile_user = await UserCRUD(auth).get(mobile=data.mobile)
|
||||
if exist_mobile_user and exist_mobile_user.id != auth.user.id:
|
||||
raise CustomException(msg="更新失败,手机号已存在")
|
||||
raise CustomException(msg="该数据已存在")
|
||||
# 新增:检查邮箱是否重复
|
||||
if data.email:
|
||||
exist_email_user = await UserCRUD(auth).get(email=data.email)
|
||||
if exist_email_user and exist_email_user.id != auth.user.id:
|
||||
raise CustomException(msg="更新失败,邮箱已存在")
|
||||
raise CustomException(msg="该数据已存在")
|
||||
user_update_data = UserUpdateSchema(**data.model_dump())
|
||||
new_user = await UserCRUD(auth).update(id=auth.user.id, data=user_update_data)
|
||||
return UserOutSchema.model_validate(new_user)
|
||||
|
||||
@classmethod
|
||||
async def set_user_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
设置用户状态
|
||||
|
||||
@@ -362,15 +360,13 @@ class UserService:
|
||||
- None
|
||||
"""
|
||||
for id in data.ids:
|
||||
user = await UserCRUD(auth).get(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg=f"用户ID {id} 不存在")
|
||||
user = await UserCRUD(auth).get_or_404(id=id)
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员状态不能修改")
|
||||
await UserCRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def change_user_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> UserOutSchema:
|
||||
async def change_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> UserOutSchema:
|
||||
"""
|
||||
修改用户密码
|
||||
|
||||
@@ -382,14 +378,14 @@ class UserService:
|
||||
- Dict: 更新后的当前用户详情字典
|
||||
"""
|
||||
if not auth.user or not auth.user.id:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if not data.old_password or not data.new_password:
|
||||
raise CustomException(msg="密码不能为空")
|
||||
|
||||
# 验证原密码
|
||||
user = await UserCRUD(auth).get(id=auth.user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if not PwdUtil.verify_password(plain_password=data.old_password, password_hash=user.password):
|
||||
raise CustomException(msg="原密码输入错误")
|
||||
|
||||
@@ -399,7 +395,7 @@ class UserService:
|
||||
return UserOutSchema.model_validate(new_user)
|
||||
|
||||
@classmethod
|
||||
async def reset_user_password_service(cls, auth: AuthSchema, data: ResetPasswordSchema) -> UserOutSchema:
|
||||
async def reset_password_service(cls, auth: AuthSchema, data: ResetPasswordSchema) -> UserOutSchema:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
@@ -416,7 +412,7 @@ class UserService:
|
||||
# 验证用户
|
||||
user = await UserCRUD(auth).get(id=data.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
|
||||
# 检查是否是超级管理员
|
||||
if user.is_superuser:
|
||||
@@ -428,7 +424,7 @@ class UserService:
|
||||
return UserOutSchema.model_validate(new_user)
|
||||
|
||||
@classmethod
|
||||
async def register_user_service(cls, auth: AuthSchema, data: UserRegisterSchema) -> UserOutSchema:
|
||||
async def register_service(cls, auth: AuthSchema, data: UserRegisterSchema) -> UserOutSchema:
|
||||
"""
|
||||
用户注册
|
||||
|
||||
@@ -442,7 +438,7 @@ class UserService:
|
||||
# 检查用户名是否存在
|
||||
username_ok = await UserCRUD(auth).get(username=data.username)
|
||||
if username_ok:
|
||||
raise CustomException(msg="账号已存在")
|
||||
raise CustomException(msg="该数据已存在")
|
||||
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
data.name = data.username
|
||||
@@ -471,7 +467,7 @@ class UserService:
|
||||
"""
|
||||
user = await UserCRUD(auth).get(username=data.username)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
raise CustomException(msg="该数据不存在")
|
||||
if user.status == 1:
|
||||
raise CustomException(msg="用户已停用")
|
||||
|
||||
@@ -488,7 +484,7 @@ class UserService:
|
||||
return UserOutSchema.model_validate(new_user)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_user_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""
|
||||
批量导入用户
|
||||
|
||||
@@ -600,7 +596,7 @@ class UserService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量导入用户失败: {e!s}")
|
||||
raise CustomException(msg=f"导入失败: {e!s}")
|
||||
raise CustomException(msg=f"导入失败: {e!s}") from e
|
||||
|
||||
@classmethod
|
||||
async def get_import_template_user_service(cls) -> bytes:
|
||||
@@ -631,7 +627,7 @@ class UserService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def export_user_list_service(cls, user_list: list[dict[str, Any]]) -> bytes:
|
||||
async def export_list_service(cls, user_list: list[dict[str, Any]]) -> bytes:
|
||||
"""
|
||||
导出用户列表为Excel文件
|
||||
|
||||
|
||||
Reference in New Issue
Block a user