upload project source code
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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='退出失败')
|
||||
|
||||
|
||||
|
||||
@@ -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编码的验证码图片')
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="批量修改部门状态成功")
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]))
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="获取初始化字典数据成功")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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]))
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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="处理时间")
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="批量修改菜单状态成功")
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]))
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="查询已启用公告列表成功")
|
||||
@@ -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)
|
||||
@@ -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='公告内容')
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="获取初始化缓存参数成功")
|
||||
@@ -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)
|
||||
@@ -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:否)")
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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="导入用户成功")
|
||||
@@ -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)
|
||||
@@ -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 # 防止级联操作
|
||||
)
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user