mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
chore: 整理项目配置与代码优化
1. 统一前端环境变量命名,新增通用项目标题配置 2. 修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析 3. 优化表格列排序、搜索过滤功能与页面布局样式 4. 重构路由与状态管理代码,移除循环依赖 5. 更新依赖版本与脚本命令,修复构建警告 6. 调整多语言文案与快捷入口配置 7. 修复通知中心样式与接口返回格式
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -19,7 +19,7 @@ DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["部
|
||||
async def get_dept_tree_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
search: Annotated[DeptQueryParam, Depends()],
|
||||
search: Annotated[DeptQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
order_by = [{"order": "asc"}]
|
||||
result_dict_tree = await DeptService(auth, db).tree(search=search, order_by=order_by)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -39,7 +39,7 @@ async def get_type_detail_controller(
|
||||
async def get_type_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[DictTypeQueryParam, Depends()],
|
||||
search: Annotated[DictTypeQueryParam, Query()],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
result_dict = await DictTypeService(auth, db).page(
|
||||
@@ -118,7 +118,7 @@ async def get_data_detail_controller(
|
||||
async def get_data_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[DictDataQueryParam, Depends()],
|
||||
search: Annotated[DictDataQueryParam, Query()],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
order_by = [{"order": "asc"}]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -38,7 +38,7 @@ async def get_log_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[LoginLogQueryParam, Depends()],
|
||||
search: Annotated[LoginLogQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await LoginLogService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
@@ -76,7 +76,7 @@ async def get_operation_log_list_controller(
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[OperationLogQueryParam, Depends()],
|
||||
search: Annotated[OperationLogQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await OperationLogService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -19,7 +19,7 @@ MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["菜
|
||||
async def get_menu_tree_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:menu:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
search: Annotated[MenuQueryParam, Depends()],
|
||||
search: Annotated[MenuQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
order_by = [{"order": "asc"}]
|
||||
result_dict_tree = await MenuService(auth, db).tree(search=search, order_by=order_by)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -30,7 +30,7 @@ async def get_notice_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[NoticeQueryParam, Depends()],
|
||||
search: Annotated[NoticeQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await NoticeService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
@@ -88,4 +88,4 @@ async def get_notice_list_available_controller(
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
result_dict = await NoticeService(auth, db).available_page()
|
||||
return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功")
|
||||
return SuccessResponse(data=result_dict.items, msg="查询已启用公告列表成功")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -21,7 +21,7 @@ async def get_obj_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[PositionQueryParam, Depends()],
|
||||
search: Annotated[PositionQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await PositionService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -21,7 +21,7 @@ async def get_role_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[RoleQueryParam, Depends()],
|
||||
search: Annotated[RoleQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await RoleService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -21,7 +21,7 @@ async def ticket_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[TicketQueryParam, Depends()],
|
||||
search: Annotated[TicketQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result = await TicketService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
|
||||
@@ -3,15 +3,11 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, File, Path, Query, Security, UploadFile, status
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.v1.module_system.auth.service import CaptchaService
|
||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||
from app.config.setting import settings
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission, db_getter, get_current_user, redis_getter
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.dependencies import AuthPermission, db_getter, get_current_user
|
||||
from app.core.logger import logger
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
@@ -78,18 +74,8 @@ async def reset_password_controller(
|
||||
@UserRouter.post("/password/forget", summary="忘记密码", response_model=ResponseSchema[UserOutSchema])
|
||||
async def forget_password_controller(
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")],
|
||||
) -> JSONResponse:
|
||||
# 安全加固:忘记密码必须先校验图形验证码(防暴力枚举用户名/手机号接管账户)
|
||||
if settings.CAPTCHA_ENABLE:
|
||||
if not data.captcha_key or not data.captcha:
|
||||
raise CustomException(msg="验证码不能为空")
|
||||
await CaptchaService.check_captcha(
|
||||
redis=redis,
|
||||
key=data.captcha_key,
|
||||
)
|
||||
|
||||
auth = AuthSchema()
|
||||
user_forget_password_result = await UserService(auth, db).forget_password(data=data)
|
||||
logger.info(f"{data.username} 重置密码成功")
|
||||
@@ -99,18 +85,8 @@ async def forget_password_controller(
|
||||
@UserRouter.post("/register", summary="用户注册", response_model=ResponseSchema[UserOutSchema])
|
||||
async def register_controller(
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
data: Annotated[UserRegisterSchema, Body(description="用户注册参数")],
|
||||
) -> JSONResponse:
|
||||
# 安全加固:注册必须先校验图形验证码(防暴力注册)
|
||||
if settings.CAPTCHA_ENABLE:
|
||||
if not data.captcha_key or not data.captcha:
|
||||
raise CustomException(msg="验证码不能为空")
|
||||
await CaptchaService.check_captcha(
|
||||
redis=redis,
|
||||
key=data.captcha_key,
|
||||
)
|
||||
|
||||
auth = AuthSchema()
|
||||
register_result = await UserService(auth, db).register(data=data)
|
||||
logger.info(f"新用户注册成功: {data.username}")
|
||||
@@ -122,7 +98,7 @@ async def get_user_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[UserQueryParam, Depends()],
|
||||
search: Annotated[UserQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result_dict = await UserService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
|
||||
@@ -71,9 +71,6 @@ class UserForgetPasswordSchema(BaseModel):
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=32, description="用户名")
|
||||
new_password: str = Field(..., min_length=6, max_length=128, description="新密码")
|
||||
mobile: str | None = Field(default=None, max_length=11, description="手机号")
|
||||
captcha_key: str | None = Field(default=None, description="图形验证码 key(必填,防暴力枚举)")
|
||||
captcha: str | None = Field(default=None, description="图形验证码")
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
@@ -97,12 +94,6 @@ class UserForgetPasswordSchema(BaseModel):
|
||||
raise ValueError("密码长度不能超过 128 位")
|
||||
return value
|
||||
|
||||
@field_validator("mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, value: str | None):
|
||||
"""校验手机号格式"""
|
||||
return mobile_validator(value)
|
||||
|
||||
|
||||
class UserChangePasswordSchema(BaseModel):
|
||||
"""修改密码"""
|
||||
@@ -189,8 +180,6 @@ class UserRegisterSchema(BaseModel):
|
||||
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
||||
email: EmailStr | None = Field(default=None, description="邮箱")
|
||||
name: str | None = Field(default=None, max_length=32, description="名称")
|
||||
captcha_key: str | None = Field(default=None, description="图形验证码 key(必填,防暴力枚举)")
|
||||
captcha: str | None = Field(default=None, description="图形验证码")
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
|
||||
@@ -29,8 +29,8 @@ from .schema import (
|
||||
UserUpdateSchema,
|
||||
)
|
||||
|
||||
# 用户管理列表/详情预加载:只需 dept(用于填充 dept_name)
|
||||
_USER_PRELOAD = ["dept"]
|
||||
# 用户管理列表/详情预加载
|
||||
_USER_PRELOAD = ["dept", "roles", "positions"]
|
||||
|
||||
# 当前用户信息预加载:需完整嵌套关联
|
||||
_USER_CURRENT_PRELOAD = ["dept", "positions", "roles.menus", "roles.depts"]
|
||||
@@ -48,6 +48,8 @@ class UserService:
|
||||
result = UserOutSchema.model_validate(user)
|
||||
if user.dept:
|
||||
result.dept_name = user.dept.name
|
||||
result.role_ids = [r.id for r in user.roles]
|
||||
result.position_ids = [p.id for p in user.positions]
|
||||
return result
|
||||
|
||||
async def get_list(
|
||||
@@ -60,6 +62,8 @@ class UserService:
|
||||
for user, item in zip(user_list, result, strict=True):
|
||||
if user.dept:
|
||||
item.dept_name = user.dept.name
|
||||
item.role_ids = [r.id for r in user.roles]
|
||||
item.position_ids = [p.id for p in user.positions]
|
||||
return result
|
||||
|
||||
async def page(
|
||||
@@ -70,14 +74,23 @@ class UserService:
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> PageResultSchema[UserOutSchema]:
|
||||
offset = (page_no - 1) * page_size
|
||||
return await UserCRUD(self.auth, self.db).page(
|
||||
page_result = await UserCRUD(self.auth, self.db).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search_to_dict(search),
|
||||
out_schema=UserOutSchema,
|
||||
preload=_USER_PRELOAD,
|
||||
)
|
||||
items: list[UserOutSchema] = []
|
||||
for user in page_result.items:
|
||||
item = UserOutSchema.model_validate(user)
|
||||
if user.dept:
|
||||
item.dept_name = user.dept.name
|
||||
item.role_ids = [r.id for r in user.roles]
|
||||
item.position_ids = [p.id for p in user.positions]
|
||||
items.append(item)
|
||||
page_result.items = items
|
||||
return page_result # type: ignore[return-value]
|
||||
|
||||
async def create(self, data: UserCreateSchema) -> UserOutSchema:
|
||||
if data.is_superuser:
|
||||
@@ -263,8 +276,6 @@ class UserService:
|
||||
raise CustomException(msg="用户已停用")
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员密码不能重置")
|
||||
if data.mobile and user.mobile != data.mobile:
|
||||
raise CustomException(msg="手机号不匹配")
|
||||
|
||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
||||
@@ -280,12 +291,14 @@ class UserService:
|
||||
username=data.username,
|
||||
password=PwdUtil.hash_password(password=data.password),
|
||||
name=data.name or data.username,
|
||||
email=data.email,
|
||||
status=0,
|
||||
)
|
||||
new_user = await UserCRUD(self.auth, self.db).create_obj_crud(data=create_data)
|
||||
create_data_dict = create_data.model_dump(exclude_none=True, exclude={"role_ids", "position_ids"})
|
||||
new_user = await UserCRUD(self.auth, self.db).create(data=create_data_dict)
|
||||
if not new_user:
|
||||
raise CustomException(msg="注册失败")
|
||||
# 注册时 auth 无用户,created_id/updated_id 未设置,补充为自身ID
|
||||
await UserCRUD(self.auth, self.db).set([new_user.id], created_id=new_user.id, updated_id=new_user.id)
|
||||
logger.info(f"新用户注册成功: {data.username}")
|
||||
return await self.detail(id=new_user.id)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -26,7 +26,7 @@ async def get_version_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[VersionQueryParam, Depends()],
|
||||
search: Annotated[VersionQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
service = VersionService(auth, db)
|
||||
result = await service.page(page_no=page.page_no, page_size=page.page_size, search=search)
|
||||
|
||||
Reference in New Issue
Block a user