mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 05:10:57 +00:00
style: 统一代码格式和字符串引号使用
refactor: 优化代码结构和可读性 feat: 添加http_limit模块实现请求限制功能 fix: 修复异步任务中使用time.sleep的问题 chore: 更新依赖项并添加pytest测试框架 docs: 更新项目描述信息 perf: 优化Redis序列化方式使用JSON替代pickle test: 添加测试相关配置和依赖
This commit is contained in:
@@ -12,7 +12,12 @@ from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.security import CustomOAuth2PasswordRequestForm
|
||||
|
||||
from .schema import CaptchaOutSchema, JWTOutSchema, LogoutPayloadSchema, RefreshTokenPayloadSchema
|
||||
from .schema import (
|
||||
CaptchaOutSchema,
|
||||
JWTOutSchema,
|
||||
LogoutPayloadSchema,
|
||||
RefreshTokenPayloadSchema,
|
||||
)
|
||||
from .service import CaptchaService, LoginService
|
||||
|
||||
AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"])
|
||||
@@ -39,7 +44,9 @@ async def login_for_access_token_controller(
|
||||
异常:
|
||||
- CustomException: 认证失败时抛出异常。
|
||||
"""
|
||||
login_token = await LoginService.authenticate_user_service(request=request, redis=redis, login_form=login_form, db=db)
|
||||
login_token = await LoginService.authenticate_user_service(
|
||||
request=request, redis=redis, login_form=login_form, db=db
|
||||
)
|
||||
|
||||
log.info(f"用户{login_form.username}登录成功")
|
||||
|
||||
@@ -49,12 +56,18 @@ async def login_for_access_token_controller(
|
||||
return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
|
||||
|
||||
|
||||
@AuthRouter.post("/token/refresh", summary="刷新token", description="刷新token", response_model=JWTOutSchema, dependencies=[Depends(get_current_user)])
|
||||
@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: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
刷新token
|
||||
@@ -70,15 +83,22 @@ async def get_new_token_controller(
|
||||
- CustomException: 刷新令牌失败时抛出异常。
|
||||
"""
|
||||
# 解析当前的访问Token以获取用户名
|
||||
new_token = await LoginService.refresh_token_service(db=db, request=request, redis=redis, refresh_token=payload)
|
||||
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)
|
||||
@AuthRouter.get(
|
||||
"/captcha/get",
|
||||
summary="获取验证码",
|
||||
description="获取登录验证码",
|
||||
response_model=CaptchaOutSchema,
|
||||
)
|
||||
async def get_captcha_for_login_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取登录验证码
|
||||
@@ -98,10 +118,15 @@ async def get_captcha_for_login_controller(
|
||||
return SuccessResponse(data=captcha, msg="获取验证码成功")
|
||||
|
||||
|
||||
@AuthRouter.post('/logout', summary="退出登录", description="退出登录", dependencies=[Depends(get_current_user)])
|
||||
@AuthRouter.post(
|
||||
"/logout",
|
||||
summary="退出登录",
|
||||
description="退出登录",
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
async def logout_controller(
|
||||
payload: LogoutPayloadSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
退出登录
|
||||
@@ -117,6 +142,6 @@ async def logout_controller(
|
||||
- CustomException: 退出登录失败时抛出异常。
|
||||
"""
|
||||
if await LoginService.logout_service(redis=redis, token=payload):
|
||||
log.info('退出成功')
|
||||
return SuccessResponse(msg='退出成功')
|
||||
return ErrorResponse(msg='退出失败')
|
||||
log.info("退出成功")
|
||||
return SuccessResponse(msg="退出成功")
|
||||
return ErrorResponse(msg="退出失败")
|
||||
|
||||
@@ -8,20 +8,22 @@ from app.api.v1.module_system.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='数据库会话')
|
||||
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')
|
||||
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("会话编号不能为空")
|
||||
@@ -30,28 +32,32 @@ class JWTPayloadSchema(BaseModel):
|
||||
|
||||
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='过期时间(秒)')
|
||||
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')
|
||||
|
||||
refresh_token: str = Field(..., min_length=1, description="刷新token")
|
||||
|
||||
|
||||
class LogoutPayloadSchema(BaseModel):
|
||||
"""退出登录载荷模型"""
|
||||
token: str = Field(..., min_length=1, description='token')
|
||||
|
||||
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编码的验证码图片')
|
||||
enable: bool = Field(default=True, description="是否启用验证码")
|
||||
key: str = Field(..., min_length=1, description="验证码唯一标识")
|
||||
img_base: str = Field(..., min_length=1, description="Base64编码的验证码图片")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import NewType
|
||||
|
||||
@@ -17,7 +16,11 @@ from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.core.security import CustomOAuth2PasswordRequestForm, create_access_token, decode_access_token
|
||||
from app.core.security import (
|
||||
CustomOAuth2PasswordRequestForm,
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
)
|
||||
from app.utils.captcha_util import CaptchaUtil
|
||||
from app.utils.common_util import get_random_character
|
||||
from app.utils.hash_bcrpy_util import PwdUtil
|
||||
@@ -32,15 +35,21 @@ from .schema import (
|
||||
RefreshTokenPayloadSchema,
|
||||
)
|
||||
|
||||
CaptchaKey = NewType('CaptchaKey', str)
|
||||
CaptchaBase64 = NewType('CaptchaBase64', str)
|
||||
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:
|
||||
async def authenticate_user_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
login_form: CustomOAuth2PasswordRequestForm,
|
||||
db: AsyncSession,
|
||||
) -> JWTOutSchema:
|
||||
"""
|
||||
用户认证
|
||||
|
||||
@@ -56,14 +65,18 @@ class LoginService:
|
||||
- CustomException: 认证失败时抛出异常。
|
||||
"""
|
||||
# 判断是否来自API文档
|
||||
referer = request.headers.get('referer', '')
|
||||
request_from_docs = referer.endswith(('docs', 'redoc'))
|
||||
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)
|
||||
await CaptchaService.check_captcha_service(
|
||||
redis=redis,
|
||||
key=login_form.captcha_key,
|
||||
captcha=login_form.captcha,
|
||||
)
|
||||
|
||||
# 用户认证
|
||||
auth = AuthSchema(db=db)
|
||||
@@ -72,7 +85,9 @@ class LoginService:
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
if not PwdUtil.verify_password(plain_password=login_form.password, password_hash=user.password):
|
||||
if not PwdUtil.verify_password(
|
||||
plain_password=login_form.password, password_hash=user.password
|
||||
):
|
||||
raise CustomException(msg="账号或密码错误")
|
||||
|
||||
if user.status == "1":
|
||||
@@ -86,12 +101,19 @@ class LoginService:
|
||||
raise CustomException(msg="登录类型不能为空")
|
||||
|
||||
# 创建token
|
||||
token = await cls.create_token_service(request=request, redis=redis, user=user, login_type=login_form.login_type)
|
||||
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:
|
||||
async def create_token_service(
|
||||
cls, request: Request, redis: Redis, user: UserModel, login_type: str
|
||||
) -> JWTOutSchema:
|
||||
"""
|
||||
创建访问令牌和刷新令牌
|
||||
|
||||
@@ -113,10 +135,10 @@ class LoginService:
|
||||
|
||||
user_agent = parse(request.headers.get("user-agent"))
|
||||
request_ip = None
|
||||
x_forwarded_for = request.headers.get('X-Forwarded-For')
|
||||
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if x_forwarded_for:
|
||||
# 取第一个 IP 地址,通常为客户端真实 IP
|
||||
request_ip = x_forwarded_for.split(',')[0].strip()
|
||||
request_ip = x_forwarded_for.split(",")[0].strip()
|
||||
else:
|
||||
# 若没有 X-Forwarded-For 头,则使用 request.client.host
|
||||
request_ip = request.client.host if request.client else "127.0.0.1"
|
||||
@@ -146,42 +168,52 @@ class LoginService:
|
||||
os=user_agent.os.family,
|
||||
browser=user_agent.browser.family,
|
||||
login_time=user.last_login,
|
||||
login_type=login_type
|
||||
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,
|
||||
))
|
||||
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}',
|
||||
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}",
|
||||
value=access_token,
|
||||
expire=int(access_expires.total_seconds())
|
||||
expire=int(access_expires.total_seconds()),
|
||||
)
|
||||
|
||||
await RedisCURD(redis).set(
|
||||
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}',
|
||||
key=f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}",
|
||||
value=refresh_token,
|
||||
expire=int(refresh_expires.total_seconds())
|
||||
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
|
||||
token_type=settings.TOKEN_TYPE,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def refresh_token_service(cls, db: AsyncSession, redis: Redis, request: Request, refresh_token: RefreshTokenPayloadSchema) -> JWTOutSchema:
|
||||
async def refresh_token_service(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
redis: Redis,
|
||||
request: Request,
|
||||
refresh_token: RefreshTokenPayloadSchema,
|
||||
) -> JWTOutSchema:
|
||||
"""
|
||||
刷新访问令牌
|
||||
|
||||
@@ -225,36 +257,40 @@ class LoginService:
|
||||
|
||||
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
|
||||
))
|
||||
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
|
||||
))
|
||||
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}',
|
||||
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}",
|
||||
value=access_token,
|
||||
expire=int(access_expires.total_seconds())
|
||||
expire=int(access_expires.total_seconds()),
|
||||
)
|
||||
|
||||
await RedisCURD(redis).set(
|
||||
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}',
|
||||
key=f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}",
|
||||
value=refresh_token_new,
|
||||
expire=int(refresh_expires.total_seconds())
|
||||
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())
|
||||
expires_in=int(access_expires.total_seconds()),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -317,7 +353,7 @@ class CaptchaService:
|
||||
await RedisCURD(redis).set(
|
||||
key=redis_key,
|
||||
value=captcha_value,
|
||||
expire=settings.CAPTCHA_EXPIRE_SECONDS
|
||||
expire=settings.CAPTCHA_EXPIRE_SECONDS,
|
||||
)
|
||||
|
||||
log.info(f"生成验证码成功,验证码:{captcha_value}")
|
||||
@@ -326,7 +362,7 @@ class CaptchaService:
|
||||
return CaptchaOutSchema(
|
||||
enable=settings.CAPTCHA_ENABLE,
|
||||
key=CaptchaKey(captcha_key),
|
||||
img_base=CaptchaBase64(f"data:image/png;base64,{captcha_base64}")
|
||||
img_base=CaptchaBase64(f"data:image/png;base64,{captcha_base64}"),
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
@@ -349,19 +385,19 @@ class CaptchaService:
|
||||
raise CustomException(msg="验证码不能为空")
|
||||
|
||||
# 获取Redis中存储的验证码
|
||||
redis_key = f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}'
|
||||
redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}"
|
||||
|
||||
captcha_value = await RedisCURD(redis).get(redis_key)
|
||||
if not captcha_value:
|
||||
log.error('验证码已过期或不存在')
|
||||
log.error("验证码已过期或不存在")
|
||||
raise CustomException(msg="验证码已过期")
|
||||
|
||||
# 验证码不区分大小写比对
|
||||
if captcha.lower() != captcha_value.lower():
|
||||
log.error(f'验证码错误,用户输入:{captcha},正确值:{captcha_value}')
|
||||
log.error(f"验证码错误,用户输入:{captcha},正确值:{captcha_value}")
|
||||
raise CustomException(msg="验证码错误")
|
||||
|
||||
# 验证成功后删除验证码,避免重复使用
|
||||
await RedisCURD(redis).delete(redis_key)
|
||||
log.info(f'验证码校验成功,key:{key}')
|
||||
log.info(f"验证码校验成功,key:{key}")
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,7 @@ DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["部
|
||||
@DeptRouter.get("/tree", summary="查询部门树", description="查询部门树")
|
||||
async def get_dept_tree_controller(
|
||||
search: Annotated[DeptQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询部门树
|
||||
@@ -35,7 +35,9 @@ async def get_dept_tree_controller(
|
||||
- CustomException: 查询部门树失败时抛出异常。
|
||||
"""
|
||||
order_by = [{"order": "asc"}]
|
||||
result_dict_list = await DeptService.get_dept_tree_service(search=search, auth=auth, order_by=order_by)
|
||||
result_dict_list = await DeptService.get_dept_tree_service(
|
||||
search=search, auth=auth, order_by=order_by
|
||||
)
|
||||
log.info("查询部门树成功")
|
||||
return SuccessResponse(data=result_dict_list, msg="查询部门树成功")
|
||||
|
||||
@@ -43,7 +45,7 @@ async def get_dept_tree_controller(
|
||||
@DeptRouter.get("/detail/{id}", summary="查询部门详情", description="查询部门详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="部门ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询部门详情
|
||||
@@ -66,7 +68,7 @@ async def get_obj_detail_controller(
|
||||
@DeptRouter.post("/create", summary="创建部门", description="创建部门")
|
||||
async def create_obj_controller(
|
||||
data: DeptCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建部门
|
||||
@@ -90,7 +92,7 @@ async def create_obj_controller(
|
||||
async def update_obj_controller(
|
||||
data: DeptUpdateSchema,
|
||||
id: Annotated[int, Path(description="部门ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改部门
|
||||
@@ -114,7 +116,7 @@ async def update_obj_controller(
|
||||
@DeptRouter.delete("/delete", summary="删除部门", description="删除部门")
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除部门
|
||||
@@ -134,10 +136,14 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除部门成功")
|
||||
|
||||
|
||||
@DeptRouter.patch("/available/setting", summary="批量修改部门状态", description="批量修改部门状态")
|
||||
@DeptRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改部门状态",
|
||||
description="批量修改部门状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:patch"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改部门状态
|
||||
|
||||
@@ -31,7 +31,12 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
|
||||
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]:
|
||||
async def get_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[DeptModel]:
|
||||
"""
|
||||
获取部门列表。
|
||||
|
||||
@@ -45,7 +50,12 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
|
||||
"""
|
||||
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]:
|
||||
async def get_tree_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[DeptModel]:
|
||||
"""
|
||||
获取部门树形列表。
|
||||
|
||||
@@ -57,7 +67,12 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
|
||||
返回:
|
||||
- Sequence[DeptModel]: 部门树形列表。
|
||||
"""
|
||||
return await self.tree_list(search=search, order_by=order_by, children_attr='children', preload=preload)
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -14,16 +14,19 @@ class DeptModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
部门模型
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_dept"
|
||||
__table_args__: dict[str, str] = ({'comment': '部门表'})
|
||||
__table_args__: dict[str, str] = {"comment": "部门表"}
|
||||
__loader_options__: list[str] = []
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
|
||||
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
||||
code: Mapped[str | None] = mapped_column(String(16), 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='邮箱')
|
||||
code: Mapped[str | None] = mapped_column(
|
||||
String(16), 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(
|
||||
@@ -31,27 +34,23 @@ class DeptModel(ModelMixin, UserMixin):
|
||||
ForeignKey("sys_dept.id", ondelete="SET NULL", onupdate="CASCADE"),
|
||||
default=None,
|
||||
index=True,
|
||||
comment="父级部门ID"
|
||||
comment="父级部门ID",
|
||||
)
|
||||
# 关联关系
|
||||
parent: Mapped["DeptModel | None"] = relationship(
|
||||
back_populates='children',
|
||||
back_populates="children",
|
||||
remote_side="DeptModel.id",
|
||||
foreign_keys=[parent_id],
|
||||
uselist=False
|
||||
uselist=False,
|
||||
)
|
||||
children: Mapped[list["DeptModel"]] = relationship(
|
||||
back_populates='parent',
|
||||
foreign_keys=[parent_id],
|
||||
lazy="selectin"
|
||||
back_populates="parent", foreign_keys=[parent_id], lazy="selectin"
|
||||
)
|
||||
roles: Mapped[list["RoleModel"]] = relationship(
|
||||
secondary="sys_role_depts",
|
||||
back_populates="depts",
|
||||
lazy="selectin"
|
||||
secondary="sys_role_depts", back_populates="depts", lazy="selectin"
|
||||
)
|
||||
users: Mapped[list["UserModel"]] = relationship(
|
||||
back_populates="dept",
|
||||
foreign_keys="UserModel.dept_id",
|
||||
lazy="selectin"
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.core.validator import DateTimeStr
|
||||
|
||||
class DeptCreateSchema(BaseModel):
|
||||
"""部门创建模型"""
|
||||
|
||||
name: str = Field(..., max_length=64, description="部门名称")
|
||||
order: int = Field(default=1, ge=0, description="显示顺序")
|
||||
code: str | None = Field(default=None, max_length=16, description="部门编码")
|
||||
@@ -17,7 +18,7 @@ class DeptCreateSchema(BaseModel):
|
||||
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, max_length=255, description="备注说明")
|
||||
|
||||
@field_validator('name')
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str):
|
||||
if not value or len(value.strip()) == 0:
|
||||
@@ -25,7 +26,7 @@ class DeptCreateSchema(BaseModel):
|
||||
value = value.replace(" ", "")
|
||||
return value
|
||||
|
||||
@field_validator('code')
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, value: str | None):
|
||||
if value is None:
|
||||
@@ -34,7 +35,8 @@ class DeptCreateSchema(BaseModel):
|
||||
if v == "":
|
||||
return None
|
||||
import re
|
||||
if not re.match(r'^[A-Za-z][A-Za-z0-9_]*$', v):
|
||||
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_]*$", v):
|
||||
raise ValueError("部门编码必须以字母开头,且仅包含字母/数字/下划线")
|
||||
return v
|
||||
|
||||
@@ -45,6 +47,7 @@ class DeptUpdateSchema(DeptCreateSchema):
|
||||
|
||||
class DeptOutSchema(DeptCreateSchema, BaseSchema):
|
||||
"""部门响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
parent_name: str | None = Field(default=None, max_length=64, description="父部门名称")
|
||||
@@ -57,8 +60,16 @@ class DeptQueryParam:
|
||||
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"]),
|
||||
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:
|
||||
|
||||
# 模糊查询字段
|
||||
|
||||
@@ -10,7 +10,12 @@ from app.utils.common_util import (
|
||||
)
|
||||
|
||||
from .crud import DeptCRUD
|
||||
from .schema import DeptCreateSchema, DeptOutSchema, DeptQueryParam, DeptUpdateSchema
|
||||
from .schema import (
|
||||
DeptCreateSchema,
|
||||
DeptOutSchema,
|
||||
DeptQueryParam,
|
||||
DeptUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class DeptService:
|
||||
@@ -35,11 +40,16 @@ class DeptService:
|
||||
if dept and dept.parent_id:
|
||||
parent = await DeptCRUD(auth).get(id=dept.parent_id)
|
||||
if parent:
|
||||
result['parent_name'] = parent.name
|
||||
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]:
|
||||
async def get_dept_tree_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DeptQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取部门树形列表。
|
||||
|
||||
@@ -52,7 +62,9 @@ class DeptService:
|
||||
- list[dict]: 部门树形列表对象。
|
||||
"""
|
||||
# 使用树形结构查询,预加载children关系
|
||||
dept_list = await DeptCRUD(auth).get_tree_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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构建树形结构
|
||||
@@ -75,10 +87,10 @@ class DeptService:
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get(name=data.name)
|
||||
if dept:
|
||||
raise CustomException(msg='创建失败,该部门已存在')
|
||||
raise CustomException(msg="创建失败,该部门已存在")
|
||||
obj = await DeptCRUD(auth).get(code=data.code)
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,编码已存在')
|
||||
raise CustomException(msg="创建失败,编码已存在")
|
||||
dept = await DeptCRUD(auth).create(data=data)
|
||||
return DeptOutSchema.model_validate(dept).model_dump()
|
||||
|
||||
@@ -100,10 +112,10 @@ class DeptService:
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
|
||||
if not dept:
|
||||
raise CustomException(msg='更新失败,该部门不存在')
|
||||
raise CustomException(msg="更新失败,该部门不存在")
|
||||
exist_dept = await DeptCRUD(auth).get(name=data.name)
|
||||
if exist_dept and exist_dept.id != id:
|
||||
raise CustomException(msg='更新失败,部门名称重复')
|
||||
raise CustomException(msg="更新失败,部门名称重复")
|
||||
dept = await DeptCRUD(auth).update(id=id, data=data)
|
||||
return DeptOutSchema.model_validate(dept).model_dump()
|
||||
|
||||
@@ -123,7 +135,7 @@ class DeptService:
|
||||
- CustomException: 当删除对象为空时抛出。
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
|
||||
# 获取所有部门列表,用于构建树形关系
|
||||
all_depts = await DeptCRUD(auth).get_list_crud()
|
||||
|
||||
@@ -27,10 +27,14 @@ from .service import DictDataService, DictTypeService
|
||||
DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["字典管理"])
|
||||
|
||||
|
||||
@DictRouter.get("/type/detail/{id}", summary="获取字典类型详情", description="获取字典类型详情")
|
||||
@DictRouter.get(
|
||||
"/type/detail/{id}",
|
||||
summary="获取字典类型详情",
|
||||
description="获取字典类型详情",
|
||||
)
|
||||
async def get_type_detail_controller(
|
||||
id: Annotated[int, Path(description="字典类型ID", ge=1)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取字典类型详情
|
||||
@@ -54,7 +58,7 @@ async def get_type_detail_controller(
|
||||
async def get_type_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[DictTypeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询字典类型
|
||||
@@ -70,15 +74,25 @@ async def get_type_list_controller(
|
||||
异常:
|
||||
- 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)
|
||||
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("查询字典类型列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询字典类型列表成功")
|
||||
|
||||
|
||||
@DictRouter.get("/type/optionselect", summary="获取全部字典类型", description="获取全部字典类型")
|
||||
@DictRouter.get(
|
||||
"/type/optionselect",
|
||||
summary="获取全部字典类型",
|
||||
description="获取全部字典类型",
|
||||
)
|
||||
async def get_type_loptionselect_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取全部字典类型
|
||||
@@ -101,7 +115,7 @@ async def get_type_loptionselect_controller(
|
||||
async def create_type_controller(
|
||||
data: DictTypeCreateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建字典类型
|
||||
@@ -127,7 +141,7 @@ async def update_type_controller(
|
||||
data: DictTypeUpdateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
id: Annotated[int, Path(description="字典类型ID", ge=1)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改字典类型
|
||||
@@ -153,7 +167,7 @@ async def update_type_controller(
|
||||
async def delete_type_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除字典类型
|
||||
@@ -174,10 +188,14 @@ async def delete_type_controller(
|
||||
return SuccessResponse(msg="删除字典类型成功")
|
||||
|
||||
|
||||
@DictRouter.patch("/type/available/setting", summary="批量修改字典类型状态", description="批量修改字典类型状态")
|
||||
@DictRouter.patch(
|
||||
"/type/available/setting",
|
||||
summary="批量修改字典类型状态",
|
||||
description="批量修改字典类型状态",
|
||||
)
|
||||
async def batch_set_available_dict_type_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:patch"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改字典类型状态
|
||||
@@ -197,10 +215,10 @@ async def batch_set_available_dict_type_controller(
|
||||
return SuccessResponse(msg="批量修改字典类型状态成功")
|
||||
|
||||
|
||||
@DictRouter.post('/type/export', summary="导出字典类型", description="导出字典类型")
|
||||
@DictRouter.post("/type/export", summary="导出字典类型", description="导出字典类型")
|
||||
async def export_type_list_controller(
|
||||
search: Annotated[DictTypeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:export"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出字典类型
|
||||
@@ -218,21 +236,23 @@ async def export_type_list_controller(
|
||||
# 获取全量数据
|
||||
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('导出字典类型成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=dict_type.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@DictRouter.get("/data/detail/{id}", summary="获取字典数据详情", description="获取字典数据详情")
|
||||
@DictRouter.get(
|
||||
"/data/detail/{id}",
|
||||
summary="获取字典数据详情",
|
||||
description="获取字典数据详情",
|
||||
)
|
||||
async def get_data_detail_controller(
|
||||
id: Annotated[int, Path(description="字典数据ID", ge=1)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取字典数据详情
|
||||
@@ -256,7 +276,7 @@ async def get_data_detail_controller(
|
||||
async def get_data_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[DictDataQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询字典数据
|
||||
@@ -275,8 +295,14 @@ async def get_data_list_controller(
|
||||
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)
|
||||
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("查询字典数据列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询字典数据列表成功")
|
||||
|
||||
@@ -285,7 +311,7 @@ async def get_data_list_controller(
|
||||
async def create_data_controller(
|
||||
data: DictDataCreateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建字典数据
|
||||
@@ -311,7 +337,7 @@ async def update_data_controller(
|
||||
data: DictDataUpdateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
id: Annotated[int, Path(description="字典数据ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改字典数据
|
||||
@@ -337,7 +363,7 @@ async def update_data_controller(
|
||||
async def delete_data_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除字典数据
|
||||
@@ -358,10 +384,14 @@ async def delete_data_controller(
|
||||
return SuccessResponse(msg="删除字典数据成功")
|
||||
|
||||
|
||||
@DictRouter.patch("/data/available/setting", summary="批量修改字典数据状态", description="批量修改字典数据状态")
|
||||
@DictRouter.patch(
|
||||
"/data/available/setting",
|
||||
summary="批量修改字典数据状态",
|
||||
description="批量修改字典数据状态",
|
||||
)
|
||||
async def batch_set_available_dict_data_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:patch"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改字典数据状态
|
||||
@@ -381,11 +411,11 @@ async def batch_set_available_dict_data_controller(
|
||||
return SuccessResponse(msg="批量修改字典数据状态成功")
|
||||
|
||||
|
||||
@DictRouter.post('/data/export', summary="导出字典数据", description="导出字典数据")
|
||||
@DictRouter.post("/data/export", summary="导出字典数据", description="导出字典数据")
|
||||
async def export_data_list_controller(
|
||||
search: Annotated[DictDataQueryParam, Depends()],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:export"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出字典数据
|
||||
@@ -401,23 +431,26 @@ async def export_data_list_controller(
|
||||
异常:
|
||||
- CustomException: 导出字典数据失败时抛出异常。
|
||||
"""
|
||||
result_dict_list = await DictDataService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict_list = await DictDataService.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('导出字典数据成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=dice_data.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@DictRouter.get('/data/info/{dict_type}', summary="根据字典类型获取数据", description="根据字典类型获取数据")
|
||||
@DictRouter.get(
|
||||
"/data/info/{dict_type}",
|
||||
summary="根据字典类型获取数据",
|
||||
description="根据字典类型获取数据",
|
||||
)
|
||||
async def get_init_dict_data_controller(
|
||||
dict_type: str,
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
dict_type: str, redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
根据字典类型获取数据
|
||||
|
||||
@@ -24,7 +24,9 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS
|
||||
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:
|
||||
async def get_obj_by_id_crud(
|
||||
self, id: int, preload: list | None = None
|
||||
) -> DictTypeModel | None:
|
||||
"""
|
||||
获取数据字典类型详情
|
||||
|
||||
@@ -40,7 +42,12 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS
|
||||
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]:
|
||||
async def get_obj_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[DictTypeModel]:
|
||||
"""
|
||||
获取数据字典类型列表
|
||||
|
||||
@@ -134,7 +141,9 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
||||
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:
|
||||
async def get_obj_by_id_crud(
|
||||
self, id: int, preload: list | None = None
|
||||
) -> DictDataModel | None:
|
||||
"""
|
||||
获取数据字典数据详情
|
||||
|
||||
@@ -150,7 +159,12 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
||||
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]:
|
||||
async def get_obj_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[DictDataModel]:
|
||||
"""
|
||||
获取数据字典数据列表
|
||||
|
||||
@@ -232,7 +246,10 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
||||
# 假设系统默认数据在remark字段中包含"系统默认"字符串
|
||||
if exclude_system:
|
||||
# 获取非系统默认数据的ID
|
||||
system_data_filter = {"id__in": ids, "remark__contains": "系统默认"}
|
||||
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列表中排除系统默认数据
|
||||
@@ -242,7 +259,9 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
||||
await self.delete(ids=ids)
|
||||
return len(ids)
|
||||
|
||||
async def get_obj_list_by_dict_type_crud(self, dict_type: str, status: str | None = "0") -> Sequence[DictDataModel]:
|
||||
async def get_obj_list_by_dict_type_crud(
|
||||
self, dict_type: str, status: str | None = "0"
|
||||
) -> Sequence[DictDataModel]:
|
||||
"""
|
||||
根据字典类型获取字典数据列表
|
||||
|
||||
|
||||
@@ -8,40 +8,59 @@ class DictTypeModel(ModelMixin):
|
||||
"""
|
||||
字典类型表
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_dict_type"
|
||||
__table_args__: dict[str, str] = ({'comment': '字典类型表'})
|
||||
__table_args__: dict[str, str] = {"comment": "字典类型表"}
|
||||
__loader_options__: list[str] = []
|
||||
|
||||
dict_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='字典名称')
|
||||
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, comment='字典类型')
|
||||
dict_name: Mapped[str] = mapped_column(String(64), 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")
|
||||
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': '字典数据表'})
|
||||
__table_args__: dict[str, str] = {"comment": "字典数据表"}
|
||||
__loader_options__: list[str] = []
|
||||
|
||||
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_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'),
|
||||
ForeignKey("sys_dict_type.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment='字典类型ID'
|
||||
comment="字典类型ID",
|
||||
)
|
||||
|
||||
# 关系定义
|
||||
dict_type_obj: Mapped[DictTypeModel] = relationship("DictTypeModel", back_populates="dict_data_list")
|
||||
dict_type_obj: Mapped[DictTypeModel] = relationship(
|
||||
"DictTypeModel", back_populates="dict_data_list"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import re
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.core.validator import DateTimeStr
|
||||
@@ -12,24 +18,24 @@ class DictTypeCreateSchema(BaseModel):
|
||||
字典类型表对应pydantic模型
|
||||
"""
|
||||
|
||||
dict_name: str = Field(..., min_length=1, max_length=64, description='字典名称')
|
||||
dict_type: str = Field(..., min_length=1, max_length=64, description='字典类型')
|
||||
status: str = Field(default='0', description='状态(0正常 1停用)')
|
||||
dict_name: str = Field(..., min_length=1, max_length=64, description="字典名称")
|
||||
dict_type: str = Field(..., min_length=1, max_length=64, 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('字典名称不能为空')
|
||||
@field_validator("dict_name")
|
||||
def validate_dict_name(self, 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_]*$'
|
||||
@field_validator("dict_type")
|
||||
def validate_dict_type(self, 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('字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)')
|
||||
raise ValueError("字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)")
|
||||
return value.strip()
|
||||
|
||||
|
||||
@@ -39,6 +45,7 @@ class DictTypeUpdateSchema(DictTypeCreateSchema):
|
||||
|
||||
class DictTypeOutSchema(DictTypeCreateSchema, BaseSchema):
|
||||
"""字典类型响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -50,13 +57,23 @@ class DictTypeQueryParam:
|
||||
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"]),
|
||||
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_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
|
||||
@@ -73,27 +90,30 @@ 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停用)')
|
||||
|
||||
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')
|
||||
@model_validator(mode="after")
|
||||
def validate_after(self):
|
||||
if not self.dict_label or not self.dict_label.strip():
|
||||
raise ValueError('字典标签不能为空')
|
||||
raise ValueError("字典标签不能为空")
|
||||
if not self.dict_value or not self.dict_value.strip():
|
||||
raise ValueError('字典键值不能为空')
|
||||
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')
|
||||
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()
|
||||
@@ -109,6 +129,7 @@ class DictDataUpdateSchema(DictDataCreateSchema):
|
||||
|
||||
class DictDataOutSchema(DictDataCreateSchema, BaseSchema):
|
||||
"""字典数据响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -121,12 +142,22 @@ class DictDataQueryParam:
|
||||
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"]),
|
||||
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_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
|
||||
|
||||
@@ -45,7 +45,12 @@ class DictTypeService:
|
||||
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]:
|
||||
async def get_obj_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DictTypeQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取数据字典类型列表
|
||||
|
||||
@@ -57,11 +62,15 @@ class DictTypeService:
|
||||
返回:
|
||||
- list[dict]: 数据字典类型详情字典列表
|
||||
"""
|
||||
obj_list = await DictTypeCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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:
|
||||
async def create_obj_service(
|
||||
cls, auth: AuthSchema, redis: Redis, data: DictTypeCreateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
创建数据字典类型
|
||||
|
||||
@@ -75,7 +84,7 @@ class DictTypeService:
|
||||
"""
|
||||
exist_obj = await DictTypeCRUD(auth).get(dict_name=data.dict_name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg='创建失败,该数据字典类型已存在')
|
||||
raise CustomException(msg="创建失败,该数据字典类型已存在")
|
||||
obj = await DictTypeCRUD(auth).create_obj_crud(data=data)
|
||||
|
||||
new_obj_dict = DictTypeOutSchema.model_validate(obj).model_dump()
|
||||
@@ -84,9 +93,9 @@ class DictTypeService:
|
||||
|
||||
try:
|
||||
await RedisCURD(redis).set(
|
||||
key=redis_key,
|
||||
value="",
|
||||
)
|
||||
key=redis_key,
|
||||
value="",
|
||||
)
|
||||
log.info(f"创建字典类型成功: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
log.error(f"创建字典类型失败: {e}")
|
||||
@@ -95,7 +104,13 @@ class DictTypeService:
|
||||
return new_obj_dict
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id: int, data: DictTypeUpdateSchema) -> dict:
|
||||
async def update_obj_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
redis: Redis,
|
||||
id: int,
|
||||
data: DictTypeUpdateSchema,
|
||||
) -> dict:
|
||||
"""
|
||||
更新数据字典类型
|
||||
|
||||
@@ -110,15 +125,17 @@ class DictTypeService:
|
||||
"""
|
||||
exist_obj = await DictTypeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='更新失败,该数据字典类型不存在')
|
||||
raise CustomException(msg="更新失败,该数据字典类型不存在")
|
||||
if exist_obj.dict_name != data.dict_name:
|
||||
raise CustomException(msg='更新失败,数据字典类型名称不可以修改')
|
||||
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})
|
||||
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
|
||||
@@ -132,7 +149,7 @@ class DictTypeService:
|
||||
list_class=item.list_class,
|
||||
is_default=item.is_default,
|
||||
status=data.status,
|
||||
description=item.description
|
||||
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())
|
||||
@@ -144,14 +161,18 @@ class DictTypeService:
|
||||
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]
|
||||
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,
|
||||
)
|
||||
key=redis_key,
|
||||
value=value,
|
||||
)
|
||||
log.info(f"更新字典类型成功并刷新缓存: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
log.error(f"更新字典类型缓存失败: {e}")
|
||||
@@ -173,16 +194,16 @@ class DictTypeService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
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='删除失败,该数据字典类型不存在')
|
||||
raise CustomException(msg="删除失败,该数据字典类型不存在")
|
||||
# 检查是否有字典数据
|
||||
exist_obj_type_list = await DictDataCRUD(auth).list(search={'dict_type': id})
|
||||
exist_obj_type_list = await DictDataCRUD(auth).list(search={"dict_type": id})
|
||||
if len(exist_obj_type_list) > 0:
|
||||
# 如果有字典数据,不能删除
|
||||
raise CustomException(msg='删除失败,该数据字典类型下存在字典数据')
|
||||
raise CustomException(msg="删除失败,该数据字典类型下存在字典数据")
|
||||
# 删除Redis缓存
|
||||
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{exist_obj.dict_type}"
|
||||
try:
|
||||
@@ -219,23 +240,27 @@ class DictTypeService:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'dict_name': '字典名称',
|
||||
'dict_type': '字典类型',
|
||||
'status': '状态',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'updated_id': '更新者ID',
|
||||
"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 '未知'
|
||||
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)
|
||||
|
||||
@@ -261,7 +286,12 @@ class DictDataService:
|
||||
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]:
|
||||
async def get_obj_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: DictDataQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取数据字典数据列表
|
||||
|
||||
@@ -273,7 +303,9 @@ class DictDataService:
|
||||
返回:
|
||||
- list[dict]: 数据字典数据详情字典列表
|
||||
"""
|
||||
obj_list = await DictDataCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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
|
||||
@@ -300,14 +332,20 @@ class DictDataService:
|
||||
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]
|
||||
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,
|
||||
key=redis_key,
|
||||
value=value,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"❌ 初始化字典数据失败 [{dict_type}]: {e}")
|
||||
@@ -363,7 +401,9 @@ class DictDataService:
|
||||
raise CustomException(msg=f"获取字典数据失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def create_obj_service(cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema) -> dict:
|
||||
async def create_obj_service(
|
||||
cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
创建数据字典数据
|
||||
|
||||
@@ -376,12 +416,16 @@ class DictDataService:
|
||||
- dict: 数据字典数据详情字典
|
||||
"""
|
||||
# 检查相同字典类型下dict_label是否已存在
|
||||
exist_label_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_label=data.dict_label)
|
||||
exist_label_obj = await DictDataCRUD(auth).get(
|
||||
dict_type=data.dict_type, dict_label=data.dict_label
|
||||
)
|
||||
if exist_label_obj:
|
||||
raise CustomException(msg=f'创建失败,该字典类型下的字典标签"{data.dict_label}"已存在')
|
||||
|
||||
# 检查相同字典类型下dict_value是否已存在
|
||||
exist_value_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_value=data.dict_value)
|
||||
exist_value_obj = await DictDataCRUD(auth).get(
|
||||
dict_type=data.dict_type, dict_value=data.dict_value
|
||||
)
|
||||
if exist_value_obj:
|
||||
raise CustomException(msg=f'创建失败,该字典类型下的字典键值"{data.dict_value}"已存在')
|
||||
|
||||
@@ -390,14 +434,18 @@ class DictDataService:
|
||||
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]
|
||||
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,
|
||||
)
|
||||
key=redis_key,
|
||||
value=value,
|
||||
)
|
||||
log.info(f"创建字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
log.error(f"创建字典数据写入缓存失败: {e}")
|
||||
@@ -406,7 +454,13 @@ class DictDataService:
|
||||
return DictDataOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id: int, data: DictDataUpdateSchema) -> dict:
|
||||
async def update_obj_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
redis: Redis,
|
||||
id: int,
|
||||
data: DictDataUpdateSchema,
|
||||
) -> dict:
|
||||
"""
|
||||
更新数据字典数据
|
||||
|
||||
@@ -421,19 +475,27 @@ class DictDataService:
|
||||
"""
|
||||
exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='更新失败,该字典数据不存在')
|
||||
raise CustomException(msg="更新失败,该字典数据不存在")
|
||||
|
||||
# 检查相同字典类型下dict_label是否已存在(排除当前记录)
|
||||
if exist_obj.dict_label != data.dict_label:
|
||||
exist_label_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_label=data.dict_label)
|
||||
exist_label_obj = await DictDataCRUD(auth).get(
|
||||
dict_type=data.dict_type, dict_label=data.dict_label
|
||||
)
|
||||
if exist_label_obj:
|
||||
raise CustomException(msg=f'更新失败,该字典类型下的字典标签"{data.dict_label}"已存在')
|
||||
raise CustomException(
|
||||
msg=f'更新失败,该字典类型下的字典标签"{data.dict_label}"已存在'
|
||||
)
|
||||
|
||||
# 检查相同字典类型下dict_value是否已存在(排除当前记录)
|
||||
if exist_obj.dict_value != data.dict_value:
|
||||
exist_value_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_value=data.dict_value)
|
||||
exist_value_obj = await DictDataCRUD(auth).get(
|
||||
dict_type=data.dict_type, dict_value=data.dict_value
|
||||
)
|
||||
if exist_value_obj:
|
||||
raise CustomException(msg=f'更新失败,该字典类型下的字典键值"{data.dict_value}"已存在')
|
||||
raise CustomException(
|
||||
msg=f'更新失败,该字典类型下的字典键值"{data.dict_value}"已存在'
|
||||
)
|
||||
|
||||
# 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态
|
||||
if exist_obj.dict_type != data.dict_type:
|
||||
@@ -441,13 +503,19 @@ class DictDataService:
|
||||
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]
|
||||
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,
|
||||
)
|
||||
key=redis_key,
|
||||
value=value,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"更新字典数据类型变更时刷新旧缓存失败: {e}")
|
||||
|
||||
@@ -455,14 +523,18 @@ class DictDataService:
|
||||
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]
|
||||
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,
|
||||
)
|
||||
key=redis_key,
|
||||
value=value,
|
||||
)
|
||||
log.info(f"更新字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
log.error(f"更新字典数据写入缓存失败: {e}")
|
||||
@@ -485,16 +557,16 @@ class DictDataService:
|
||||
"""
|
||||
try:
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
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} 删除失败,该字典数据不存在')
|
||||
raise CustomException(msg=f"{id} 删除失败,该字典数据不存在")
|
||||
# 系统默认字典数据不允许删除
|
||||
if exist_obj.is_default:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的系统默认字典数据不允许删除')
|
||||
raise CustomException(msg=f"删除失败,ID为{id}的系统默认字典数据不允许删除")
|
||||
|
||||
# 获取所有需要清除的缓存键
|
||||
dict_types_to_clear = set()
|
||||
@@ -550,29 +622,33 @@ class DictDataService:
|
||||
- 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',
|
||||
"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["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 '未知'
|
||||
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)
|
||||
|
||||
@@ -22,7 +22,7 @@ LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["日
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[OperationLogQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询日志
|
||||
@@ -38,8 +38,14 @@ async def get_obj_list_controller(
|
||||
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)
|
||||
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("查询日志成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询日志成功")
|
||||
|
||||
@@ -47,7 +53,7 @@ async def get_obj_list_controller(
|
||||
@LogRouter.get("/detail/{id}", summary="日志详情", description="日志详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="操作日志ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取日志详情
|
||||
@@ -67,7 +73,7 @@ async def get_obj_detail_controller(
|
||||
@LogRouter.delete("/delete", summary="删除日志", description="删除日志")
|
||||
async def delete_obj_log_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除日志
|
||||
@@ -87,7 +93,7 @@ async def delete_obj_log_controller(
|
||||
@LogRouter.post("/export", summary="导出日志", description="导出日志")
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[OperationLogQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:export"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:log:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出日志
|
||||
@@ -100,13 +106,13 @@ async def export_obj_list_controller(
|
||||
- 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('导出日志成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=log.xlsx"},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,9 @@ from .model import OperationLogModel
|
||||
from .schema import OperationLogCreateSchema
|
||||
|
||||
|
||||
class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, OperationLogCreateSchema]):
|
||||
class OperationLogCRUD(
|
||||
CRUDBase[OperationLogModel, OperationLogCreateSchema, OperationLogCreateSchema]
|
||||
):
|
||||
"""
|
||||
操作日志数据层。
|
||||
"""
|
||||
@@ -31,7 +33,9 @@ class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, Ope
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list | None = None) -> OperationLogModel | None:
|
||||
async def get_by_id_crud(
|
||||
self, id: int, preload: list | None = None
|
||||
) -> OperationLogModel | None:
|
||||
"""
|
||||
根据ID获取操作日志详情。
|
||||
|
||||
@@ -44,7 +48,12 @@ class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, Ope
|
||||
"""
|
||||
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]:
|
||||
async def get_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[OperationLogModel]:
|
||||
"""
|
||||
获取操作日志列表。
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ class OperationLogModel(ModelMixin, UserMixin):
|
||||
- 1: 登录日志
|
||||
- 2: 操作日志
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_log"
|
||||
__table_args__: dict[str, str] = ({'comment': '系统日志表'})
|
||||
__table_args__: dict[str, str] = {"comment": "系统日志表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
type: Mapped[int] = mapped_column(Integer, comment="日志类型(1登录日志 2操作日志)")
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.core.validator import DateTimeStr
|
||||
|
||||
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="请求方法")
|
||||
@@ -58,6 +59,7 @@ class OperationLogCreateSchema(BaseModel):
|
||||
|
||||
class OperationLogOutSchema(OperationLogCreateSchema, BaseSchema, UserBySchema):
|
||||
"""日志响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -73,10 +75,18 @@ class OperationLogQueryParam:
|
||||
response_code: int | None = Query(None, description="响应状态码"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
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="更新人")
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.request_path = ("like", f"%{request_path}%") if request_path else None
|
||||
|
||||
@@ -3,7 +3,11 @@ from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import OperationLogCRUD
|
||||
from .schema import OperationLogCreateSchema, OperationLogOutSchema, OperationLogQueryParam
|
||||
from .schema import (
|
||||
OperationLogCreateSchema,
|
||||
OperationLogOutSchema,
|
||||
OperationLogQueryParam,
|
||||
)
|
||||
|
||||
|
||||
class OperationLogService:
|
||||
@@ -28,7 +32,12 @@ class OperationLogService:
|
||||
return log_dict
|
||||
|
||||
@classmethod
|
||||
async def get_log_list_service(cls, auth: AuthSchema, search: OperationLogQueryParam | None = None, order_by: list | None = None) -> list[dict]:
|
||||
async def get_log_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: OperationLogQueryParam | None = None,
|
||||
order_by: list | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取日志列表
|
||||
|
||||
@@ -41,7 +50,9 @@ class OperationLogService:
|
||||
- list[dict]: 日志详情字典列表
|
||||
"""
|
||||
|
||||
log_list = await OperationLogCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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
|
||||
|
||||
@@ -74,7 +85,7 @@ class OperationLogService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
await OperationLogCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
@@ -90,32 +101,36 @@ class OperationLogService:
|
||||
"""
|
||||
# 操作日志字段映射
|
||||
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',
|
||||
"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 '失败'
|
||||
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 '未知'
|
||||
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)
|
||||
|
||||
@@ -19,7 +19,7 @@ MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["菜
|
||||
@MenuRouter.get("/tree", summary="查询菜单树", description="查询菜单树")
|
||||
async def get_menu_tree_controller(
|
||||
search: Annotated[MenuQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询菜单树。
|
||||
@@ -31,7 +31,9 @@ async def get_menu_tree_controller(
|
||||
- JSONResponse: 包含菜单树的 JSON 响应。
|
||||
"""
|
||||
order_by = [{"order": "asc"}]
|
||||
result_dict_list = await MenuService.get_menu_tree_service(search=search, auth=auth, order_by=order_by)
|
||||
result_dict_list = await MenuService.get_menu_tree_service(
|
||||
search=search, auth=auth, order_by=order_by
|
||||
)
|
||||
log.info("查询菜单树成功")
|
||||
return SuccessResponse(data=result_dict_list, msg="查询菜单树成功")
|
||||
|
||||
@@ -39,7 +41,7 @@ async def get_menu_tree_controller(
|
||||
@MenuRouter.get("/detail/{id}", summary="查询菜单详情", description="查询菜单详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="菜单ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询菜单详情。
|
||||
@@ -58,7 +60,7 @@ async def get_obj_detail_controller(
|
||||
@MenuRouter.post("/create", summary="创建菜单", description="创建菜单")
|
||||
async def create_obj_controller(
|
||||
data: MenuCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建菜单。
|
||||
@@ -78,7 +80,7 @@ async def create_obj_controller(
|
||||
async def update_obj_controller(
|
||||
data: MenuUpdateSchema,
|
||||
id: Annotated[int, Path(description="菜单ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改菜单。
|
||||
@@ -98,7 +100,7 @@ async def update_obj_controller(
|
||||
@MenuRouter.delete("/delete", summary="删除菜单", description="删除菜单")
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除菜单。
|
||||
@@ -114,10 +116,14 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除菜单成功")
|
||||
|
||||
|
||||
@MenuRouter.patch("/available/setting", summary="批量修改菜单状态", description="批量修改菜单状态")
|
||||
@MenuRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改菜单状态",
|
||||
description="批量修改菜单状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:patch"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:menu:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改菜单状态。
|
||||
|
||||
@@ -31,7 +31,12 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
|
||||
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]:
|
||||
async def get_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list[str] | None = None,
|
||||
) -> Sequence[MenuModel]:
|
||||
"""
|
||||
获取菜单列表。
|
||||
|
||||
@@ -45,7 +50,12 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
|
||||
"""
|
||||
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]:
|
||||
async def get_tree_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list[str] | None = None,
|
||||
) -> Sequence[MenuModel]:
|
||||
"""
|
||||
获取菜单树形列表。
|
||||
|
||||
@@ -57,7 +67,12 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
|
||||
返回:
|
||||
- Sequence[MenuModel]: 菜单树形列表。
|
||||
"""
|
||||
return await self.tree_list(search=search, order_by=order_by, children_attr='children', preload=preload)
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -19,49 +19,75 @@ class MenuModel(ModelMixin):
|
||||
- 3: 按钮/权限(页面内按钮权限)
|
||||
- 4: 外部链接
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_menu"
|
||||
__table_args__: dict[str, str] = ({'comment': '菜单表'})
|
||||
__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:query)')
|
||||
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:否)')
|
||||
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:query)"
|
||||
)
|
||||
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'),
|
||||
ForeignKey("sys_menu.id", ondelete="SET NULL"),
|
||||
default=None,
|
||||
index=True,
|
||||
comment='父菜单ID'
|
||||
comment="父菜单ID",
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
parent: Mapped["MenuModel | None"] = relationship(
|
||||
back_populates='children',
|
||||
back_populates="children",
|
||||
remote_side="MenuModel.id",
|
||||
foreign_keys="MenuModel.parent_id",
|
||||
uselist=False
|
||||
uselist=False,
|
||||
)
|
||||
children: Mapped[list["MenuModel"] | None] = relationship(
|
||||
back_populates='parent',
|
||||
back_populates="parent",
|
||||
foreign_keys="MenuModel.parent_id",
|
||||
order_by="MenuModel.order"
|
||||
order_by="MenuModel.order",
|
||||
)
|
||||
roles: Mapped[list["RoleModel"]] = relationship(
|
||||
secondary="sys_role_menus",
|
||||
back_populates="menus",
|
||||
lazy="selectin"
|
||||
secondary="sys_role_menus", back_populates="menus", lazy="selectin"
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.core.validator import DateTimeStr, menu_request_validator
|
||||
|
||||
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="显示顺序")
|
||||
@@ -22,20 +23,35 @@ class MenuCreateSchema(BaseModel):
|
||||
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}]")
|
||||
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')
|
||||
@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"]:
|
||||
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()
|
||||
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:
|
||||
@@ -53,18 +69,20 @@ class MenuCreateSchema(BaseModel):
|
||||
raise ValueError("组件路径不能以 / 开头")
|
||||
return values
|
||||
|
||||
@model_validator(mode='after')
|
||||
@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="父菜单名称")
|
||||
@@ -78,14 +96,24 @@ class MenuQueryParam:
|
||||
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:外链)"),
|
||||
type: Literal[1, 2, 3, 4] | None = Query(
|
||||
None, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)"
|
||||
),
|
||||
permission: str | None = Query(None, description="权限标识"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
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="更新人")
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name)
|
||||
|
||||
@@ -10,7 +10,12 @@ from app.utils.common_util import (
|
||||
)
|
||||
|
||||
from .crud import MenuCRUD
|
||||
from .schema import MenuCreateSchema, MenuOutSchema, MenuQueryParam, MenuUpdateSchema
|
||||
from .schema import (
|
||||
MenuCreateSchema,
|
||||
MenuOutSchema,
|
||||
MenuQueryParam,
|
||||
MenuUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class MenuService:
|
||||
@@ -41,7 +46,12 @@ class MenuService:
|
||||
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]:
|
||||
async def get_menu_tree_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: MenuQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取菜单树形列表。
|
||||
|
||||
@@ -54,7 +64,9 @@ class MenuService:
|
||||
- list[dict]: 菜单树形列表对象。
|
||||
"""
|
||||
# 使用树形结构查询,预加载children关系
|
||||
menu_list = await MenuCRUD(auth).get_tree_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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构建树形结构
|
||||
@@ -74,7 +86,7 @@ class MenuService:
|
||||
"""
|
||||
menu = await MenuCRUD(auth).get(name=data.name)
|
||||
if menu:
|
||||
raise CustomException(msg='创建失败,该菜单已存在')
|
||||
raise CustomException(msg="创建失败,该菜单已存在")
|
||||
|
||||
new_menu = await MenuCRUD(auth).create(data=data)
|
||||
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
|
||||
@@ -95,19 +107,21 @@ class MenuService:
|
||||
"""
|
||||
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
||||
if not menu:
|
||||
raise CustomException(msg='更新失败,该菜单不存在')
|
||||
raise CustomException(msg="更新失败,该菜单不存在")
|
||||
exist_menu = await MenuCRUD(auth).get(name=data.name)
|
||||
if exist_menu and exist_menu.id != id:
|
||||
raise CustomException(msg='更新失败,菜单名称重复')
|
||||
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='更新失败,父级菜单不存在')
|
||||
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))
|
||||
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
|
||||
@@ -125,7 +139,7 @@ class MenuService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
|
||||
# 获取所有菜单列表,用于构建树形关系
|
||||
all_menus = await MenuCRUD(auth).get_list_crud()
|
||||
|
||||
@@ -22,7 +22,7 @@ NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=[
|
||||
@NoticeRouter.get("/detail/{id}", summary="获取公告详情", description="获取公告详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="公告ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取公告详情。
|
||||
@@ -43,7 +43,7 @@ async def get_obj_detail_controller(
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[NoticeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询公告。
|
||||
@@ -56,8 +56,14 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- 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)
|
||||
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("查询公告列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询公告列表成功")
|
||||
|
||||
@@ -65,7 +71,7 @@ async def get_obj_list_controller(
|
||||
@NoticeRouter.post("/create", summary="创建公告", description="创建公告")
|
||||
async def create_obj_controller(
|
||||
data: NoticeCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建公告。
|
||||
@@ -86,7 +92,7 @@ async def create_obj_controller(
|
||||
async def update_obj_controller(
|
||||
data: NoticeUpdateSchema,
|
||||
id: Annotated[int, Path(description="公告ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改公告。
|
||||
@@ -107,7 +113,7 @@ async def update_obj_controller(
|
||||
@NoticeRouter.delete("/delete", summary="删除公告", description="删除公告")
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除公告。
|
||||
@@ -124,10 +130,14 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除公告成功")
|
||||
|
||||
|
||||
@NoticeRouter.patch("/available/setting", summary="批量修改公告状态", description="批量修改公告状态")
|
||||
@NoticeRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改公告状态",
|
||||
description="批量修改公告状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:patch"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改公告状态。
|
||||
@@ -144,10 +154,10 @@ async def batch_set_available_obj_controller(
|
||||
return SuccessResponse(msg="批量修改公告状态成功")
|
||||
|
||||
|
||||
@NoticeRouter.post('/export', summary="导出公告", description="导出公告")
|
||||
@NoticeRouter.post("/export", summary="导出公告", description="导出公告")
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[NoticeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:export"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出公告。
|
||||
@@ -161,18 +171,18 @@ async def export_obj_list_controller(
|
||||
"""
|
||||
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('导出公告成功')
|
||||
log.info("导出公告成功")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=notice.xlsx'}
|
||||
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: Annotated[AuthSchema, Depends(get_current_user)]
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取全局启用公告。
|
||||
|
||||
@@ -33,7 +33,12 @@ class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]):
|
||||
"""
|
||||
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]:
|
||||
async def get_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[NoticeModel]:
|
||||
"""
|
||||
获取公告列表。
|
||||
|
||||
|
||||
@@ -8,10 +8,13 @@ class NoticeModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
通知公告表
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_notice"
|
||||
__table_args__: dict[str, str] = ({'comment': '通知公告表'})
|
||||
__table_args__: dict[str, str] = {"comment": "通知公告表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
notice_title: Mapped[str] = mapped_column(String(64), nullable=False, comment='公告标题')
|
||||
notice_type: Mapped[str] = mapped_column(String(1), nullable=False, comment='公告类型(1通知 2公告)')
|
||||
notice_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='公告内容')
|
||||
notice_title: Mapped[str] = mapped_column(String(64), nullable=False, comment="公告标题")
|
||||
notice_type: Mapped[str] = mapped_column(
|
||||
String(1), nullable=False, comment="公告类型(1通知 2公告)"
|
||||
)
|
||||
notice_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment="公告内容")
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr
|
||||
@@ -7,9 +13,10 @@ from app.core.validator import DateTimeStr
|
||||
|
||||
class NoticeCreateSchema(BaseModel):
|
||||
"""公告通知创建模型"""
|
||||
notice_title: str = Field(..., max_length=50, description='公告标题')
|
||||
notice_type: str = Field(..., description='公告类型(1通知 2公告)')
|
||||
notice_content: str = Field(..., description='公告内容')
|
||||
|
||||
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="描述")
|
||||
|
||||
@@ -20,7 +27,7 @@ class NoticeCreateSchema(BaseModel):
|
||||
raise ValueError("公告类型仅支持 '1'(通知) 或 '2'(公告)")
|
||||
return value
|
||||
|
||||
@model_validator(mode='after')
|
||||
@model_validator(mode="after")
|
||||
def _validate_after(self):
|
||||
if not self.notice_title.strip():
|
||||
raise ValueError("公告标题不能为空")
|
||||
@@ -35,6 +42,7 @@ class NoticeUpdateSchema(NoticeCreateSchema):
|
||||
|
||||
class NoticeOutSchema(NoticeCreateSchema, BaseSchema, UserBySchema):
|
||||
"""公告通知响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -47,10 +55,18 @@ class NoticeQueryParam:
|
||||
notice_type: str | None = Query(None, description="公告类型"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
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="更新人")
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.notice_title = ("like", notice_title)
|
||||
|
||||
@@ -4,7 +4,12 @@ from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import NoticeCRUD
|
||||
from .schema import NoticeCreateSchema, NoticeOutSchema, NoticeQueryParam, NoticeUpdateSchema
|
||||
from .schema import (
|
||||
NoticeCreateSchema,
|
||||
NoticeOutSchema,
|
||||
NoticeQueryParam,
|
||||
NoticeUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class NoticeService:
|
||||
@@ -38,11 +43,19 @@ class NoticeService:
|
||||
返回:
|
||||
- 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]
|
||||
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]:
|
||||
async def get_notice_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: NoticeQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取公告列表。
|
||||
|
||||
@@ -54,8 +67,13 @@ class NoticeService:
|
||||
返回:
|
||||
- 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]
|
||||
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:
|
||||
@@ -74,12 +92,14 @@ class NoticeService:
|
||||
"""
|
||||
notice = await NoticeCRUD(auth).get(notice_title=data.notice_title)
|
||||
if notice:
|
||||
raise CustomException(msg='创建失败,该公告通知已存在')
|
||||
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:
|
||||
async def update_notice_service(
|
||||
cls, auth: AuthSchema, id: int, data: NoticeUpdateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
更新公告。
|
||||
|
||||
@@ -96,10 +116,10 @@ class NoticeService:
|
||||
"""
|
||||
notice = await NoticeCRUD(auth).get_by_id_crud(id=id)
|
||||
if not notice:
|
||||
raise CustomException(msg='更新失败,该公告通知不存在')
|
||||
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='更新失败,公告通知标题重复')
|
||||
raise CustomException(msg="更新失败,公告通知标题重复")
|
||||
notice_obj = await NoticeCRUD(auth).update_crud(id=id, data=data)
|
||||
return NoticeOutSchema.model_validate(notice_obj).model_dump()
|
||||
|
||||
@@ -116,11 +136,11 @@ class NoticeService:
|
||||
- CustomException: 删除失败,删除对象不能为空或该公告通知不存在。
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
notice = await NoticeCRUD(auth).get_by_id_crud(id=id)
|
||||
if not notice:
|
||||
raise CustomException(msg='删除失败,该公告通知不存在')
|
||||
raise CustomException(msg="删除失败,该公告通知不存在")
|
||||
await NoticeCRUD(auth).delete_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
@@ -149,25 +169,29 @@ class NoticeService:
|
||||
- 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',
|
||||
"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["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 '未知'
|
||||
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)
|
||||
|
||||
@@ -22,7 +22,7 @@ ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=["
|
||||
@ParamsRouter.get("/detail/{id}", summary="获取参数详情", description="获取参数详情")
|
||||
async def get_type_detail_controller(
|
||||
id: Annotated[int, Path(description="参数ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:detail"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取参数详情
|
||||
@@ -39,10 +39,14 @@ async def get_type_detail_controller(
|
||||
return SuccessResponse(data=result_dict, msg="获取参数详情成功")
|
||||
|
||||
|
||||
@ParamsRouter.get("/key/{config_key}", summary="根据配置键获取参数详情", description="根据配置键获取参数详情")
|
||||
@ParamsRouter.get(
|
||||
"/key/{config_key}",
|
||||
summary="根据配置键获取参数详情",
|
||||
description="根据配置键获取参数详情",
|
||||
)
|
||||
async def get_obj_by_key_controller(
|
||||
config_key: Annotated[str, Path(description="配置键")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
根据配置键获取参数详情
|
||||
@@ -59,10 +63,14 @@ async def get_obj_by_key_controller(
|
||||
return SuccessResponse(data=result_dict, msg="根据配置键获取参数详情成功")
|
||||
|
||||
|
||||
@ParamsRouter.get("/value/{config_key}", summary="根据配置键获取参数值", description="根据配置键获取参数值")
|
||||
@ParamsRouter.get(
|
||||
"/value/{config_key}",
|
||||
summary="根据配置键获取参数值",
|
||||
description="根据配置键获取参数值",
|
||||
)
|
||||
async def get_config_value_by_key_controller(
|
||||
config_key: Annotated[str, Path(description="配置键")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
根据配置键获取参数值
|
||||
@@ -74,7 +82,9 @@ async def get_config_value_by_key_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含参数值的 JSON 响应
|
||||
"""
|
||||
result_value = await ParamsService.get_config_value_by_key_service(config_key=config_key, auth=auth)
|
||||
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="根据配置键获取参数值成功")
|
||||
|
||||
@@ -96,8 +106,14 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- 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)
|
||||
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("获取参数列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询参数列表成功")
|
||||
|
||||
@@ -106,7 +122,7 @@ async def get_obj_list_controller(
|
||||
async def create_obj_controller(
|
||||
data: ParamsCreateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:create"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建参数
|
||||
@@ -129,7 +145,7 @@ async def update_objs_controller(
|
||||
data: ParamsUpdateSchema,
|
||||
id: Annotated[int, Path(description="参数ID")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:update"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改参数
|
||||
@@ -152,7 +168,7 @@ async def update_objs_controller(
|
||||
async def delete_obj_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:delete"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除参数
|
||||
@@ -170,10 +186,10 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除参数成功")
|
||||
|
||||
|
||||
@ParamsRouter.post('/export', summary="导出参数", description="导出参数")
|
||||
@ParamsRouter.post("/export", summary="导出参数", description="导出参数")
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[ParamsQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:export"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出参数
|
||||
@@ -187,22 +203,21 @@ async def export_obj_list_controller(
|
||||
"""
|
||||
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('导出参数成功')
|
||||
log.info("导出参数成功")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=params.xlsx'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=params.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@ParamsRouter.post("/upload", summary="上传文件", dependencies=[Depends(AuthPermission(["module_system:param:upload"]))])
|
||||
async def upload_file_controller(
|
||||
file: UploadFile,
|
||||
request: Request
|
||||
) -> JSONResponse:
|
||||
@ParamsRouter.post(
|
||||
"/upload",
|
||||
summary="上传文件",
|
||||
dependencies=[Depends(AuthPermission(["module_system:param:upload"]))],
|
||||
)
|
||||
async def upload_file_controller(file: UploadFile, request: Request) -> JSONResponse:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
@@ -215,7 +230,7 @@ async def upload_file_controller(
|
||||
"""
|
||||
result_str = await ParamsService.upload_service(base_url=str(request.base_url), file=file)
|
||||
log.info(f"上传文件: {result_str}")
|
||||
return SuccessResponse(data=result_str, msg='上传文件成功')
|
||||
return SuccessResponse(data=result_str, msg="上传文件成功")
|
||||
|
||||
|
||||
@ParamsRouter.get("/info", summary="获取初始化缓存参数", description="获取初始化缓存参数")
|
||||
|
||||
@@ -33,7 +33,9 @@ class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]):
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_by_key_crud(self, key: str, preload: list | None = None) -> ParamsModel | None:
|
||||
async def get_obj_by_key_crud(
|
||||
self, key: str, preload: list | None = None
|
||||
) -> ParamsModel | None:
|
||||
"""
|
||||
根据key获取配置管理型详情
|
||||
|
||||
@@ -46,7 +48,12 @@ class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]):
|
||||
"""
|
||||
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]:
|
||||
async def get_obj_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[ParamsModel]:
|
||||
"""
|
||||
获取配置管理型列表
|
||||
|
||||
|
||||
@@ -8,11 +8,17 @@ class ParamsModel(ModelMixin):
|
||||
"""
|
||||
参数配置表
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_param"
|
||||
__table_args__: dict[str, str] = ({'comment': '系统参数表'})
|
||||
__table_args__: dict[str, str] = {"comment": "系统参数表"}
|
||||
__loader_options__: list[str] = []
|
||||
|
||||
config_name: Mapped[str] = mapped_column(String(64), 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:否)")
|
||||
config_name: Mapped[str] = mapped_column(String(64), 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:否)",
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.core.validator import DateTimeStr
|
||||
|
||||
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="参数键值")
|
||||
@@ -14,13 +15,14 @@ class ParamsCreateSchema(BaseModel):
|
||||
status: str = Field(default="0", description="状态(True:正常 False:停用)")
|
||||
description: str | None = Field(default=None, max_length=500, description="描述")
|
||||
|
||||
@field_validator('config_key')
|
||||
@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('参数键名必须以小写字母开头,仅包含小写字母/数字/_.-')
|
||||
|
||||
if not re.match(r"^[a-z][a-z0-9_.-]*$", v):
|
||||
raise ValueError("参数键名必须以小写字母开头,仅包含小写字母/数字/_.-")
|
||||
return v
|
||||
|
||||
|
||||
@@ -30,6 +32,7 @@ class ParamsUpdateSchema(ParamsCreateSchema):
|
||||
|
||||
class ParamsOutSchema(ParamsCreateSchema, BaseSchema):
|
||||
"""配置响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -43,8 +46,16 @@ class ParamsQueryParam:
|
||||
config_type: bool | None = Query(None, description="系统内置((True:是 False:否))"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"])
|
||||
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:
|
||||
# 模糊查询字段
|
||||
# 模糊查询字段
|
||||
|
||||
@@ -14,13 +14,19 @@ from app.utils.excel_util import ExcelUtil
|
||||
from app.utils.upload_util import UploadUtil
|
||||
|
||||
from .crud import ParamsCRUD
|
||||
from .schema import ParamsCreateSchema, ParamsOutSchema, ParamsQueryParam, ParamsUpdateSchema
|
||||
from .schema import (
|
||||
ParamsCreateSchema,
|
||||
ParamsOutSchema,
|
||||
ParamsQueryParam,
|
||||
ParamsUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class ParamsService:
|
||||
"""
|
||||
配置管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_obj_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
@@ -50,7 +56,7 @@ class ParamsService:
|
||||
"""
|
||||
obj = await ParamsCRUD(auth).get_obj_by_key_crud(key=config_key)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'配置键 {config_key} 不存在')
|
||||
raise CustomException(msg=f"配置键 {config_key} 不存在")
|
||||
return ParamsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
@@ -67,11 +73,16 @@ class ParamsService:
|
||||
"""
|
||||
obj = await ParamsCRUD(auth).get_obj_by_key_crud(key=config_key)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'配置键 {config_key} 不存在')
|
||||
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]:
|
||||
async def get_obj_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: ParamsQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取配置管理型列表
|
||||
|
||||
@@ -85,13 +96,17 @@ class ParamsService:
|
||||
"""
|
||||
obj_list = None
|
||||
if search:
|
||||
obj_list = await ParamsCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
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:
|
||||
async def create_obj_service(
|
||||
cls, auth: AuthSchema, redis: Redis, data: ParamsCreateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
创建配置管理型
|
||||
|
||||
@@ -105,7 +120,7 @@ class ParamsService:
|
||||
"""
|
||||
exist_obj = await ParamsCRUD(auth).get(config_key=data.config_key)
|
||||
if exist_obj:
|
||||
raise CustomException(msg='创建失败,该配置key已存在')
|
||||
raise CustomException(msg="创建失败,该配置key已存在")
|
||||
obj = await ParamsCRUD(auth).create_obj_crud(data=data)
|
||||
|
||||
new_obj_dict = ParamsOutSchema.model_validate(obj).model_dump()
|
||||
@@ -127,7 +142,9 @@ class ParamsService:
|
||||
return new_obj_dict
|
||||
|
||||
@classmethod
|
||||
async def update_obj_service(cls, auth: AuthSchema, redis: Redis, id: int, data: ParamsUpdateSchema) -> dict:
|
||||
async def update_obj_service(
|
||||
cls, auth: AuthSchema, redis: Redis, id: int, data: ParamsUpdateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
更新配置管理型
|
||||
|
||||
@@ -142,13 +159,13 @@ class ParamsService:
|
||||
"""
|
||||
exist_obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='更新失败,该数系统配置不存在')
|
||||
raise CustomException(msg="更新失败,该数系统配置不存在")
|
||||
if exist_obj.config_key != data.config_key:
|
||||
raise CustomException(msg='更新失败,系统配置key不允许修改')
|
||||
raise CustomException(msg="更新失败,系统配置key不允许修改")
|
||||
|
||||
new_obj = await ParamsCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not new_obj:
|
||||
raise CustomException(msg='更新失败,系统配置不存在')
|
||||
raise CustomException(msg="更新失败,系统配置不存在")
|
||||
new_obj_dict = ParamsOutSchema.model_validate(new_obj).model_dump()
|
||||
|
||||
# 同步redis
|
||||
@@ -182,15 +199,17 @@ class ParamsService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
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='删除失败,该数据字典类型不存在')
|
||||
raise CustomException(msg="删除失败,该数据字典类型不存在")
|
||||
# 检查是否是否初始化类型
|
||||
if exist_obj.config_type:
|
||||
# 如果有字典数据,不能删除
|
||||
raise CustomException(msg=f'{exist_obj.config_name} 删除失败,系统初始化配置不可以删除')
|
||||
raise CustomException(
|
||||
msg=f"{exist_obj.config_name} 删除失败,系统初始化配置不可以删除"
|
||||
)
|
||||
|
||||
await ParamsCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
@@ -219,24 +238,28 @@ class ParamsService:
|
||||
- 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',
|
||||
"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 '未知'
|
||||
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)
|
||||
|
||||
@@ -255,10 +278,10 @@ class ParamsService:
|
||||
filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url)
|
||||
|
||||
return UploadResponseSchema(
|
||||
file_path=f'{filepath}',
|
||||
file_path=f"{filepath}",
|
||||
file_name=filename,
|
||||
origin_name=file.filename,
|
||||
file_url=f'{file_url}',
|
||||
file_url=f"{file_url}",
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
@@ -282,7 +305,7 @@ class ParamsService:
|
||||
try:
|
||||
# 保存到Redis并设置过期时间
|
||||
for config in config_obj:
|
||||
redis_key = (f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{config.config_key}")
|
||||
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(
|
||||
@@ -338,7 +361,7 @@ class ParamsService:
|
||||
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"
|
||||
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:ip_black_list",
|
||||
]
|
||||
|
||||
# 批量获取配置
|
||||
@@ -349,20 +372,23 @@ class ParamsService:
|
||||
"demo_enable": False,
|
||||
"ip_white_list": [],
|
||||
"white_api_list_path": [],
|
||||
"ip_black_list": []
|
||||
"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
|
||||
config_result["demo_enable"] = (
|
||||
demo_config.get("config_value", False)
|
||||
if isinstance(demo_config, dict)
|
||||
else False
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
log.error("解析演示模式配置失败")
|
||||
|
||||
# 解析IP白名单配置
|
||||
if config_values[1]:
|
||||
|
||||
try:
|
||||
ip_white_config = json.loads(config_values[1])
|
||||
# 确保是列表类型
|
||||
@@ -375,7 +401,9 @@ class ParamsService:
|
||||
try:
|
||||
white_api_config = json.loads(config_values[2])
|
||||
# 确保是列表类型
|
||||
config_result["white_api_list_path"] = json.loads(white_api_config.get("config_value", []))
|
||||
config_result["white_api_list_path"] = json.loads(
|
||||
white_api_config.get("config_value", [])
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
log.error("解析API白名单配置失败")
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
|
||||
from .schema import PositionCreateSchema, PositionQueryParam, PositionUpdateSchema
|
||||
from .schema import (
|
||||
PositionCreateSchema,
|
||||
PositionQueryParam,
|
||||
PositionUpdateSchema,
|
||||
)
|
||||
from .service import PositionService
|
||||
|
||||
PositionRouter = APIRouter(route_class=OperationLogRoute, prefix="/position", tags=["岗位管理"])
|
||||
@@ -39,8 +43,14 @@ async def get_obj_list_controller(
|
||||
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)
|
||||
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("查询岗位列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询岗位列表成功")
|
||||
|
||||
@@ -127,7 +137,11 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除岗位成功")
|
||||
|
||||
|
||||
@PositionRouter.patch("/available/setting", summary="批量修改岗位状态", description="批量修改岗位状态")
|
||||
@PositionRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改岗位状态",
|
||||
description="批量修改岗位状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:patch"]))],
|
||||
@@ -147,7 +161,7 @@ async def batch_set_available_obj_controller(
|
||||
return SuccessResponse(msg="批量修改岗位状态成功")
|
||||
|
||||
|
||||
@PositionRouter.post('/export', summary="导出岗位", description="导出岗位")
|
||||
@PositionRouter.post("/export", summary="导出岗位", description="导出岗位")
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[PositionQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:export"]))],
|
||||
@@ -162,14 +176,16 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 岗位Excel文件流
|
||||
"""
|
||||
position_query_result = await PositionService.get_position_list_service(search=search, auth=auth)
|
||||
position_export_result = await PositionService.export_position_list_service(position_list=position_query_result)
|
||||
log.info('导出岗位成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=position.xlsx"},
|
||||
)
|
||||
|
||||
@@ -21,7 +21,9 @@ class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateS
|
||||
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:
|
||||
async def get_by_id_crud(
|
||||
self, id: int, preload: list[str] | None = None
|
||||
) -> PositionModel | None:
|
||||
"""
|
||||
根据 id 获取岗位信息。
|
||||
|
||||
@@ -34,7 +36,12 @@ class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateS
|
||||
"""
|
||||
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]:
|
||||
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]:
|
||||
"""
|
||||
获取岗位列表。
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ class PositionModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
岗位模型
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_position"
|
||||
__table_args__: dict[str, str] = ({'comment': '岗位表'})
|
||||
__table_args__: dict[str, str] = {"comment": "岗位表"}
|
||||
__loader_options__: list[str] = ["users", "created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位名称")
|
||||
@@ -24,5 +25,5 @@ class PositionModel(ModelMixin, UserMixin):
|
||||
users: Mapped[list["UserModel"]] = relationship(
|
||||
secondary="sys_user_positions",
|
||||
back_populates="positions",
|
||||
lazy="selectin"
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
@@ -7,17 +7,18 @@ from app.core.validator import DateTimeStr
|
||||
|
||||
class PositionCreateSchema(BaseModel):
|
||||
"""岗位创建模型"""
|
||||
|
||||
name: str = Field(..., max_length=64, description="岗位名称")
|
||||
order: int = Field(default=1, ge=1, 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')
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError('岗位名称不能为空')
|
||||
raise ValueError("岗位名称不能为空")
|
||||
return v
|
||||
|
||||
|
||||
@@ -27,6 +28,7 @@ class PositionUpdateSchema(PositionCreateSchema):
|
||||
|
||||
class PositionOutSchema(PositionCreateSchema, BaseSchema, UserBySchema):
|
||||
"""岗位信息响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -38,10 +40,18 @@ class PositionQueryParam:
|
||||
name: str | None = Query(None, description="岗位名称"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
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="更新人")
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name)
|
||||
|
||||
@@ -4,7 +4,12 @@ from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import PositionCRUD
|
||||
from .schema import PositionCreateSchema, PositionOutSchema, PositionQueryParam, PositionUpdateSchema
|
||||
from .schema import (
|
||||
PositionCreateSchema,
|
||||
PositionOutSchema,
|
||||
PositionQueryParam,
|
||||
PositionUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class PositionService:
|
||||
@@ -26,7 +31,12 @@ class PositionService:
|
||||
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]:
|
||||
async def get_position_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: PositionQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取岗位列表
|
||||
|
||||
@@ -38,8 +48,12 @@ class PositionService:
|
||||
返回:
|
||||
- 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]
|
||||
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:
|
||||
@@ -55,12 +69,14 @@ class PositionService:
|
||||
"""
|
||||
position = await PositionCRUD(auth).get(name=data.name)
|
||||
if position:
|
||||
raise CustomException(msg='创建失败,该岗位已存在')
|
||||
raise CustomException(msg="创建失败,该岗位已存在")
|
||||
new_position = await PositionCRUD(auth).create(data=data)
|
||||
return PositionOutSchema.model_validate(new_position).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_position_service(cls, auth: AuthSchema, id: int, data: PositionUpdateSchema) -> dict:
|
||||
async def update_position_service(
|
||||
cls, auth: AuthSchema, id: int, data: PositionUpdateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
更新岗位
|
||||
|
||||
@@ -74,10 +90,10 @@ class PositionService:
|
||||
"""
|
||||
position = await PositionCRUD(auth).get_by_id_crud(id=id)
|
||||
if not position:
|
||||
raise CustomException(msg='更新失败,该岗位不存在')
|
||||
raise CustomException(msg="更新失败,该岗位不存在")
|
||||
exist_position = await PositionCRUD(auth).get(name=data.name)
|
||||
if exist_position and exist_position.id != id:
|
||||
raise CustomException(msg='更新失败,岗位名称重复')
|
||||
raise CustomException(msg="更新失败,岗位名称重复")
|
||||
updated_position = await PositionCRUD(auth).update(id=id, data=data)
|
||||
return PositionOutSchema.model_validate(updated_position).model_dump()
|
||||
|
||||
@@ -94,15 +110,17 @@ class PositionService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
position = await PositionCRUD(auth).get_by_id_crud(id=id)
|
||||
if not position:
|
||||
raise CustomException(msg='删除失败,该岗位不存在')
|
||||
raise CustomException(msg="删除失败,该岗位不存在")
|
||||
await PositionCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_position_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
async def set_position_available_service(
|
||||
cls, auth: AuthSchema, data: BatchSetAvailable
|
||||
) -> None:
|
||||
"""
|
||||
设置岗位状态
|
||||
|
||||
@@ -127,21 +145,25 @@ class PositionService:
|
||||
- bytes: 导出的Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'name': '岗位名称',
|
||||
'order': '显示顺序',
|
||||
'status': '状态',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'updated_id': '更新者ID',
|
||||
"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 '未知'
|
||||
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)
|
||||
|
||||
@@ -13,7 +13,12 @@ from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
|
||||
from .schema import RoleCreateSchema, RolePermissionSettingSchema, RoleQueryParam, RoleUpdateSchema
|
||||
from .schema import (
|
||||
RoleCreateSchema,
|
||||
RolePermissionSettingSchema,
|
||||
RoleQueryParam,
|
||||
RoleUpdateSchema,
|
||||
)
|
||||
from .service import RoleService
|
||||
|
||||
RoleRouter = APIRouter(route_class=OperationLogRoute, prefix="/role", tags=["角色管理"])
|
||||
@@ -39,8 +44,14 @@ async def get_obj_list_controller(
|
||||
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)
|
||||
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("查询角色成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询角色成功")
|
||||
|
||||
@@ -127,7 +138,11 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除角色成功")
|
||||
|
||||
|
||||
@RoleRouter.patch("/available/setting", summary="批量修改角色状态", description="批量修改角色状态")
|
||||
@RoleRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改角色状态",
|
||||
description="批量修改角色状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:patch"]))],
|
||||
@@ -167,7 +182,7 @@ async def set_role_permission_controller(
|
||||
return SuccessResponse(msg="授权角色成功")
|
||||
|
||||
|
||||
@RoleRouter.post('/export', summary="导出角色", description="导出角色")
|
||||
@RoleRouter.post("/export", summary="导出角色", description="导出角色")
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[RoleQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:export"]))],
|
||||
@@ -184,12 +199,10 @@ async def export_obj_list_controller(
|
||||
"""
|
||||
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('导出角色成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=role.xlsx"},
|
||||
)
|
||||
|
||||
@@ -35,7 +35,12 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
|
||||
"""
|
||||
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]:
|
||||
async def get_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list | None = None,
|
||||
preload: list | None = None,
|
||||
) -> Sequence[RoleModel]:
|
||||
"""
|
||||
获取角色列表
|
||||
|
||||
|
||||
@@ -17,20 +17,21 @@ class RoleMenusModel(MappedBase):
|
||||
|
||||
定义角色与菜单的多对多关系,用于权限控制
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_role_menus"
|
||||
__table_args__: dict[str, str] = ({'comment': '角色菜单关联表'})
|
||||
__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"
|
||||
comment="角色ID",
|
||||
)
|
||||
menu_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
primary_key=True,
|
||||
comment="菜单ID"
|
||||
comment="菜单ID",
|
||||
)
|
||||
|
||||
|
||||
@@ -41,20 +42,21 @@ class RoleDeptsModel(MappedBase):
|
||||
定义角色与部门的多对多关系,用于数据权限控制
|
||||
仅当角色的data_scope=5(自定义数据权限)时使用此表
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_role_depts"
|
||||
__table_args__: dict[str, str] = ({'comment': '角色部门关联表'})
|
||||
__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"
|
||||
comment="角色ID",
|
||||
)
|
||||
dept_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("sys_dept.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
primary_key=True,
|
||||
comment="部门ID"
|
||||
comment="部门ID",
|
||||
)
|
||||
|
||||
|
||||
@@ -62,29 +64,33 @@ class RoleModel(ModelMixin):
|
||||
"""
|
||||
角色模型
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_role"
|
||||
__table_args__: dict[str, str] = ({'comment': '角色表'})
|
||||
__table_args__: dict[str, str] = {"comment": "角色表"}
|
||||
__loader_options__: list[str] = ["menus", "depts"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称")
|
||||
code: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True, comment="角色编码")
|
||||
code: Mapped[str | None] = mapped_column(
|
||||
String(16), 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:自定义)")
|
||||
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"
|
||||
order_by="MenuModel.order",
|
||||
)
|
||||
depts: Mapped[list["DeptModel"]] = relationship(
|
||||
secondary="sys_role_depts",
|
||||
back_populates="roles",
|
||||
lazy="selectin"
|
||||
secondary="sys_role_depts", back_populates="roles", lazy="selectin"
|
||||
)
|
||||
users: Mapped[list["UserModel"]] = relationship(
|
||||
secondary="sys_user_roles",
|
||||
back_populates="roles",
|
||||
lazy="selectin"
|
||||
secondary="sys_user_roles", back_populates="roles", lazy="selectin"
|
||||
)
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.api.v1.module_system.dept.schema import DeptOutSchema
|
||||
from app.api.v1.module_system.menu.schema import MenuOutSchema
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.core.validator import DateTimeStr, code_validator, role_permission_request_validator
|
||||
from app.core.validator import (
|
||||
DateTimeStr,
|
||||
code_validator,
|
||||
role_permission_request_validator,
|
||||
)
|
||||
|
||||
|
||||
class RoleCreateSchema(BaseModel):
|
||||
"""角色创建模型"""
|
||||
|
||||
name: str = Field(..., max_length=64, description="角色名称")
|
||||
code: str | None = Field(default=None, max_length=16, description="角色编码")
|
||||
order: int | None = Field(default=1, ge=1, description='显示排序')
|
||||
data_scope: int | None = Field(default=1, description='数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)')
|
||||
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="描述")
|
||||
|
||||
@@ -24,12 +38,16 @@ class RoleCreateSchema(BaseModel):
|
||||
|
||||
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')
|
||||
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)
|
||||
@@ -41,10 +59,11 @@ 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='角色部门列表')
|
||||
menus: list[MenuOutSchema] = Field(default_factory=list, description="角色菜单列表")
|
||||
depts: list[DeptOutSchema] = Field(default_factory=list, description="角色部门列表")
|
||||
|
||||
|
||||
class RoleQueryParam:
|
||||
@@ -55,8 +74,16 @@ class RoleQueryParam:
|
||||
name: str | None = Query(None, description="角色名称"),
|
||||
description: str | None = Query(None, description="描述"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"])
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,13 @@ from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import RoleCRUD
|
||||
from .schema import RoleCreateSchema, RoleOutSchema, RolePermissionSettingSchema, RoleQueryParam, RoleUpdateSchema
|
||||
from .schema import (
|
||||
RoleCreateSchema,
|
||||
RoleOutSchema,
|
||||
RolePermissionSettingSchema,
|
||||
RoleQueryParam,
|
||||
RoleUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class RoleService:
|
||||
@@ -28,7 +34,12 @@ class RoleService:
|
||||
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]:
|
||||
async def get_role_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: RoleQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取角色列表
|
||||
|
||||
@@ -57,10 +68,10 @@ class RoleService:
|
||||
"""
|
||||
role = await RoleCRUD(auth).get(name=data.name)
|
||||
if role:
|
||||
raise CustomException(msg='创建失败,该角色已存在')
|
||||
raise CustomException(msg="创建失败,该角色已存在")
|
||||
obj = await RoleCRUD(auth).get(code=data.code)
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,编码已存在')
|
||||
raise CustomException(msg="创建失败,编码已存在")
|
||||
new_role = await RoleCRUD(auth).create(data=data)
|
||||
return RoleOutSchema.model_validate(new_role).model_dump()
|
||||
|
||||
@@ -79,10 +90,10 @@ class RoleService:
|
||||
"""
|
||||
role = await RoleCRUD(auth).get_by_id_crud(id=id)
|
||||
if not role:
|
||||
raise CustomException(msg='更新失败,该角色不存在')
|
||||
raise CustomException(msg="更新失败,该角色不存在")
|
||||
exist_role = await RoleCRUD(auth).get(name=data.name)
|
||||
if exist_role and exist_role.id != id:
|
||||
raise CustomException(msg='更新失败,角色名称重复')
|
||||
raise CustomException(msg="更新失败,角色名称重复")
|
||||
updated_role = await RoleCRUD(auth).update(id=id, data=data)
|
||||
return RoleOutSchema.model_validate(updated_role).model_dump()
|
||||
|
||||
@@ -99,15 +110,17 @@ class RoleService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
role = await RoleCRUD(auth).get_by_id_crud(id=id)
|
||||
if not role:
|
||||
raise CustomException(msg='删除失败,该角色不存在')
|
||||
raise CustomException(msg="删除失败,该角色不存在")
|
||||
await RoleCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_role_permission_service(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None:
|
||||
async def set_role_permission_service(
|
||||
cls, auth: AuthSchema, data: RolePermissionSettingSchema
|
||||
) -> None:
|
||||
"""
|
||||
设置角色权限
|
||||
|
||||
@@ -122,7 +135,9 @@ class RoleService:
|
||||
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)
|
||||
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:
|
||||
@@ -157,32 +172,36 @@ class RoleService:
|
||||
"""
|
||||
# 字段映射配置
|
||||
mapping_dict = {
|
||||
'id': '角色编号',
|
||||
'name': '角色名称',
|
||||
'order': '显示顺序',
|
||||
'data_scope': '数据权限',
|
||||
'status': '状态',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'updated_id': '更新者ID',
|
||||
"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: '自定义数据权限'
|
||||
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 '未知'
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import urllib.parse
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Request, UploadFile
|
||||
@@ -33,7 +32,7 @@ UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["用
|
||||
|
||||
@UserRouter.get("/current/info", summary="查询当前用户信息", description="查询当前用户信息")
|
||||
async def get_current_user_info_controller(
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)]
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询当前用户信息
|
||||
@@ -46,14 +45,15 @@ async def get_current_user_info_controller(
|
||||
"""
|
||||
result_dict = await UserService.get_current_user_info_service(auth=auth)
|
||||
log.info("获取当前用户信息成功")
|
||||
return SuccessResponse(data=result_dict, msg='获取当前用户信息成功')
|
||||
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:
|
||||
@UserRouter.post(
|
||||
"/current/avatar/upload",
|
||||
summary="上传当前用户头像",
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
async def user_avatar_upload_controller(file: UploadFile, request: Request) -> JSONResponse:
|
||||
"""
|
||||
上传当前用户头像
|
||||
|
||||
@@ -66,13 +66,17 @@ async def user_avatar_upload_controller(
|
||||
"""
|
||||
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='上传头像成功')
|
||||
return SuccessResponse(data=result_str, msg="上传头像成功")
|
||||
|
||||
|
||||
@UserRouter.put("/current/info/update", summary="更新当前用户基本信息", description="更新当前用户基本信息")
|
||||
@UserRouter.put(
|
||||
"/current/info/update",
|
||||
summary="更新当前用户基本信息",
|
||||
description="更新当前用户基本信息",
|
||||
)
|
||||
async def update_current_user_info_controller(
|
||||
data: CurrentUserUpdateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)]
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
更新当前用户基本信息
|
||||
@@ -86,13 +90,17 @@ async def update_current_user_info_controller(
|
||||
"""
|
||||
result_dict = await UserService.update_current_user_info_service(data=data, auth=auth)
|
||||
log.info(f"更新当前用户基本信息成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg='更新当前用户基本信息成功')
|
||||
return SuccessResponse(data=result_dict, msg="更新当前用户基本信息成功")
|
||||
|
||||
|
||||
@UserRouter.put("/current/password/change", summary="修改当前用户密码", description="修改当前用户密码")
|
||||
@UserRouter.put(
|
||||
"/current/password/change",
|
||||
summary="修改当前用户密码",
|
||||
description="修改当前用户密码",
|
||||
)
|
||||
async def change_current_user_password_controller(
|
||||
data: UserChangePasswordSchema,
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)]
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改当前用户密码
|
||||
@@ -106,13 +114,13 @@ async def change_current_user_password_controller(
|
||||
"""
|
||||
result_dict = await UserService.change_user_password_service(data=data, auth=auth)
|
||||
log.info(f"修改密码成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg='修改密码成功, 请重新登录')
|
||||
return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录")
|
||||
|
||||
|
||||
@UserRouter.put("/reset/password", summary="重置密码", description="重置密码")
|
||||
async def reset_password_controller(
|
||||
data: ResetPasswordSchema,
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)]
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
重置密码
|
||||
@@ -126,10 +134,10 @@ async def reset_password_controller(
|
||||
"""
|
||||
result_dict = await UserService.reset_user_password_service(data=data, auth=auth)
|
||||
log.info(f"重置密码成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg='重置密码成功')
|
||||
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
||||
|
||||
|
||||
@UserRouter.post('/register', summary="注册用户", description="注册用户")
|
||||
@UserRouter.post("/register", summary="注册用户", description="注册用户")
|
||||
async def register_user_controller(
|
||||
data: UserRegisterSchema,
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
@@ -147,10 +155,10 @@ async def register_user_controller(
|
||||
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='注册用户成功')
|
||||
return SuccessResponse(data=user_register_result, msg="注册用户成功")
|
||||
|
||||
|
||||
@UserRouter.post('/forget/password', summary="忘记密码", description="忘记密码")
|
||||
@UserRouter.post("/forget/password", summary="忘记密码", description="忘记密码")
|
||||
async def forget_password_controller(
|
||||
data: UserForgetPasswordSchema,
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
@@ -168,7 +176,7 @@ async def forget_password_controller(
|
||||
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='重置密码成功')
|
||||
return SuccessResponse(data=user_forget_password_result, msg="重置密码成功")
|
||||
|
||||
|
||||
@UserRouter.get("/list", summary="查询用户", description="查询用户")
|
||||
@@ -188,8 +196,14 @@ async def get_obj_list_controller(
|
||||
返回:
|
||||
- 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)
|
||||
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("查询用户成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询用户成功")
|
||||
|
||||
@@ -211,7 +225,7 @@ async def get_obj_detail_controller(
|
||||
"""
|
||||
result_dict = await UserService.get_detail_by_id_service(id=id, auth=auth)
|
||||
log.info(f"获取用户详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg='获取用户详情成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取用户详情成功")
|
||||
|
||||
|
||||
@UserRouter.post("/create", summary="创建用户", description="创建用户")
|
||||
@@ -280,7 +294,11 @@ async def delete_obj_controller(
|
||||
return SuccessResponse(msg="删除用户成功")
|
||||
|
||||
|
||||
@UserRouter.patch("/available/setting", summary="批量修改用户状态", description="批量修改用户状态")
|
||||
@UserRouter.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改用户状态",
|
||||
description="批量修改用户状态",
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:patch"]))],
|
||||
@@ -300,7 +318,12 @@ async def batch_set_available_obj_controller(
|
||||
return SuccessResponse(msg="批量修改用户状态成功")
|
||||
|
||||
|
||||
@UserRouter.post('/import/template', summary="获取用户导入模板", description="获取用户导入模板", dependencies=[Depends(AuthPermission(["module_system:user:download"]))])
|
||||
@UserRouter.post(
|
||||
"/import/template",
|
||||
summary="获取用户导入模板",
|
||||
description="获取用户导入模板",
|
||||
dependencies=[Depends(AuthPermission(["module_system:user:download"]))],
|
||||
)
|
||||
async def export_obj_template_controller() -> StreamingResponse:
|
||||
"""
|
||||
获取用户导入模板
|
||||
@@ -309,19 +332,19 @@ async def export_obj_template_controller() -> StreamingResponse:
|
||||
- StreamingResponse: 用户导入模板流响应
|
||||
"""
|
||||
user_import_template_result = await UserService.get_import_template_user_service()
|
||||
log.info('获取用户导入模板成功')
|
||||
log.info("获取用户导入模板成功")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(user_import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename={urllib.parse.quote("用户导入模板.xlsx")}',
|
||||
'Access-Control-Expose-Headers': 'Content-Disposition'
|
||||
}
|
||||
"Content-Disposition": f"attachment; filename={urllib.parse.quote('用户导入模板.xlsx')}",
|
||||
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@UserRouter.post('/export', summary="导出用户", description="导出用户")
|
||||
@UserRouter.post("/export", summary="导出用户", description="导出用户")
|
||||
async def export_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[UserQueryParam, Depends()],
|
||||
@@ -338,23 +361,23 @@ async def export_obj_list_controller(
|
||||
返回:
|
||||
- StreamingResponse: 用户导出模板流响应
|
||||
"""
|
||||
user_list = await UserService.get_user_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
user_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('导出用户成功')
|
||||
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'
|
||||
}
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=user.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@UserRouter.post('/import/data', summary="导入用户", description="导入用户")
|
||||
@UserRouter.post("/import/data", summary="导入用户", description="导入用户")
|
||||
async def import_obj_list_controller(
|
||||
file: UploadFile,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:import"]))]
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:import"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
导入用户
|
||||
@@ -366,6 +389,8 @@ async def import_obj_list_controller(
|
||||
返回:
|
||||
- JSONResponse: 导入用户JSON响应
|
||||
"""
|
||||
batch_import_result = await UserService.batch_import_user_service(file=file, auth=auth, update_support=True)
|
||||
batch_import_result = await UserService.batch_import_user_service(
|
||||
file=file, auth=auth, update_support=True
|
||||
)
|
||||
log.info(f"导入用户成功: {batch_import_result}")
|
||||
return SuccessResponse(data=batch_import_result, msg="导入用户成功")
|
||||
|
||||
@@ -8,7 +8,11 @@ from app.api.v1.module_system.role.crud import RoleCRUD
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from .model import UserModel
|
||||
from .schema import UserCreateSchema, UserForgetPasswordSchema, UserUpdateSchema
|
||||
from .schema import (
|
||||
UserCreateSchema,
|
||||
UserForgetPasswordSchema,
|
||||
UserUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
@@ -24,7 +28,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
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:
|
||||
async def get_by_id_crud(
|
||||
self, id: int, preload: list[str | Any] | None = None
|
||||
) -> UserModel | None:
|
||||
"""
|
||||
根据id获取用户信息
|
||||
|
||||
@@ -40,7 +46,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
id=id,
|
||||
)
|
||||
|
||||
async def get_by_username_crud(self, username: str, preload: list[str | Any] | None = None) -> UserModel | None:
|
||||
async def get_by_username_crud(
|
||||
self, username: str, preload: list[str | Any] | None = None
|
||||
) -> UserModel | None:
|
||||
"""
|
||||
根据用户名获取用户信息
|
||||
|
||||
@@ -56,7 +64,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
username=username,
|
||||
)
|
||||
|
||||
async def get_by_mobile_crud(self, mobile: str, preload: list[str | Any] | None = None) -> UserModel | None:
|
||||
async def get_by_mobile_crud(
|
||||
self, mobile: str, preload: list[str | Any] | None = None
|
||||
) -> UserModel | None:
|
||||
"""
|
||||
根据手机号获取用户信息
|
||||
|
||||
@@ -72,7 +82,12 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
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]:
|
||||
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]:
|
||||
"""
|
||||
获取用户列表
|
||||
|
||||
@@ -151,7 +166,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
||||
"""
|
||||
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)})
|
||||
position_objs = await PositionCRUD(self.auth).get_list_crud(
|
||||
search={"id": ("in", position_ids)}
|
||||
)
|
||||
else:
|
||||
position_objs = []
|
||||
|
||||
|
||||
@@ -18,20 +18,21 @@ class UserRolesModel(MappedBase):
|
||||
|
||||
定义用户与角色的多对多关系
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_user_roles"
|
||||
__table_args__: dict[str, str] = ({'comment': '用户角色关联表'})
|
||||
__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"
|
||||
comment="用户ID",
|
||||
)
|
||||
role_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
primary_key=True,
|
||||
comment="角色ID"
|
||||
comment="角色ID",
|
||||
)
|
||||
|
||||
|
||||
@@ -41,20 +42,21 @@ class UserPositionsModel(MappedBase):
|
||||
|
||||
定义用户与岗位的多对多关系
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_user_positions"
|
||||
__table_args__: dict[str, str] = ({'comment': '用户岗位关联表'})
|
||||
__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"
|
||||
comment="用户ID",
|
||||
)
|
||||
position_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("sys_position.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
primary_key=True,
|
||||
comment="岗位ID"
|
||||
comment="岗位ID",
|
||||
)
|
||||
|
||||
|
||||
@@ -62,46 +64,61 @@ 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(64), nullable=False, unique=True, comment="用户名/登录账号")
|
||||
__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(64), 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:未知)")
|
||||
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="最后登录时间")
|
||||
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登录")
|
||||
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登录")
|
||||
|
||||
dept_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('sys_dept.id', ondelete="SET NULL", onupdate="CASCADE"),
|
||||
ForeignKey("sys_dept.id", ondelete="SET NULL", onupdate="CASCADE"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="部门ID"
|
||||
comment="部门ID",
|
||||
)
|
||||
dept: Mapped["DeptModel | None"] = relationship(
|
||||
back_populates="users",
|
||||
foreign_keys=[dept_id],
|
||||
lazy="selectin"
|
||||
back_populates="users", foreign_keys=[dept_id], lazy="selectin"
|
||||
)
|
||||
roles: Mapped[list["RoleModel"]] = relationship(
|
||||
secondary="sys_user_roles",
|
||||
back_populates="users",
|
||||
lazy="selectin"
|
||||
secondary="sys_user_roles", back_populates="users", lazy="selectin"
|
||||
)
|
||||
positions: Mapped[list["PositionModel"]] = relationship(
|
||||
secondary="sys_user_positions",
|
||||
back_populates="users",
|
||||
lazy="selectin"
|
||||
secondary="sys_user_positions", back_populates="users", lazy="selectin"
|
||||
)
|
||||
|
||||
# 覆盖 UserMixin 的关系定义,显式指定 foreign_keys 避免自引用混淆
|
||||
@@ -111,7 +128,7 @@ class UserModel(ModelMixin, UserMixin):
|
||||
remote_side="UserModel.id",
|
||||
lazy="selectin",
|
||||
uselist=False,
|
||||
viewonly=True # 防止级联操作
|
||||
viewonly=True, # 防止级联操作
|
||||
)
|
||||
|
||||
updated_by: Mapped["UserModel | None"] = relationship(
|
||||
@@ -120,5 +137,5 @@ class UserModel(ModelMixin, UserMixin):
|
||||
remote_side="UserModel.id",
|
||||
lazy="selectin",
|
||||
uselist=False,
|
||||
viewonly=True # 防止级联操作
|
||||
viewonly=True, # 防止级联操作
|
||||
)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
EmailStr,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.api.v1.module_system.menu.schema import MenuOutSchema
|
||||
from app.api.v1.module_system.role.schema import RoleOutSchema
|
||||
@@ -11,6 +18,7 @@ from app.core.validator import DateTimeStr, email_validator, mobile_validator
|
||||
|
||||
class CurrentUserUpdateSchema(BaseModel):
|
||||
"""基础用户信息"""
|
||||
|
||||
name: str | None = Field(default=None, description="名称")
|
||||
mobile: str | None = Field(default=None, description="手机号")
|
||||
email: EmailStr | None = Field(default=None, description="邮箱")
|
||||
@@ -48,12 +56,13 @@ class CurrentUserUpdateSchema(BaseModel):
|
||||
|
||||
class UserRegisterSchema(BaseModel):
|
||||
"""注册"""
|
||||
|
||||
name: str | None = Field(default=None, description="名称")
|
||||
mobile: str | None = Field(default=None, description="手机号")
|
||||
username: str = Field(..., description="账号")
|
||||
password: str = Field(..., description="密码哈希值")
|
||||
role_ids: list[int] | None = Field(default=[1], description='角色ID')
|
||||
created_id: int | None = Field(default=1, description='创建人ID')
|
||||
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")
|
||||
@@ -69,6 +78,7 @@ class UserRegisterSchema(BaseModel):
|
||||
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
|
||||
@@ -88,6 +98,7 @@ class UserRegisterSchema(BaseModel):
|
||||
|
||||
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="手机号")
|
||||
@@ -100,18 +111,21 @@ class UserForgetPasswordSchema(BaseModel):
|
||||
|
||||
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="用户名")
|
||||
@@ -119,13 +133,14 @@ class UserCreateSchema(CurrentUserUpdateSchema):
|
||||
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')
|
||||
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="最后登录时间")
|
||||
@@ -133,16 +148,17 @@ class UserUpdateSchema(UserCreateSchema):
|
||||
|
||||
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='菜单')
|
||||
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:
|
||||
@@ -152,12 +168,24 @@ class UserQueryParam:
|
||||
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-.]+$'),
|
||||
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_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:
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import io
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
@@ -63,7 +61,12 @@ class UserService:
|
||||
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]:
|
||||
async def get_user_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: UserQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取用户列表
|
||||
|
||||
@@ -99,17 +102,17 @@ class UserService:
|
||||
raise CustomException(msg="用户名不能为空")
|
||||
# 检查是否试图创建超级管理员
|
||||
if data.is_superuser:
|
||||
raise CustomException(msg='不允许创建超级管理员')
|
||||
raise CustomException(msg="不允许创建超级管理员")
|
||||
# 检查用户名是否存在
|
||||
user = await UserCRUD(auth).get_by_username_crud(username=data.username)
|
||||
if user:
|
||||
raise CustomException(msg='已存在相同用户名称的账号')
|
||||
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='部门不存在')
|
||||
raise CustomException(msg="部门不存在")
|
||||
# 创建用户
|
||||
if data.password:
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
@@ -121,7 +124,9 @@ class UserService:
|
||||
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)
|
||||
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
|
||||
@@ -145,36 +150,39 @@ class UserService:
|
||||
# 检查用户是否存在
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg='用户不存在')
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
# 检查是否尝试修改超级管理员
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg='超级管理员不允许修改')
|
||||
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='已存在相同的账号')
|
||||
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='更新失败,手机号已存在')
|
||||
raise CustomException(msg="更新失败,手机号已存在")
|
||||
# 新增:检查邮箱是否重复
|
||||
if data.email:
|
||||
exist_email_user = await UserCRUD(auth).get(email=data.email)
|
||||
if exist_email_user and exist_email_user.id != id:
|
||||
raise CustomException(msg='更新失败,邮箱已存在')
|
||||
raise CustomException(msg="更新失败,邮箱已存在")
|
||||
# 检查部门是否存在且可用
|
||||
if data.dept_id:
|
||||
dept = await DeptCRUD(auth).get_by_id_crud(id=data.dept_id)
|
||||
if not dept:
|
||||
raise CustomException(msg='部门不存在')
|
||||
raise CustomException(msg="部门不存在")
|
||||
if dept.status == "1":
|
||||
raise CustomException(msg='部门已被禁用')
|
||||
raise CustomException(msg="部门已被禁用")
|
||||
|
||||
# 更新用户 - 排除不应被修改的字段, 更新不更新密码
|
||||
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids", "last_login", "password"})
|
||||
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)
|
||||
|
||||
# 更新角色和岗位
|
||||
@@ -182,19 +190,23 @@ class UserService:
|
||||
# 检查角色是否都存在且可用
|
||||
roles = await RoleCRUD(auth).get_list_crud(search={"id": ("in", data.role_ids)})
|
||||
if len(roles) != len(data.role_ids):
|
||||
raise CustomException(msg='部分角色不存在')
|
||||
raise CustomException(msg="部分角色不存在")
|
||||
if not all(role.status for role in roles):
|
||||
raise CustomException(msg='部分角色已被禁用')
|
||||
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)})
|
||||
positions = await PositionCRUD(auth).get_list_crud(
|
||||
search={"id": ("in", data.position_ids)}
|
||||
)
|
||||
if len(positions) != len(data.position_ids):
|
||||
raise CustomException(msg='部分岗位不存在')
|
||||
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)
|
||||
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
|
||||
@@ -212,7 +224,7 @@ class UserService:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=id)
|
||||
if not user:
|
||||
@@ -255,7 +267,10 @@ class UserService:
|
||||
# 获取菜单权限
|
||||
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"}])
|
||||
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:
|
||||
@@ -268,15 +283,24 @@ class UserService:
|
||||
}
|
||||
|
||||
# 使用树形结构查询,预加载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 []
|
||||
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:
|
||||
async def update_current_user_info_service(
|
||||
cls, auth: AuthSchema, data: CurrentUserUpdateSchema
|
||||
) -> dict:
|
||||
"""
|
||||
更新当前用户信息
|
||||
|
||||
@@ -298,12 +322,12 @@ class UserService:
|
||||
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='更新失败,手机号已存在')
|
||||
raise CustomException(msg="更新失败,手机号已存在")
|
||||
# 新增:检查邮箱是否重复
|
||||
if data.email:
|
||||
exist_email_user = await UserCRUD(auth).get(email=data.email)
|
||||
if exist_email_user and exist_email_user.id != auth.user.id:
|
||||
raise CustomException(msg='更新失败,邮箱已存在')
|
||||
raise CustomException(msg="更新失败,邮箱已存在")
|
||||
user_update_data = UserUpdateSchema(**data.model_dump())
|
||||
new_user = await UserCRUD(auth).update(id=auth.user.id, data=user_update_data)
|
||||
return UserOutSchema.model_validate(new_user).model_dump()
|
||||
@@ -343,14 +367,16 @@ class UserService:
|
||||
filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url)
|
||||
|
||||
return UploadResponseSchema(
|
||||
file_path=f'{filepath}',
|
||||
file_path=f"{filepath}",
|
||||
file_name=filename,
|
||||
origin_name=file.filename,
|
||||
file_url=f'{file_url}',
|
||||
file_url=f"{file_url}",
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def change_user_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> dict:
|
||||
async def change_user_password_service(
|
||||
cls, auth: AuthSchema, data: UserChangePasswordSchema
|
||||
) -> dict:
|
||||
"""
|
||||
修改用户密码
|
||||
|
||||
@@ -364,18 +390,22 @@ class UserService:
|
||||
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='密码不能为空')
|
||||
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='原密码输入错误')
|
||||
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)
|
||||
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
|
||||
@@ -391,7 +421,7 @@ class UserService:
|
||||
- Dict: 更新后的当前用户详情字典
|
||||
"""
|
||||
if not data.password:
|
||||
raise CustomException(msg='密码不能为空')
|
||||
raise CustomException(msg="密码不能为空")
|
||||
|
||||
# 验证用户
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=data.id)
|
||||
@@ -404,7 +434,9 @@ class UserService:
|
||||
|
||||
# 更新密码
|
||||
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)
|
||||
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
|
||||
@@ -422,7 +454,7 @@ class UserService:
|
||||
# 检查用户名是否存在
|
||||
username_ok = await UserCRUD(auth).get_by_username_crud(username=data.username)
|
||||
if username_ok:
|
||||
raise CustomException(msg='账号已存在')
|
||||
raise CustomException(msg="账号已存在")
|
||||
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
data.name = data.username
|
||||
@@ -438,7 +470,9 @@ class UserService:
|
||||
return UserOutSchema.model_validate(result).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def forget_password_service(cls, auth: AuthSchema, data: UserForgetPasswordSchema) -> dict:
|
||||
async def forget_password_service(
|
||||
cls, auth: AuthSchema, data: UserForgetPasswordSchema
|
||||
) -> dict:
|
||||
"""
|
||||
用户忘记密码
|
||||
|
||||
@@ -460,11 +494,15 @@ class UserService:
|
||||
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)
|
||||
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:
|
||||
async def batch_import_user_service(
|
||||
cls, auth: AuthSchema, file: UploadFile, update_support: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
批量导入用户
|
||||
|
||||
@@ -478,13 +516,13 @@ class UserService:
|
||||
"""
|
||||
|
||||
header_dict = {
|
||||
'部门编号': 'dept_id',
|
||||
'用户名': 'username',
|
||||
'名称': 'name',
|
||||
'邮箱': 'email',
|
||||
'手机号': 'mobile',
|
||||
'性别': 'gender',
|
||||
'状态': 'status'
|
||||
"部门编号": "dept_id",
|
||||
"用户名": "username",
|
||||
"名称": "name",
|
||||
"邮箱": "email",
|
||||
"手机号": "mobile",
|
||||
"性别": "gender",
|
||||
"状态": "status",
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -505,7 +543,7 @@ class UserService:
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
required_fields = ['username', 'name', 'dept_id']
|
||||
required_fields = ["username", "name", "dept_id"]
|
||||
errors = []
|
||||
for field in required_fields:
|
||||
missing_rows = df[df[field].isnull()].index.tolist()
|
||||
@@ -526,23 +564,25 @@ class UserService:
|
||||
try:
|
||||
count = count + 1
|
||||
# 数据转换
|
||||
gender = 1 if row['gender'] == '男' else (2 if row['gender'] == '女' else 1)
|
||||
status = "0" if row['status'] == '正常' else "1"
|
||||
gender = 1 if row["gender"] == "男" else (2 if row["gender"] == "女" else 1)
|
||||
status = "0" if row["status"] == "正常" else "1"
|
||||
|
||||
# 构建用户数据
|
||||
user_data = {
|
||||
"username": str(row['username']).strip(),
|
||||
"name": str(row['name']).strip(),
|
||||
"email": str(row['email']).strip(),
|
||||
"mobile": str(row['mobile']).strip(),
|
||||
"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") # 设置默认密码
|
||||
"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"])
|
||||
exists_user = await UserCRUD(auth).get_by_username_crud(
|
||||
username=user_data["username"]
|
||||
)
|
||||
if exists_user:
|
||||
# 检查是否是超级管理员
|
||||
if exists_user.is_superuser:
|
||||
@@ -581,13 +621,24 @@ class UserService:
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
header_list = ['部门编号', '用户名', '名称', '邮箱', '手机号', '性别', '状态']
|
||||
selector_header_list = ['性别', '状态']
|
||||
option_list = [{'性别': ['男', '女', '未知']}, {'状态': ['正常', '停用']}]
|
||||
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
|
||||
option_list=option_list,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -606,31 +657,35 @@ class UserService:
|
||||
|
||||
# 定义字段映射
|
||||
mapping_dict = {
|
||||
'id': '用户编号',
|
||||
'avatar': '头像',
|
||||
'username': '用户名称',
|
||||
'name': '用户昵称',
|
||||
'dept_name': '部门',
|
||||
'email': '邮箱',
|
||||
'mobile': '手机号',
|
||||
'gender': '性别',
|
||||
'status': '状态',
|
||||
'is_superuser': '是否超级管理员',
|
||||
'last_login': '最后登录时间',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'updated_id': '更新者ID',
|
||||
"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 '未知'
|
||||
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