chore: 批量优化项目代码,修复多处细节问题

本次提交包含多项优化和修复:
1. 修复CRUD初始化参数传递、搜索参数处理逻辑
2. 更新环境配置中的大模型相关参数
3. 重构部分服务方法命名,统一代码风格
4. 新增多个枚举类型,补充模型关联关系和加载选项
5. 优化查询参数类实现,完善字段校验逻辑
6. 调整Pydantic模型字段注释和类型定义
7. 简化并移除冗余的CRUD方法实现
8. 新增超级管理员权限装饰器
9. 修复邮件日志模型的租户关联和字段定义
This commit is contained in:
zhangtao
2026-06-20 23:37:58 +08:00
parent bbe77930a8
commit 4e2b668d7b
96 changed files with 2914 additions and 1936 deletions
@@ -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,
)
+11 -4
View File
@@ -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:
"""
字典类型管理模块服务
字典类型管理服务
提供字典类型 CRUDRedis 缓存同步字典数据联动更新批量启/禁用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:
"""
字典数据管理模块服务
字典数据管理服务
提供字典数据 CRUDRedis 缓存同步初始化字典批量启/禁用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="删除操作日志成功")
+1 -1
View File
@@ -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="备注")
+37 -19
View File
@@ -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
+91 -40
View File
@@ -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_dayssmtp 主机等
平台参数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:
"""
配置管理模块服务
参数管理服务
提供参数 CRUDRedis 缓存同步初始化配置批量启/禁用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),
+17 -5
View File
@@ -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")
+32 -39
View File
@@ -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)
+32 -36
View File
@@ -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文件