chore: 整理项目配置与代码优化

1.  统一前端环境变量命名,新增通用项目标题配置
2.  修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析
3.  优化表格列排序、搜索过滤功能与页面布局样式
4.  重构路由与状态管理代码,移除循环依赖
5.  更新依赖版本与脚本命令,修复构建警告
6.  调整多语言文案与快捷入口配置
7.  修复通知中心样式与接口返回格式
This commit is contained in:
zhangtao
2026-07-26 23:33:17 +08:00
parent df00863b6c
commit 698e198280
78 changed files with 1142 additions and 1134 deletions
@@ -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)