upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
from typing import Union, Dict
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from redis.asyncio.client import Redis
from app.common.response import ErrorResponse, SuccessResponse
from app.core.router_class import OperationLogRoute
from app.core.security import CustomOAuth2PasswordRequestForm
from app.core.logger import log
from app.config.setting import settings
from app.core.dependencies import (
db_getter,
get_current_user,
redis_getter
)
from .service import (
LoginService,
CaptchaService
)
from .schema import (
CaptchaOutSchema,
JWTOutSchema,
RefreshTokenPayloadSchema,
LogoutPayloadSchema
)
AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"])
@AuthRouter.post("/login", summary="登录", description="登录", response_model=JWTOutSchema)
async def login_for_access_token_controller(
request: Request,
redis: Redis = Depends(redis_getter),
login_form: CustomOAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(db_getter),
) -> Union[JSONResponse, Dict]:
"""
用户登录
参数:
- request (Request): FastAPI请求对象
- login_form (CustomOAuth2PasswordRequestForm): 登录表单数据
- db (AsyncSession): 数据库会话对象
返回:
- JWTOutSchema: 包含访问令牌和刷新令牌的响应模型
异常:
- CustomException: 认证失败时抛出异常。
"""
login_token = await LoginService.authenticate_user_service(request=request, redis=redis, login_form=login_form, db=db)
log.info(f"用户{login_form.username}登录成功")
# 如果是文档请求,则不记录日志:http://localhost:8000/api/v1/docs
if settings.DOCS_URL in request.headers.get("referer", ""):
return login_token.model_dump()
return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
@AuthRouter.post("/token/refresh", summary="刷新token", description="刷新token", response_model=JWTOutSchema, dependencies=[Depends(get_current_user)])
async def get_new_token_controller(
request: Request,
payload: RefreshTokenPayloadSchema,
db: AsyncSession = Depends(db_getter),
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""
刷新token
参数:
- request (Request): FastAPI请求对象
- payload (RefreshTokenPayloadSchema): 刷新令牌负载模型
返回:
- JWTOutSchema: 包含新的访问令牌和刷新令牌的响应模型
异常:
- CustomException: 刷新令牌失败时抛出异常。
"""
# 解析当前的访问Token以获取用户名
new_token = await LoginService.refresh_token_service(db=db, request=request, redis=redis, refresh_token=payload)
token_dict = new_token.model_dump()
log.info(f"刷新token成功: {token_dict}")
return SuccessResponse(data=token_dict, msg="刷新成功")
@AuthRouter.get("/captcha/get", summary="获取验证码", description="获取登录验证码", response_model=CaptchaOutSchema)
async def get_captcha_for_login_controller(
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""
获取登录验证码
参数:
- redis (Redis): Redis客户端对象
返回:
- CaptchaOutSchema: 包含验证码图片和key的响应模型
异常:
- CustomException: 获取验证码失败时抛出异常。
"""
# 获取验证码
captcha = await CaptchaService.get_captcha_service(redis=redis)
log.info(f"获取验证码成功")
return SuccessResponse(data=captcha, msg="获取验证码成功")
@AuthRouter.post('/logout', summary="退出登录", description="退出登录", dependencies=[Depends(get_current_user)])
async def logout_controller(
payload: LogoutPayloadSchema,
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""
退出登录
参数:
- payload (LogoutPayloadSchema): 退出登录负载模型
- redis (Redis): Redis客户端对象
返回:
- JSONResponse: 包含退出登录结果的响应模型
异常:
- CustomException: 退出登录失败时抛出异常。
"""
if await LoginService.logout_service(redis=redis, token=payload):
log.info('退出成功')
return SuccessResponse(msg='退出成功')
return ErrorResponse(msg='退出失败')

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field, BaseModel, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
from ..user.model import UserModel
class AuthSchema(BaseModel):
"""权限认证模型"""
model_config = ConfigDict(arbitrary_types_allowed=True)
user: UserModel | None = Field(default=None, description='用户信息')
check_data_scope: bool = Field(default=True, description='是否检查数据权限')
db: AsyncSession = Field(description='数据库会话')
class JWTPayloadSchema(BaseModel):
"""JWT载荷模型"""
sub: str = Field(..., description='用户登录信息')
is_refresh: bool = Field(default=False, description='是否刷新token')
exp: datetime | int = Field(..., description='过期时间')
@model_validator(mode='after')
def validate_fields(self):
if not self.sub or len(self.sub.strip()) == 0:
raise ValueError("会话编号不能为空")
return self
class JWTOutSchema(BaseModel):
"""JWT响应模型"""
model_config = ConfigDict(from_attributes=True)
access_token: str = Field(..., min_length=1, description='访问token')
refresh_token: str = Field(..., min_length=1, description='刷新token')
token_type: str = Field(default='Bearer', description='token类型')
expires_in: int = Field(..., gt=0, description='过期时间(秒)')
class RefreshTokenPayloadSchema(BaseModel):
"""刷新Token载荷模型"""
refresh_token: str = Field(..., min_length=1, description='刷新token')
class LogoutPayloadSchema(BaseModel):
"""退出登录载荷模型"""
token: str = Field(..., min_length=1, description='token')
class CaptchaOutSchema(BaseModel):
"""验证码响应模型"""
model_config = ConfigDict(from_attributes=True)
enable: bool = Field(default=True, description='是否启用验证码')
key: str = Field(..., min_length=1, description='验证码唯一标识')
img_base: str = Field(..., min_length=1, description='Base64编码的验证码图片')

View File

@@ -0,0 +1,377 @@
# -*- coding: utf-8 -*-
import json
import uuid
from typing import NewType
from fastapi import Request
from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timedelta
from user_agents import parse
from app.common.enums import RedisInitKeyConfig
from app.utils.common_util import get_random_character
from app.utils.captcha_util import CaptchaUtil
from app.utils.ip_local_util import IpLocalUtil
from app.utils.hash_bcrpy_util import PwdUtil
from app.core.redis_crud import RedisCURD
from app.core.exceptions import CustomException
from app.core.logger import log
from app.config.setting import settings
from app.core.security import (
CustomOAuth2PasswordRequestForm,
create_access_token,
decode_access_token
)
from app.api.v1.module_monitor.online.schema import OnlineOutSchema
from ..user.crud import UserCRUD
from ..user.model import UserModel
from .schema import (
JWTPayloadSchema,
JWTOutSchema,
AuthSchema,
CaptchaOutSchema,
LogoutPayloadSchema,
RefreshTokenPayloadSchema
)
CaptchaKey = NewType('CaptchaKey', str)
CaptchaBase64 = NewType('CaptchaBase64', str)
class LoginService:
"""登录认证服务"""
@classmethod
async def authenticate_user_service(cls, request: Request, redis: Redis, login_form: CustomOAuth2PasswordRequestForm, db: AsyncSession) -> JWTOutSchema:
"""
用户认证
参数:
- request (Request): FastAPI请求对象
- login_form (CustomOAuth2PasswordRequestForm): 登录表单数据
- db (AsyncSession): 数据库会话对象
返回:
- JWTOutSchema: 包含访问令牌和刷新令牌的响应模型
异常:
- CustomException: 认证失败时抛出异常。
"""
# 判断是否来自API文档
referer = request.headers.get('referer', '')
request_from_docs = referer.endswith(('docs', 'redoc'))
# 验证码校验
if settings.CAPTCHA_ENABLE and not request_from_docs:
if not login_form.captcha_key or not login_form.captcha:
raise CustomException(msg="验证码不能为空")
await CaptchaService.check_captcha_service(redis=redis, key=login_form.captcha_key, captcha=login_form.captcha)
# 用户认证
auth = AuthSchema(db=db)
user = await UserCRUD(auth).get_by_username_crud(username=login_form.username)
if not user:
raise CustomException(msg="用户不存在")
if not PwdUtil.verify_password(plain_password=login_form.password, password_hash=user.password):
raise CustomException(msg="账号或密码错误")
if not user.status:
raise CustomException(msg="用户已被停用")
# 更新最后登录时间
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
if not user:
raise CustomException(msg="用户不存在")
if not login_form.login_type:
raise CustomException(msg="登录类型不能为空")
# 创建token
token = await cls.create_token_service(request=request, redis=redis, user=user, login_type=login_form.login_type)
return token
@classmethod
async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema:
"""
创建访问令牌和刷新令牌
参数:
- request (Request): FastAPI请求对象
- redis (Redis): Redis客户端对象
- user (UserModel): 用户模型对象
- login_type (str): 登录类型
返回:
- JWTOutSchema: 包含访问令牌和刷新令牌的响应模型
异常:
- CustomException: 创建令牌失败时抛出异常。
"""
# 生成会话编号
session_id = str(uuid.uuid4())
request.scope["session_id"] = session_id
user_agent = parse(request.headers.get("user-agent"))
request_ip = None
x_forwarded_for = request.headers.get('X-Forwarded-For')
if x_forwarded_for:
# 取第一个 IP 地址,通常为客户端真实 IP
request_ip = x_forwarded_for.split(',')[0].strip()
else:
# 若没有 X-Forwarded-For 头,则使用 request.client.host
if request.client:
request_ip = request.client.host
else:
request_ip = "127.0.0.1"
login_location = await IpLocalUtil.get_ip_location(request_ip)
request.scope["login_location"] = login_location
# 确保在请求上下文中设置用户名和会话ID
request.scope["user_username"] = user.username
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
now = datetime.now()
# 记录租户信息到日志
log.info(f"用户ID: {user.id}, 用户名: {user.username} 正在生成JWT令牌")
# 生成会话信息
session_info=OnlineOutSchema(
session_id=session_id,
user_id=user.id,
name=user.name,
user_name=user.username,
ipaddr=request_ip,
login_location=login_location,
os=user_agent.os.family,
browser = user_agent.browser.family,
login_time=user.last_login,
login_type=login_type
).model_dump_json()
access_token = create_access_token(payload=JWTPayloadSchema(
sub=session_info,
is_refresh=False,
exp=now + access_expires,
))
refresh_token = create_access_token(payload=JWTPayloadSchema(
sub=session_info,
is_refresh=True,
exp=now + refresh_expires,
))
# 设置新的token
await RedisCURD(redis).set(
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}',
value=access_token,
expire=int(access_expires.total_seconds())
)
await RedisCURD(redis).set(
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}',
value=refresh_token,
expire=int(refresh_expires.total_seconds())
)
return JWTOutSchema(
access_token=access_token,
refresh_token=refresh_token,
expires_in=int(access_expires.total_seconds()),
token_type=settings.TOKEN_TYPE
)
@classmethod
async def refresh_token_service(cls, db: AsyncSession, redis: Redis, request: Request, refresh_token: RefreshTokenPayloadSchema) -> JWTOutSchema:
"""
刷新访问令牌
参数:
- db (AsyncSession): 数据库会话对象
- redis (Redis): Redis客户端对象
- request (Request): FastAPI请求对象
- refresh_token (RefreshTokenPayloadSchema): 刷新令牌数据
返回:
- JWTOutSchema: 新的令牌对象
异常:
- CustomException: 刷新令牌无效时抛出异常
"""
token_payload: JWTPayloadSchema = decode_access_token(token = refresh_token.refresh_token)
if not token_payload.is_refresh:
raise CustomException(msg="非法凭证,请传入刷新令牌")
# 去 Redis 查完整信息
session_info = json.loads(token_payload.sub)
session_id = session_info.get("session_id")
user_id = session_info.get("user_id")
if not session_id or not user_id:
raise CustomException(msg="非法凭证,无法获取会话编号或用户ID")
# 用户认证
auth = AuthSchema(db=db)
user = await UserCRUD(auth).get_by_id_crud(id=user_id)
if not user:
raise CustomException(msg="刷新token失败用户不存在")
# 记录刷新令牌时的租户信息
log.info(f"用户ID: {user.id}, 用户名: {user.username} 正在刷新JWT令牌")
# 设置新的 token
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
now = datetime.now()
session_info_json = json.dumps(session_info)
access_token = create_access_token(payload=JWTPayloadSchema(
sub=session_info_json,
is_refresh=False,
exp=now + access_expires
))
refresh_token_new = create_access_token(payload=JWTPayloadSchema(
sub=session_info_json,
is_refresh=True,
exp=now + refresh_expires
))
# 覆盖写入 Redis
await RedisCURD(redis).set(
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}',
value=access_token,
expire=int(access_expires.total_seconds())
)
await RedisCURD(redis).set(
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}',
value=refresh_token_new,
expire=int(refresh_expires.total_seconds())
)
return JWTOutSchema(
access_token=access_token,
refresh_token=refresh_token_new,
token_type=settings.TOKEN_TYPE,
expires_in=int(access_expires.total_seconds())
)
@classmethod
async def logout_service(cls, redis: Redis, token: LogoutPayloadSchema) -> bool:
"""
退出登录
参数:
- redis (Redis): Redis客户端对象
- token (LogoutPayloadSchema): 退出登录令牌数据
返回:
- bool: 退出成功返回True
异常:
- CustomException: 令牌无效时抛出异常
"""
payload: JWTPayloadSchema = decode_access_token(token=token.token)
session_info = json.loads(payload.sub)
session_id = session_info.get("session_id")
if not session_id:
raise CustomException(msg="非法凭证,无法获取会话编号")
# 删除Redis中的在线用户、访问令牌、刷新令牌
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
log.info(f"用户退出登录成功,会话编号:{session_id}")
return True
class CaptchaService:
"""验证码服务"""
@classmethod
async def get_captcha_service(cls, redis: Redis) -> dict[str, CaptchaKey | CaptchaBase64]:
"""
获取验证码
参数:
- redis (Redis): Redis客户端对象
返回:
- dict[str, CaptchaKey | CaptchaBase64]: 包含验证码key和base64图片的字典
异常:
- CustomException: 验证码服务未启用时抛出异常
"""
if not settings.CAPTCHA_ENABLE:
raise CustomException(msg="未开启验证码服务")
# 生成验证码图片和值
captcha_base64, captcha_value = CaptchaUtil.captcha_arithmetic()
captcha_key = get_random_character()
# 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{captcha_key}"
await RedisCURD(redis).set(
key=redis_key,
value=captcha_value,
expire=settings.CAPTCHA_EXPIRE_SECONDS
)
log.info(f"生成验证码成功,验证码:{captcha_value}")
# 返回验证码信息
return CaptchaOutSchema(
enable=settings.CAPTCHA_ENABLE,
key=CaptchaKey(captcha_key),
img_base=CaptchaBase64(f"data:image/png;base64,{captcha_base64}")
).model_dump()
@classmethod
async def check_captcha_service(cls, redis: Redis, key: str, captcha: str) -> bool:
"""
校验验证码
参数:
- redis (Redis): Redis客户端对象
- key (str): 验证码key
- captcha (str): 用户输入的验证码
返回:
- bool: 验证通过返回True
异常:
- CustomException: 验证码无效或错误时抛出异常
"""
if not captcha:
raise CustomException(msg="验证码不能为空")
# 获取Redis中存储的验证码
redis_key = f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}'
captcha_value = await RedisCURD(redis).get(redis_key)
if not captcha_value:
log.error('验证码已过期或不存在')
raise CustomException(msg="验证码已过期")
# 验证码不区分大小写比对
if captcha.lower() != captcha_value.lower():
log.error(f'验证码错误,用户输入:{captcha},正确值:{captcha_value}')
raise CustomException(msg="验证码错误")
# 验证成功后删除验证码,避免重复使用
await RedisCURD(redis).delete(redis_key)
log.info(f'验证码校验成功,key:{key}')
return True

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse
from app.core.dependencies import AuthPermission
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from ..auth.schema import AuthSchema
from .service import DeptService
from .schema import (
DeptCreateSchema,
DeptUpdateSchema,
DeptQueryParam
)
DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["部门管理"])
@DeptRouter.get("/tree", summary="查询部门树", description="查询部门树")
async def get_dept_tree_controller(
search: DeptQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:query"]))
) -> JSONResponse:
"""
查询部门树
参数:
- search (DeptQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含部门树的响应模型
异常:
- CustomException: 查询部门树失败时抛出异常。
"""
order_by = [{"order": "asc"}]
result_dict_list = await DeptService.get_dept_tree_service(search=search, auth=auth, order_by=order_by)
log.info(f"查询部门树成功")
return SuccessResponse(data=result_dict_list, msg="查询部门树成功")
@DeptRouter.get("/detail/{id}", summary="查询部门详情", description="查询部门详情")
async def get_obj_detail_controller(
id: int = Path(..., description="部门ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:query"]))
) -> JSONResponse:
"""
查询部门详情
参数:
- id (int): 部门ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含部门详情的响应模型
异常:
- CustomException: 查询部门详情失败时抛出异常。
"""
result_dict = await DeptService.get_dept_detail_service(id=id, auth=auth)
log.info(f"查询部门详情成功 {id}")
return SuccessResponse(data=result_dict, msg="查询部门详情成功")
@DeptRouter.post("/create", summary="创建部门", description="创建部门")
async def create_obj_controller(
data: DeptCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:create"]))
) -> JSONResponse:
"""
创建部门
参数:
- data (DeptCreateSchema): 创建部门负载模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建部门结果的响应模型
异常:
- CustomException: 创建部门失败时抛出异常。
"""
result_dict = await DeptService.create_dept_service(data=data, auth=auth)
log.info(f"创建部门成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建部门成功")
@DeptRouter.put("/update/{id}", summary="修改部门", description="修改部门")
async def update_obj_controller(
data: DeptUpdateSchema,
id: int = Path(..., description="部门ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:update"]))
) -> JSONResponse:
"""
修改部门
参数:
- data (DeptUpdateSchema): 修改部门负载模型
- id (int): 部门ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改部门结果的响应模型
异常:
- CustomException: 修改部门失败时抛出异常。
"""
result_dict = await DeptService.update_dept_service(auth=auth, id=id, data=data)
log.info(f"修改部门成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改部门成功")
@DeptRouter.delete("/delete", summary="删除部门", description="删除部门")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:delete"]))
) -> JSONResponse:
"""
删除部门
参数:
- ids (list[int]): 部门ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除部门结果的响应模型
异常:
- CustomException: 删除部门失败时抛出异常。
"""
await DeptService.delete_dept_service(ids=ids, auth=auth)
log.info(f"删除部门成功: {ids}")
return SuccessResponse(msg="删除部门成功")
@DeptRouter.patch("/available/setting", summary="批量修改部门状态", description="批量修改部门状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:dept:patch"]))
) -> JSONResponse:
"""
批量修改部门状态
参数:
- data (BatchSetAvailable): 批量修改部门状态负载模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含批量修改部门状态结果的响应模型
异常:
- CustomException: 批量修改部门状态失败时抛出异常。
"""
await DeptService.batch_set_available_service(data=data, auth=auth)
log.info(f"批量修改部门状态成功: {data.ids}")
return SuccessResponse(msg="批量修改部门状态成功")

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import DeptModel
from .schema import DeptCreateSchema, DeptUpdateSchema
class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
"""部门模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化部门CRUD"""
self.auth = auth
super().__init__(model=DeptModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list | None = None) -> DeptModel | None:
"""
根据 id 获取部门信息。
参数:
- id (int): 部门 ID。
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- DeptModel | None: 部门信息,未找到返回 None。
"""
obj = await self.get(id=id, preload=preload)
if not obj:
return None
return obj
async def get_list_crud(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.list(search=search, order_by=order_by, preload=preload)
async def get_tree_list_crud(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)
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
批量设置部门可用状态。
参数:
- ids (list[int]): 部门 ID 列表。
- status (str): 可用状态。
返回:
- None
"""
await self.set(ids=ids, status=status)
async def get_name_crud(self, id: int) -> str | None:
"""
根据 id 获取部门名称。
参数:
- id (int): 部门 ID。
返回:
- str | None: 部门名称,未找到返回 None。
"""
obj = await self.get(id=id)
return obj.name if obj else None

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from app.core.base_model import ModelMixin
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):
"""
部门模型
"""
__tablename__: str = "sys_dept"
__table_args__: dict[str, str] = ({'comment': '部门表'})
name: Mapped[str] = mapped_column(String(40), nullable=False, comment="部门名称")
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="部门编码")
leader: Mapped[str | None] = mapped_column(String(32), default=None, comment='部门负责人')
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机')
email: Mapped[str | None] = mapped_column(String(64), default=None, comment='邮箱')
# 树形结构字段
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("sys_dept.id", ondelete="SET NULL", onupdate="CASCADE"),
default=None,
index=True,
comment="父级部门ID"
)
# 关联关系
parent: Mapped["DeptModel | None"] = relationship(
back_populates='children',
remote_side="DeptModel.id",
foreign_keys=[parent_id],
uselist=False
)
children: Mapped[list["DeptModel"]] = relationship(
back_populates='parent',
foreign_keys=[parent_id],
lazy="selectin"
)
roles: Mapped[list["RoleModel"]] = relationship(
secondary="sys_role_depts",
back_populates="depts",
lazy="selectin"
)
users: Mapped[list["UserModel"]] = relationship(
back_populates="dept",
foreign_keys="UserModel.dept_id",
lazy="selectin"
)

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field, field_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema
class DeptCreateSchema(BaseModel):
"""部门创建模型"""
name: str = Field(..., max_length=40, description="部门名称")
order: int = Field(default=1, ge=0, description="显示顺序")
code: str | None = Field(default=None, max_length=60, description="部门编码")
leader: str | None = Field(default=None, max_length=20, description="部门负责人")
phone: str | None = Field(default=None, max_length=11, description="手机")
email: str | None = Field(default=None, max_length=64, description="邮箱")
parent_id: int | None = Field(default=None, ge=0, description="父部门ID")
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="备注说明")
@field_validator('name')
@classmethod
def validate_name(cls, value: str):
if not value or len(value.strip()) == 0:
raise ValueError("部门名称不能为空")
value = value.replace(" ", "")
return value
@field_validator('code')
@classmethod
def validate_code(cls, value: str | None):
if value is None:
return value
v = value.strip()
if v == "":
return None
import re
if not re.match(r'^[A-Za-z][A-Za-z0-9_]*$', v):
raise ValueError("部门编码必须以字母开头,且仅包含字母/数字/下划线")
return v
class DeptUpdateSchema(DeptCreateSchema):
"""部门更新模型"""
...
class DeptOutSchema(DeptCreateSchema, BaseSchema):
"""部门响应模型"""
model_config = ConfigDict(from_attributes=True)
parent_name: str | None = Field(default=None, max_length=64, description="父部门名称")
class DeptQueryParam:
"""部门管理查询参数"""
def __init__(
self,
name: str | None = Query(None, description="部门名称"),
status: str | None = Query(None, description="部门状态(True正常 False停用)"),
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"]),
) -> None:
# 模糊查询字段
self.name = ("like", name)
# 精确查询字段
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.common_util import (
get_parent_id_map,
get_parent_recursion,
get_child_id_map,
get_child_recursion,
traversal_to_tree
)
from ..auth.schema import AuthSchema
from .crud import DeptCRUD
from .schema import (
DeptCreateSchema,
DeptUpdateSchema,
DeptOutSchema,
DeptQueryParam
)
class DeptService:
"""
部门管理模块服务层
"""
@classmethod
async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取部门详情。
参数:
- auth (AuthSchema): 认证对象。
- id (int): 部门 ID。
返回:
- dict: 部门详情对象。
"""
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
result = DeptOutSchema.model_validate(dept).model_dump()
if dept and dept.parent_id:
parent = await DeptCRUD(auth).get(id=dept.parent_id)
if parent:
result['parent_name'] = parent.name
return result
@classmethod
async def get_dept_tree_service(cls, auth: AuthSchema, search: DeptQueryParam | None= None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取部门树形列表。
参数:
- auth (AuthSchema): 认证对象。
- search (DeptQueryParam | None): 查询参数对象。
- order_by (list[dict] | None): 排序参数。
返回:
- list[dict]: 部门树形列表对象。
"""
# 使用树形结构查询预加载children关系
dept_list = await DeptCRUD(auth).get_tree_list_crud(search=search.__dict__, order_by=order_by)
# 转换为字典列表
dept_dict_list = [DeptOutSchema.model_validate(dept).model_dump() for dept in dept_list]
# 使用traversal_to_tree构建树形结构
return traversal_to_tree(dept_dict_list)
@classmethod
async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> dict:
"""
创建部门。
参数:
- auth (AuthSchema): 认证对象。
- data (DeptCreateSchema): 部门创建对象。
返回:
- dict: 新创建的部门对象。
异常:
- CustomException: 当部门已存在时抛出。
"""
dept = await DeptCRUD(auth).get(name=data.name)
if dept:
raise CustomException(msg='创建失败,该部门已存在')
obj = await DeptCRUD(auth).get(code=data.code)
if obj:
raise CustomException(msg='创建失败,编码已存在')
dept = await DeptCRUD(auth).create(data=data)
return DeptOutSchema.model_validate(dept).model_dump()
@classmethod
async def update_dept_service(cls, auth: AuthSchema, id:int, data: DeptUpdateSchema) -> dict:
"""
更新部门。
参数:
- auth (AuthSchema): 认证对象。
- id (int): 部门 ID。
- data (DeptUpdateSchema): 部门更新对象。
返回:
- dict: 更新后的部门对象。
异常:
- CustomException: 当部门不存在或名称重复时抛出。
"""
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
if not dept:
raise CustomException(msg='更新失败,该部门不存在')
exist_dept = await DeptCRUD(auth).get(name=data.name)
if exist_dept and exist_dept.id != id:
raise CustomException(msg='更新失败,部门名称重复')
dept = await DeptCRUD(auth).update(id=id, data=data)
return DeptOutSchema.model_validate(dept).model_dump()
@classmethod
async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除部门。
参数:
- auth (AuthSchema): 认证对象。
- ids (List[int]): 部门 ID 列表。
返回:
- None
异常:
- CustomException: 当删除对象为空或部门不存在时抛出。
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
if not dept:
raise CustomException(msg='删除失败,该部门不存在')
# 校验是否存在子级部门,存在则禁止删除
dept_list = await DeptCRUD(auth).get_list_crud()
id_map = get_child_id_map(model_list=dept_list)
for id in ids:
descendants = get_child_recursion(id=id, id_map=id_map)
if len(descendants) > 1:
raise CustomException(msg='删除失败,存在子级部门,请先删除子级部门')
await DeptCRUD(auth).delete(ids=ids)
@classmethod
async def batch_set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
批量设置部门可用状态。
参数:
- auth (AuthSchema): 认证对象。
- data (BatchSetAvailable): 批量设置可用状态对象。
返回:
- None
"""
dept_list = await DeptCRUD(auth).get_list_crud()
total_ids = []
if data.status:
id_map = get_parent_id_map(model_list=dept_list)
for dept_id in data.ids:
enable_ids = get_parent_recursion(id=dept_id, id_map=id_map)
total_ids.extend(enable_ids)
else:
id_map = get_child_id_map(model_list=dept_list)
for dept_id in data.ids:
disable_ids = get_child_recursion(id=dept_id, id_map=id_map)
total_ids.extend(disable_ids)
await DeptCRUD(auth).set_available_crud(ids=total_ids, status=data.status)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,425 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from redis.asyncio.client import Redis
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import BatchSetAvailable
from app.core.dependencies import AuthPermission, redis_getter
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from ..auth.schema import AuthSchema
from .service import DictTypeService, DictDataService
from .schema import (
DictTypeCreateSchema,
DictTypeUpdateSchema,
DictDataCreateSchema,
DictDataUpdateSchema,
DictDataQueryParam,
DictTypeQueryParam
)
DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["字典管理"])
@DictRouter.get("/type/detail/{id}", summary="获取字典类型详情", description="获取字典类型详情")
async def get_type_detail_controller(
id: int = Path(..., description="字典类型ID", ge=1),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:query"]))
) -> JSONResponse:
"""
获取字典类型详情
参数:
- id (int): 字典类型ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含字典类型详情的响应模型
异常:
- CustomException: 获取字典类型详情失败时抛出异常。
"""
result_dict = await DictTypeService.get_obj_detail_service(id=id, auth=auth)
log.info(f"获取字典类型详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取字典类型详情成功")
@DictRouter.get("/type/list", summary="查询字典类型", description="查询字典类型")
async def get_type_list_controller(
page: PaginationQueryParam = Depends(),
search: DictTypeQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:query"]))
) -> JSONResponse:
"""
查询字典类型
参数:
- page (PaginationQueryParam): 分页参数模型
- search (DictTypeQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含查询字典类型结果的响应模型
异常:
- CustomException: 查询字典类型失败时抛出异常。
"""
result_dict_list = await DictTypeService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询字典类型列表成功")
return SuccessResponse(data=result_dict, msg="查询字典类型列表成功")
@DictRouter.get("/type/optionselect", summary="获取全部字典类型", description="获取全部字典类型")
async def get_type_loptionselect_controller(
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:query"]))
) -> JSONResponse:
"""
获取全部字典类型
参数:
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含全部字典类型的响应模型
异常:
- CustomException: 获取字典类型列表失败时抛出异常。
"""
result_dict_list = await DictTypeService.get_obj_list_service(auth=auth)
log.info(f"获取字典类型列表成功")
return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功")
@DictRouter.post("/type/create", summary="创建字典类型", description="创建字典类型")
async def create_type_controller(
data: DictTypeCreateSchema,
redis: Redis = Depends(redis_getter),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:create"]))
) -> JSONResponse:
"""
创建字典类型
参数:
- data (DictTypeCreateSchema): 创建字典类型的入参模型
- redis (Redis): Redis数据库连接
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建字典类型结果的响应模型
异常:
- CustomException: 创建字典类型失败时抛出异常。
"""
result_dict = await DictTypeService.create_obj_service(auth=auth, redis=redis, data=data)
log.info(f"创建字典类型成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建字典类型成功")
@DictRouter.put("/type/update/{id}", summary="修改字典类型", description="修改字典类型")
async def update_type_controller(
data: DictTypeUpdateSchema,
redis: Redis = Depends(redis_getter),
id: int = Path(..., description="字典类型ID", ge=1),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:update"]))
) -> JSONResponse:
"""
修改字典类型
参数:
- data (DictTypeUpdateSchema): 修改字典类型的入参模型
- redis (Redis): Redis数据库连接
- id (int): 字典类型ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改字典类型结果的响应模型
异常:
- CustomException: 修改字典类型失败时抛出异常。
"""
result_dict = await DictTypeService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
log.info(f"修改字典类型成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改字典类型成功")
@DictRouter.delete("/type/delete", summary="删除字典类型", description="删除字典类型")
async def delete_type_controller(
redis: Redis = Depends(redis_getter),
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:delete"]))
) -> JSONResponse:
"""
删除字典类型
参数:
- redis (Redis): Redis数据库连接
- ids (list[int]): 字典类型ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除字典类型结果的响应模型
异常:
- CustomException: 删除字典类型失败时抛出异常。
"""
await DictTypeService.delete_obj_service(auth=auth, redis=redis, ids=ids)
log.info(f"删除字典类型成功: {ids}")
return SuccessResponse(msg="删除字典类型成功")
@DictRouter.patch("/type/available/setting", summary="批量修改字典类型状态", description="批量修改字典类型状态")
async def batch_set_available_dict_type_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:patch"]))
) -> JSONResponse:
"""
批量修改字典类型状态
参数:
- data (BatchSetAvailable): 批量修改字典类型状态负载模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含批量修改字典类型状态结果的响应模型
异常:
- CustomException: 批量修改字典类型状态失败时抛出异常。
"""
await DictTypeService.set_obj_available_service(auth=auth, data=data)
log.info(f"批量修改字典类型状态成功: {data.ids}")
return SuccessResponse(msg="批量修改字典类型状态成功")
@DictRouter.post('/type/export', summary="导出字典类型", description="导出字典类型")
async def export_type_list_controller(
search: DictTypeQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:export"]))
) -> StreamingResponse:
"""
导出字典类型
参数:
- search (DictTypeQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse: 包含导出字典类型结果的响应模型
异常:
- CustomException: 导出字典类型失败时抛出异常。
"""
# 获取全量数据
result_dict_list = await DictTypeService.get_obj_list_service(search=search, auth=auth)
export_result = await DictTypeService.export_obj_service(data_list=result_dict_list)
log.info('导出字典类型成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=dict_type.xlsx'
}
)
@DictRouter.get("/data/detail/{id}", summary="获取字典数据详情", description="获取字典数据详情")
async def get_data_detail_controller(
id: int = Path(..., description="字典数据ID", ge=1),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:query"]))
) -> JSONResponse:
"""
获取字典数据详情
参数:
- id (int): 字典数据ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含字典数据详情的响应模型
异常:
- CustomException: 获取字典数据详情失败时抛出异常。
"""
result_dict = await DictDataService.get_obj_detail_service(id=id, auth=auth)
log.info(f"获取字典数据详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取字典数据详情成功")
@DictRouter.get("/data/list", summary="查询字典数据", description="查询字典数据")
async def get_data_list_controller(
page: PaginationQueryParam = Depends(),
search: DictDataQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:query"]))
) -> JSONResponse:
"""
查询字典数据
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (DictDataQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含字典数据列表的响应模型
异常:
- CustomException: 查询字典数据列表失败时抛出异常。
"""
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict_list = await DictDataService.get_obj_list_service(auth=auth, search=search, order_by=order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询字典数据列表成功")
return SuccessResponse(data=result_dict, msg="查询字典数据列表成功")
@DictRouter.post("/data/create", summary="创建字典数据", description="创建字典数据")
async def create_data_controller(
data: DictDataCreateSchema,
redis: Redis = Depends(redis_getter),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:create"]))
) -> JSONResponse:
"""
创建字典数据
参数:
- data (DictDataCreateSchema): 创建字典数据负载模型
- redis (Redis): Redis数据库连接
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建字典数据结果的响应模型
异常:
- CustomException: 创建字典数据失败时抛出异常。
"""
result_dict = await DictDataService.create_obj_service(auth=auth, redis=redis, data=data)
log.info(f"创建字典数据成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建字典数据成功")
@DictRouter.put("/data/update/{id}", summary="修改字典数据", description="修改字典数据")
async def update_data_controller(
data: DictDataUpdateSchema,
redis: Redis = Depends(redis_getter),
id: int = Path(..., description="字典数据ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:update"]))
) -> JSONResponse:
"""
修改字典数据
参数:
- data (DictDataUpdateSchema): 修改字典数据负载模型
- redis (Redis): Redis数据库连接
- id (int): 字典数据ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改字典数据结果的响应模型
异常:
- CustomException: 修改字典数据失败时抛出异常。
"""
result_dict = await DictDataService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
log.info(f"修改字典数据成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改字典数据成功")
@DictRouter.delete("/data/delete", summary="删除字典数据", description="删除字典数据")
async def delete_data_controller(
redis: Redis = Depends(redis_getter),
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:delete"]))
) -> JSONResponse:
"""
删除字典数据
参数:
- redis (Redis): Redis数据库连接
- ids (list[int]): 字典数据ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除字典数据结果的响应模型
异常:
- CustomException: 删除字典数据失败时抛出异常。
"""
await DictDataService.delete_obj_service(auth=auth, redis=redis, ids=ids)
log.info(f"删除字典数据成功: {ids}")
return SuccessResponse(msg="删除字典数据成功")
@DictRouter.patch("/data/available/setting", summary="批量修改字典数据状态", description="批量修改字典数据状态")
async def batch_set_available_dict_data_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:patch"]))
) -> JSONResponse:
"""
批量修改字典数据状态
参数:
- data (BatchSetAvailable): 批量修改字典数据状态负载模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含批量修改字典数据状态结果的响应模型
异常:
- CustomException: 批量修改字典数据状态失败时抛出异常。
"""
await DictDataService.set_obj_available_service(auth=auth, data=data)
log.info(f"批量修改字典数据状态成功: {data.ids}")
return SuccessResponse(msg="批量修改字典数据状态成功")
@DictRouter.post('/data/export', summary="导出字典数据", description="导出字典数据")
async def export_data_list_controller(
search: DictDataQueryParam = Depends(),
page: PaginationQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:export"]))
) -> StreamingResponse:
"""
导出字典数据
参数:
- search (DictDataQueryParam): 查询参数模型
- page (PaginationQueryParam): 分页参数模型
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse: 包含导出字典数据结果的响应模型
异常:
- CustomException: 导出字典数据失败时抛出异常。
"""
result_dict_list = await DictDataService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by)
export_result = await DictDataService.export_obj_service(data_list=result_dict_list)
log.info('导出字典数据成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=dice_data.xlsx'
}
)
@DictRouter.get('/data/info/{dict_type}', summary="根据字典类型获取数据", description="根据字典类型获取数据")
async def get_init_dict_data_controller(
dict_type: str,
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""
根据字典类型获取数据
参数:
- dict_type (str): 字典类型
- redis (Redis): Redis数据库连接
返回:
- JSONResponse: 包含根据字典类型获取数据结果的响应模型
异常:
- CustomException: 根据字典类型获取数据失败时抛出异常。
"""
dict_data_query_result = await DictDataService.get_init_dict_service(
redis=redis, dict_type=dict_type
)
log.info(f"获取初始化字典数据成功:{dict_data_query_result}")
return SuccessResponse(data=dict_data_query_result, msg="获取初始化字典数据成功")

View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.dict.model import DictDataModel, DictTypeModel
from app.api.v1.module_system.dict.schema import DictDataCreateSchema, DictDataUpdateSchema, DictTypeCreateSchema, DictTypeUpdateSchema
from app.api.v1.module_system.auth.schema import AuthSchema
class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateSchema]):
"""数据字典类型数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化数据字典类型CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=DictTypeModel, auth=auth)
async def get_obj_by_id_crud(self, id: int, preload: list | None = None) -> DictTypeModel | None:
"""
获取数据字典类型详情
参数:
- id (int): 数据字典类型ID
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- DictTypeModel | None: 数据字典类型模型,如果不存在则为None
"""
# 添加默认预加载字典数据关系
if preload is None:
preload = []
return await self.get(id=id, preload=preload)
async def get_obj_list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[DictTypeModel]:
"""
获取数据字典类型列表
参数:
- search (dict | None): 查询参数,默认值为None
- order_by (list[dict] | None): 排序参数,默认值为None
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[DictTypeModel]: 数据字典类型模型序列
"""
# 添加默认预加载字典数据关系
if preload is None:
preload = []
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: DictTypeCreateSchema) -> DictTypeModel | None:
"""
创建数据字典类型
参数:
- data (DictTypeCreateSchema): 数据字典类型创建模型
返回:
- DictTypeModel | None: 创建的数据字典类型模型,如果创建失败则为None
"""
return await self.create(data=data)
async def update_obj_crud(self, id: int, data: DictTypeUpdateSchema) -> DictTypeModel | None:
"""
更新数据字典类型
参数:
- id (int): 数据字典类型ID
- data (DictTypeUpdateSchema): 数据字典类型更新模型
返回:
- DictTypeModel | None: 更新的数据字典类型模型,如果更新失败则为None
"""
return await self.update(id=id, data=data)
async def delete_obj_crud(self, ids: list[int]) -> None:
"""
删除数据字典类型
参数:
- ids (list[int]): 数据字典类型ID列表
返回:
- None
"""
return await self.delete(ids=ids)
async def set_obj_available_crud(self, ids: list[int], status: str) -> None:
"""
设置数据字典类型的可用状态
参数:
- ids (list[int]): 数据字典类型ID列表
- status (str): 可用状态,0表示正常,1表示停用
返回:
- None
"""
return await self.set(ids=ids, status=status)
async def batch_delete_obj_crud(self, ids: list[int]) -> int:
"""
批量删除数据字典类型
参数:
- ids (List[int]): 数据字典类型ID列表
返回:
- int: 删除的记录数量
"""
await self.delete(ids=ids)
return len(ids)
class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateSchema]):
"""数据字典数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化数据字典数据CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=DictDataModel, auth=auth)
async def get_obj_by_id_crud(self, id: int, preload: list | None = None) -> DictDataModel | None:
"""
获取数据字典数据详情
参数:
- id (int): 数据字典数据ID
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- DictDataModel | None: 数据字典数据模型,如果不存在则为None
"""
# 添加默认预加载字典类型关系
if preload is None:
preload = []
return await self.get(id=id, preload=preload)
async def get_obj_list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[DictDataModel]:
"""
获取数据字典数据列表
参数:
- search (dict | None): 查询参数,默认值为None
- order_by (list[dict] | None): 排序参数,默认值为None
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[DictDataModel]: 数据字典数据模型序列
"""
# 添加默认预加载字典类型关系
if preload is None:
preload = []
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: DictDataCreateSchema) -> DictDataModel | None:
"""
创建数据字典数据
参数:
- data (DictDataCreateSchema): 数据字典数据创建模型
返回:
- DictDataModel | None: 创建的数据字典数据模型,如果创建失败则为None
"""
return await self.create(data=data)
async def update_obj_crud(self, id: int, data: DictDataUpdateSchema) -> DictDataModel | None:
"""
更新数据字典数据
参数:
- id (int): 数据字典数据ID
- data (DictDataUpdateSchema): 数据字典数据更新模型
返回:
- DictDataModel | None: 更新的数据字典数据模型,如果更新失败则为None
"""
return await self.update(id=id, data=data)
async def delete_obj_crud(self, ids: list[int]) -> None:
"""
删除数据字典数据
参数:
- ids (list[int]): 数据字典数据ID列表
返回:
- None
"""
return await self.delete(ids=ids)
async def set_obj_available_crud(self, ids: list[int], status: str) -> None:
"""
设置数据字典数据的可用状态
参数:
- ids (list[int]): 数据字典数据ID列表
- status (str): 可用状态,0表示正常,1表示停用
返回:
- None
"""
return await self.set(ids=ids, status=status)
async def batch_delete_obj_crud(self, ids: list[int], exclude_system: bool = True) -> int:
"""
批量删除数据字典数据
参数:
- ids (List[int]): 数据字典数据ID列表
- exclude_system (bool): 是否排除系统默认数据默认为True
返回:
- int: 删除的记录数量
"""
# 如果需要排除系统默认数据,可以在这里添加过滤逻辑
# 假设系统默认数据在remark字段中包含"系统默认"字符串
if exclude_system:
# 获取非系统默认数据的ID
system_data_filter = {"id__in": ids, "remark__contains": "系统默认"}
system_data = await self.list(search=system_data_filter)
system_ids = [item.id for item in system_data]
# 从待删除ID列表中排除系统默认数据
ids = [id for id in ids if id not in system_ids]
if ids:
await self.delete(ids=ids)
return len(ids)
async def get_obj_list_by_dict_type_crud(self, dict_type: str, status: str | None = "0") -> Sequence[DictDataModel]:
"""
根据字典类型获取字典数据列表
参数:
- dict_type (str): 字典类型
- status (str | None): 状态过滤None表示不过滤
返回:
- Sequence[DictDataModel]: 数据字典数据模型序列
"""
search = {"dict_type": dict_type}
if status is not None:
search["status"] = status
order_by = [{"id": "asc"}]
return await self.list(search=search, order_by=order_by)

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from sqlalchemy import String, Integer, Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import ModelMixin
class DictTypeModel(ModelMixin):
"""
字典类型表
"""
__tablename__: str = "sys_dict_type"
__table_args__: dict[str, str] = ({'comment': '字典类型表'})
dict_name: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典名称')
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, comment='字典类型')
# 关系定义
dict_data_list: Mapped[list["DictDataModel"]] = relationship("DictDataModel", back_populates="dict_type_obj", cascade="all, delete-orphan")
class DictDataModel(ModelMixin):
"""
字典数据表
"""
__tablename__: str = "sys_dict_data"
__table_args__: dict[str, str] = ({'comment': '字典数据表'})
dict_sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment='字典排序')
dict_label: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典标签')
dict_value: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典键值')
css_class: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='样式属性(其他样式扩展)')
list_class: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='表格回显样式')
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment='是否默认True是 False否')
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典类型')
# 添加外键关系同时保留dict_type字段用于业务查询
dict_type_id: Mapped[int] = mapped_column(
Integer,
ForeignKey('sys_dict_type.id', ondelete='CASCADE'),
nullable=False,
comment='字典类型ID'
)
# 关系定义
dict_type_obj: Mapped[DictTypeModel] = relationship("DictTypeModel", back_populates="dict_data_list")

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
import re
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema
class DictTypeCreateSchema(BaseModel):
"""
字典类型表对应pydantic模型
"""
dict_name: str = Field(..., min_length=1, max_length=64, description='字典名称')
dict_type: str = Field(..., min_length=1, max_length=100, description='字典类型')
status: str = Field(default='0', description='状态0正常 1停用')
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator('dict_name')
def validate_dict_name(cls, value: str):
if not value or value.strip() == '':
raise ValueError('字典名称不能为空')
return value.strip()
@field_validator('dict_type')
def validate_dict_type(cls, value: str):
if not value or value.strip() == '':
raise ValueError('字典类型不能为空')
regexp = r'^[a-z][a-z0-9_]*$'
if not re.match(regexp, value):
raise ValueError('字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)')
return value.strip()
class DictTypeUpdateSchema(DictTypeCreateSchema):
"""字典类型更新模型"""
...
class DictTypeOutSchema(DictTypeCreateSchema, BaseSchema):
"""字典类型响应模型"""
model_config = ConfigDict(from_attributes=True)
class DictTypeQueryParam:
"""字典类型查询参数"""
def __init__(
self,
dict_name: str | None = Query(default=None, description="字典名称", max_length=100),
dict_type: str | None = Query(default=None, description="字典类型", max_length=100),
status: str | None = Query(default=None, description="状态0正常 1停用"),
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"]),
) -> None:
super().__init__()
# 模糊查询字段
self.dict_name = ("like", f"%{dict_name.strip()}%") if dict_name and dict_name.strip() else None
# 精确查询字段
self.dict_type = dict_type.strip() if dict_type else None
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))
class DictDataCreateSchema(BaseModel):
"""
字典数据表对应pydantic模型
"""
dict_sort: int = Field(..., ge=1, le=999, description='字典排序')
dict_label: str = Field(..., max_length=100, description='字典标签')
dict_value: str = Field(..., max_length=100, description='字典键值')
dict_type: str = Field(..., max_length=100, description='字典类型')
dict_type_id: int = Field(..., description='字典类型ID')
css_class: str | None = Field(default=None, max_length=100, description='样式属性(其他样式扩展)')
list_class: str | None = Field(default=None, description='表格回显样式')
is_default: bool = Field(default=False, description='是否默认True是 False否')
status: str = Field(default='0', description='状态0正常 1停用')
description: str | None = Field(default=None, max_length=255, description="描述")
@model_validator(mode='after')
def validate_after(self):
if not self.dict_label or not self.dict_label.strip():
raise ValueError('字典标签不能为空')
if not self.dict_value or not self.dict_value.strip():
raise ValueError('字典键值不能为空')
if not self.dict_type or not self.dict_type.strip():
raise ValueError('字典类型不能为空')
if not hasattr(self, 'dict_type_id') or self.dict_type_id <= 0:
raise ValueError('字典类型ID不能为空且必须大于0')
# 确保字符串字段被正确处理
self.dict_label = self.dict_label.strip()
self.dict_value = self.dict_value.strip()
self.dict_type = self.dict_type.strip()
return self
class DictDataUpdateSchema(DictDataCreateSchema):
"""字典数据更新模型"""
...
class DictDataOutSchema(DictDataCreateSchema, BaseSchema):
"""字典数据响应模型"""
model_config = ConfigDict(from_attributes=True)
class DictDataQueryParam:
"""字典数据查询参数"""
def __init__(
self,
dict_label: str | None = Query(default=None, description="字典标签", max_length=100),
dict_type: str | None = Query(default=None, description="字典类型", max_length=100),
dict_type_id: int | None = Query(default=None, description="字典类型ID"),
status: str | None = Query(default=None, description="状态0正常 1停用"),
created_time: list[DateTimeStr] | None = Query(default=None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
updated_time: list[DateTimeStr] | None = Query(default=None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
) -> None:
# 模糊查询字段
self.dict_label = ("like", f"%{dict_label.strip()}%") if dict_label and dict_label.strip() else None
# 精确查询字段
self.dict_type = dict_type.strip() if dict_type else None
self.dict_type_id = dict_type_id
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,573 @@
# -*- coding: utf-8 -*-
import json
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.utils.excel_util import ExcelUtil
from app.core.database import async_db_session
from app.core.base_schema import BatchSetAvailable
from app.core.redis_crud import RedisCURD
from app.core.exceptions import CustomException
from app.core.logger import log
from app.api.v1.module_system.auth.schema import AuthSchema
from .schema import (
DictDataCreateSchema,
DictDataOutSchema,
DictDataUpdateSchema,
DictTypeCreateSchema,
DictTypeOutSchema,
DictTypeUpdateSchema,
DictDataQueryParam,
DictTypeQueryParam
)
from .crud import DictDataCRUD, DictTypeCRUD
class DictTypeService:
"""
字典类型管理模块服务层
"""
@classmethod
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取数据字典类型详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 数据字典类型ID
返回:
- dict: 数据字典类型详情字典
"""
obj = await DictTypeCRUD(auth).get_obj_by_id_crud(id=id)
return DictTypeOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_obj_list_service(cls, auth: AuthSchema, search: DictTypeQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取数据字典类型列表
参数:
- auth (AuthSchema): 认证信息模型
- search (DictTypeQueryParam | None): 搜索条件模型
- order_by (list[dict] | None): 排序字段列表
返回:
- list[dict]: 数据字典类型详情字典列表
"""
obj_list = await DictTypeCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
return [DictTypeOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: DictTypeCreateSchema) -> dict:
"""
创建数据字典类型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- data (DictTypeCreateSchema): 数据字典类型创建模型
返回:
- dict: 数据字典类型详情字典
"""
exist_obj = await DictTypeCRUD(auth).get(dict_name=data.dict_name)
if exist_obj:
raise CustomException(msg='创建失败,该数据字典类型已存在')
obj = await DictTypeCRUD(auth).create_obj_crud(data=data)
new_obj_dict = DictTypeOutSchema.model_validate(obj).model_dump()
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{data.dict_type}"
try:
await RedisCURD(redis).set(
key=redis_key,
value="",
)
log.info(f"创建字典类型成功: {new_obj_dict}")
except Exception as e:
log.error(f"创建字典类型失败: {e}")
raise CustomException(msg=f"创建字典类型失败 {e}")
return new_obj_dict
@classmethod
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id:int, data: DictTypeUpdateSchema) -> dict:
"""
更新数据字典类型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- id (int): 数据字典类型ID
- data (DictTypeUpdateSchema): 数据字典类型更新模型
返回:
- dict: 数据字典类型详情字典
"""
exist_obj = await DictTypeCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='更新失败,该数据字典类型不存在')
if exist_obj.dict_name != data.dict_name:
raise CustomException(msg='更新失败,数据字典类型名称不可以修改')
dict_data_list = []
# 如果字典类型修改或状态变更则修改对应字典数据的类型和状态并更新Redis缓存
if exist_obj.dict_type != data.dict_type or exist_obj.status != data.status:
# 检查字典数据类型是否被修改
exist_obj_type_list = await DictDataCRUD(auth).list(search={'dict_type': exist_obj.dict_type})
if exist_obj_type_list:
for item in exist_obj_type_list:
item.dict_type = data.dict_type
dict_data = DictDataUpdateSchema(
dict_sort=item.dict_sort,
dict_label=item.dict_label,
dict_value=item.dict_value,
dict_type=data.dict_type,
dict_type_id=item.dict_type_id,
css_class=item.css_class,
list_class=item.list_class,
is_default=item.is_default,
status=data.status,
description=item.description
)
obj = await DictDataCRUD(auth).update_obj_crud(id=item.id, data=dict_data)
dict_data_list.append(DictDataOutSchema.model_validate(obj).model_dump())
obj = await DictTypeCRUD(auth).update_obj_crud(id=id, data=data)
new_obj_dict = DictTypeOutSchema.model_validate(obj).model_dump()
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据,确保包含最新状态
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
log.info(f"更新字典类型成功并刷新缓存: {new_obj_dict}")
except Exception as e:
log.error(f"更新字典类型缓存失败: {e}")
raise CustomException(msg=f"更新字典类型缓存失败 {e}")
return new_obj_dict
@classmethod
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
"""
删除数据字典类型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- ids (list[int]): 数据字典类型ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
exist_obj = await DictTypeCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='删除失败,该数据字典类型不存在')
# 检查是否有字典数据
exist_obj_type_list = await DictDataCRUD(auth).list(search={'dict_type': id})
if len(exist_obj_type_list) > 0:
# 如果有字典数据,不能删除
raise CustomException(msg='删除失败,该数据字典类型下存在字典数据')
# 删除Redis缓存
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{exist_obj.dict_type}"
try:
await RedisCURD(redis).delete(redis_key)
log.info(f"删除字典类型成功: {id}")
except Exception as e:
log.error(f"删除字典类型失败: {e}")
raise CustomException(msg=f"删除字典类型失败")
await DictTypeCRUD(auth).delete_obj_crud(ids=ids)
@classmethod
async def set_obj_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
设置数据字典类型状态
参数:
- auth (AuthSchema): 认证信息模型
- data (BatchSetAvailable): 批量设置状态模型
返回:
- None
"""
await DictTypeCRUD(auth).set_obj_available_crud(ids=data.ids, status=data.status)
@classmethod
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
"""
导出数据字典类型列表
参数:
- data_list (list[dict]): 数据字典类型列表
返回:
- bytes: Excel文件字节流
"""
mapping_dict = {
'id': '编号',
'dict_name': '字典名称',
'dict_type': '字典类型',
'status': '状态',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 复制数据并转换状态
data = data_list.copy()
for item in data:
# 处理状态
item['status'] = '启用' if item.get('status') == '0' else '停用'
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
class DictDataService:
"""
字典数据管理模块服务层
"""
@classmethod
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取数据字典数据详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 数据字典数据ID
返回:
- dict: 数据字典数据详情字典
"""
obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id)
return DictDataOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_obj_list_service(cls, auth: AuthSchema, search: DictDataQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取数据字典数据列表
参数:
- auth (AuthSchema): 认证信息模型
- search (DictDataQueryParam | None): 搜索条件模型
- order_by (list[dict] | None): 排序字段列表
返回:
- list[dict]: 数据字典数据详情字典列表
"""
obj_list = await DictDataCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
return [DictDataOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def init_dict_service(cls, redis: Redis):
"""
应用初始化: 获取所有字典类型对应的字典数据信息并缓存service
参数:
- redis (Redis): Redis客户端
返回:
- None
"""
try:
async with async_db_session() as session:
async with session.begin():
# 在初始化过程中,不需要检查数据权限
auth = AuthSchema(db=session, check_data_scope=False)
obj_list = await DictTypeCRUD(auth).get_obj_list_crud()
if not obj_list:
log.warning("未找到任何字典类型数据")
return
success_count = 0
fail_count = 0
for obj in obj_list:
dict_type = obj.dict_type
try:
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
# 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type}"
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
success_count += 1
log.info(f"✅ 字典数据缓存成功: {dict_type}")
except Exception as e:
fail_count += 1
log.error(f"❌ 初始化字典数据失败 [{dict_type}]: {e}")
# 继续处理其他字典类型,不中断整个初始化过程
log.info(f"字典数据初始化完成 - 成功: {success_count}, 失败: {fail_count}")
except Exception as e:
log.error(f"字典初始化过程发生错误: {e}")
# 只在严重错误时抛出异常,允许单个字典加载失败
raise CustomException(msg=f"字典数据初始化失败: {str(e)}")
@classmethod
async def get_init_dict_service(cls, redis: Redis, dict_type: str)->list[dict]:
"""
从缓存获取字典数据列表信息service
参数:
- redis (Redis): Redis客户端
- dict_type (str): 字典类型
返回:
- list[dict]: 字典数据列表
"""
try:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type}"
obj_list_dict = await RedisCURD(redis).get(redis_key)
# 确保返回数据正确序列化
if obj_list_dict:
if isinstance(obj_list_dict, str):
try:
return json.loads(obj_list_dict)
except json.JSONDecodeError:
log.warning(f"字典数据反序列化失败,尝试重新初始化缓存: {dict_type}")
elif isinstance(obj_list_dict, list):
return obj_list_dict
# 缓存不存在或格式错误时重新初始化
await cls.init_dict_service(redis)
obj_list_dict = await RedisCURD(redis).get(redis_key)
if not obj_list_dict:
raise CustomException(msg="数据字典不存在")
# 再次确保返回数据正确序列化
if isinstance(obj_list_dict, str):
try:
return json.loads(obj_list_dict)
except json.JSONDecodeError:
raise CustomException(msg="字典数据格式错误")
return obj_list_dict
except CustomException:
raise
except Exception as e:
log.error(f"获取字典缓存失败: {str(e)}")
raise CustomException(msg=f"获取字典数据失败: {str(e)}")
@classmethod
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema) -> dict:
"""
创建数据字典数据
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- data (DictDataCreateSchema): 数据字典数据创建模型
返回:
- dict: 数据字典数据详情字典
"""
exist_obj = await DictDataCRUD(auth).get(dict_label=data.dict_label)
if exist_obj:
raise CustomException(msg='创建失败,该字典数据已存在')
obj = await DictDataCRUD(auth).create_obj_crud(data=data)
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
log.info(f"创建字典数据写入缓存成功: {obj}")
except Exception as e:
log.error(f"创建字典数据写入缓存失败: {e}")
raise CustomException(msg=f"创建字典数据失败 {e}")
return DictDataOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id:int, data: DictDataUpdateSchema) -> dict:
"""
更新数据字典数据
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- id (int): 数据字典数据ID
- data (DictDataUpdateSchema): 数据字典数据更新模型
返回:
- Dict: 数据字典数据详情字典
"""
exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='更新失败,该字典数据不存在')
if exist_obj.id != id:
raise CustomException(msg='更新失败,数据字典数据重复')
# 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态
if exist_obj.dict_type != data.dict_type:
dict_type = await DictTypeCRUD(auth).get(dict_type=exist_obj.dict_type)
if dict_type:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type.dict_type}"
try:
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': dict_type.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
except Exception as e:
log.error(f"更新字典数据类型变更时刷新旧缓存失败: {e}")
obj = await DictDataCRUD(auth).update_obj_crud(id=id, data=data)
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
log.info(f"更新字典数据写入缓存成功: {obj}")
except Exception as e:
log.error(f"更新字典数据写入缓存失败: {e}")
raise CustomException(msg=f"更新字典数据失败 {e}")
return DictDataOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
"""
删除数据字典数据
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis客户端
- ids (list[int]): 数据字典数据ID列表
返回:
- None
"""
try:
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
# 首先检查是否包含系统默认数据
for id in ids:
exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg=f'{id} 删除失败,该字典数据不存在')
# 系统默认字典数据不允许删除
if exist_obj.is_default:
raise CustomException(msg=f'删除失败ID为{id}的系统默认字典数据不允许删除')
# 获取所有需要清除的缓存键
dict_types_to_clear = set()
for id in ids:
exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id)
if exist_obj:
dict_types_to_clear.add(exist_obj.dict_type)
# 执行删除操作
await DictDataCRUD(auth).delete_obj_crud(ids=ids)
# 清除缓存
for dict_type in dict_types_to_clear:
try:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type}"
await RedisCURD(redis).delete(redis_key)
log.info(f"清除字典缓存成功: {dict_type}")
except Exception as e:
log.warning(f"清除字典缓存失败: {e}")
# 缓存清除失败不影响删除操作
log.info(f"删除字典数据成功ID列表: {ids}")
except CustomException:
raise
except Exception as e:
log.error(f"删除字典数据失败: {str(e)}")
raise CustomException(msg=f"删除字典数据失败: {str(e)}")
@classmethod
async def set_obj_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
批量修改数据字典数据状态
参数:
- auth (AuthSchema): 认证信息模型
- data (BatchSetAvailable): 批量修改数据字典数据状态负载模型
返回:
- None
"""
await DictDataCRUD(auth).set_obj_available_crud(ids=data.ids, status=data.status)
@classmethod
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
"""
导出数据字典数据列表
参数:
- data_list (list[dict]): 数据字典数据列表
返回:
- bytes: Excel文件字节流
"""
mapping_dict = {
'id': '编号',
'dict_sort': '字典排序',
'dict_label': '字典标签',
'dict_value': '字典键值',
'dict_type': '字典类型',
'css_class': '样式属性',
'list_class': '表格回显样式',
'is_default': '是否默认',
'status': '状态',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 复制数据并转换状态
data = data_list.copy()
for item in data:
# 处理状态
item['status'] = '启用' if item.get('status') == '0' else '停用'
# 处理是否默认
item['is_default'] = '' if item.get('is_default') else ''
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.request import PaginationService
from app.common.response import SuccessResponse, StreamResponse
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.core.dependencies import AuthPermission
from app.core.base_params import PaginationQueryParam
from app.core.logger import log
from ..auth.schema import AuthSchema
from .schema import OperationLogQueryParam
from .service import OperationLogService
LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["日志管理"])
@LogRouter.get("/list", summary="查询日志", description="查询日志")
async def get_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: OperationLogQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:log:query"]))
) -> JSONResponse:
"""
查询日志
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (OperationLogQueryParam): 日志查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含分页日志详情的 JSON 响应模型
"""
order_by = [{"created_time": "desc"}]
if page.order_by:
order_by = page.order_by
result_dict_list = await OperationLogService.get_log_list_service(search=search, auth=auth, order_by=order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询日志成功")
return SuccessResponse(data=result_dict, msg="查询日志成功")
@LogRouter.get("/detail/{id}", summary="日志详情", description="日志详情")
async def get_obj_detail_controller(
id: int = Path(..., description="操作日志ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:log:query"]))
) -> JSONResponse:
"""
获取日志详情
参数:
- id (int): 操作日志ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含日志详情的 JSON 响应模型
"""
result_dict = await OperationLogService.get_log_detail_service(id=id, auth=auth)
log.info(f"查询日志成功 {id}")
return SuccessResponse(data=result_dict, msg="获取日志详情成功")
@LogRouter.delete("/delete", summary="删除日志", description="删除日志")
async def delete_obj_log_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:log:delete"]))
) -> JSONResponse:
"""
删除日志
参数:
- ids (list[int]): 日志 ID 列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除结果的 JSON 响应模型
"""
await OperationLogService.delete_log_service(ids=ids, auth=auth)
log.info(f"删除日志成功 {ids}")
return SuccessResponse(msg="删除日志成功")
@LogRouter.post("/export", summary="导出日志", description="导出日志")
async def export_obj_list_controller(
search: OperationLogQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:log:export"]))
) -> StreamingResponse:
"""
导出日志
参数:
- search (OperationLogQueryParam): 日志查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse: 包含导出日志的流式响应模型
"""
operation_log_list = await OperationLogService.get_log_list_service(search=search, auth=auth)
operation_log_export_result = await OperationLogService.export_log_list_service(operation_log_list=operation_log_list)
log.info('导出日志成功')
return StreamResponse(
data=bytes2file_response(operation_log_export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=log.xlsx'
}
)

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import OperationLogModel
from .schema import OperationLogCreateSchema
class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, OperationLogCreateSchema]):
"""
操作日志数据层。
"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化操作日志CRUD。
"""
self.auth = auth
super().__init__(model=OperationLogModel, auth=auth)
async def create_crud(self, data: OperationLogCreateSchema) -> OperationLogModel | None:
"""
创建操作日志记录。
参数:
- data (OperationLogCreateSchema): 操作日志创建模型。
返回:
- OperationLogModel | None: 创建后的日志记录。
"""
return await self.create(data=data)
async def get_by_id_crud(self, id: int, preload: list | None = None) -> OperationLogModel | None:
"""
根据ID获取操作日志详情。
参数:
- id (int): 操作日志ID。
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- OperationLogModel | None: 操作日志记录。
"""
return await self.get(id=id, preload=preload)
async def get_list_crud(self, search: dict | None = None, order_by: list | None = None, preload: list | None = None) -> Sequence[OperationLogModel]:
"""
获取操作日志列表。
参数:
- search (Dict | None): 搜索条件字典。
- order_by (List[Dict[str, str]] | None): 排序字段列表。
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[OperationLogModel]: 操作日志列表。
"""
return await self.list(search=search, order_by=order_by, preload=preload)

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from sqlalchemy import String, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class OperationLogModel(ModelMixin, UserMixin):
"""
系统日志模型
日志类型:
- 1: 登录日志
- 2: 操作日志
"""
__tablename__: str = "sys_log"
__table_args__: dict[str, str] = ({'comment': '系统日志表'})
__loader_options__: list[str] = ["created_by", "updated_by"]
type: Mapped[int] = mapped_column(Integer, comment="日志类型(1登录日志 2操作日志)")
request_path: Mapped[str] = mapped_column(String(255), comment="请求路径")
request_method: Mapped[str] = mapped_column(String(10), comment="请求方式")
request_payload: Mapped[str | None] = mapped_column(Text, comment="请求体")
request_ip: Mapped[str | None] = mapped_column(String(50), comment="请求IP地址")
login_location: Mapped[str | None] = mapped_column(String(255), comment="登录位置")
request_os: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="操作系统")
request_browser: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="浏览器")
response_code: Mapped[int] = mapped_column(Integer, comment="响应状态码")
response_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="响应体")
process_time: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="处理时间")

View File

@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
import re
from pydantic import BaseModel, ConfigDict, Field, field_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema, UserBySchema
class OperationLogCreateSchema(BaseModel):
"""日志创建模型"""
type: int | None = Field(default=None, description="日志类型(1登录日志 2操作日志)")
request_path: str | None = Field(default=None, description="请求路径")
request_method: str | None = Field(default=None, description="请求方法")
request_payload: str | None = Field(default=None, description="请求负载")
request_ip: str | None = Field(default=None, description="请求 IP 地址")
login_location: str | None = Field(default=None, description="登录位置")
request_os: str | None = Field(default=None, description="请求操作系统")
request_browser: str | None = Field(default=None, description="请求浏览器")
response_code: int | None = Field(default=None, description="响应状态码")
response_json: str | None = Field(default=None, description="响应 JSON 数据")
process_time: str | None = Field(default=None, description="处理时间")
status: str = Field(default="0", description="是否成功")
description: str | None = Field(default=None, max_length=255, description="描述")
created_id: int | None = Field(default=None, description="创建人ID")
updated_id: int | None = Field(default=None, description="更新人ID")
@field_validator("type")
@classmethod
def _validate_type(cls, value: int):
if value is None:
return value
if value not in {1, 2}:
raise ValueError("日志类型仅支持 1(登录) 或 2(操作)")
return value
@field_validator("request_method")
@classmethod
def _validate_method(cls, value: str):
if value is None:
return value
allowed = {"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
if value.upper() not in allowed:
raise ValueError(f"请求方法必须为 {', '.join(sorted(allowed))}")
return value.upper()
@field_validator("request_ip")
@classmethod
def _validate_ip(cls, value: str | None):
if value is None or value == "":
return value
ipv4 = r"^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$"
ipv6 = r"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"
if not re.match(ipv4, value) and not re.match(ipv6, value):
raise ValueError("请求IP必须为有效的IPv4或IPv6地址")
return value
class OperationLogOutSchema(OperationLogCreateSchema, BaseSchema, UserBySchema):
"""日志响应模型"""
model_config = ConfigDict(from_attributes=True)
class OperationLogQueryParam:
"""操作日志查询参数"""
def __init__(
self,
type: int | None = Query(None, description="日志类型(1:登录日志, 2:操作日志)"),
request_path: str | None = Query(None, description="请求路径"),
request_method: str | None = Query(None, description="请求方法"),
request_ip: str | None = Query(None, description="请求IP"),
response_code: int | 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="更新人"),
) -> None:
# 模糊查询字段
self.request_path = ("like", f"%{request_path}%") if request_path else None
# 精确查询字段
self.created_id = created_id
self.updated_id = updated_id
self.request_method = request_method
self.request_ip = request_ip
self.response_code = response_code
self.type = type
# 时间范围查询 - 增加对单个时间参数的处理
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from ..auth.schema import AuthSchema
from .crud import OperationLogCRUD
from .schema import (
OperationLogCreateSchema,
OperationLogOutSchema,
OperationLogQueryParam
)
class OperationLogService:
"""
日志模块服务层
"""
@classmethod
async def get_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取日志详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 日志 ID
返回:
- dict: 日志详情字典
"""
log = await OperationLogCRUD(auth).get_by_id_crud(id=id)
log_dict = OperationLogOutSchema.model_validate(log).model_dump()
return log_dict
@classmethod
async def get_log_list_service(cls, auth: AuthSchema, search: OperationLogQueryParam | None = None, order_by: list | None = None) -> list[dict]:
"""
获取日志列表
参数:
- auth (AuthSchema): 认证信息模型
- search (OperationLogQueryParam | None): 日志查询参数模型
- order_by (list | None): 排序字段列表
返回:
- list[dict]: 日志详情字典列表
"""
log_list = await OperationLogCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
log_dict_list = [OperationLogOutSchema.model_validate(log).model_dump() for log in log_list]
return log_dict_list
@classmethod
async def create_log_service(cls, auth: AuthSchema, data: OperationLogCreateSchema) -> dict:
"""
创建日志
参数:
- auth (AuthSchema): 认证信息模型
- data (OperationLogCreateSchema): 日志创建模型
返回:
- dict: 日志详情字典
"""
new_log = await OperationLogCRUD(auth).create(data=data)
new_log_dict = OperationLogOutSchema.model_validate(new_log).model_dump()
return new_log_dict
@classmethod
async def delete_log_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除日志
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 日志 ID 列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
await OperationLogCRUD(auth).delete(ids=ids)
@classmethod
async def export_log_list_service(cls, operation_log_list: list[dict]) -> bytes:
"""
导出日志信息
参数:
- operation_log_list (list[dict]): 操作日志信息列表
返回:
- bytes: 操作日志信息excel的二进制数据
"""
# 操作日志字段映射
mapping_dict = {
'id': '编号',
'type': '日志类型',
'request_path': '请求URL',
'request_method': '请求方式',
'request_payload': '请求参数',
'request_ip': '操作地址',
'login_location': '登录位置',
'request_os': '操作系统',
'request_browser': '浏览器',
'response_json': '返回参数',
'response_code': '相应状态',
'process_time': '处理时间',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 处理数据
data = operation_log_list.copy()
for item in data:
# 处理状态
item['response_code'] = '成功' if item.get('response_code') == 200 else '失败'
# 处理日志类型 - 修正与schema.py保持一致
item['type'] = '登录日志' if item.get('type') == 1 else '操作日志'
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse
from app.core.dependencies import AuthPermission
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from ..auth.schema import AuthSchema
from .service import MenuService
from .schema import (
MenuCreateSchema,
MenuUpdateSchema,
MenuQueryParam
)
MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["菜单管理"])
@MenuRouter.get("/tree", summary="查询菜单树", description="查询菜单树")
async def get_menu_tree_controller(
search: MenuQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:query"]))
) -> JSONResponse:
"""
查询菜单树。
参数:
- search (MenuQueryParam): 查询参数模型。
返回:
- JSONResponse: 包含菜单树的 JSON 响应。
"""
order_by = [{"order": "asc"}]
result_dict_list = await MenuService.get_menu_tree_service(search=search, auth=auth, order_by=order_by)
log.info(f"查询菜单树成功")
return SuccessResponse(data=result_dict_list, msg="查询菜单树成功")
@MenuRouter.get("/detail/{id}", summary="查询菜单详情", description="查询菜单详情")
async def get_obj_detail_controller(
id: int = Path(..., description="菜单ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:query"]))
) -> JSONResponse:
"""
查询菜单详情。
参数:
- id (int): 菜单ID。
返回:
- JSONResponse: 包含菜单详情的 JSON 响应。
"""
result_dict = await MenuService.get_menu_detail_service(id=id, auth=auth)
log.info(f"查询菜单情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取菜单成功")
@MenuRouter.post("/create", summary="创建菜单", description="创建菜单")
async def create_obj_controller(
data: MenuCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:create"]))
) -> JSONResponse:
"""
创建菜单。
参数:
- data (MenuCreateSchema): 菜单创建模型。
返回:
- JSONResponse: 包含创建菜单的 JSON 响应。
"""
result_dict = await MenuService.create_menu_service(data=data, auth=auth)
log.info(f"创建菜单成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建菜单成功")
@MenuRouter.put("/update/{id}", summary="修改菜单", description="修改菜单")
async def update_obj_controller(
data: MenuUpdateSchema,
id: int = Path(..., description="菜单ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:update"]))
) -> JSONResponse:
"""
修改菜单。
参数:
- id (int): 菜单ID。
- data (MenuUpdateSchema): 菜单更新模型。
返回:
- JSONResponse: 包含修改菜单的 JSON 响应。
"""
result_dict = await MenuService.update_menu_service(id=id, data=data, auth=auth)
log.info(f"修改菜单成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改菜单成功")
@MenuRouter.delete("/delete", summary="删除菜单", description="删除菜单")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:delete"]))
) -> JSONResponse:
"""
删除菜单。
参数:
- ids (list[int]): 菜单ID列表。
返回:
- JSONResponse: 包含删除菜单的 JSON 响应。
"""
await MenuService.delete_menu_service(ids=ids, auth=auth)
log.info(f"删除菜单成功: {ids}")
return SuccessResponse(msg="删除菜单成功")
@MenuRouter.patch("/available/setting", summary="批量修改菜单状态", description="批量修改菜单状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:menu:patch"]))
) -> JSONResponse:
"""
批量修改菜单状态。
参数:
- data (BatchSetAvailable): 批量修改菜单状态模型。
返回:
- JSONResponse: 批量修改菜单状态的 JSON 响应。
"""
await MenuService.set_menu_available_service(data=data, auth=auth)
log.info(f"批量修改菜单状态成功: {data.ids}")
return SuccessResponse(msg="批量修改菜单状态成功")

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import MenuModel
from .schema import MenuCreateSchema, MenuUpdateSchema
class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
"""菜单模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化菜单CRUD"""
self.auth = auth
super().__init__(model=MenuModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> MenuModel | None:
"""
根据 id 获取菜单信息。
参数:
- id (int): 菜单 ID。
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- MenuModel | None: 菜单信息,未找到返回 None。
"""
obj = await self.get(id=id, preload=preload)
if not obj:
return None
return obj
async def get_list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list[str] | None = None) -> Sequence[MenuModel]:
"""
获取菜单列表。
参数:
- search (dict | None): 搜索条件。
- order_by (list[dict] | None): 排序字段列表。
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[MenuModel]: 菜单列表。
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def get_tree_list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list[str] | None = None) -> Sequence[MenuModel]:
"""
获取菜单树形列表。
参数:
- search (dict | None): 搜索条件。
- order_by (list[dict] | None): 排序字段列表。
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[MenuModel]: 菜单树形列表。
"""
return await self.tree_list(search=search, order_by=order_by, children_attr='children', preload=preload)
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
批量设置菜单可用状态。
参数:
- ids (list[int]): 菜单 ID 列表。
- status (str): 可用状态。
返回:
- None
"""
await self.set(ids=ids, status=status)

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, String, Integer, JSON, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from app.core.base_model import ModelMixin
if TYPE_CHECKING:
from app.api.v1.module_system.role.model import RoleModel
class MenuModel(ModelMixin):
"""
菜单表 - 用于存储系统菜单信息
菜单类型说明:
- 1: 目录(一级菜单)
- 2: 菜单(二级菜单)
- 3: 按钮/权限(页面内按钮权限)
- 4: 外部链接
"""
__tablename__: str = "sys_menu"
__table_args__: dict[str, str] = ({'comment': '菜单表'})
__loader_options__: list[str] = ["roles"]
name: Mapped[str] = mapped_column(String(50), nullable=False, comment='菜单名称')
type: Mapped[int] = mapped_column(Integer, nullable=False, default=2, comment='菜单类型(1:目录 2:菜单 3:按钮/权限 4:链接)')
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment='显示排序')
permission: Mapped[str | None] = mapped_column(String(100), comment='权限标识(如:module_system:user:list)')
icon: Mapped[str | None] = mapped_column(String(50), comment='菜单图标')
route_name: Mapped[str | None] = mapped_column(String(100), comment='路由名称')
route_path: Mapped[str | None] = mapped_column(String(200), comment='路由路径')
component_path: Mapped[str | None] = mapped_column(String(200), comment='组件路径')
redirect: Mapped[str | None] = mapped_column(String(200), comment='重定向地址')
hidden: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment='是否隐藏(True:隐藏 False:显示)')
keep_alive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment='是否缓存(True:是 False:否)')
always_show: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment='是否始终显示(True:是 False:否)')
title: Mapped[str | None] = mapped_column(String(50), comment='菜单标题')
params: Mapped[list[dict[str, str]] | None] = mapped_column(JSON, comment='路由参数(JSON对象)')
affix: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment='是否固定标签页(True:是 False:否)')
# 树形结构
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey('sys_menu.id', ondelete='SET NULL'),
default=None,
index=True,
comment='父菜单ID'
)
# 关联关系
parent: Mapped["MenuModel | None"] = relationship(
back_populates='children',
remote_side="MenuModel.id",
foreign_keys="MenuModel.parent_id",
uselist=False
)
children: Mapped[list["MenuModel"] | None] = relationship(
back_populates='parent',
foreign_keys="MenuModel.parent_id",
order_by="MenuModel.order"
)
roles: Mapped[list["RoleModel"]] = relationship(
secondary="sys_role_menus",
back_populates="menus",
lazy="selectin"
)

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.validator import menu_request_validator
from app.core.base_schema import BaseSchema
class MenuCreateSchema(BaseModel):
"""菜单创建模型"""
name: str = Field(..., max_length=50, description="菜单名称")
type: int = Field(..., ge=1, le=4, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)")
order: int = Field(..., ge=1, description="显示顺序")
permission: str | None = Field(default=None, max_length=100, description="权限标识")
icon: str | None = Field(default=None, max_length=100, description="菜单图标")
route_name: str | None = Field(default=None, max_length=100, description="路由名称")
route_path: str | None = Field(default=None, max_length=200, description="路由地址")
component_path: str | None = Field(default=None, max_length=255, description="组件路径")
redirect: str | None = Field(default=None, max_length=200, description="重定向地址")
hidden: bool = Field(default=False, description="是否隐藏(True:是 False:否)")
keep_alive: bool = Field(default=True, description="是否缓存(True:是 False:否)")
always_show: bool = Field(default=False, description="是否始终显示(True:是 False:否)")
title: str | None = Field(default=None, max_length=50, description="菜单标题")
params: list[dict[str, str]] | None = Field(default=None, description="路由参数,格式为[{key: string, value: string}]")
affix: bool = Field(default=False, description="是否固定标签页(True:是 False:否)")
parent_id: int | None = Field(default=None, ge=1, description="父菜单ID")
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
# 字符串去空格
for k in ["name", "icon", "permission", "route_name", "route_path", "component_path", "redirect", "title", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" else values[k].strip()
# 父ID转整型
if "parent_id" in values and isinstance(values["parent_id"], str):
try:
values["parent_id"] = int(values["parent_id"].strip())
except Exception:
pass
# 路由名/路径规范
import re
if "route_name" in values and isinstance(values["route_name"], str):
rn = values["route_name"]
if rn and not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,99}$", rn):
raise ValueError("路由名称需字母开头,仅含字母/数字/_ . -")
if "route_path" in values and isinstance(values["route_path"], str):
rp = values["route_path"]
if rp and not rp.startswith("/"):
raise ValueError("路由路径需以 / 开头")
return values
@model_validator(mode='after')
def validate_fields(self):
return menu_request_validator(self)
class MenuUpdateSchema(MenuCreateSchema):
"""菜单更新模型"""
parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
class MenuOutSchema(MenuCreateSchema, BaseSchema):
"""菜单响应模型"""
model_config = ConfigDict(from_attributes=True)
parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
class MenuQueryParam:
"""菜单管理查询参数"""
def __init__(
self,
name: str | None = Query(None, description="菜单名称"),
route_path: str | None = Query(None, description="路由地址"),
component_path: str | None = Query(None, description="组件路径"),
type: Literal[1,2,3,4] | None = Query(None, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)"),
permission: str | None = Query(None, description="权限标识"),
status: str | None = Query(None, description="菜单状态(0:启用 1:禁用)"),
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
) -> None:
# 模糊查询字段
self.name = ("like", name)
self.route_path = ("like", route_path)
self.component_path = ("like", component_path)
self.permission = ("like", permission)
# 精确查询字段
self.type = type
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))

View File

@@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.common_util import (
get_parent_id_map,
get_parent_recursion,
get_child_id_map,
get_child_recursion,
traversal_to_tree
)
from ..auth.schema import AuthSchema
from .crud import MenuCRUD
from .schema import (
MenuCreateSchema,
MenuUpdateSchema,
MenuOutSchema,
MenuQueryParam
)
class MenuService:
"""
菜单模块服务层
"""
@classmethod
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取菜单详情。
参数:
- auth (AuthSchema): 认证对象。
- id (int): 菜单ID。
返回:
- dict: 菜单详情对象。
"""
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
# 创建实例后再设置parent_name属性
menu_out = MenuOutSchema.model_validate(menu)
if menu and menu.parent_id:
parent = await MenuCRUD(auth).get_by_id_crud(id=menu.parent_id)
if parent:
menu_out.parent_name = parent.name
return menu_out.model_dump()
@classmethod
async def get_menu_tree_service(cls, auth: AuthSchema, search: MenuQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取菜单树形列表。
参数:
- auth (AuthSchema): 认证对象。
- search (MenuQueryParam | None): 查询参数对象。
- order_by (list[dict] | None): 排序参数列表。
返回:
- list[dict]: 菜单树形列表对象。
"""
# 使用树形结构查询预加载children关系
menu_list = await MenuCRUD(auth).get_tree_list_crud(search=search.__dict__, order_by=order_by)
# 转换为字典列表
menu_dict_list = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_list]
# 使用traversal_to_tree构建树形结构
return traversal_to_tree(menu_dict_list)
@classmethod
async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> dict:
"""
创建菜单。
参数:
- auth (AuthSchema): 认证对象。
- data (MenuCreateSchema): 创建参数对象。
返回:
- dict: 创建的菜单对象。
"""
menu = await MenuCRUD(auth).get(name=data.name)
if menu:
raise CustomException(msg='创建失败,该菜单已存在')
new_menu = await MenuCRUD(auth).create(data=data)
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
return new_menu_dict
@classmethod
async def update_menu_service(cls, auth: AuthSchema,id:int, data: MenuUpdateSchema) -> dict:
"""
更新菜单。
参数:
- auth (AuthSchema): 认证对象。
- id (int): 菜单ID。
- data (MenuUpdateSchema): 更新参数对象。
返回:
- dict: 更新的菜单对象。
"""
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
if not menu:
raise CustomException(msg='更新失败,该菜单不存在')
exist_menu = await MenuCRUD(auth).get(name=data.name)
if exist_menu and exist_menu.id != id:
raise CustomException(msg='更新失败,菜单名称重复')
if data.parent_id:
parent_menu = await MenuCRUD(auth).get_by_id_crud(id=data.parent_id)
if not parent_menu:
raise CustomException(msg='更新失败,父级菜单不存在')
data.parent_name = parent_menu.name
new_menu = await MenuCRUD(auth).update(id=id, data=data)
await cls.set_menu_available_service(auth=auth, data=BatchSetAvailable(ids=[id], status=data.status))
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
return new_menu_dict
@classmethod
async def delete_menu_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除菜单。
参数:
- auth (AuthSchema): 认证对象。
- ids (list[int]): 菜单ID列表。
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
if not menu:
raise CustomException(msg='删除失败,该菜单不存在')
# 校验是否存在子级菜单,存在则禁止删除
menu_list = await MenuCRUD(auth).get_list_crud()
id_map = get_child_id_map(model_list=menu_list)
for id in ids:
descendants = get_child_recursion(id=id, id_map=id_map)
if len(descendants) > 1:
raise CustomException(msg='删除失败,存在子级菜单,请先删除子级菜单')
await MenuCRUD(auth).delete(ids=ids)
@classmethod
async def set_menu_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
递归获取所有父、子级菜单,然后批量修改菜单可用状态。
参数:
- auth (AuthSchema): 认证对象。
- data (BatchSetAvailable): 批量设置可用参数对象。
返回:
- None
"""
menu_list = await MenuCRUD(auth).get_list_crud()
total_ids = []
if data.status:
# 激活,则需要把所有父级菜单都激活
id_map = get_parent_id_map(model_list=menu_list)
for menu_id in data.ids:
enable_ids = get_parent_recursion(id=menu_id, id_map=id_map)
total_ids.extend(enable_ids)
else:
# 禁止,则需要把所有子级菜单都禁止
id_map = get_child_id_map(model_list=menu_list)
for menu_id in data.ids:
disable_ids = get_child_recursion(id=menu_id, id_map=id_map)
total_ids.extend(disable_ids)
await MenuCRUD(auth).set_available_crud(ids=total_ids, status=data.status)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path, Query
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission, get_current_user, db_getter
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from sqlalchemy.ext.asyncio import AsyncSession
from ..auth.schema import AuthSchema
from .service import NoticeService
from .schema import (
NoticeCreateSchema,
NoticeUpdateSchema,
NoticeQueryParam,
NoticeOutSchema
)
from .crud import NoticeCRUD
NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=["公告通知"])
@NoticeRouter.get("/mini/list", summary="小程序消息列表", description="小程序获取消息列表(无需后台权限)")
async def get_notice_mini_list_controller(
page_no: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=50, description="每页数量"),
db: AsyncSession = Depends(db_getter),
) -> JSONResponse:
auth = AuthSchema(db=db, check_data_scope=False)
offset = (page_no - 1) * page_size
result = await NoticeCRUD(auth).page(
offset=offset,
limit=page_size,
order_by=[{"created_time": "desc"}],
search={"status": "0"},
out_schema=NoticeOutSchema,
)
log.info("小程序查询消息列表成功")
return SuccessResponse(data=result, msg="获取消息列表成功")
@NoticeRouter.get("/detail/{id}", summary="获取公告详情", description="获取公告详情")
async def get_obj_detail_controller(
id: int = Path(..., description="公告ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:query"]))
) -> JSONResponse:
"""
获取公告详情。
参数:
- id (int): 公告ID。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含公告详情的响应模型。
"""
result_dict = await NoticeService.get_notice_detail_service(id=id, auth=auth)
log.info(f"获取公告详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取公告详情成功")
@NoticeRouter.get("/list", summary="查询公告", description="查询公告")
async def get_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: NoticeQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:query"]))
) -> JSONResponse:
"""
查询公告。
参数:
- page (PaginationQueryParam): 分页查询参数模型。
- search (NoticeQueryParam): 查询公告参数模型。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含分页公告详情的响应模型。
"""
result_dict_list = await NoticeService.get_notice_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询公告列表成功")
return SuccessResponse(data=result_dict, msg="查询公告列表成功")
@NoticeRouter.post("/create", summary="创建公告", description="创建公告")
async def create_obj_controller(
data: NoticeCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:create"]))
) -> JSONResponse:
"""
创建公告。
参数:
- data (NoticeCreateSchema): 创建公告负载模型。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含创建公告结果的响应模型。
"""
result_dict = await NoticeService.create_notice_service(auth=auth, data=data)
log.info(f"创建公告成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建公告成功")
@NoticeRouter.put("/update/{id}", summary="修改公告", description="修改公告")
async def update_obj_controller(
data: NoticeUpdateSchema,
id: int = Path(..., description="公告ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:update"]))
) -> JSONResponse:
"""
修改公告。
参数:
- data (NoticeUpdateSchema): 修改公告负载模型。
- id (int): 公告ID。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含修改公告结果的响应模型。
"""
result_dict = await NoticeService.update_notice_service(auth=auth, id=id, data=data)
log.info(f"修改公告成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改公告成功")
@NoticeRouter.delete("/delete", summary="删除公告", description="删除公告")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:delete"]))
) -> JSONResponse:
"""
删除公告。
参数:
- ids (list[int]): 公告ID列表。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含删除公告结果的响应模型。
"""
await NoticeService.delete_notice_service(auth=auth, ids=ids)
log.info(f"删除公告成功: {ids}")
return SuccessResponse(msg="删除公告成功")
@NoticeRouter.patch("/available/setting", summary="批量修改公告状态", description="批量修改公告状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:patch"]))
) -> JSONResponse:
"""
批量修改公告状态。
参数:
- data (BatchSetAvailable): 批量修改公告状态负载模型。
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含批量修改公告状态结果的响应模型。
"""
await NoticeService.set_notice_available_service(auth=auth, data=data)
log.info(f"批量修改公告状态成功: {data.ids}")
return SuccessResponse(msg="批量修改公告状态成功")
@NoticeRouter.post('/export', summary="导出公告", description="导出公告")
async def export_obj_list_controller(
search: NoticeQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:notice:export"]))
) -> StreamingResponse:
"""
导出公告。
参数:
- search (NoticeQueryParam): 查询公告参数模型。
- auth (AuthSchema): 认证信息模型。
返回:
- StreamingResponse: 包含导出公告的流式响应模型。
"""
result_dict_list = await NoticeService.get_notice_list_service(search=search, auth=auth)
export_result = await NoticeService.export_notice_service(notice_list=result_dict_list)
log.info('导出公告成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {'Content-Disposition': 'attachment; filename=notice.xlsx'}
)
@NoticeRouter.get("/available", summary="获取全局启用公告", description="获取全局启用公告")
async def get_obj_list_available_controller(
auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse:
"""
获取全局启用公告。
参数:
- auth (AuthSchema): 认证信息模型。
返回:
- JSONResponse: 包含分页已启用公告详情的响应模型。
"""
result_dict_list = await NoticeService.get_notice_list_available_service(auth=auth)
result_dict = await PaginationService.paginate(data_list= result_dict_list)
log.info(f"查询已启用公告列表成功")
return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功")

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import NoticeModel
from .schema import NoticeCreateSchema, NoticeUpdateSchema
class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]):
"""公告数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化公告数据层。
参数:
- auth (AuthSchema): 认证信息模型。
"""
self.auth = auth
super().__init__(model=NoticeModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list | None = None) -> NoticeModel | None:
"""
根据ID获取公告详情。
参数:
- id (int): 公告ID。
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- NoticeModel | None: 公告模型实例。
"""
return await self.get(id=id, preload=preload)
async def get_list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[NoticeModel]:
"""
获取公告列表。
参数:
- search (dict | None): 查询参数。
- order_by (list[dict] | None): 排序参数。
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[NoticeModel]: 公告模型实例列表。
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_crud(self, data: NoticeCreateSchema) -> NoticeModel | None:
"""
创建公告。
参数:
- data (NoticeCreateSchema): 公告创建模型。
返回:
- NoticeModel | None: 公告模型实例。
"""
return await self.create(data=data)
async def update_crud(self, id: int, data: NoticeUpdateSchema) -> NoticeModel | None:
"""
更新公告。
参数:
- id (int): 公告ID。
- data (NoticeUpdateSchema): 公告更新模型。
返回:
- NoticeModel | None: 公告模型实例。
"""
return await self.update(id=id, data=data)
async def delete_crud(self, ids: list[int]) -> None:
"""
删除公告。
参数:
- ids (list[int]): 公告ID列表。
返回:
- None
"""
return await self.delete(ids=ids)
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
设置公告的可用状态。
参数:
- ids (list[int]): 公告ID列表。
- status (str): 可用状态。
返回:
- None
"""
return await self.set(ids=ids, status=status)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class NoticeModel(ModelMixin, UserMixin):
"""
通知公告表
"""
__tablename__: str = "sys_notice"
__table_args__: dict[str, str] = ({'comment': '通知公告表'})
__loader_options__: list[str] = ["created_by", "updated_by"]
notice_title: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告标题')
notice_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告类型(1通知 2公告)')
notice_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='公告内容')

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema, UserBySchema
class NoticeCreateSchema(BaseModel):
"""公告通知创建模型"""
notice_title: str = Field(..., max_length=50, description='公告标题')
notice_type: str = Field(..., description='公告类型1通知 2公告')
notice_content: str = Field(..., description='公告内容')
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("notice_type")
@classmethod
def _validate_notice_type(cls, value: str):
if value not in {"1", "2"}:
raise ValueError("公告类型仅支持 '1'(通知) 或 '2'(公告)")
return value
@model_validator(mode='after')
def _validate_after(self):
if not self.notice_title.strip():
raise ValueError("公告标题不能为空")
if not self.notice_content.strip():
raise ValueError("公告内容不能为空")
return self
class NoticeUpdateSchema(NoticeCreateSchema):
"""公告通知更新模型"""
...
class NoticeOutSchema(NoticeCreateSchema, BaseSchema, UserBySchema):
"""公告通知响应模型"""
model_config = ConfigDict(from_attributes=True)
class NoticeQueryParam:
"""公告通知查询参数"""
def __init__(
self,
notice_title: str | None = Query(None, description="公告标题"),
notice_type: 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"]),
created_id: int | None = Query(None, description="创建人"),
updated_id: int | None = Query(None, description="更新人"),
) -> None:
# 模糊查询字段
self.notice_title = ("like", notice_title)
# 精确查询字段
self.created_id = created_id
self.updated_id = updated_id
self.status = status
self.notice_type = notice_type
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from ..auth.schema import AuthSchema
from .schema import NoticeCreateSchema, NoticeUpdateSchema, NoticeOutSchema, NoticeQueryParam
from .crud import NoticeCRUD
class NoticeService:
"""
公告管理模块服务层
"""
@classmethod
async def get_notice_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取公告详情。
参数:
- auth (AuthSchema): 认证信息模型。
- id (int): 公告ID。
返回:
- Dict: 公告详情字典。
"""
notice_obj = await NoticeCRUD(auth).get_by_id_crud(id=id)
return NoticeOutSchema.model_validate(notice_obj).model_dump()
@classmethod
async def get_notice_list_available_service(cls, auth: AuthSchema) -> list[dict]:
"""
获取可用的公告列表。
参数:
- auth (AuthSchema): 认证信息模型。
返回:
- list[dict]: 可用公告详情字典列表。
"""
notice_obj_list = await NoticeCRUD(auth).get_list_crud(search={'status': '0',})
return [NoticeOutSchema.model_validate(notice_obj).model_dump() for notice_obj in notice_obj_list]
@classmethod
async def get_notice_list_service(cls, auth: AuthSchema, search: NoticeQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取公告列表。
参数:
- auth (AuthSchema): 认证信息模型。
- search (NoticeQueryParam | None): 查询参数模型。
- order_by (list[dict] | None): 排序参数列表。
返回:
- list[dict]: 公告详情字典列表。
"""
notice_obj_list = await NoticeCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [NoticeOutSchema.model_validate(notice_obj).model_dump() for notice_obj in notice_obj_list]
@classmethod
async def create_notice_service(cls, auth: AuthSchema, data: NoticeCreateSchema) -> dict:
"""
创建公告。
参数:
- auth (AuthSchema): 认证信息模型。
- data (NoticeCreateSchema): 创建公告负载模型。
返回:
- dict: 创建的公告详情字典。
异常:
- CustomException: 创建失败,该公告通知已存在。
"""
notice = await NoticeCRUD(auth).get(notice_title=data.notice_title)
if notice:
raise CustomException(msg='创建失败,该公告通知已存在')
notice_obj = await NoticeCRUD(auth).create_crud(data=data)
return NoticeOutSchema.model_validate(notice_obj).model_dump()
@classmethod
async def update_notice_service(cls, auth: AuthSchema, id: int, data: NoticeUpdateSchema) -> dict:
"""
更新公告。
参数:
- auth (AuthSchema): 认证信息模型。
- id (int): 公告ID。
- data (NoticeUpdateSchema): 更新公告负载模型。
返回:
- dict: 更新的公告详情字典。
异常:
- CustomException: 更新失败,该公告通知不存在或公告通知标题重复。
"""
notice = await NoticeCRUD(auth).get_by_id_crud(id=id)
if not notice:
raise CustomException(msg='更新失败,该公告通知不存在')
exist_notice = await NoticeCRUD(auth).get(notice_title=data.notice_title)
if exist_notice and exist_notice.id != id:
raise CustomException(msg='更新失败,公告通知标题重复')
notice_obj = await NoticeCRUD(auth).update_crud(id=id, data=data)
return NoticeOutSchema.model_validate(notice_obj).model_dump()
@classmethod
async def delete_notice_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除公告。
参数:
- auth (AuthSchema): 认证信息模型。
- ids (list[int]): 删除的ID列表。
异常:
- CustomException: 删除失败,删除对象不能为空或该公告通知不存在。
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
notice = await NoticeCRUD(auth).get_by_id_crud(id=id)
if not notice:
raise CustomException(msg='删除失败,该公告通知不存在')
await NoticeCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_notice_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
批量设置公告状态。
参数:
- auth (AuthSchema): 认证信息模型。
- data (BatchSetAvailable): 批量设置可用负载模型。
异常:
- CustomException: 批量设置失败,该公告通知不存在。
"""
await NoticeCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def export_notice_service(cls, notice_list: list[dict]) -> bytes:
"""
导出公告列表。
参数:
- notice_list (list[dict]): 公告详情字典列表。
返回:
- bytes: Excel 文件的字节流。
"""
mapping_dict = {
'id': '编号',
'notice_title': '公告标题',
'notice_type': '公告类型1通知 2公告',
'notice_content': '公告内容',
'status': '状态',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 复制数据并转换状态
data = notice_list.copy()
for item in data:
# 处理状态
item['status'] = '启用' if item.get('status') == '0' else '停用'
# 处理公告类型
item['notice_type'] = '通知' if item.get('notice_type') == '1' else '公告'
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from redis.asyncio.client import Redis
from app.common.request import PaginationService
from app.common.response import StreamResponse, SuccessResponse
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission, redis_getter
from app.core.logger import log
from app.core.exceptions import CustomException
from ..auth.schema import AuthSchema
from .schema import ParamsCreateSchema, ParamsUpdateSchema, ParamsQueryParam
from .service import ParamsService
ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=["参数管理"])
@ParamsRouter.get("/detail/{id}", summary="获取参数详情", description="获取参数详情")
async def get_type_detail_controller(
id: int = Path(..., description="参数ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:query"]))
) -> JSONResponse:
"""
获取参数详情
参数:
- id (int): 参数ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含参数详情的 JSON 响应
"""
result_dict = await ParamsService.get_obj_detail_service(id=id, auth=auth)
log.info(f"获取参数详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取参数详情成功")
@ParamsRouter.get("/key/{config_key}", summary="根据配置键获取参数详情", description="根据配置键获取参数详情")
async def get_obj_by_key_controller(
config_key: str = Path(..., description="配置键"),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:query"]))
) -> JSONResponse:
"""
根据配置键获取参数详情
参数:
- config_key (str): 配置键
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含参数详情的 JSON 响应
"""
result_dict = await ParamsService.get_obj_by_key_service(config_key=config_key, auth=auth)
log.info(f"根据配置键获取参数详情成功 {config_key}")
return SuccessResponse(data=result_dict, msg="根据配置键获取参数详情成功")
@ParamsRouter.get("/value/{config_key}", summary="根据配置键获取参数值", description="根据配置键获取参数值")
async def get_config_value_by_key_controller(
config_key: str = Path(..., description="配置键"),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:query"]))
) -> JSONResponse:
"""
根据配置键获取参数值
参数:
- config_key (str): 配置键
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含参数值的 JSON 响应
"""
result_value = await ParamsService.get_config_value_by_key_service(config_key=config_key, auth=auth)
log.info(f"根据配置键获取参数值成功 {config_key}")
return SuccessResponse(data=result_value, msg="根据配置键获取参数值成功")
@ParamsRouter.get("/list", summary="获取参数列表", description="获取参数列表")
async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(["module_system:param:query"])),
page: PaginationQueryParam = Depends(),
search: ParamsQueryParam = Depends(),
) -> JSONResponse:
"""
获取参数列表
参数:
- auth (AuthSchema): 认证信息模型
- page (PaginationQueryParam): 分页查询参数模型
- search (ParamsQueryParam): 参数查询参数模型
返回:
- JSONResponse: 包含参数列表的 JSON 响应
"""
result_dict_list = await ParamsService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"获取参数列表成功")
return SuccessResponse(data=result_dict, msg="查询参数列表成功")
@ParamsRouter.post("/create", summary="创建参数", description="创建参数")
async def create_obj_controller(
data: ParamsCreateSchema,
redis: Redis = Depends(redis_getter),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:create"]))
) -> JSONResponse:
"""
创建参数
参数:
- data (ParamsCreateSchema): 参数创建模型
- redis (Redis): Redis 客户端实例
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建参数结果的 JSON 响应
"""
result_dict = await ParamsService.create_obj_service(auth=auth, redis=redis, data=data)
log.info(f"创建参数成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建参数成功")
@ParamsRouter.put("/update/{id}", summary="修改参数", description="修改参数")
async def update_objs_controller(
data: ParamsUpdateSchema,
id: int = Path(..., description="参数ID"),
redis: Redis = Depends(redis_getter),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:update"]))
) -> JSONResponse:
"""
修改参数
参数:
- data (ParamsUpdateSchema): 参数更新模型
- id (int): 参数ID
- redis (Redis): Redis 客户端实例
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改参数结果的 JSON 响应
"""
result_dict = await ParamsService.update_obj_service(auth=auth, redis=redis, id=id, data=data)
log.info(f"更新参数成功 {result_dict}")
return SuccessResponse(data=result_dict, msg="更新参数成功")
@ParamsRouter.delete("/delete", summary="删除参数", description="删除参数")
async def delete_obj_controller(
redis: Redis = Depends(redis_getter),
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:delete"]))
) -> JSONResponse:
"""
删除参数
参数:
- redis (Redis): Redis 客户端实例
- ids (list[int]): 参数ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除参数结果的 JSON 响应
"""
await ParamsService.delete_obj_service(auth=auth, redis=redis, ids=ids)
log.info(f"删除参数成功: {ids}")
return SuccessResponse(msg="删除参数成功")
@ParamsRouter.post('/export', summary="导出参数", description="导出参数")
async def export_obj_list_controller(
search: ParamsQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:param:export"]))
) -> StreamingResponse:
"""
导出参数
参数:
- search (ParamsQueryParam): 参数查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse: 包含导出参数的 Excel 文件流响应
"""
result_dict_list = await ParamsService.get_obj_list_service(search=search, auth=auth)
export_result = await ParamsService.export_obj_service(data_list=result_dict_list)
log.info('导出参数成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=params.xlsx'
}
)
@ParamsRouter.post("/upload", summary="上传文件", description="上传文件到阿里云OSS", dependencies=[Depends(AuthPermission([]))])
async def upload_file_controller(
file: UploadFile
) -> JSONResponse:
"""
后台管理员上传文件到阿里云OSS
参数:
- file (UploadFile): 上传的文件对象
返回:
- JSONResponse: 包含上传文件结果的 JSON 响应
"""
if not file or not file.filename:
raise CustomException(msg="请选择要上传的文件")
log.info(f"开始上传文件: {file.filename}, 大小: {file.size} bytes")
try:
result_str = await ParamsService.upload_service(file=file)
log.info(f"文件上传成功: {file.filename}")
return SuccessResponse(data=result_str, msg='上传文件成功')
except CustomException as e:
log.error(f"文件上传失败: {file.filename}, 错误: {str(e)}")
raise
except Exception as e:
log.error(f"文件上传异常: {file.filename}, 错误: {str(e)}")
raise CustomException(msg="文件上传失败")
@ParamsRouter.get("/info", summary="获取初始化缓存参数", description="获取初始化缓存参数")
async def get_init_obj_controller(
redis: Redis = Depends(redis_getter),
) -> JSONResponse:
"""
获取初始化缓存参数
参数:
- redis (Redis): Redis 客户端实例
返回:
- JSONResponse: 获取初始化缓存参数的 JSON 响应
"""
result_dict = await ParamsService.get_init_config_service(redis=redis)
log.info(f"获取初始化缓存参数成功 {result_dict}")
return SuccessResponse(data=result_dict, msg="获取初始化缓存参数成功")

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import ParamsModel
from .schema import ParamsCreateSchema, ParamsUpdateSchema
class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]):
"""配置管理数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化配置CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=ParamsModel, auth=auth)
async def get_obj_by_id_crud(self, id: int, preload: list | None = None) -> ParamsModel | None:
"""
获取配置管理型详情
参数:
- id (int): 配置管理型ID
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- ParamsModel | None: 配置管理型模型实例
"""
return await self.get(id=id, preload=preload)
async def get_obj_by_key_crud(self, key: str, preload: list | None = None) -> ParamsModel | None:
"""
根据key获取配置管理型详情
参数:
- key (str): 配置管理型key
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- ParamsModel | None: 配置管理型模型实例
"""
return await self.get(config_key=key, preload=preload)
async def get_obj_list_crud(self, search: dict | None = None, order_by: list | None = None, preload: list | None = None) -> Sequence[ParamsModel]:
"""
获取配置管理型列表
参数:
- search (dict | None): 查询参数对象。
- order_by (list | None): 排序参数列表。
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[ParamsModel]: 配置管理型模型实例列表
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: ParamsCreateSchema) -> ParamsModel | None:
"""
创建配置管理型
参数:
- data (ParamsCreateSchema): 创建配置管理型负载模型
返回:
- ParamsModel | None: 配置管理型模型实例
"""
return await self.create(data=data)
async def update_obj_crud(self, id: int, data: ParamsUpdateSchema) -> ParamsModel | None:
"""
更新配置管理型
参数:
- id (int): 配置管理型ID
- data (ParamsUpdateSchema): 更新配置管理型负载模型
返回:
- ParamsModel | None: 配置管理型模型实例
"""
return await self.update(id=id, data=data)
async def delete_obj_crud(self, ids: list[int]) -> None:
"""
删除配置管理型
参数:
- ids (list[int]): 配置管理型ID列表
返回:
- None
"""
return await self.delete(ids=ids)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from sqlalchemy import String, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin
class ParamsModel(ModelMixin):
"""
参数配置表
"""
__tablename__: str = "sys_param"
__table_args__: dict[str, str] = ({'comment': '系统参数表'})
config_name: Mapped[str] = mapped_column(String(500), nullable=False, comment='参数名称')
config_key: Mapped[str] = mapped_column(String(500), nullable=False, comment='参数键名')
config_value: Mapped[str | None] = mapped_column(String(500), comment='参数键值')
config_type: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True, comment="系统内置(True:是 False:否)")

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field, field_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema
class ParamsCreateSchema(BaseModel):
"""配置创建模型"""
config_name: str = Field(..., max_length=64, description="参数名称")
config_key: str = Field(..., max_length=500, description="参数键名")
config_value: str | None = Field(default=None, description="参数键值")
config_type: bool = Field(default=False, description="系统内置(True:是 False:否)")
status: str = Field(default="0", description="状态(True:正常 False:停用)")
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
class ParamsUpdateSchema(ParamsCreateSchema):
"""配置更新模型"""
...
class ParamsOutSchema(ParamsCreateSchema, BaseSchema):
"""配置响应模型"""
model_config = ConfigDict(from_attributes=True)
class ParamsQueryParam:
"""配置管理查询参数"""
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:否))"),
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"]),
) -> None:
# 模糊查询字段
self.config_name = ("like", config_name)
self.config_key = ("like", config_key)
# 精确查询字段
self.config_type = config_type
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,396 @@
# -*- coding: utf-8 -*-
import json
from redis.asyncio.client import Redis
from fastapi import UploadFile
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.database import async_db_session
from app.core.redis_crud import RedisCURD
from app.utils.excel_util import ExcelUtil
from app.utils.upload_util import UploadUtil
from app.utils.oss_util import OSSUtil
from app.core.base_schema import UploadResponseSchema
from app.core.exceptions import CustomException
from app.core.logger import log
from ..auth.schema import AuthSchema
from .schema import ParamsOutSchema, ParamsUpdateSchema, ParamsCreateSchema, ParamsQueryParam
from .crud import ParamsCRUD
class ParamsService:
"""
配置管理模块服务层
"""
@classmethod
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取配置详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 配置管理型ID
返回:
- dict: 配置管理型模型实例字典表示
"""
obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
return ParamsOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_obj_by_key_service(cls, auth: AuthSchema, config_key: str) -> dict:
"""
根据配置键获取配置详情
参数:
- auth (AuthSchema): 认证信息模型
- config_key (str): 配置管理型key
返回:
- Dict: 配置管理型模型实例字典表示
"""
obj = await ParamsCRUD(auth).get_obj_by_key_crud(key=config_key)
if not obj:
raise CustomException(msg=f'配置键 {config_key} 不存在')
return ParamsOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_config_value_by_key_service(cls, auth: AuthSchema, config_key: str) -> str | None:
"""
根据配置键获取配置值
参数:
- auth (AuthSchema): 认证信息模型
- config_key (str): 配置管理型key
返回:
- str | None: 配置值字符串或None
"""
obj = await ParamsCRUD(auth).get_obj_by_key_crud(key=config_key)
if not obj:
raise CustomException(msg=f'配置键 {config_key} 不存在')
return obj.config_value
@classmethod
async def get_obj_list_service(cls, auth: AuthSchema, search: ParamsQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取配置管理型列表
参数:
- auth (AuthSchema): 认证信息模型
- search (ParamsQueryParam | None): 查询参数对象
- order_by (list[dict] | None): 排序参数列表
返回:
- list[dict]: 配置管理型模型实例字典列表表示
"""
obj_list = None
if search:
obj_list = await ParamsCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
else:
obj_list = await ParamsCRUD(auth).get_obj_list_crud()
return [ParamsOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: ParamsCreateSchema) -> dict:
"""
创建配置管理型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis 客户端实例
- data (ParamsCreateSchema): 配置管理型创建模型
返回:
- dict: 新创建的配置管理型模型实例字典表示
"""
exist_obj = await ParamsCRUD(auth).get(config_key=data.config_key)
if exist_obj:
raise CustomException(msg='创建失败该配置key已存在')
obj = await ParamsCRUD(auth).create_obj_crud(data=data)
new_obj_dict = ParamsOutSchema.model_validate(obj).model_dump()
# 同步redis
redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{data.config_key}"
try:
result = await RedisCURD(redis).set(
key=redis_key,
value="",
)
if not result:
log.error(f"同步配置到缓存失败: {new_obj_dict}")
raise CustomException(msg="同步配置到缓存失败")
except Exception as e:
log.error(f"创建字典类型失败: {e}")
raise CustomException(msg=f"创建字典类型失败 {e}")
return new_obj_dict
@classmethod
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id:int, data: ParamsUpdateSchema) -> dict:
"""
更新配置管理型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis 客户端实例
- id (int): 配置管理型ID
- data (ParamsUpdateSchema): 配置管理型更新模型
返回:
- Dict: 更新后的配置管理型模型实例字典表示
"""
exist_obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='更新失败,该数系统配置不存在')
if exist_obj.config_key != data.config_key:
raise CustomException(msg='更新失败系统配置key不允许修改')
new_obj = await ParamsCRUD(auth).update_obj_crud(id=id, data=data)
if not new_obj:
raise CustomException(msg='更新失败,系统配置不存在')
new_obj_dict = ParamsOutSchema.model_validate(new_obj).model_dump()
# 同步redis
redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{new_obj.config_key}"
try:
value = json.dumps(new_obj_dict, ensure_ascii=False)
result = await RedisCURD(redis).set(
key=redis_key,
value=value,
)
if not result:
log.error(f"同步配置到缓存失败: {new_obj_dict}")
raise CustomException(msg="同步配置到缓存失败")
except Exception as e:
log.error(f"更新系统配置失败: {e}")
raise CustomException(msg="更新系统配置失败")
return new_obj_dict
@classmethod
async def delete_obj_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None:
"""
删除配置管理型
参数:
- auth (AuthSchema): 认证信息模型
- redis (Redis): Redis 客户端实例
- ids (list[int]): 配置管理型ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
exist_obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='删除失败,该数据字典类型不存在')
# 检查是否是否初始化类型
if exist_obj.config_type:
# 如果有字典数据,不能删除
raise CustomException(msg=f'{exist_obj.config_name} 删除失败,系统初始化配置不可以删除')
await ParamsCRUD(auth).delete_obj_crud(ids=ids)
# 同步删除Redis缓存
for id in ids:
exist_obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
continue
redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{exist_obj.config_key}"
try:
await RedisCURD(redis).delete(redis_key)
log.info(f"删除系统配置成功: {id}")
except Exception as e:
log.error(f"删除系统配置失败: {e}")
raise CustomException(msg="删除字典类型失败")
@classmethod
async def export_obj_service(cls, data_list: list[dict]) -> bytes:
"""
导出系统配置列表
参数:
- data_list (list[dict]): 系统配置模型实例字典列表表示
返回:
- bytes: Excel文件二进制数据
"""
mapping_dict = {
'id': '编号',
'config_name': '参数名称',
'config_key': '参数键名',
'config_value': '参数键值',
'config_type': '系统内置((True:是 False:否))',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 复制数据并转换状态
data = data_list.copy()
for item in data:
# 处理状态
item['config_type'] = '' if item.get('config_type') else ''
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
@classmethod
async def upload_service(cls, file: UploadFile) -> dict:
"""
上传文件到OSS
参数:
- file (UploadFile): 上传的文件对象
返回:
- dict: 上传文件的响应模型实例字典表示
"""
# 使用OSS上传替代本地上传
oss_util = OSSUtil()
filename, oss_key, file_url = await oss_util.upload_file(file=file)
log.info(f"文件上传OSS成功: {filename}")
return UploadResponseSchema(
file_path=oss_key, # OSS对象键
file_name=filename, # 生成的文件名
origin_name=file.filename, # 原始文件名
file_url=file_url, # OSS访问URL
).model_dump()
@classmethod
async def init_config_service(cls, redis: Redis) -> None:
"""
初始化系统配置
参数:
- redis (Redis): Redis 客户端实例
返回:
- None
"""
async with async_db_session() as session:
async with session.begin():
# 在初始化过程中,不需要检查数据权限
auth = AuthSchema(db=session, check_data_scope=False)
config_obj = await ParamsCRUD(auth).get_obj_list_crud()
if not config_obj:
raise CustomException(msg="系统配置不存在")
try:
# 保存到Redis并设置过期时间
for config in config_obj:
redis_key = (f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{config.config_key}")
config_obj_dict = ParamsOutSchema.model_validate(config).model_dump()
value = json.dumps(config_obj_dict, ensure_ascii=False)
result = await RedisCURD(redis).set(
key=redis_key,
value=value,
)
log.info(f"✅ 系统配置缓存成功: {config.config_key}")
if not result:
log.error(f"❌️ 初始化系统配置失败: {config_obj_dict}")
raise CustomException(msg="初始化系统配置失败")
except Exception as e:
log.error(f"❌️ 初始化系统配置失败: {e}")
raise CustomException(msg="初始化系统配置失败")
@classmethod
async def get_init_config_service(cls, redis: Redis) -> list[dict]:
"""
获取系统配置
参数:
- redis (Redis): Redis 客户端实例
返回:
- list[dict]: 系统配置模型实例字典列表表示
"""
redis_keys = await RedisCURD(redis).get_keys(f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:*")
redis_configs = await RedisCURD(redis).mget(redis_keys)
configs = []
for config in redis_configs:
if not config:
continue
try:
new_config = json.loads(config)
configs.append(new_config)
except Exception as e:
log.error(f"解析系统配置数据失败: {e}")
continue
return configs
@classmethod
async def get_system_config_for_middleware(cls, redis: Redis) -> dict:
"""
获取中间件所需的系统配置
参数:
- redis (Redis): Redis 客户端实例
返回:
- dict: 包含演示模式、IP白名单、API白名单和IP黑名单的配置字典
"""
# 定义需要获取的配置键
config_keys = [
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:demo_enable",
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:ip_white_list",
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:white_api_list_path",
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:ip_black_list"
]
# 批量获取配置
config_values = await RedisCURD(redis).mget(config_keys)
# 初始化默认配置
config_result = {
"demo_enable": False,
"ip_white_list": [],
"white_api_list_path": [],
"ip_black_list": []
}
# 解析演示模式配置
if config_values[0]:
try:
demo_config = json.loads(config_values[0])
config_result["demo_enable"] = demo_config.get("config_value", False) if isinstance(demo_config, dict) else False
except json.JSONDecodeError:
log.error(f"解析演示模式配置失败")
# 解析IP白名单配置
if config_values[1]:
try:
ip_white_config = json.loads(config_values[1])
# 确保是列表类型
config_result["ip_white_list"] = json.loads(ip_white_config.get("config_value", []))
except json.JSONDecodeError:
log.error(f"解析IP白名单配置失败")
# 解析IP黑名单
# 解析API路径白名单
if config_values[2]:
try:
white_api_config = json.loads(config_values[2])
# 确保是列表类型
config_result["white_api_list_path"] = json.loads(white_api_config.get("config_value", []))
except json.JSONDecodeError:
log.error(f"解析API白名单配置失败")
# 解析IP黑名单
if config_values[3]:
try:
black_ip_config = json.loads(config_values[3])
# 确保是列表类型
config_result["ip_black_list"] = json.loads(black_ip_config.get("config_value", []))
except json.JSONDecodeError:
log.error(f"解析IP黑名单配置失败")
return config_result

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from ..auth.schema import AuthSchema
from .service import PositionService
from .schema import (
PositionCreateSchema,
PositionUpdateSchema,
PositionQueryParam
)
PositionRouter = APIRouter(route_class=OperationLogRoute, prefix="/position", tags=["岗位管理"])
@PositionRouter.get("/list", summary="查询岗位", description="查询岗位")
async def get_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: PositionQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:position:query"])),
) -> JSONResponse:
"""
查询岗位列表
参数:
- page (PaginationQueryParam): 分页查询参数
- search (PositionQueryParam): 查询参数
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 分页查询结果
"""
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict_list = await PositionService.get_position_list_service(search=search, auth=auth, order_by=order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询岗位列表成功")
return SuccessResponse(data=result_dict, msg="查询岗位列表成功")
@PositionRouter.get("/detail/{id}", summary="查询岗位详情", description="查询岗位详情")
async def get_obj_detail_controller(
id: int = Path(..., description="岗位ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:position:query"])),
) -> JSONResponse:
"""
查询岗位详情
参数:
- id (int): 岗位ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 岗位详情对象
"""
result_dict = await PositionService.get_position_detail_service(id=id, auth=auth)
log.info(f"查询岗位详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取岗位详情成功")
@PositionRouter.post("/create", summary="创建岗位", description="创建岗位")
async def create_obj_controller(
data: PositionCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:position:create"])),
) -> JSONResponse:
"""
创建岗位
参数:
- data (PositionCreateSchema): 创建岗位模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 岗位详情对象
"""
result_dict = await PositionService.create_position_service(data=data, auth=auth)
log.info(f"创建岗位成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建岗位成功")
@PositionRouter.put("/update/{id}", summary="修改岗位", description="修改岗位")
async def update_obj_controller(
data: PositionUpdateSchema,
id: int = Path(..., description="岗位ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:position:update"])),
) -> JSONResponse:
"""
修改岗位
参数:
- data (PositionUpdateSchema): 修改岗位模型
- id (int): 岗位ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 岗位详情对象
"""
result_dict = await PositionService.update_position_service(id=id, data=data, auth=auth)
log.info(f"修改岗位成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改岗位成功")
@PositionRouter.delete("/delete", summary="删除岗位", description="删除岗位")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:position:delete"])),
) -> JSONResponse:
"""
删除岗位
参数:
- ids (list[int]): ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 成功消息
"""
await PositionService.delete_position_service(ids=ids, auth=auth)
log.info(f"删除岗位成功: {ids}")
return SuccessResponse(msg="删除岗位成功")
@PositionRouter.patch("/available/setting", summary="批量修改岗位状态", description="批量修改岗位状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:position:patch"])),
) -> JSONResponse:
"""
批量修改岗位状态
参数:
- data (BatchSetAvailable): 批量修改岗位状态模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 成功消息
"""
await PositionService.set_position_available_service(data=data, auth=auth)
log.info(f"批量修改岗位状态成功: {data.ids}")
return SuccessResponse(msg="批量修改岗位状态成功")
@PositionRouter.post('/export', summary="导出岗位", description="导出岗位")
async def export_obj_list_controller(
search: PositionQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:position:export"])),
) -> StreamingResponse:
"""
导出岗位
参数:
- search (PositionQueryParam): 查询参数
- auth (AuthSchema): 认证信息模型
返回:
- 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)
log.info('导出岗位成功')
return StreamResponse(
data=bytes2file_response(position_export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=position.xlsx'
}
)

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
from typing import Sequence, Any
from app.core.base_crud import CRUDBase
from ..auth.schema import AuthSchema
from .model import PositionModel
from .schema import PositionCreateSchema, PositionUpdateSchema
class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateSchema]):
"""岗位模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化岗位CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=PositionModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> PositionModel | None:
"""
根据 id 获取岗位信息。
参数:
- id (int): 岗位 ID。
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- PositionModel | None: 岗位信息,未找到返回 None。
"""
return await self.get(id=id, preload=preload)
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, Any]] | None = None, preload: list[str] | None = None) -> Sequence[PositionModel]:
"""
获取岗位列表。
参数:
- search (dict | None): 搜索条件。
- order_by (list[dict[str, Any]] | None): 排序字段列表。
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[PositionModel]: 岗位列表。
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
批量设置岗位可用状态。
参数:
- ids (list[int]): 岗位 ID 列表。
- status (bool): 可用状态。
返回:
- None
"""
await self.set(ids=ids, status=status)
async def get_name_crud(self, ids: list[int]) -> list[str]:
"""
根据 id 列表获取岗位名称。
参数:
- ids (list[int]): 岗位 ID 列表。
返回:
- list[str]: 岗位名称列表。
"""
position_names = []
for id in ids:
obj = await self.get(id=id)
if obj:
position_names.append(obj.name)
return position_names

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import String, Integer
from sqlalchemy.orm import relationship, Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
if TYPE_CHECKING:
from app.api.v1.module_system.user.model import UserModel
class PositionModel(ModelMixin, UserMixin):
"""
岗位模型
"""
__tablename__: str = "sys_position"
__table_args__: dict[str, str] = ({'comment': '岗位表'})
__loader_options__: list[str] = ["users", "created_by", "updated_by"]
name: Mapped[str] = mapped_column(String(40), nullable=False, comment="岗位名称")
order: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="显示排序")
# 关联关系
users: Mapped[list["UserModel"]] = relationship(
secondary="sys_user_positions",
back_populates="positions",
lazy="selectin"
)

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
from fastapi import Query
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema, UserBySchema
class PositionCreateSchema(BaseModel):
"""岗位创建模型"""
name: str = Field(..., max_length=64, description="岗位名称")
order: int = Field(default=1, ge=1, description='显示排序')
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@classmethod
def _validate_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError('岗位名称不能为空')
return v
class PositionUpdateSchema(PositionCreateSchema):
"""岗位更新模型"""
...
class PositionOutSchema(PositionCreateSchema, BaseSchema, UserBySchema):
"""岗位信息响应模型"""
model_config = ConfigDict(from_attributes=True)
...
class PositionQueryParam:
"""岗位管理查询参数"""
def __init__(
self,
name: Optional[str] = Query(None, description="岗位名称"),
status: Optional[str] = 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="更新人"),
) -> None:
# 模糊查询字段
self.name = ("like", name)
# 精确查询字段
self.created_id = created_id
self.updated_id = updated_id
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from ..auth.schema import AuthSchema
from .crud import PositionCRUD
from .schema import (
PositionCreateSchema,
PositionUpdateSchema,
PositionOutSchema,
PositionQueryParam
)
class PositionService:
"""岗位模块服务层"""
@classmethod
async def get_position_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取岗位详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 岗位ID
返回:
- Dict: 岗位详情对象
"""
position = await PositionCRUD(auth).get_by_id_crud(id=id)
return PositionOutSchema.model_validate(position).model_dump()
@classmethod
async def get_position_list_service(cls, auth: AuthSchema, search: PositionQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
"""
获取岗位列表
参数:
- auth (AuthSchema): 认证信息模型
- search (PositionQueryParam | None): 查询参数对象
- order_by (list[dict] | None): 排序参数列表
返回:
- list[dict]: 岗位列表对象
"""
position_list = await PositionCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [PositionOutSchema.model_validate(position).model_dump() for position in position_list]
@classmethod
async def create_position_service(cls, auth: AuthSchema, data: PositionCreateSchema) -> dict:
"""
创建岗位
参数:
- auth (AuthSchema): 认证信息模型
- data (PositionCreateSchema): 岗位创建模型
返回:
- Dict: 创建的岗位对象
"""
position = await PositionCRUD(auth).get(name=data.name)
if position:
raise CustomException(msg='创建失败,该岗位已存在')
new_position = await PositionCRUD(auth).create(data=data)
return PositionOutSchema.model_validate(new_position).model_dump()
@classmethod
async def update_position_service(cls, auth: AuthSchema, id:int, data: PositionUpdateSchema) -> dict:
"""
更新岗位
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 岗位ID
- data (PositionUpdateSchema): 岗位更新模型
返回:
- dict: 更新的岗位对象
"""
position = await PositionCRUD(auth).get_by_id_crud(id=id)
if not position:
raise CustomException(msg='更新失败,该岗位不存在')
exist_position = await PositionCRUD(auth).get(name=data.name)
if exist_position and exist_position.id != id:
raise CustomException(msg='更新失败,岗位名称重复')
updated_position = await PositionCRUD(auth).update(id=id, data=data)
return PositionOutSchema.model_validate(updated_position).model_dump()
@classmethod
async def delete_position_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除岗位
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 岗位ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
position = await PositionCRUD(auth).get_by_id_crud(id=id)
if not position:
raise CustomException(msg='删除失败,该岗位不存在')
await PositionCRUD(auth).delete(ids=ids)
@classmethod
async def set_position_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
设置岗位状态
参数:
- auth (AuthSchema): 认证信息模型
- data (BatchSetAvailable): 批量设置状态模型
返回:
- None
"""
await PositionCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def export_position_list_service(cls, position_list: list[dict]) -> bytes:
"""
导出岗位列表
参数:
- position_list (list[dict]): 岗位列表对象
返回:
- bytes: 导出的Excel文件字节流
"""
mapping_dict = {
'id': '编号',
'name': '岗位名称',
'order': '显示顺序',
'status': '状态',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 复制数据并转换状态
data = position_list.copy()
for item in data:
item['status'] = '启用' if item.get('status') == '0' else '停用'
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from ..auth.schema import AuthSchema
from .service import RoleService
from .schema import (
RoleCreateSchema,
RoleUpdateSchema,
RolePermissionSettingSchema,
RoleQueryParam
)
RoleRouter = APIRouter(route_class=OperationLogRoute, prefix="/role", tags=["角色管理"])
@RoleRouter.get("/list", summary="查询角色", description="查询角色")
async def get_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: RoleQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:role:query"])),
) -> JSONResponse:
"""
查询角色
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (RoleQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 分页查询结果JSON响应
"""
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict_list = await RoleService.get_role_list_service(search=search, auth=auth, order_by=order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询角色成功")
return SuccessResponse(data=result_dict, msg="查询角色成功")
@RoleRouter.get("/detail/{id}", summary="查询角色详情", description="查询角色详情")
async def get_obj_detail_controller(
id: int = Path(..., description="角色ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:role:query"])),
) -> JSONResponse:
"""
查询角色详情
参数:
- id (int): 角色ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 角色详情JSON响应
"""
result_dict = await RoleService.get_role_detail_service(id=id, auth=auth)
log.info(f"获取角色详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取角色详情成功")
@RoleRouter.post("/create", summary="创建角色", description="创建角色")
async def create_obj_controller(
data: RoleCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:role:create"])),
) -> JSONResponse:
"""
创建角色
参数:
- data (RoleCreateSchema): 创建角色模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 创建角色JSON响应
"""
result_dict = await RoleService.create_role_service(data=data, auth=auth)
log.info(f"创建角色成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建角色成功")
@RoleRouter.put("/update/{id}", summary="修改角色", description="修改角色")
async def update_obj_controller(
data: RoleUpdateSchema,
id: int = Path(..., description="角色ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:role:update"])),
) -> JSONResponse:
"""
修改角色
参数:
- data (RoleUpdateSchema): 修改角色模型
- id (int): 角色ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 修改角色JSON响应
"""
result_dict = await RoleService.update_role_service(id=id, data=data, auth=auth)
log.info(f"修改角色成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改角色成功")
@RoleRouter.delete("/delete", summary="删除角色", description="删除角色")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:role:delete"])),
) -> JSONResponse:
"""
删除角色
参数:
- ids (list[int]): ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 删除角色JSON响应
"""
await RoleService.delete_role_service(ids=ids, auth=auth)
log.info(f"删除角色成功: {ids}")
return SuccessResponse(msg="删除角色成功")
@RoleRouter.patch("/available/setting", summary="批量修改角色状态", description="批量修改角色状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:role:patch"])),
) -> JSONResponse:
"""
批量修改角色状态
参数:
- data (BatchSetAvailable): 批量修改角色状态模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 批量修改角色状态JSON响应
"""
await RoleService.set_role_available_service(data=data, auth=auth)
log.info(f"批量修改角色状态成功: {data.ids}")
return SuccessResponse(msg="批量修改角色状态成功")
@RoleRouter.patch("/permission/setting", summary="角色授权", description="角色授权")
async def set_role_permission_controller(
data: RolePermissionSettingSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:role:permission"])),
) -> JSONResponse:
"""
角色授权
参数:
- data (RolePermissionSettingSchema): 角色授权模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 角色授权JSON响应
"""
await RoleService.set_role_permission_service(data=data, auth=auth)
log.info(f"设置角色权限成功: {data}")
return SuccessResponse(msg="授权角色成功")
@RoleRouter.post('/export', summary="导出角色", description="导出角色")
async def export_obj_list_controller(
search: RoleQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:role:export"])),
) -> StreamingResponse:
"""
导出角色
参数:
- search (RoleQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- 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)
log.info('导出角色成功')
return StreamResponse(
data=bytes2file_response(role_export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=role.xlsx'
}
)

View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from .model import RoleModel
from .schema import RoleCreateSchema, RoleUpdateSchema
from ..auth.schema import AuthSchema
from ..menu.crud import MenuCRUD
from ..dept.crud import DeptCRUD
class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
"""角色模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化角色模块数据层
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=RoleModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list | None = None) -> RoleModel | None:
"""
根据id获取角色信息
参数:
- id (int): 角色ID
- preload (list | None): 预加载选项
返回:
- RoleModel | None: 角色模型对象
"""
return await self.get(id=id, preload=preload)
async def get_list_crud(self, search: dict | None = None, order_by: list | None = None, preload: list | None = None) -> Sequence[RoleModel]:
"""
获取角色列表
参数:
- search (dict | None): 查询参数
- order_by (list | None): 排序参数
- preload (list | None): 预加载选项
返回:
- Sequence[RoleModel]: 角色模型对象列表
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def set_role_menus_crud(self, role_ids: list[int], menu_ids: list[int]) -> None:
"""
设置角色的菜单权限
参数:
- role_ids (List[int]): 角色ID列表
- menu_ids (List[int]): 菜单ID列表
返回:
- None
"""
roles = await self.list(search={"id": ("in", role_ids)})
menus = await MenuCRUD(self.auth).get_list_crud(search={"id": ("in", menu_ids)})
for obj in roles:
relationship = obj.menus
relationship.clear()
relationship.extend(menus)
await self.auth.db.flush()
async def set_role_data_scope_crud(self, role_ids: list[int], data_scope: int) -> None:
"""
设置角色的数据范围
参数:
- role_ids (list[int]): 角色ID列表
- data_scope (int): 数据范围
返回:
- None
"""
await self.set(ids=role_ids, data_scope=data_scope)
async def set_role_depts_crud(self, role_ids: list[int], dept_ids: list[int]) -> None:
"""
设置角色的部门权限
参数:
- role_ids (list[int]): 角色ID列表
- dept_ids (list[int]): 部门ID列表
返回:
- None
"""
roles = await self.list(search={"id": ("in", role_ids)})
depts = await DeptCRUD(self.auth).get_list_crud(search={"id": ("in", dept_ids)})
for obj in roles:
relationship = obj.depts
relationship.clear()
relationship.extend(depts)
await self.auth.db.flush()
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
设置角色的可用状态
参数:
- ids (list[int]): 角色ID列表
- status (str): 可用状态
返回:
- None
"""
await self.set(ids=ids, status=status)

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from app.core.base_model import MappedBase, ModelMixin, UserMixin
if TYPE_CHECKING:
from app.api.v1.module_system.menu.model import MenuModel
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.user.model import UserModel
class RoleMenusModel(MappedBase):
"""
角色菜单关联表
定义角色与菜单的多对多关系,用于权限控制
"""
__tablename__: str = "sys_role_menus"
__table_args__: dict[str, str] = ({'comment': '角色菜单关联表'})
role_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="角色ID"
)
menu_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="菜单ID"
)
class RoleDeptsModel(MappedBase):
"""
角色部门关联表
定义角色与部门的多对多关系,用于数据权限控制
仅当角色的data_scope=5(自定义数据权限)时使用此表
"""
__tablename__: str = "sys_role_depts"
__table_args__: dict[str, str] = ({'comment': '角色部门关联表'})
role_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="角色ID"
)
dept_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_dept.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="部门ID"
)
class RoleModel(ModelMixin):
"""
角色模型
"""
__tablename__: str = "sys_role"
__table_args__: dict[str, str] = ({'comment': '角色表'})
__loader_options__: list[str] = ["menus", "depts"]
name: Mapped[str] = mapped_column(String(40), nullable=False, comment="角色名称")
code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="角色编码")
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
data_scope: Mapped[int] = mapped_column(Integer, default=1, nullable=False, comment="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)")
# 关联关系 (继承自UserMixin)
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"
)

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema
from app.core.validator import role_permission_request_validator
from ..dept.schema import DeptOutSchema
from ..menu.schema import MenuOutSchema
class RoleCreateSchema(BaseModel):
"""角色创建模型"""
name: str = Field(..., max_length=40, description="角色名称")
code: str | None = Field(default=None, max_length=40, description="角色编码")
order: int | None = Field(default=1, ge=1, description='显示排序')
data_scope: int | None = Field(default=1, description='数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)')
status: str = Field(default="0", description="是否启用")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("code")
@classmethod
def validate_code(cls, value: str | None):
if value is None:
return value
import re
v = value.strip()
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,39}$", v):
raise ValueError("角色编码需字母开头,允许字母/数字/下划线长度2-40")
return v
class RolePermissionSettingSchema(BaseModel):
"""角色权限配置模型"""
data_scope: int = Field(default=1, description='数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)')
role_ids: list[int] = Field(default_factory=list, description='角色ID列表')
menu_ids: list[int] = Field(default_factory=list, description='菜单ID列表')
dept_ids: list[int] = Field(default_factory=list, description='部门ID列表')
@model_validator(mode='after')
def validate_fields(self):
"""验证权限配置字段"""
return role_permission_request_validator(self)
class RoleUpdateSchema(RoleCreateSchema):
"""角色更新模型"""
...
class RoleOutSchema(RoleCreateSchema, BaseSchema):
"""角色信息响应模型"""
model_config = ConfigDict(from_attributes=True)
menus: list[MenuOutSchema] = Field(default_factory=list, description='角色菜单列表')
depts: list[DeptOutSchema] = Field(default_factory=list, description='角色部门列表')
class RoleQueryParam:
"""角色管理查询参数"""
def __init__(
self,
name: 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"]),
) -> None:
# 模糊查询字段
self.name = ("like", name)
# 精确查询字段
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
from typing import Any
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from ..auth.schema import AuthSchema
from .crud import RoleCRUD
from .schema import (
RoleCreateSchema,
RoleUpdateSchema,
RolePermissionSettingSchema,
RoleOutSchema,
RoleQueryParam
)
class RoleService:
"""角色模块服务层"""
@classmethod
async def get_role_detail_service(cls, auth: AuthSchema, id: int) -> dict:
"""
获取角色详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 角色ID
返回:
- dict: 角色详情字典
"""
role = await RoleCRUD(auth).get_by_id_crud(id=id)
return RoleOutSchema.model_validate(role).model_dump()
@classmethod
async def get_role_list_service(cls, auth: AuthSchema, search: RoleQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
"""
获取角色列表
参数:
- auth (AuthSchema): 认证信息模型
- search (RoleQueryParam | None): 查询参数模型
- order_by (list[dict[str, str]] | None): 排序参数列表
返回:
- list[dict]: 角色详情字典列表
"""
role_list = await RoleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [RoleOutSchema.model_validate(role).model_dump() for role in role_list]
@classmethod
async def create_role_service(cls, auth: AuthSchema, data: RoleCreateSchema) -> dict:
"""
创建角色
参数:
- auth (AuthSchema): 认证信息模型
- data (RoleCreateSchema): 创建角色模型
返回:
- dict: 新创建的角色详情字典
"""
role = await RoleCRUD(auth).get(name=data.name)
if role:
raise CustomException(msg='创建失败,该角色已存在')
obj = await RoleCRUD(auth).get(code=data.code)
if obj:
raise CustomException(msg='创建失败,编码已存在')
new_role = await RoleCRUD(auth).create(data=data)
return RoleOutSchema.model_validate(new_role).model_dump()
@classmethod
async def update_role_service(cls, auth: AuthSchema, id: int, data: RoleUpdateSchema) -> dict:
"""
更新角色
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 角色ID
- data (RoleUpdateSchema): 更新角色模型
返回:
- dict: 更新后的角色详情字典
"""
role = await RoleCRUD(auth).get_by_id_crud(id=id)
if not role:
raise CustomException(msg='更新失败,该角色不存在')
exist_role = await RoleCRUD(auth).get(name=data.name)
if exist_role and exist_role.id != id:
raise CustomException(msg='更新失败,角色名称重复')
updated_role = await RoleCRUD(auth).update(id=id, data=data)
return RoleOutSchema.model_validate(updated_role).model_dump()
@classmethod
async def delete_role_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除角色
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 角色ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
role = await RoleCRUD(auth).get_by_id_crud(id=id)
if not role:
raise CustomException(msg='删除失败,该角色不存在')
await RoleCRUD(auth).delete(ids=ids)
@classmethod
async def set_role_permission_service(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None:
"""
设置角色权限
参数:
- auth (AuthSchema): 认证信息模型
- data (RolePermissionSettingSchema): 角色权限设置模型
返回:
- None
"""
# 设置角色菜单权限
await RoleCRUD(auth).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids)
# 设置数据权限范围
await RoleCRUD(auth).set_role_data_scope_crud(role_ids=data.role_ids, data_scope=data.data_scope)
# 设置自定义数据权限部门
if data.data_scope == 5 and data.dept_ids:
await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids)
else:
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:
"""
设置角色可用状态
参数:
- auth (AuthSchema): 认证信息模型
- data (BatchSetAvailable): 批量设置可用状态模型
返回:
- None
"""
await RoleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def export_role_list_service(cls, role_list: list[dict[str, Any]]) -> bytes:
"""
导出角色列表
参数:
- role_list (list[dict[str, Any]]): 角色详情字典列表
返回:
- bytes: Excel文件字节流
"""
# 字段映射配置
mapping_dict = {
'id': '角色编号',
'name': '角色名称',
'order': '显示顺序',
'data_scope': '数据权限',
'status': '状态',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'created_id': '创建者ID',
'updated_id': '更新者ID',
}
# 数据权限映射
data_scope_map = {
1: '仅本人数据权限',
2: '本部门数据权限',
3: '本部门及以下数据权限',
4: '全部数据权限',
5: '自定义数据权限'
}
# 处理数据
data = role_list.copy()
for item in data:
item['status'] = '启用' if item.get('status') == '0' else '停用'
item['data_scope'] = data_scope_map.get(item.get('data_scope', 1), '')
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,369 @@
# -*- coding: utf-8 -*-
import urllib.parse
from fastapi import APIRouter, Depends, Body, Path, UploadFile, Request
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.core.dependencies import db_getter, get_current_user, AuthPermission
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import BatchSetAvailable
from app.core.logger import log
from ..auth.schema import AuthSchema
from .service import UserService
from .schema import (
CurrentUserUpdateSchema,
ResetPasswordSchema,
UserCreateSchema,
UserForgetPasswordSchema,
UserRegisterSchema,
UserUpdateSchema,
UserChangePasswordSchema,
UserQueryParam
)
UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["用户管理"])
@UserRouter.get("/current/info", summary="查询当前用户信息", description="查询当前用户信息")
async def get_current_user_info_controller(
auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse:
"""
查询当前用户信息
参数:
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 当前用户信息JSON响应
"""
result_dict = await UserService.get_current_user_info_service(auth=auth)
log.info(f"获取当前用户信息成功")
return SuccessResponse(data=result_dict, msg='获取当前用户信息成功')
@UserRouter.post("/current/avatar/upload", summary="上传当前用户头像", dependencies=[Depends(get_current_user)])
async def user_avatar_upload_controller(
file: UploadFile,
request: Request
) -> JSONResponse:
"""
上传当前用户头像
参数:
- file (UploadFile): 上传的文件
- request (Request): 请求对象
返回:
- JSONResponse: 上传头像JSON响应
"""
result_str = await UserService.upload_avatar_service(base_url=str(request.base_url), file=file)
log.info(f"上传头像成功: {result_str}")
return SuccessResponse(data=result_str, msg='上传头像成功')
@UserRouter.put("/current/info/update", summary="更新当前用户基本信息", description="更新当前用户基本信息")
async def update_current_user_info_controller(
data: CurrentUserUpdateSchema,
auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse:
"""
更新当前用户基本信息
参数:
- data (CurrentUserUpdateSchema): 当前用户更新模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 更新当前用户基本信息JSON响应
"""
result_dict = await UserService.update_current_user_info_service(data=data, auth=auth)
log.info(f"更新当前用户基本信息成功: {result_dict}")
return SuccessResponse(data=result_dict, msg='更新当前用户基本信息成功')
@UserRouter.put("/current/password/change", summary="修改当前用户密码", description="修改当前用户密码")
async def change_current_user_password_controller(
data: UserChangePasswordSchema,
auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse:
"""
修改当前用户密码
参数:
- data (UserChangePasswordSchema): 用户密码修改模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 修改密码JSON响应
"""
result_dict = await UserService.change_user_password_service(data=data, auth=auth)
log.info(f"修改密码成功: {result_dict}")
return SuccessResponse(data=result_dict, msg='修改密码成功, 请重新登录')
@UserRouter.put("/reset/password", summary="重置密码", description="重置密码")
async def reset_password_controller(
data: ResetPasswordSchema,
auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse:
"""
重置密码
参数:
- data (ResetPasswordSchema): 重置密码模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 重置密码JSON响应
"""
result_dict = await UserService.reset_user_password_service(data=data, auth=auth)
log.info(f"重置密码成功: {result_dict}")
return SuccessResponse(data=result_dict, msg='重置密码成功')
@UserRouter.post('/register', summary="注册用户", description="注册用户")
async def register_user_controller(
data: UserRegisterSchema,
db: AsyncSession = Depends(db_getter),
) -> JSONResponse:
"""
注册用户
参数:
- data (UserRegisterSchema): 用户注册模型
- db (AsyncSession): 异步数据库会话
返回:
- JSONResponse: 注册用户JSON响应
"""
auth = AuthSchema(db=db)
user_register_result = await UserService.register_user_service(data=data, auth=auth)
log.info(f"{data.username} 注册用户成功: {user_register_result}")
return SuccessResponse(data=user_register_result, msg='注册用户成功')
@UserRouter.post('/forget/password', summary="忘记密码", description="忘记密码")
async def forget_password_controller(
data: UserForgetPasswordSchema,
db: AsyncSession = Depends(db_getter),
) -> JSONResponse:
"""
忘记密码
参数:
- data (UserForgetPasswordSchema): 用户忘记密码模型
- db (AsyncSession): 异步数据库会话
返回:
- JSONResponse: 忘记密码JSON响应
"""
auth = AuthSchema(db=db)
user_forget_password_result = await UserService.forget_password_service(data=data, auth=auth)
log.info(f"{data.username} 重置密码成功: {user_forget_password_result}")
return SuccessResponse(data=user_forget_password_result, msg='重置密码成功')
@UserRouter.get("/list", summary="查询用户", description="查询用户")
async def get_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: UserQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:user:query"])),
) -> JSONResponse:
"""
查询用户
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (UserQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 分页查询结果JSON响应
"""
result_dict_list = await UserService.get_user_list_service(search=search, auth=auth, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
log.info(f"查询用户成功")
return SuccessResponse(data=result_dict, msg="查询用户成功")
@UserRouter.get("/detail/{id}", summary="查询用户详情", description="查询用户详情")
async def get_obj_detail_controller(
id: int = Path(..., description="用户ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:user:query"])),
) -> JSONResponse:
"""
查询用户详情
参数:
- id (int): 用户ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 用户详情JSON响应
"""
result_dict = await UserService.get_detail_by_id_service(id=id, auth=auth)
log.info(f"获取用户详情成功 {id}")
return SuccessResponse(data=result_dict, msg='获取用户详情成功')
@UserRouter.post("/create", summary="创建用户", description="创建用户")
async def create_obj_controller(
data: UserCreateSchema,
auth: AuthSchema = Depends(AuthPermission(["module_system:user:create"])),
) -> JSONResponse:
"""
创建用户
**注意**:
- 创建用户时, 默认密码为: <PASSWORD>
- 创建用户时, 默认用户状态为: 启用
参数:
- data (UserCreateSchema): 用户创建模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 创建用户JSON响应
"""
result_dict = await UserService.create_user_service(data=data, auth=auth)
log.info(f"创建用户成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建用户成功")
@UserRouter.put("/update/{id}", summary="修改用户", description="修改用户")
async def update_obj_controller(
data: UserUpdateSchema,
id: int = Path(..., description="用户ID"),
auth: AuthSchema = Depends(AuthPermission(["module_system:user:update"])),
) -> JSONResponse:
"""
修改用户
参数:
- data (UserUpdateSchema): 用户修改模型
- id (int): 用户ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 修改用户JSON响应
"""
result_dict = await UserService.update_user_service(id=id, data=data, auth=auth)
log.info(f"修改用户成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改用户成功")
@UserRouter.delete("/delete", summary="删除用户", description="删除用户")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["module_system:user:delete"])),
) -> JSONResponse:
"""
删除用户
参数:
- ids (list[int]): 用户ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 删除用户JSON响应
"""
await UserService.delete_user_service(ids=ids, auth=auth)
log.info(f"删除用户成功: {ids}")
return SuccessResponse(msg="删除用户成功")
@UserRouter.patch("/available/setting", summary="批量修改用户状态", description="批量修改用户状态")
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["module_system:user:patch"])),
) -> JSONResponse:
"""
批量修改用户状态
参数:
- data (BatchSetAvailable): 批量修改用户状态模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 批量修改用户状态JSON响应
"""
await UserService.set_user_available_service(data=data, auth=auth)
log.info(f"批量修改用户状态成功: {data.ids}")
return SuccessResponse(msg="批量修改用户状态成功")
@UserRouter.post('/import/template', summary="获取用户导入模板", description="获取用户导入模板", dependencies=[Depends(AuthPermission(["module_system:user:import"]))])
async def export_obj_template_controller()-> StreamingResponse:
"""
获取用户导入模板
返回:
- StreamingResponse: 用户导入模板流响应
"""
user_import_template_result = await UserService.get_import_template_user_service()
log.info('获取用户导入模板成功')
return StreamResponse(
data=bytes2file_response(user_import_template_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': f'attachment; filename={urllib.parse.quote("用户导入模板.xlsx")}',
'Access-Control-Expose-Headers': 'Content-Disposition'
}
)
@UserRouter.post('/export', summary="导出用户", description="导出用户")
async def export_obj_list_controller(
page: PaginationQueryParam = Depends(),
search: UserQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["module_system:user:export"])),
) -> StreamingResponse:
"""
导出用户
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (UserQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- 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)
log.info('导出用户成功')
return StreamResponse(
data=bytes2file_response(user_export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=user.xlsx'
}
)
@UserRouter.post('/import/data', summary="导入用户", description="导入用户")
async def import_obj_list_controller(
file: UploadFile,
auth: AuthSchema = Depends(AuthPermission(["module_system:user:import"]))
) -> JSONResponse:
"""
导入用户
参数:
- file (UploadFile): 用户导入文件
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 导入用户JSON响应
"""
batch_import_result = await UserService.batch_import_user_service(file=file, auth=auth, update_support=True)
log.info(f"导入用户成功: {batch_import_result}")
return SuccessResponse(data=batch_import_result, msg="导入用户成功")

View File

@@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
from typing import Sequence, Any
from datetime import datetime
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import UserModel
from .schema import UserCreateSchema, UserForgetPasswordSchema, UserUpdateSchema
from ..role.crud import RoleCRUD
from ..position.crud import PositionCRUD
class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
"""用户模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化用户CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=UserModel, auth=auth)
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> UserModel | None:
"""
根据id获取用户信息
参数:
- id (int): 用户ID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- UserModel | None: 用户信息,如果不存在则为None
"""
return await self.get(
preload=preload,
id=id,
)
async def get_by_username_crud(self, username: str, preload: list[str | Any] | None = None) -> UserModel | None:
"""
根据用户名获取用户信息
参数:
- username (str): 用户名
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- UserModel | None: 用户信息,如果不存在则为None
"""
return await self.get(
preload=preload,
username=username,
)
async def get_by_mobile_crud(self, mobile: str, preload: list[str | Any] | None = None) -> UserModel | None:
"""
根据手机号获取用户信息
参数:
- mobile (str): 手机号
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- UserModel | None: 用户信息,如果不存在则为None
"""
return await self.get(
preload=preload,
mobile=mobile,
)
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[UserModel]:
"""
获取用户列表
参数:
- search (dict | None): 查询参数对象。
- order_by (list[dict[str, str]] | None): 排序参数列表。
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[UserModel]: 用户列表
"""
return await self.list(
search=search,
order_by=order_by,
preload=preload,
)
async def update_last_login_crud(self, id: int) -> UserModel | None:
"""
更新用户最后登录时间
参数:
- id (int): 用户ID
返回:
- UserModel | None: 更新后的用户信息
"""
return await self.update(id=id, data={"last_login": datetime.now()})
async def set_available_crud(self, ids: list[int], status: str) -> None:
"""
批量设置用户可用状态
参数:
- ids (list[int]): 用户ID列表
- status (bool): 可用状态
返回:
- None:
"""
await self.set(ids=ids, status=status)
async def set_user_roles_crud(self, user_ids: list[int], role_ids: list[int]) -> None:
"""
批量设置用户角色
参数:
- user_ids (list[int]): 用户ID列表
- role_ids (list[int]): 角色ID列表
返回:
- None:
"""
user_objs = await self.list(search={"id": ("in", user_ids)})
if role_ids:
role_objs = await RoleCRUD(self.auth).get_list_crud(search={"id": ("in", role_ids)})
else:
role_objs = []
for obj in user_objs:
relationship = obj.roles
relationship.clear()
relationship.extend(role_objs)
await self.auth.db.flush()
async def set_user_positions_crud(self, user_ids: list[int], position_ids: list[int]) -> None:
"""
批量设置用户岗位
参数:
- user_ids (list[int]): 用户ID列表
- position_ids (list[int]): 岗位ID列表
返回:
- None:
"""
user_objs = await self.list(search={"id": ("in", user_ids)})
if position_ids:
position_objs = await PositionCRUD(self.auth).get_list_crud(search={"id": ("in", position_ids)})
else:
position_objs = []
for obj in user_objs:
relationship = obj.positions
relationship.clear()
relationship.extend(position_objs)
await self.auth.db.flush()
async def change_password_crud(self, id: int, password_hash: str) -> UserModel:
"""
修改用户密码
参数:
- id (int): 用户ID
- password_hash (str): 密码哈希值
返回:
- UserModel: 更新后的用户信息
"""
return await self.update(id=id, data=UserUpdateSchema(password=password_hash))
async def forget_password_crud(self, id: int, password_hash: str) -> UserModel:
"""
重置密码
参数:
- id (int): 用户ID
- password_hash (str): 密码哈希值
返回:
- UserModel: 更新后的用户信息
"""
return await self.update(id=id, data=UserUpdateSchema(password=password_hash))
async def register_user_crud(self, data: UserForgetPasswordSchema) -> UserModel:
"""
用户注册
参数:
- data (UserForgetPasswordSchema): 用户注册信息
返回:
- UserModel: 注册成功的用户信息
"""
return await self.create(data=UserCreateSchema(**data.model_dump()))
async def get_by_wx_openid_crud(self, wx_openid: str, preload: list[str | Any] | None = None) -> UserModel | None:
"""
根据微信小程序OpenID获取用户信息
参数:
- wx_openid (str): 微信小程序OpenID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- UserModel | None: 用户信息,如果不存在则为None
"""
return await self.get(
preload=preload,
wx_openid=wx_openid,
)
async def bind_wx_openid_crud(self, id: int, wx_openid: str, wx_unionid: str | None = None) -> UserModel | None:
"""
绑定微信小程序OpenID
参数:
- id (int): 用户ID
- wx_openid (str): 微信小程序OpenID
- wx_unionid (str | None): 微信UnionID
返回:
- UserModel | None: 更新后的用户信息
"""
data = {"wx_openid": wx_openid}
if wx_unionid:
data["wx_unionid"] = wx_unionid
return await self.update(id=id, data=data)

View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from datetime import datetime
from sqlalchemy import Boolean, String, Integer, DateTime, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from app.core.base_model import MappedBase, ModelMixin, UserMixin
if TYPE_CHECKING:
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.position.model import PositionModel
from app.api.v1.module_system.role.model import RoleModel
class UserRolesModel(MappedBase):
"""
用户角色关联表
定义用户与角色的多对多关系
"""
__tablename__: str = "sys_user_roles"
__table_args__: dict[str, str] = ({'comment': '用户角色关联表'})
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_user.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="用户ID"
)
role_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="角色ID"
)
class UserPositionsModel(MappedBase):
"""
用户岗位关联表
定义用户与岗位的多对多关系
"""
__tablename__: str = "sys_user_positions"
__table_args__: dict[str, str] = ({'comment': '用户岗位关联表'})
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_user.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="用户ID"
)
position_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_position.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="岗位ID"
)
class UserModel(ModelMixin, UserMixin):
"""
用户模型
"""
__tablename__: str = "sys_user"
__table_args__: dict[str, str] = ({'comment': '用户表'})
__loader_options__: list[str] = ["dept", "roles", "positions", "created_by", "updated_by"]
username: Mapped[str] = mapped_column(String(32), nullable=False, unique=True, comment="用户名/登录账号")
password: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码哈希")
name: Mapped[str] = mapped_column(String(32), nullable=False, comment="昵称")
mobile: Mapped[str | None] = mapped_column(String(11), nullable=True, unique=True, comment="手机号")
email: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, comment="邮箱")
gender: Mapped[str | None] = mapped_column(String(1), default='2', nullable=True, comment="性别(0:男 1:女 2:未知)")
avatar: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="头像URL地址")
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment="是否超管")
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="最后登录时间")
gitee_login: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="Gitee登录")
github_login: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="Github登录")
wx_login: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="微信登录")
qq_login: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="QQ登录")
wx_openid: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, comment="微信小程序OpenID")
wx_unionid: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True, comment="微信UnionID")
inviter_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True, comment="邀请人ID")
partner_role: Mapped[str | None] = mapped_column(String(255), 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"
)
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"
)
# 覆盖 UserMixin 的关系定义,显式指定 foreign_keys 避免自引用混淆
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 # 防止级联操作
)

View File

@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, EmailStr, field_validator
from urllib.parse import urlparse
from app.core.validator import DateTimeStr, mobile_validator
from app.core.base_schema import BaseSchema, CommonSchema, UserBySchema
from app.core.validator import DateTimeStr
from app.api.v1.module_system.menu.schema import MenuOutSchema
from app.api.v1.module_system.role.schema import RoleOutSchema
class CurrentUserUpdateSchema(BaseModel):
"""基础用户信息"""
name: str | None = Field(default=None, max_length=32, description="名称")
mobile: str | None = Field(default=None, description="手机号")
email: EmailStr | None = Field(default=None, description="邮箱")
gender: str | None = Field(default=None, description="性别")
avatar: str | None = Field(default=None, description="头像")
@field_validator("mobile")
@classmethod
def validate_mobile(cls, value: str | None):
return mobile_validator(value)
@field_validator("avatar")
@classmethod
def validate_avatar(cls, value: str | None):
if not value:
return value
parsed = urlparse(value)
if parsed.scheme in ("http", "https") and parsed.netloc:
return value
raise ValueError("头像地址需为有效的HTTP/HTTPS URL")
class UserRegisterSchema(BaseModel):
"""注册"""
name: str | None = Field(default=None, max_length=32, description="名称")
mobile: str | None = Field(default=None, description="手机号")
username: str = Field(..., max_length=32, description="账号")
password: str = Field(..., max_length=128, description="密码哈希值")
role_ids: list[int] | None = Field(default=[1], description='角色ID')
created_id: int | None = Field(default=1, description='创建人ID')
description: str | None = Field(default=None, max_length=255, description="备注")
@field_validator("mobile")
@classmethod
def validate_mobile(cls, value: str | None):
return mobile_validator(value)
@field_validator("username")
@classmethod
def validate_username(cls, value: str):
v = value.strip()
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
class UserForgetPasswordSchema(BaseModel):
"""忘记密码"""
username: str = Field(..., max_length=32, description="用户名")
new_password: str = Field(..., max_length=128, description="新密码")
mobile: str | None = Field(default=None, description="手机号")
@field_validator("mobile")
@classmethod
def validate_mobile(cls, value: str | None):
return mobile_validator(value)
class UserChangePasswordSchema(BaseModel):
"""修改密码"""
old_password: str = Field(..., max_length=128, description="旧密码")
new_password: str = Field(..., max_length=128, description="新密码")
class ResetPasswordSchema(BaseModel):
"""重置密码"""
id: int = Field(..., description="主键ID")
password: str = Field(..., min_length=6, max_length=128, description="新密码")
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, max_length=128, description="密码哈希值")
status: str = Field(default="0", description="是否可用")
description: str | None = Field(default=None, max_length=255, description="备注")
is_superuser: bool | None = Field(default=False, description="是否超管")
dept_id: int | None = Field(default=None, description='部门ID')
role_ids: list[int] | None = Field(default=[], description='角色ID')
position_ids: list[int] | None = Field(default=[], description='岗位ID')
class UserUpdateSchema(UserCreateSchema):
"""更新"""
model_config = ConfigDict(from_attributes=True)
last_login: DateTimeStr | None = Field(default=None, description="最后登录时间")
class UserOutSchema(UserUpdateSchema, BaseSchema, UserBySchema):
"""响应"""
model_config = ConfigDict(arbitrary_types_allowed=True, from_attributes=True)
gitee_login: str | None = Field(default=None, max_length=32, description="Gitee登录")
github_login: str | None = Field(default=None, max_length=32, description="Github登录")
wx_login: str | None = Field(default=None, max_length=32, description="微信登录")
qq_login: str | None = Field(default=None, max_length=32, description="QQ登录")
dept_name: str | None = Field(default=None, description='部门名称')
dept: CommonSchema | None = Field(default=None, description='部门')
positions: list[CommonSchema] | None = Field(default=[], description='岗位')
roles: list[RoleOutSchema] | None = Field(default=[], description='角色')
menus: list[MenuOutSchema] | None = Field(default=[], description='菜单')
class UserQueryParam:
"""用户管理查询参数"""
def __init__(
self,
username: str | None = Query(None, description="用户名"),
name: str | None = Query(None, description="名称"),
mobile: str | None = Query(None, description="手机号", pattern=r'^1[3-9]\d{9}$'),
email: str | None = Query(None, description="邮箱", pattern=r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'),
dept_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="更新人"),
) -> None:
# 模糊查询字段
self.username = ("like", username)
self.name = ("like", name)
self.mobile = ("like", mobile)
self.email = ("like", email)
# 精确查询字段
self.dept_id = dept_id
self.created_id = created_id
self.updated_id = updated_id
self.status = status
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = ("between", (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = ("between", (updated_time[0], updated_time[1]))

View File

@@ -0,0 +1,631 @@
# -*- coding: utf-8 -*-
import io
from typing import Any
from fastapi import UploadFile
import pandas as pd
from app.core.exceptions import CustomException
from app.utils.hash_bcrpy_util import PwdUtil
from app.core.base_schema import BatchSetAvailable, UploadResponseSchema
from app.core.logger import log
from app.utils.common_util import traversal_to_tree
from app.utils.excel_util import ExcelUtil
from app.utils.upload_util import UploadUtil
from ..position.crud import PositionCRUD
from ..role.crud import RoleCRUD
from ..menu.crud import MenuCRUD
from ..dept.crud import DeptCRUD
from ..auth.schema import AuthSchema
from ..menu.schema import MenuOutSchema
from .crud import UserCRUD
from .schema import (
CurrentUserUpdateSchema,
ResetPasswordSchema,
UserOutSchema,
UserCreateSchema,
UserUpdateSchema,
UserChangePasswordSchema,
UserRegisterSchema,
UserForgetPasswordSchema,
UserQueryParam
)
class UserService:
"""用户模块服务层"""
@classmethod
async def get_detail_by_id_service(cls, auth: AuthSchema, id: int) -> dict:
"""
根据ID获取用户详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 用户ID
返回:
- dict: 用户详情字典
"""
user = await UserCRUD(auth).get_by_id_crud(id=id)
if not user:
raise CustomException(msg="用户不存在")
# 如果用户绑定了部门,则获取部门名称
if user.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=user.dept_id)
UserOutSchema.dept_name = dept.name if dept else None
else:
UserOutSchema.dept_name = None
return UserOutSchema.model_validate(user).model_dump()
@classmethod
async def get_user_list_service(cls, auth: AuthSchema, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
"""
获取用户列表
参数:
- auth (AuthSchema): 认证信息模型
- search (UserQueryParam | None): 查询参数对象。
- order_by (list[dict[str, str]] | None): 排序参数列表。
返回:
- list[dict]: 用户详情字典列表
"""
user_list = await UserCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
user_dict_list = []
for user in user_list:
user_dict = UserOutSchema.model_validate(user).model_dump()
user_dict_list.append(user_dict)
return user_dict_list
@classmethod
async def create_user_service(cls, data: UserCreateSchema, auth: AuthSchema) -> dict:
"""
创建用户
参数:
- data (UserCreateSchema): 用户创建信息
- auth (AuthSchema): 认证信息模型
返回:
- dict: 创建后的用户详情字典
"""
if not data.username:
raise CustomException(msg="用户名不能为空")
# 检查是否试图创建超级管理员
if data.is_superuser:
raise CustomException(msg='不允许创建超级管理员')
# 检查用户名是否存在
user = await UserCRUD(auth).get_by_username_crud(username=data.username)
if user:
raise CustomException(msg='已存在相同用户名称的账号')
# 检查部门是否存在
if data.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=data.dept_id)
if not dept:
raise CustomException(msg='部门不存在')
# 创建用户
if data.password:
data.password = PwdUtil.set_password_hash(password=data.password)
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
# 创建用户
new_user = await UserCRUD(auth).create(data=user_dict)
# 设置角色
if data.role_ids and len(data.role_ids) > 0:
await UserCRUD(auth).set_user_roles_crud(user_ids=[new_user.id], role_ids=data.role_ids)
# 设置岗位
if data.position_ids and len(data.position_ids) > 0:
await UserCRUD(auth).set_user_positions_crud(user_ids=[new_user.id], position_ids=data.position_ids)
new_user_dict = UserOutSchema.model_validate(new_user).model_dump()
return new_user_dict
@classmethod
async def update_user_service(cls, id: int, data: UserUpdateSchema, auth: AuthSchema) -> dict:
"""
更新用户
参数:
- id (int): 用户ID
- data (UserUpdateSchema): 用户更新信息
- auth (AuthSchema): 认证信息模型
返回:
- Dict: 更新后的用户详情字典
"""
if not data.username:
raise CustomException(msg="账号不能为空")
# 检查用户是否存在
user = await UserCRUD(auth).get_by_id_crud(id=id)
if not user:
raise CustomException(msg='用户不存在')
# 检查是否尝试修改超级管理员
if user.is_superuser:
raise CustomException(msg='超级管理员不允许修改')
# 检查用户名是否重复
exist_user = await UserCRUD(auth).get_by_username_crud(username=data.username)
if exist_user and exist_user.id != id:
raise CustomException(msg='已存在相同的账号')
# 新增:检查手机号是否重复
if data.mobile:
exist_mobile_user = await UserCRUD(auth).get_by_mobile_crud(mobile=data.mobile)
if exist_mobile_user and exist_mobile_user.id != id:
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='更新失败,邮箱已存在')
# 检查部门是否存在且可用
if data.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=data.dept_id)
if not dept:
raise CustomException(msg='部门不存在')
if not dept.status:
raise CustomException(msg='部门已被禁用')
# 更新用户 - 排除不应被修改的字段, 更新不更新密码
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids", "last_login", "password"})
new_user = await UserCRUD(auth).update(id=id, data=user_dict)
# 更新角色和岗位
if data.role_ids and len(data.role_ids) > 0:
# 检查角色是否都存在且可用
roles = await RoleCRUD(auth).get_list_crud(search={"id": ("in", data.role_ids)})
if len(roles) != len(data.role_ids):
raise CustomException(msg='部分角色不存在')
if not all(role.status for role in roles):
raise CustomException(msg='部分角色已被禁用')
await UserCRUD(auth).set_user_roles_crud(user_ids=[id], role_ids=data.role_ids)
if data.position_ids and len(data.position_ids) > 0:
# 检查岗位是否都存在且可用
positions = await PositionCRUD(auth).get_list_crud(search={"id": ("in", data.position_ids)})
if len(positions) != len(data.position_ids):
raise CustomException(msg='部分岗位不存在')
if not all(position.status for position in positions):
raise CustomException(msg='部分岗位已被禁用')
await UserCRUD(auth).set_user_positions_crud(user_ids=[id], position_ids=data.position_ids)
user_dict = UserOutSchema.model_validate(new_user).model_dump()
return user_dict
@classmethod
async def delete_user_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
删除用户
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 用户ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
user = await UserCRUD(auth).get_by_id_crud(id=id)
if not user:
raise CustomException(msg="用户不存在")
if user.is_superuser:
raise CustomException(msg="超级管理员不能删除")
if user.status:
raise CustomException(msg="用户已启用,不能删除")
if auth.user and auth.user.id == id:
raise CustomException(msg="不能删除当前登陆用户")
# 删除用户角色关联数据
await UserCRUD(auth).set_user_roles_crud(user_ids=ids, role_ids=[])
# 删除用户岗位关联数据
await UserCRUD(auth).set_user_positions_crud(user_ids=ids, position_ids=[])
# 删除用户
await UserCRUD(auth).delete(ids=ids)
@classmethod
async def get_current_user_info_service(cls, auth: AuthSchema) -> dict:
"""
获取当前用户信息
参数:
- auth (AuthSchema): 认证信息模型
返回:
- Dict: 当前用户详情字典
"""
# 获取用户基本信息
if not auth.user or not auth.user.id:
raise CustomException(msg="用户不存在")
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
# 获取部门名称
if user and user.dept:
UserOutSchema.dept_name = user.dept.name
user_dict = UserOutSchema.model_validate(user).model_dump()
# 获取菜单权限
if auth.user and auth.user.is_superuser:
# 使用树形结构查询预加载children关系
menu_all = await MenuCRUD(auth).get_tree_list_crud(search={'type': ('in', [1, 2, 4]), 'status': '0'}, order_by=[{"order": "asc"}])
menus = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_all]
else:
# 收集用户所有角色的菜单ID使用列表推导式优化代码
menu_ids = {
menu.id
for role in auth.user.roles or []
for menu in role.menus
if menu.status and menu.type in [1, 2, 4]
}
# 使用树形结构查询预加载children关系
menus = [
MenuOutSchema.model_validate(menu).model_dump()
for menu in await MenuCRUD(auth).get_tree_list_crud(search={'id': ('in', list(menu_ids))}, order_by=[{"order": "asc"}])
] if menu_ids else []
user_dict["menus"] = traversal_to_tree(menus)
return user_dict
@classmethod
async def update_current_user_info_service(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> dict:
"""
更新当前用户信息
参数:
- auth (AuthSchema): 认证信息模型
- data (CurrentUserUpdateSchema): 当前用户更新信息
返回:
- Dict: 更新后的当前用户详情字典
"""
if not auth.user or not auth.user.id:
raise CustomException(msg="用户不存在")
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
if not user:
raise CustomException(msg="用户不存在")
if user.is_superuser:
raise CustomException(msg="超级管理员不能修改个人信息")
# 新增:检查手机号是否重复
if data.mobile:
exist_mobile_user = await UserCRUD(auth).get_by_mobile_crud(mobile=data.mobile)
if exist_mobile_user and exist_mobile_user.id != auth.user.id:
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='更新失败,邮箱已存在')
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).model_dump()
@classmethod
async def set_user_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""
设置用户状态
参数:
- auth (AuthSchema): 认证信息模型
- data (BatchSetAvailable): 批量设置用户状态数据
返回:
- None
"""
for id in data.ids:
user = await UserCRUD(auth).get_by_id_crud(id=id)
if not user:
raise CustomException(msg=f"用户ID {id} 不存在")
if user.is_superuser:
raise CustomException(msg="超级管理员状态不能修改")
await UserCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def upload_avatar_service(cls, base_url: str, file: UploadFile) -> dict:
"""
上传用户头像
参数:
- base_url (str): 基础URL
- file (UploadFile): 上传的文件
返回:
- Dict: 上传头像响应字典
"""
filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url)
return UploadResponseSchema(
file_path=f'{filepath}',
file_name=filename,
origin_name=file.filename,
file_url=f'{file_url}',
).model_dump()
@classmethod
async def change_user_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> dict:
"""
修改用户密码
参数:
- auth (AuthSchema): 认证信息模型
- data (UserChangePasswordSchema): 用户密码修改数据
返回:
- Dict: 更新后的当前用户详情字典
"""
if not auth.user or not auth.user.id:
raise CustomException(msg="用户不存在")
if not data.old_password or not data.new_password:
raise CustomException(msg='密码不能为空')
# 验证原密码
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
if not user:
raise CustomException(msg="用户不存在")
if not PwdUtil.verify_password(plain_password=data.old_password, password_hash=user.password):
raise CustomException(msg='原密码输入错误')
# 更新密码
new_password_hash = PwdUtil.set_password_hash(password=data.new_password)
new_user = await UserCRUD(auth).change_password_crud(id=user.id, password_hash=new_password_hash)
return UserOutSchema.model_validate(new_user).model_dump()
@classmethod
async def reset_user_password_service(cls, auth: AuthSchema, data: ResetPasswordSchema) -> dict:
"""
重置用户密码
参数:
- auth (AuthSchema): 认证信息模型
- data (ResetPasswordSchema): 用户密码重置数据
返回:
- Dict: 更新后的当前用户详情字典
"""
if not data.password:
raise CustomException(msg='密码不能为空')
# 验证用户
user = await UserCRUD(auth).get_by_id_crud(id=data.id)
if not user:
raise CustomException(msg="用户不存在")
# 检查是否是超级管理员
if user.is_superuser:
raise CustomException(msg="超级管理员密码不能重置")
# 更新密码
new_password_hash = PwdUtil.set_password_hash(password=data.password)
new_user = await UserCRUD(auth).change_password_crud(id=data.id, password_hash=new_password_hash)
return UserOutSchema.model_validate(new_user).model_dump()
@classmethod
async def register_user_service(cls, auth: AuthSchema, data: UserRegisterSchema) -> dict:
"""
用户注册
参数:
- auth (AuthSchema): 认证信息模型
- data (UserRegisterSchema): 用户注册数据
返回:
- Dict: 注册后的用户详情字典
"""
# 检查用户名是否存在
username_ok = await UserCRUD(auth).get_by_username_crud(username=data.username)
if username_ok:
raise CustomException(msg='账号已存在')
data.password = PwdUtil.set_password_hash(password=data.password)
data.name = data.username
create_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
# 设置默认用户类型为普通用户
create_dict.setdefault("user_type", "0")
# 设置创建人ID
if auth.user and auth.user.id:
create_dict["created_id"] = auth.user.id
result = await UserCRUD(auth).create(data=create_dict)
if data.role_ids:
await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids)
return UserOutSchema.model_validate(result).model_dump()
@classmethod
async def forget_password_service(cls, auth: AuthSchema, data: UserForgetPasswordSchema) -> dict:
"""
用户忘记密码
参数:
- auth (AuthSchema): 认证信息模型
- data (UserForgetPasswordSchema): 用户忘记密码数据
返回:
- Dict: 更新后的当前用户详情字典
"""
user = await UserCRUD(auth).get_by_username_crud(username=data.username)
if not user:
raise CustomException(msg="用户不存在")
if not user.status:
raise CustomException(msg="用户已停用")
# 检查是否是超级管理员
if user.is_superuser:
raise CustomException(msg="超级管理员密码不能重置")
new_password_hash = PwdUtil.set_password_hash(password=data.new_password)
new_user = await UserCRUD(auth).forget_password_crud(id=user.id, password_hash=new_password_hash)
return UserOutSchema.model_validate(new_user).model_dump()
@classmethod
async def batch_import_user_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""
批量导入用户
参数:
- auth (AuthSchema): 认证信息模型
- file (UploadFile): 上传的Excel文件
- update_support (bool, optional): 是否支持更新已存在用户. 默认值为False.
返回:
- str: 导入结果消息
"""
header_dict = {
'部门编号': 'dept_id',
'用户名': 'username',
'名称': 'name',
'邮箱': 'email',
'手机号': 'mobile',
'性别': 'gender',
'状态': 'status'
}
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
# 检查表头是否完整
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
required_fields = ['username', 'name', 'dept_id']
for field in required_fields:
missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}")
error_msgs = []
success_count = 0
count = 0
# 处理每一行数据
for index, row in df.iterrows():
try:
count = count + 1
# 数据转换
gender = 1 if row['gender'] == '' else (2 if row['gender'] == '' else 1)
status = True if row['status'] == '正常' else False
# 构建用户数据
user_data = {
"username": str(row['username']).strip(),
"name": str(row['name']).strip(),
"email": str(row['email']).strip(),
"mobile": str(row['mobile']).strip(),
"gender": gender,
"status": status,
"dept_id": int(row['dept_id']),
"password": PwdUtil.set_password_hash(password="123456") # 设置默认密码
}
# 处理用户导入
exists_user = await UserCRUD(auth).get_by_username_crud(username=user_data["username"])
if exists_user:
# 检查是否是超级管理员
if exists_user.is_superuser:
error_msgs.append(f"{count}行: 超级管理员不允许修改")
continue
if update_support:
user_update_data = UserUpdateSchema(**user_data)
await UserCRUD(auth).update(id=exists_user.id, data=user_update_data)
success_count += 1
else:
error_msgs.append(f"{count}行: 用户 {user_data['username']} 已存在")
else:
user_create_data = UserCreateSchema(**user_data)
await UserCRUD(auth).create(data=user_create_data)
success_count += 1
except Exception as e:
error_msgs.append(f"{count}行: 异常{str(e)}")
continue
# 返回详细的导入结果
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
log.error(f"批量导入用户失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def get_import_template_user_service(cls) -> bytes:
"""
获取用户导入模板
返回:
- bytes: Excel文件字节流
"""
header_list = ['部门编号', '用户名', '名称', '邮箱', '手机号', '性别', '状态']
selector_header_list = ['性别', '状态']
option_list = [{'性别': ['', '', '未知']}, {'状态': ['正常', '停用']}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@classmethod
async def export_user_list_service(cls, user_list: list[dict[str, Any]]) -> bytes:
"""
导出用户列表为Excel文件
参数:
- user_list (List[Dict[str, Any]]): 用户列表
返回:
- bytes: Excel文件字节流
"""
if not user_list:
raise CustomException(msg="没有数据可导出")
# 定义字段映射
mapping_dict = {
'id': '用户编号',
'avatar': '头像',
'username': '用户名称',
'name': '用户昵称',
'dept_name': '部门',
'email': '邮箱',
'mobile': '手机号',
'gender': '性别',
'status': '状态',
'is_superuser': '是否超级管理员',
'last_login': '最后登录时间',
'description': '备注',
'created_time': '创建时间',
'updated_time': '更新时间',
'updated_id': '更新者ID',
}
# 复制数据并转换
# creator = {'id': 1, 'name': '管理员', 'username': 'admin'}
data = user_list.copy()
for item in data:
item['status'] = '启用' if item.get('status') == "0" else '停用'
gender = item.get('gender')
item['gender'] = '' if gender == '1' else ('' if gender == '2' else '未知')
item['is_superuser'] = '' if item.get('is_superuser') else ''
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)