chore: 完成项目多批次优化与功能迭代

本次提交包含多项改进:
1. 国际化补充:新增通知空状态文案中英文支持
2. 启动优化:重构banner输出、移除环境参数依赖,简化启动日志
3. 配置清理:移除冗余的REDIS_ENABLE/SQL_DB_ENABLE配置项,同步更新env示例与测试配置
4. 接口分页改造:将全局分页参数从Query改为Depends自动解析,统一排序逻辑
5. 菜单权限优化:调整菜单可见范围默认值、修复平台菜单路由路径错误
6. 异常处理增强:完善数据库异常捕获逻辑,新增连接失败专项处理
7. API令牌重构:重命名API令牌路由与权限标识,拆分前端API文件
8. 定时任务整合:将系统任务注册移入调度器初始化逻辑
9. 数据库连接优化:新增连接检查,简化建表/删表逻辑
10. 控制台美化:重构启动控制台面板,优化信息展示格式
11. 租户/角色服务优化:移除不必要的超级管理员装饰器,统一分页排序逻辑
12. 用户菜单适配:修复超级用户菜单过滤逻辑,移除scope强制校验
This commit is contained in:
zhangtao
2026-07-15 01:21:53 +08:00
parent 8d0e76c694
commit 3f7d5aa4b9
51 changed files with 257 additions and 300 deletions
@@ -37,7 +37,7 @@ async def get_session_detail_controller(
@ChatRouter.get("/list", summary="查询会话列表", response_model=ResponseSchema[dict]) @ChatRouter.get("/list", summary="查询会话列表", response_model=ResponseSchema[dict])
async def get_session_list_controller( async def get_session_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ChatSessionQueryParam, Query(description="查询参数")], search: Annotated[ChatSessionQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
service = ChatService(auth) service = ChatService(auth)
@@ -27,9 +27,6 @@ _HEALTH_STREAM_INTERVAL = 30 # 秒
async def _check_database() -> DependencyStatus: async def _check_database() -> DependencyStatus:
"""检查数据库连接""" """检查数据库连接"""
try: try:
if not settings.SQL_DB_ENABLE:
return DependencyStatus(status=0)
start = time.perf_counter() start = time.perf_counter()
async with async_db_session() as session: async with async_db_session() as session:
await session.execute(text("SELECT 1")) await session.execute(text("SELECT 1"))
@@ -43,9 +40,6 @@ async def _check_database() -> DependencyStatus:
async def _check_redis(request: Request) -> DependencyStatus: async def _check_redis(request: Request) -> DependencyStatus:
"""检查 Redis 连接""" """检查 Redis 连接"""
try: try:
if not settings.REDIS_ENABLE:
return DependencyStatus(status=0)
redis = getattr(request.app.state, "redis", None) redis = getattr(request.app.state, "redis", None)
if not redis: if not redis:
return DependencyStatus(status=0) return DependencyStatus(status=0)
@@ -20,7 +20,7 @@ GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["
@GenRouter.get("/list", summary="查询代码生成业务表列表", response_model=ResponseSchema[list[GenTableOutSchema]]) @GenRouter.get("/list", summary="查询代码生成业务表列表", response_model=ResponseSchema[list[GenTableOutSchema]])
async def gen_table_list_controller( async def gen_table_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[GenTableQueryParam, Query(description="查询参数")], search: Annotated[GenTableQueryParam, Query(description="查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -39,7 +39,7 @@ async def gen_table_list_controller(
@GenRouter.get("/db/list", summary="查询数据库表列表", response_model=ResponseSchema[PageResultSchema[GenDBTableSchema]]) @GenRouter.get("/db/list", summary="查询数据库表列表", response_model=ResponseSchema[PageResultSchema[GenDBTableSchema]])
async def get_gen_db_table_list_controller( async def get_gen_db_table_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:dblist:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:dblist:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[GenTableQueryParam, Query(description="查询参数")], search: Annotated[GenTableQueryParam, Query(description="查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -24,7 +24,7 @@ _STATS_NS = "online_stats"
@OnlineRouter.get("/list", summary="获取在线用户列表", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Security(AuthPermission(["module_monitor:online:query"]))]) @OnlineRouter.get("/list", summary="获取在线用户列表", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Security(AuthPermission(["module_monitor:online:query"]))])
async def get_online_list_controller( async def get_online_list_controller(
redis: Annotated[Redis, Depends(redis_getter)], redis: Annotated[Redis, Depends(redis_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[OnlineQueryParam, Query(description="在线用户查询参数")], search: Annotated[OnlineQueryParam, Query(description="在线用户查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await OnlineService.get_online_list(redis=redis, search=search) result_dict_list = await OnlineService.get_online_list(redis=redis, search=search)
@@ -1,6 +1,6 @@
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Body, File, Form, Query, Request, Security, UploadFile, status from fastapi import APIRouter, Body, Depends, File, Form, Query, Request, Security, UploadFile, status
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from app.api.v1.module_common.file.service import FileService from app.api.v1.module_common.file.service import FileService
@@ -20,7 +20,7 @@ ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", ta
@ResourceRouter.get("/list", summary="获取目录列表", response_model=ResponseSchema[list[ResourceItemSchema]], dependencies=[Security(AuthPermission(["module_monitor:resource:query"]))]) @ResourceRouter.get("/list", summary="获取目录列表", response_model=ResponseSchema[list[ResourceItemSchema]], dependencies=[Security(AuthPermission(["module_monitor:resource:query"]))])
async def get_directory_list_controller( async def get_directory_list_controller(
request: Request, request: Request,
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ResourceSearchQueryParam, Query(description="资源查询参数")], search: Annotated[ResourceSearchQueryParam, Query(description="资源查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url)) result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
@@ -28,7 +28,7 @@ async def invoice_apply_controller(
@InvoiceRouter.get("/mine/list", summary="我的发票列表", response_model=ResponseSchema[PageResultSchema[InvoiceOutSchema]]) @InvoiceRouter.get("/mine/list", summary="我的发票列表", response_model=ResponseSchema[PageResultSchema[InvoiceOutSchema]])
async def invoice_list_my_controller( async def invoice_list_my_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:invoice:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:invoice:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[InvoiceQueryParam, Query(description="发票查询参数")], search: Annotated[InvoiceQueryParam, Query(description="发票查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -41,8 +41,8 @@ class MenuCreateSchema(BaseModel):
active_path: str | None = Field(default=None, max_length=200, description="激活菜单路径") active_path: str | None = Field(default=None, max_length=200, description="激活菜单路径")
show_badge: bool = Field(default=False, description="是否显示红点角标") show_badge: bool = Field(default=False, description="是否显示红点角标")
show_text_badge: str | None = Field(default=None, max_length=20, description="文字角标内容") show_text_badge: str | None = Field(default=None, max_length=20, description="文字角标内容")
scope: Literal["platform", "tenant"] = Field( scope: Literal["platform", "tenant"] | None = Field(
default="tenant", default=None,
description="菜单可见范围(platform:仅平台 tenant:租户可用)", description="菜单可见范围(platform:仅平台 tenant:租户可用)",
) )
@@ -3,7 +3,7 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, BatchSetAvailable from app.core.base_schema import AuthSchema, BatchSetAvailable
from app.core.exceptions import CustomException, require_superadmin from app.core.exceptions import CustomException
from app.utils.common_util import ( from app.utils.common_util import (
get_child_id_map, get_child_id_map,
get_child_recursion, get_child_recursion,
@@ -80,7 +80,6 @@ class MenuService:
menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list] menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list]
return traversal_to_tree(menu_dict_list) return traversal_to_tree(menu_dict_list)
@require_superadmin
async def create(self, data: MenuCreateSchema) -> MenuOutSchema: async def create(self, data: MenuCreateSchema) -> MenuOutSchema:
search: dict[str, Any] = {} search: dict[str, Any] = {}
if data.title is not None: if data.title is not None:
@@ -97,7 +96,6 @@ class MenuService:
new_menu = await MenuCRUD(self.auth, self.db).create(data=data) new_menu = await MenuCRUD(self.auth, self.db).create(data=data)
return MenuOutSchema.model_validate(new_menu) return MenuOutSchema.model_validate(new_menu)
@require_superadmin
async def update(self, id: int, data: MenuUpdateSchema) -> MenuOutSchema: async def update(self, id: int, data: MenuUpdateSchema) -> MenuOutSchema:
_ = await MenuCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该菜单不存在") _ = await MenuCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该菜单不存在")
await self._validate_parent_child_type(data.parent_id, data.type) await self._validate_parent_child_type(data.parent_id, data.type)
@@ -127,7 +125,6 @@ class MenuService:
menu_out.parent_name = parent.name menu_out.parent_name = parent.name
return menu_out return menu_out
@require_superadmin
async def delete(self, ids: list[int]) -> None: async def delete(self, ids: list[int]) -> None:
if not ids: if not ids:
raise CustomException(msg="删除失败,删除对象不能为空") raise CustomException(msg="删除失败,删除对象不能为空")
@@ -143,7 +140,6 @@ class MenuService:
delete_ids = list(delete_ids_set) delete_ids = list(delete_ids_set)
await MenuCRUD(self.auth, self.db).delete(ids=delete_ids) await MenuCRUD(self.auth, self.db).delete(ids=delete_ids)
@require_superadmin
async def set_available(self, data: BatchSetAvailable) -> None: async def set_available(self, data: BatchSetAvailable) -> None:
menu_list = await MenuCRUD(self.auth, self.db).get_list() menu_list = await MenuCRUD(self.auth, self.db).get_list()
total_ids = [] total_ids = []
@@ -55,7 +55,7 @@ async def order_detail_controller(
async def order_list_controller( async def order_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[OrderQueryParam, Query(description="查询参数")], search: Annotated[OrderQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
items, total = await OrderService.get_list( items, total = await OrderService.get_list(
@@ -157,7 +157,7 @@ async def order_pay_mock_callback_controller(
async def order_refund_list_controller( async def order_refund_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
status: Annotated[int | None, Query(description="退款状态筛选")] = None, status: Annotated[int | None, Query(description="退款状态筛选")] = None,
) -> JSONResponse: ) -> JSONResponse:
offset = (page.page_no - 1) * page.page_size offset = (page.page_no - 1) * page.page_size
@@ -43,7 +43,7 @@ async def get_obj_detail_controller(
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[PackageQueryParam, Query(description="查询参数")], search: Annotated[PackageQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await PackageService(auth, db).page( result_dict = await PackageService(auth, db).page(
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_platform.menu.model import MenuModel from app.api.v1.module_platform.menu.model import MenuModel
from app.api.v1.module_platform.tenant.model import TenantModel from app.api.v1.module_platform.tenant.model import TenantModel
from app.core.base_schema import AuthSchema, PageResultSchema from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException, require_superadmin from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.utils.common_util import search_to_dict from app.utils.common_util import search_to_dict
@@ -29,17 +29,14 @@ class PackageService:
self.auth = auth self.auth = auth
self.db = db self.db = db
@require_superadmin
async def get_options(self) -> list[dict[str, Any]]: async def get_options(self) -> list[dict[str, Any]]:
"""获取套餐下拉选项,委托给 PackageCRUD""" """获取套餐下拉选项,委托给 PackageCRUD"""
return await PackageCRUD(self.auth, self.db).get_options() return await PackageCRUD(self.auth, self.db).get_options()
@require_superadmin
async def detail(self, id: int) -> PackageOutSchema: async def detail(self, id: int) -> PackageOutSchema:
obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id) obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id)
return PackageOutSchema.model_validate(obj) return PackageOutSchema.model_validate(obj)
@require_superadmin
async def page( async def page(
self, self,
page_no: int, page_no: int,
@@ -55,7 +52,6 @@ class PackageService:
out_schema=PackageOutSchema, out_schema=PackageOutSchema,
) )
@require_superadmin
async def create(self, data: PackageCreateSchema) -> PackageOutSchema: async def create(self, data: PackageCreateSchema) -> PackageOutSchema:
if await PackageCRUD(self.auth, self.db).get(name=data.name): if await PackageCRUD(self.auth, self.db).get(name=data.name):
raise CustomException(msg="创建失败,套餐名称已存在") raise CustomException(msg="创建失败,套餐名称已存在")
@@ -67,7 +63,6 @@ class PackageService:
logger.info(f"创建套餐成功: {result.name}") logger.info(f"创建套餐成功: {result.name}")
return result return result
@require_superadmin
async def update(self, id: int, data: PackageUpdateSchema) -> PackageOutSchema: async def update(self, id: int, data: PackageUpdateSchema) -> PackageOutSchema:
obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id) obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id)
@@ -86,7 +81,6 @@ class PackageService:
updated = await PackageCRUD(self.auth, self.db).update(id=id, data=data) updated = await PackageCRUD(self.auth, self.db).update(id=id, data=data)
return PackageOutSchema.model_validate(updated) return PackageOutSchema.model_validate(updated)
@require_superadmin
async def delete(self, ids: list[int]) -> None: async def delete(self, ids: list[int]) -> None:
if not ids: if not ids:
raise CustomException(msg="删除失败,删除对象不能为空") raise CustomException(msg="删除失败,删除对象不能为空")
@@ -53,17 +53,14 @@ async def get_obj_detail_controller(
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[TenantQueryParam, Query(description="查询参数")], search: Annotated[TenantQueryParam, Depends()],
) -> JSONResponse: ) -> JSONResponse:
order_by = [{"id": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict = await TenantService(auth, db).page( result_dict = await TenantService(auth, db).page(
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
search=search, search=search,
order_by=order_by, order_by=page.order_by,
) )
return SuccessResponse(data=result_dict, msg="查询租户列表成功") return SuccessResponse(data=result_dict, msg="查询租户列表成功")
@@ -249,7 +246,7 @@ async def order_create_controller(
async def order_list_controller( async def order_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:order:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:order:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
) -> JSONResponse: ) -> JSONResponse:
result = await TenantService.get_self_order_list( result = await TenantService.get_self_order_list(
auth=auth, auth=auth,
@@ -26,7 +26,7 @@ class TenantCreateSchema(BaseModel):
domain: str | None = Field(default=None, max_length=255, description="域名") domain: str | None = Field(default=None, max_length=255, description="域名")
logo_url: str | None = Field(default=None, max_length=500, description="Logo URL") logo_url: str | None = Field(default=None, max_length=500, description="Logo URL")
sort: int = Field(default=0, ge=0, description="排序") sort: int = Field(default=0, ge=0, description="排序")
package_id: int = Field(..., gt=0, description="关联套餐ID(必选,决定租户可用的菜单与配额)") package_id: int | None = Field(default=None, gt=0, description="关联套餐ID")
version: str | None = Field(default=None, max_length=20, description="版本号") version: str | None = Field(default=None, max_length=20, description="版本号")
favicon: str | None = Field(default=None, max_length=500, description="favicon地址") favicon: str | None = Field(default=None, max_length=500, description="favicon地址")
login_bg: str | None = Field(default=None, max_length=500, description="登录背景地址") login_bg: str | None = Field(default=None, max_length=500, description="登录背景地址")
@@ -28,7 +28,7 @@ from app.api.v1.module_system.user.schema import UserCreateSchema
from app.common.enums import OrderTypeEnum, RedisInitKeyConfig from app.common.enums import OrderTypeEnum, RedisInitKeyConfig
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.database import async_db_session from app.core.database import async_db_session
from app.core.exceptions import CustomException, require_superadmin from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.core.redis_crud import RedisCURD from app.core.redis_crud import RedisCURD
from app.utils.common_util import search_to_dict from app.utils.common_util import search_to_dict
@@ -119,7 +119,6 @@ class TenantService:
out_schema=TenantOutSchema, out_schema=TenantOutSchema,
) )
@require_superadmin
async def create(self, data: TenantCreateSchema) -> TenantCreateResult: async def create(self, data: TenantCreateSchema) -> TenantCreateResult:
# ① 预校验:name / code 唯一 # ① 预校验:name / code 唯一
if await TenantCRUD(self.auth, self.db).get(name=data.name): if await TenantCRUD(self.auth, self.db).get(name=data.name):
@@ -215,7 +214,6 @@ class TenantService:
), ),
) )
@require_superadmin
async def update(self, id: int, data: TenantUpdateSchema) -> TenantOutSchema: async def update(self, id: int, data: TenantUpdateSchema) -> TenantOutSchema:
"""更新租户 """更新租户
@@ -313,7 +311,6 @@ class TenantService:
await self.db.flush() await self.db.flush()
logger.info(f"租户[{tenant_id}]套餐变更:已清理角色中不再可用的菜单关联, available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}") logger.info(f"租户[{tenant_id}]套餐变更:已清理角色中不再可用的菜单关联, available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}")
@require_superadmin
async def delete(self, ids: list[int]) -> None: async def delete(self, ids: list[int]) -> None:
"""批量删除租户(含级联资源检查:用户/部门/角色/岗位) """批量删除租户(含级联资源检查:用户/部门/角色/岗位)
@@ -703,16 +700,20 @@ class TenantService:
返回: 返回:
- None - None
""" """
async with async_db_session() as session, session.begin(): try:
stmt = select(TenantModel) async with async_db_session() as session, session.begin():
result = await session.execute(stmt) stmt = select(TenantModel)
tenants = result.scalars().all() result = await session.execute(stmt)
tenants = result.scalars().all()
for tenant in tenants: for tenant in tenants:
config = {field: getattr(tenant, field, None) for field in TenantService.CONFIG_FIELDS} config = {field: getattr(tenant, field, None) for field in TenantService.CONFIG_FIELDS}
await TenantService._sync_configs_to_redis(redis, tenant.id, config) await TenantService._sync_configs_to_redis(redis, tenant.id, config)
logger.info(f"✅ 租户[{tenant.name}](id={tenant.id}) 配置已缓存到 Redis") logger.info(f"✅ 租户[{tenant.name}](id={tenant.id}) 配置已缓存到 Redis")
except Exception as e:
logger.error(f"❌️ 初始化租户配置到 Redis 失败: {e}")
raise CustomException(msg="初始化租户配置到 Redis 失败") from e
async def renew(self, tenant_id: int, end_time: str) -> TenantOutSchema: async def renew(self, tenant_id: int, end_time: str) -> TenantOutSchema:
"""租户续期:延长 end_time 并恢复为 active 状态 """租户续期:延长 end_time 并恢复为 active 状态
@@ -23,12 +23,12 @@ from .schema import (
) )
from .service import ApiTokenService from .service import ApiTokenService
ApiTokenRouter = APIRouter(route_class=OperationLogRoute, prefix="/token", tags=["平台-API令牌"]) ApiTokenRouter = APIRouter(route_class=OperationLogRoute, prefix="/api_token", tags=["系统-API令牌"])
@ApiTokenRouter.post("/create", summary="创建 API Token", response_model=ResponseSchema[ApiTokenCreatedSchema]) @ApiTokenRouter.post("/create", summary="创建 API Token", response_model=ResponseSchema[ApiTokenCreatedSchema])
async def create_token_controller( async def create_token_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[ApiTokenCreateSchema, Body(description="创建参数")], data: Annotated[ApiTokenCreateSchema, Body(description="创建参数")],
) -> JSONResponse: ) -> JSONResponse:
@@ -39,9 +39,9 @@ async def create_token_controller(
@ApiTokenRouter.get("/list", summary="查询 token 列表", response_model=ResponseSchema[PageResultSchema[ApiTokenOutSchema]]) @ApiTokenRouter.get("/list", summary="查询 token 列表", response_model=ResponseSchema[PageResultSchema[ApiTokenOutSchema]])
async def get_token_list_controller( async def get_token_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ApiTokenQueryParam, Query(description="查询参数")], search: Annotated[ApiTokenQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result = await ApiTokenService(auth, db).page( result = await ApiTokenService(auth, db).page(
@@ -55,7 +55,7 @@ async def get_token_list_controller(
@ApiTokenRouter.get("/detail/{id}", summary="token 详情", response_model=ResponseSchema[ApiTokenOutSchema]) @ApiTokenRouter.get("/detail/{id}", summary="token 详情", response_model=ResponseSchema[ApiTokenOutSchema])
async def get_token_detail_controller( async def get_token_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:detail"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:detail"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="token ID", ge=1)], id: Annotated[int, Path(description="token ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
@@ -65,7 +65,7 @@ async def get_token_detail_controller(
@ApiTokenRouter.post("/{id}/reset", summary="重置 token(重新生成 secret)", response_model=ResponseSchema[ApiTokenCreatedSchema]) @ApiTokenRouter.post("/{id}/reset", summary="重置 token(重新生成 secret)", response_model=ResponseSchema[ApiTokenCreatedSchema])
async def reset_token_controller( async def reset_token_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:reset"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:reset"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="token ID", ge=1)], id: Annotated[int, Path(description="token ID", ge=1)],
data: Annotated[ApiTokenResetSchema, Body(description="可选项")], data: Annotated[ApiTokenResetSchema, Body(description="可选项")],
@@ -77,7 +77,7 @@ async def reset_token_controller(
@ApiTokenRouter.patch("/{id}/status", summary="启用/禁用 token", response_model=ResponseSchema[None]) @ApiTokenRouter.patch("/{id}/status", summary="启用/禁用 token", response_model=ResponseSchema[None])
async def set_token_status_controller( async def set_token_status_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:patch"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:patch"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="token ID", ge=1)], id: Annotated[int, Path(description="token ID", ge=1)],
status: Annotated[int, Body(description="状态", ge=0, le=2)], status: Annotated[int, Body(description="状态", ge=0, le=2)],
@@ -88,7 +88,7 @@ async def set_token_status_controller(
@ApiTokenRouter.delete("/{id}", summary="删除 token", response_model=ResponseSchema[None]) @ApiTokenRouter.delete("/{id}", summary="删除 token", response_model=ResponseSchema[None])
async def delete_token_controller( async def delete_token_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="token ID", ge=1)], id: Annotated[int, Path(description="token ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
@@ -98,7 +98,7 @@ async def delete_token_controller(
@ApiTokenRouter.post("/{id}/reveal", summary="查看 token 明文(需二次验证)", response_model=ResponseSchema[ApiTokenRevealOutSchema]) @ApiTokenRouter.post("/{id}/reveal", summary="查看 token 明文(需二次验证)", response_model=ResponseSchema[ApiTokenRevealOutSchema])
async def reveal_token_controller( async def reveal_token_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:token:reveal"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:api_token:reveal"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="token ID", ge=1)], id: Annotated[int, Path(description="token ID", ge=1)],
data: Annotated[ApiTokenRevealSchema, Body(description="需输入当前用户密码")], data: Annotated[ApiTokenRevealSchema, Body(description="需输入当前用户密码")],
@@ -43,7 +43,7 @@ async def get_type_detail_controller(
@DictRouter.get("/type/list", summary="查询字典类型", response_model=ResponseSchema[PageResultSchema[DictTypeOutSchema]]) @DictRouter.get("/type/list", summary="查询字典类型", response_model=ResponseSchema[PageResultSchema[DictTypeOutSchema]])
async def get_type_list_controller( async def get_type_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页查询参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictTypeQueryParam, Query(description="字典类型查询参数")], search: Annotated[DictTypeQueryParam, Query(description="字典类型查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -145,7 +145,7 @@ async def get_data_detail_controller(
@DictRouter.get("/data/list", summary="查询字典数据", response_model=ResponseSchema[PageResultSchema[DictDataOutSchema]]) @DictRouter.get("/data/list", summary="查询字典数据", response_model=ResponseSchema[PageResultSchema[DictDataOutSchema]])
async def get_data_list_controller( async def get_data_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")], search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -208,7 +208,7 @@ async def batch_set_available_dict_data_controller(
@DictRouter.post("/data/export", summary="导出字典数据") @DictRouter.post("/data/export", summary="导出字典数据")
async def export_data_list_controller( async def export_data_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:export"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:export"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")], search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> StreamingResponse: ) -> StreamingResponse:
@@ -366,7 +366,7 @@ class DictDataService:
logger.error(f"❌ 初始化字典数据失败 [{dict_type}]: {e}") logger.error(f"❌ 初始化字典数据失败 [{dict_type}]: {e}")
except Exception as e: except Exception as e:
logger.error(f"字典初始化过程发生错误: {e}") logger.error(f"❌️ 字典初始化过程发生错误: {e}")
raise CustomException(msg="字典数据初始化失败") from e raise CustomException(msg="字典数据初始化失败") from e
@staticmethod @staticmethod
@@ -36,7 +36,7 @@ async def get_log_detail_controller(
async def get_log_list_controller( async def get_log_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[LoginLogQueryParam, Query(description="登录日志查询参数")], search: Annotated[LoginLogQueryParam, Query(description="登录日志查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await LoginLogService(auth, db).page( result_dict = await LoginLogService(auth, db).page(
@@ -74,7 +74,7 @@ async def get_operation_log_detail_controller(
async def get_operation_log_list_controller( async def get_operation_log_list_controller(
auth: Annotated[AuthSchema, Depends(get_current_user)], auth: Annotated[AuthSchema, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[OperationLogQueryParam, Query(description="操作日志查询参数")], search: Annotated[OperationLogQueryParam, Query(description="操作日志查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await OperationLogService(auth, db).page( result_dict = await OperationLogService(auth, db).page(
@@ -33,7 +33,7 @@ async def get_notice_detail_controller(
async def get_notice_list_controller( async def get_notice_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[NoticeQueryParam, Query(description="公告查询参数")], search: Annotated[NoticeQueryParam, Query(description="公告查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await NoticeService(auth, db).page( result_dict = await NoticeService(auth, db).page(
@@ -31,7 +31,7 @@ async def get_param_detail_controller(
async def get_param_list_controller( async def get_param_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ParamsQueryParam, Query(description="参数查询参数")], search: Annotated[ParamsQueryParam, Query(description="参数查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await ParamsService(auth, db).page( result_dict = await ParamsService(auth, db).page(
@@ -309,10 +309,14 @@ class ParamsService:
@staticmethod @staticmethod
async def init_cache(redis: Redis) -> None: async def init_cache(redis: Redis) -> None:
"""启动时初始化系统参数到 Redis。""" """启动时初始化系统参数到 Redis。"""
config_obj = await ParamsService._load_all_configs_from_db() try:
if not config_obj: config_obj = await ParamsService._load_all_configs_from_db()
raise CustomException(msg="该数据不存在") if not config_obj:
await ParamsService._sync_configs_to_redis(redis, config_obj) raise CustomException(msg="该数据不存在")
await ParamsService._sync_configs_to_redis(redis, config_obj)
except Exception as e:
logger.error(f"❌️ 初始化系统参数到 Redis 失败: {e}")
raise CustomException(msg="初始化系统参数到 Redis 失败") from e
@staticmethod @staticmethod
async def get_init_cache(redis: Redis, tenant_id: int = 1) -> list[dict]: async def get_init_cache(redis: Redis, tenant_id: int = 1) -> list[dict]:
@@ -25,17 +25,14 @@ _POS_NS = "position"
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[PositionQueryParam, Query(description="岗位查询参数")], search: Annotated[PositionQueryParam, Query(description="岗位查询参数")],
) -> JSONResponse: ) -> JSONResponse:
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict = await PositionService(auth, db).page( result_dict = await PositionService(auth, db).page(
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
search=search, search=search,
order_by=order_by, order_by=page.order_by,
) )
return SuccessResponse(data=result_dict, msg="查询岗位列表成功") return SuccessResponse(data=result_dict, msg="查询岗位列表成功")
@@ -25,17 +25,14 @@ _ROLE_NS = "role"
async def get_role_list_controller( async def get_role_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[RoleQueryParam, Query(description="角色查询参数")], search: Annotated[RoleQueryParam, Query(description="角色查询参数")],
) -> JSONResponse: ) -> JSONResponse:
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict = await RoleService(auth, db).page( result_dict = await RoleService(auth, db).page(
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
search=search, search=search,
order_by=order_by, order_by=page.order_by,
) )
return SuccessResponse(data=result_dict, msg="查询角色成功") return SuccessResponse(data=result_dict, msg="查询角色成功")
@@ -19,7 +19,7 @@ TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=[
async def ticket_list_controller( async def ticket_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[TicketQueryParam, Query(description="工单查询参数")], search: Annotated[TicketQueryParam, Query(description="工单查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result = await TicketService(auth, db).page( result = await TicketService(auth, db).page(
@@ -87,7 +87,7 @@ async def ticket_comment_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
ticket_id: Annotated[int, Path(description="工单ID")], ticket_id: Annotated[int, Path(description="工单ID")],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
) -> JSONResponse: ) -> JSONResponse:
result = await TicketCommentService(auth, db).page(ticket_id=ticket_id, page_no=page.page_no, page_size=page.page_size) result = await TicketCommentService(auth, db).page(ticket_id=ticket_id, page_no=page.page_no, page_size=page.page_size)
return SuccessResponse(data=result, msg="查询成功") return SuccessResponse(data=result, msg="查询成功")
@@ -97,7 +97,7 @@ async def forget_password_controller(
async def get_user_list_controller( async def get_user_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[UserQueryParam, Query(description="用户查询参数")], search: Annotated[UserQueryParam, Query(description="用户查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await UserService(auth, db).page( result_dict = await UserService(auth, db).page(
@@ -178,7 +178,7 @@ async def export_user_import_template_controller() -> StreamingResponse:
async def export_user_list_controller( async def export_user_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:export"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:export"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[UserQueryParam, Query(description="用户查询参数")], search: Annotated[UserQueryParam, Query(description="用户查询参数")],
) -> StreamingResponse: ) -> StreamingResponse:
user_list = await UserService(auth, db).get_list(search=search, order_by=page.order_by) user_list = await UserService(auth, db).get_list(search=search, order_by=page.order_by)
@@ -205,9 +205,8 @@ class UserService:
_pc_only = {"client": "pc"} _pc_only = {"client": "pc"}
if self.auth.user.is_superuser: if self.auth.user.is_superuser:
scope_filter = {"scope": "tenant"} if self.auth.user.tenant_id else {"scope": "platform"}
menu_all = await MenuCRUD(self.auth, self.db).tree_list( menu_all = await MenuCRUD(self.auth, self.db).tree_list(
search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only, **scope_filter}, search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only},
order_by=[{"order": "asc"}], order_by=[{"order": "asc"}],
) )
menus_raw = [MenuOutSchema.model_validate(menu) for menu in menu_all] menus_raw = [MenuOutSchema.model_validate(menu) for menu in menu_all]
@@ -230,6 +229,9 @@ class UserService:
if menu_ids if menu_ids
else [] else []
) )
for menu in menus_raw:
menu.scope = None
menu_tree = [MenuTreeOutSchema(**item) for item in traversal_to_tree([menu.model_dump(mode="json") for menu in menus_raw])] menu_tree = [MenuTreeOutSchema(**item) for item in traversal_to_tree([menu.model_dump(mode="json") for menu in menus_raw])]
user_dict.menus = menu_tree user_dict.menus = menu_tree
return user_dict return user_dict
@@ -25,7 +25,7 @@ VersionRouter = APIRouter(route_class=OperationLogRoute, prefix="/versions", tag
async def get_version_list_controller( async def get_version_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[VersionQueryParam, Query(description="查询参数")], search: Annotated[VersionQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
service = VersionService(auth, db) service = VersionService(auth, db)
@@ -1,6 +1,7 @@
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.core.base_schema import BaseQueryParam from app.core.base_schema import BaseQueryParam
from app.core.validator import DateTimeStr
class VersionCreateSchema(BaseModel): class VersionCreateSchema(BaseModel):
@@ -26,8 +27,8 @@ class VersionOutSchema(VersionCreateSchema):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: int = Field(description="主键ID") id: int = Field(description="主键ID")
created_time: str | None = Field(default=None, description="创建时间") created_time: DateTimeStr | None = Field(default=None, description="创建时间")
updated_time: str | None = Field(default=None, description="更新时间") updated_time: DateTimeStr | None = Field(default=None, description="更新时间")
class VersionStatusSchema(BaseModel): class VersionStatusSchema(BaseModel):
@@ -99,18 +99,15 @@ async def remove_job_controller(
@JobRouter.get("/log/list", summary="查询执行日志列表", response_model=ResponseSchema[PageResultSchema[JobOutSchema]]) @JobRouter.get("/log/list", summary="查询执行日志列表", response_model=ResponseSchema[PageResultSchema[JobOutSchema]])
async def get_job_log_list_controller( async def get_job_log_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[JobQueryParam, Query(description="查询参数")], search: Annotated[JobQueryParam, Query(description="查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
order_by = [{"created_time": "desc"}]
if page.order_by:
order_by = page.order_by
result_dict = await JobService(auth, db).get_job_log_page( result_dict = await JobService(auth, db).get_job_log_page(
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
search=search, search=search,
order_by=order_by, order_by=page.order_by,
) )
return SuccessResponse(data=result_dict, msg="查询执行日志列表成功") return SuccessResponse(data=result_dict, msg="查询执行日志列表成功")
@@ -39,7 +39,7 @@ async def get_obj_detail_controller(
@NodeRouter.get("/list", summary="查询节点", response_model=ResponseSchema[PageResultSchema[NodeOutSchema]]) @NodeRouter.get("/list", summary="查询节点", response_model=ResponseSchema[PageResultSchema[NodeOutSchema]])
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[NodeQueryParam, Query(description="查询参数")], search: Annotated[NodeQueryParam, Query(description="查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
@@ -29,7 +29,7 @@ async def get_workflow_detail_controller(
async def get_workflow_list_controller( async def get_workflow_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[WorkflowQueryParam, Query(description="查询参数")], search: Annotated[WorkflowQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
result_dict = await WorkflowService(auth, db).get_workflow_page( result_dict = await WorkflowService(auth, db).get_workflow_page(
@@ -40,18 +40,15 @@ async def get_workflow_node_type_detail_controller(
async def get_workflow_node_type_list_controller( async def get_workflow_node_type_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[WorkflowNodeTypeQueryParam, Query(description="查询参数")], search: Annotated[WorkflowNodeTypeQueryParam, Query(description="查询参数")],
) -> JSONResponse: ) -> JSONResponse:
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
if page.order_by:
order_by = page.order_by
service = WorkflowNodeTypeService(auth, db) service = WorkflowNodeTypeService(auth, db)
result_dict = await service.get_page( result_dict = await service.get_page(
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
search=search, search=search,
order_by=order_by, order_by=page.order_by,
) )
return SuccessResponse(data=result_dict, msg="查询节点列表成功") return SuccessResponse(data=result_dict, msg="查询节点列表成功")
-2
View File
@@ -89,7 +89,6 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
# ******************** 数据库配置 ******************* # # ******************** 数据库配置 ******************* #
# ================================================= # # ================================================= #
SQL_DB_ENABLE: bool = True # 是否启用数据库
DATABASE_ECHO: bool | Literal["debug"] = False # 是否显示SQL日志 DATABASE_ECHO: bool | Literal["debug"] = False # 是否显示SQL日志
ECHO_POOL: bool | Literal["debug"] = False # 是否显示连接池日志 ECHO_POOL: bool | Literal["debug"] = False # 是否显示连接池日志
POOL_SIZE: int = 10 # 连接池大小 POOL_SIZE: int = 10 # 连接池大小
@@ -115,7 +114,6 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
# ******************** Redis配置 ******************* # # ******************** Redis配置 ******************* #
# ================================================= # # ================================================= #
REDIS_ENABLE: bool = True # 是否启用Redis
REDIS_HOST: str = "localhost" REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379 REDIS_PORT: int = 6379
REDIS_DB_NAME: int = 1 REDIS_DB_NAME: int = 1
+37 -6
View File
@@ -98,12 +98,43 @@ class SchedulerUtil:
@classmethod @classmethod
async def init_scheduler(cls, redis: Redis | None = None) -> None: async def init_scheduler(cls, redis: Redis | None = None) -> None:
"""应用启动时初始化定时任务调度器。""" """应用启动时初始化定时任务调度器(含系统级周期任务注册)。
if redis:
cls.redis_instance = redis 返回:
scheduler.start() - None
scheduler.add_listener(_dispatch_job_event, EVENT_ALL) """
scheduler.resume() try:
if redis:
cls.redis_instance = redis
scheduler.start()
scheduler.add_listener(_dispatch_job_event, EVENT_ALL)
scheduler.resume()
# 注册系统级定时任务
from app.api.v1.module_platform.tenant.service import TenantService
from app.api.v1.module_platform.order.service import OrderService
from app.api.v1.module_system.log.service import OperationLogService
cls.register_system_job(
"system_tenant_expiry_check", TenantService.check_tenant_expiry,
trigger=IntervalTrigger(hours=1), name="租户到期检查",
)
cls.register_system_job(
"system_clean_expired", TenantService.clean_expired_tenants,
trigger=CronTrigger(day=1, hour=2, minute=0), name="过期租户归档清理",
)
cls.register_system_job(
"system_cancel_expired_orders", OrderService.cancel_expired_orders,
trigger=IntervalTrigger(minutes=30), name="超时订单取消",
)
cls.register_system_job(
"system_cleanup_operation_log", OperationLogService.cleanup_operation_log,
trigger=CronTrigger(day_of_week="sun", hour=3, minute=0), name="操作日志清理",
)
logger.info("✅ 4 个系统周期任务已注册(租户到期检查/归档清理/订单取消/日志清理)")
except Exception as e:
logger.error(f"❌ 定时任务调度器初始化失败: {e}")
raise
@classmethod @classmethod
def register_system_job(cls, job_id: str, func: Callable, trigger: Any, name: str) -> None: def register_system_job(cls, job_id: str, func: Callable, trigger: Any, name: str) -> None:
+16 -12
View File
@@ -1,5 +1,6 @@
import json import json
from datetime import datetime from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@@ -147,29 +148,32 @@ class PageResultSchema[T](BaseModel):
class PaginationQueryParam(BaseModel): class PaginationQueryParam(BaseModel):
"""分页 —— 自动继承 page_no / page_size / order_by,子类无需重复声明""" """分页 —— order_by 以 JSON 字符串传递,避免 Depends() 模式下 list 字段被当 body 验证。"""
page_no: int = Field(default=1, description="当前页码", ge=1) page_no: int = Field(default=1, description="当前页码", ge=1)
page_size: int = Field(default=10, description="每页数量", ge=1, le=100) page_size: int = Field(default=10, description="每页数量", ge=1, le=100)
order_by: list = Field( order_by: Any = Field(
default_factory=lambda: [{"id": "desc"}], default=None,
description="排序字段,格式:[{'field1': 'asc'}, {'field2': 'desc'}]", description="排序字段 JSON 字符串, 格式:[{'field1': 'asc'}, {'field2': 'desc'}]",
) )
@field_validator("order_by", mode="before") @field_validator("order_by")
@classmethod @classmethod
def parse_order_by(cls, v: object) -> list: def validate_order_by(cls, v: Any) -> Any:
"""校验 order_by:None→默认升序,str→json.loads 转 list,list→直接返回,其他→抛异常。"""
if v is None: if v is None:
return [{"id": "desc"}] return [{"id": "asc"}]
if isinstance(v, str): if isinstance(v, str):
try: try:
return json.loads(v) result = json.loads(v)
except (ValueError, json.JSONDecodeError): if not isinstance(result, list):
return [{"id": "desc"}] raise ValueError("order_by 必须是 JSON 数组字符串,例如 [{\"id\":\"asc\"}]")
return result
except json.JSONDecodeError:
raise ValueError("order_by 字符串无法解析为 JSON,请传入有效的 JSON 数组字符串,例如 [{\"id\":\"asc\"}]")
if isinstance(v, list): if isinstance(v, list):
return v return v
return [{"id": "desc"}] raise ValueError(f"order_by 类型无效: {type(v).__name__},预期为 JSON 数组字符串或列表")
class BaseQueryParam(BaseModel): class BaseQueryParam(BaseModel):
"""created_time + updated_time —— 子类自动继承""" """created_time + updated_time —— 子类自动继承"""
+27 -29
View File
@@ -12,13 +12,10 @@ from sqlalchemy.orm import sessionmaker
from app.config.setting import settings from app.config.setting import settings
from app.core.base_model import MappedBase from app.core.base_model import MappedBase
from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
def create_engine_and_session( def create_engine_and_session(db_url: str = settings.DB_URI) -> tuple[Engine, sessionmaker]:
db_url: str = settings.DB_URI,
) -> tuple[Engine, sessionmaker]:
"""创建同步数据库引擎和会话工厂。 """创建同步数据库引擎和会话工厂。
参数: 参数:
@@ -28,11 +25,6 @@ def create_engine_and_session(
- tuple[Engine, sessionmaker]: 同步数据库引擎和会话工厂。 - tuple[Engine, sessionmaker]: 同步数据库引擎和会话工厂。
""" """
try: try:
if not settings.SQL_DB_ENABLE:
raise CustomException(
msg="请先开启数据库连接",
data="请启用 app/config/setting.py: SQL_DB_ENABLE",
)
# 同步数据库引擎 # 同步数据库引擎
engine: Engine = create_engine( engine: Engine = create_engine(
url=db_url, url=db_url,
@@ -49,9 +41,7 @@ def create_engine_and_session(
return engine, SessionLocal return engine, SessionLocal
def create_async_engine_and_session( def create_async_engine_and_session(db_url: str = settings.ASYNC_DB_URI) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
db_url: str = settings.ASYNC_DB_URI,
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
"""获取异步数据库会话连接。 """获取异步数据库会话连接。
参数: 参数:
@@ -61,11 +51,6 @@ def create_async_engine_and_session(
- tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: 异步数据库引擎和会话工厂。 - tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: 异步数据库引擎和会话工厂。
""" """
try: try:
if not settings.SQL_DB_ENABLE:
raise CustomException(
msg="请先开启数据库连接",
data="请启用 app/config/setting.py: SQL_DB_ENABLE",
)
# 异步数据库引擎 # 异步数据库引擎
if settings.DATABASE_TYPE == "sqlite": if settings.DATABASE_TYPE == "sqlite":
async_engine = create_async_engine( async_engine = create_async_engine(
@@ -107,6 +92,17 @@ def create_async_engine_and_session(
engine, db_session = create_engine_and_session() engine, db_session = create_engine_and_session()
async_engine, async_db_session = create_async_engine_and_session() async_engine, async_db_session = create_async_engine_and_session()
async def check_db() -> None:
"""检查数据库连接是否正常。"""
try:
with engine.connect():
pass
logger.info("✅ 数据库连接正常")
except Exception as e:
logger.error(f"❌ 数据库连接失败: {e}")
raise e
async def create_tables() -> None: async def create_tables() -> None:
"""创建数据库表(根据 ORM metadata)。 """创建数据库表(根据 ORM metadata)。
@@ -114,8 +110,12 @@ async def create_tables() -> None:
返回: 返回:
- None - None
""" """
async with async_engine.begin() as coon: try:
await coon.run_sync(MappedBase.metadata.create_all) async with async_engine.begin() as coon:
await coon.run_sync(MappedBase.metadata.create_all)
except Exception as e:
logger.error(f"❌ 数据库表结构初始化失败: {e}")
raise e
async def drop_tables() -> None: async def drop_tables() -> None:
@@ -124,8 +124,12 @@ async def drop_tables() -> None:
返回: 返回:
- None - None
""" """
async with async_engine.begin() as conn: try:
await conn.run_sync(MappedBase.metadata.drop_all) async with async_engine.begin() as conn:
await conn.run_sync(MappedBase.metadata.drop_all)
except Exception as e:
logger.error(f"❌ 数据库表结构删除失败: {e}")
raise e
async def redis_connect(app: FastAPI, status: bool) -> Redis | None: async def redis_connect(app: FastAPI, status: bool) -> Redis | None:
@@ -138,12 +142,6 @@ async def redis_connect(app: FastAPI, status: bool) -> Redis | None:
返回: 返回:
- Redis | None: Redis连接实例,如果连接失败则返回None。 - Redis | None: Redis连接实例,如果连接失败则返回None。
""" """
if not settings.REDIS_ENABLE:
raise CustomException(
msg="请先开启Redis连接",
data="请启用 app/core/config.py: REDIS_ENABLE",
)
if status: if status:
try: try:
rd = await Redis.from_url( rd = await Redis.from_url(
@@ -159,10 +157,10 @@ async def redis_connect(app: FastAPI, status: bool) -> Redis | None:
return rd return rd
except exceptions.AuthenticationError as e: except exceptions.AuthenticationError as e:
logger.error(f"❌ 数据库 Redis 认证失败: {e}") logger.error(f"❌ 数据库 Redis 认证失败: {e}")
raise return None
except exceptions.TimeoutError as e: except exceptions.TimeoutError as e:
logger.error(f"❌ 数据库 Redis 连接超时: {e}") logger.error(f"❌ 数据库 Redis 连接超时: {e}")
raise return None
except exceptions.RedisError as e: except exceptions.RedisError as e:
logger.error(f"❌ 数据库 Redis 连接错误: {e}") logger.error(f"❌ 数据库 Redis 连接错误: {e}")
raise raise
+3 -10
View File
@@ -122,17 +122,12 @@ def handle_exception(app: FastAPI) -> None:
@app.exception_handler(SQLAlchemyError) @app.exception_handler(SQLAlchemyError)
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) -> JSONResponse: async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) -> JSONResponse:
exc_type = type(exc).__name__ exc_type = type(exc).__name__
logger.error(
"[数据库异常] %s %s | type=%s | detail=%s",
request.method,
request.url.path,
exc_type,
exc,
)
if isinstance(exc, IntegrityError): if isinstance(exc, IntegrityError):
detail = str(exc.orig) if exc.orig else str(exc) detail = str(exc.orig) if exc.orig else str(exc)
expose_detail = detail if settings.ENVIRONMENT != EnvironmentEnum.PROD else None expose_detail = detail if settings.ENVIRONMENT != EnvironmentEnum.PROD else None
if "connect" in detail or "connection" in detail:
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_403_SERVICE_UNAVAILABLE, data=expose_detail)
if "Duplicate entry" in detail: if "Duplicate entry" in detail:
return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=expose_detail) return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
if "foreign key constraint" in detail: if "foreign key constraint" in detail:
@@ -141,9 +136,7 @@ def handle_exception(app: FastAPI) -> None:
return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=expose_detail) return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=expose_detail) return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
lower = str(exc).lower() logger.error("[数据库异常] %s %s | type=%s | detail=%s", request.method, request.url.path, exc_type, exc)
if "connect" in lower or "connection" in lower:
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_503_SERVICE_UNAVAILABLE, data=exc_type)
return ErrorResponse(msg=f"数据库操作失败: {exc_type}", status_code=status.HTTP_400_BAD_REQUEST, data=str(exc)) return ErrorResponse(msg=f"数据库操作失败: {exc_type}", status_code=status.HTTP_400_BAD_REQUEST, data=str(exc))
@app.exception_handler(ValueError) @app.exception_handler(ValueError)
+25 -67
View File
@@ -20,73 +20,39 @@ from .utils.console import console_end, console_start
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
from app.api.v1.module_platform.order.service import OrderService
from app.api.v1.module_platform.tenant.service import TenantService from app.api.v1.module_platform.tenant.service import TenantService
from app.api.v1.module_system.dict.service import DictDataService from app.api.v1.module_system.dict.service import DictDataService
from app.api.v1.module_system.log.service import OperationLogService
from app.api.v1.module_system.params.service import ParamsService from app.api.v1.module_system.params.service import ParamsService
from app.core.ap_scheduler import SchedulerUtil from app.core.ap_scheduler import SchedulerUtil
from app.core.database import async_engine, redis_connect from app.core.database import async_engine, redis_connect
from app.scripts.initialize import InitializeData from app.scripts.initialize import InitializeData
async def _init_system_caches() -> None: await InitializeData().init_db()
"""Redis 缓存初始化(参数/数据字典/租户配置)。""" logger.info("✅ {}数据库初始化完成", settings.DATABASE_TYPE)
await ParamsService.init_cache(redis=app.state.redis) await redis_connect(app, status=True)
logger.info("✅ Redis系统参数初始化完成") logger.info("✅ Redis 连接初始化完成")
await DictDataService.init_cache(redis=app.state.redis) await ParamsService.init_cache(redis=app.state.redis)
logger.info("✅ Redis数据字典初始化完成") logger.info("✅ Redis系统参数初始化完成")
await TenantService.init_cache(redis=app.state.redis) await DictDataService.init_cache(redis=app.state.redis)
logger.info("✅ Redis租户配置初始化完成") logger.info("✅ Redis数据字典初始化完成")
await TenantService.init_cache(redis=app.state.redis)
logger.info("✅ Redis租户配置初始化完成")
await SchedulerUtil.init_scheduler(redis=app.state.redis)
logger.info("✅ 定时任务调度器初始化完成")
FastAPICache.init(RedisBackend(app.state.redis), prefix="fastapi-admin-cache")
logger.info("✅ fastapi-admin-cache 初始化完成")
app.state.limiter = limiter
logger.info("✅ 请求限流器初始化完成")
def _register_system_jobs() -> None: console_start(
"""注册系统级定时任务(拆分自 SchedulerUtil._register_system_jobs)。""" host=settings.SERVER_HOST,
from apscheduler.triggers.cron import CronTrigger port=settings.SERVER_PORT,
from apscheduler.triggers.interval import IntervalTrigger reload=settings.DEBUG,
database_ready=True,
SchedulerUtil.register_system_job( redis_ready=True,
"system_tenant_expiry_check", TenantService.check_tenant_expiry, scheduler_ready=SchedulerUtil.is_running(),
trigger=IntervalTrigger(hours=1), name="租户到期检查", limiter_ready=True,
) )
SchedulerUtil.register_system_job(
"system_clean_expired", TenantService.clean_expired_tenants,
trigger=CronTrigger(day=1, hour=2, minute=0), name="过期租户归档清理",
)
SchedulerUtil.register_system_job(
"system_cancel_expired_orders", OrderService.cancel_expired_orders,
trigger=IntervalTrigger(minutes=30), name="超时订单取消",
)
SchedulerUtil.register_system_job(
"system_cleanup_operation_log", OperationLogService.cleanup_operation_log,
trigger=CronTrigger(day_of_week="sun", hour=3, minute=0), name="操作日志清理",
)
logger.info("✅ 4 个系统周期任务已注册(租户到期检查/归档清理/订单取消/日志清理)")
try:
await InitializeData().init_db()
logger.info("✅ {}数据库初始化完成", settings.DATABASE_TYPE)
await redis_connect(app, status=True)
logger.info("✅ Redis 连接初始化完成")
await _init_system_caches()
await SchedulerUtil.init_scheduler(redis=app.state.redis)
logger.info("✅ 定时任务调度器初始化完成")
_register_system_jobs()
FastAPICache.init(RedisBackend(app.state.redis), prefix="fastapi-admin-cache")
logger.info("✅ fastapi-admin-cache 初始化完成")
app.state.limiter = limiter
logger.info("✅ 请求限流器初始化完成")
console_start(
host=settings.SERVER_HOST,
port=settings.SERVER_PORT,
reload=settings.DEBUG,
database_ready=True,
redis_ready=True,
scheduler_ready=SchedulerUtil.is_running(),
limiter_ready=True,
)
except Exception as e:
logger.error("❌ 应用初始化失败: {}", e)
raise SystemExit(1)
yield yield
@@ -152,14 +118,10 @@ def register_docs(app: FastAPI) -> None:
swagger_ui_redirect_url = str(app.swagger_ui_oauth2_redirect_url) swagger_ui_redirect_url = str(app.swagger_ui_oauth2_redirect_url)
root_openapi_url = str(app.root_path) + str(app.openapi_url) root_openapi_url = str(app.root_path) + str(app.openapi_url)
# 为文档路由标记 __slower_exempt__ 以跳过 slowapi 中间件限流
@app.get(swagger_ui_redirect_url, include_in_schema=False) @app.get(swagger_ui_redirect_url, include_in_schema=False)
async def swagger_ui_redirect(): async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html() return get_swagger_ui_oauth2_redirect_html()
swagger_ui_redirect.__slower_exempt__ = True
@app.get(settings.DOCS_URL, include_in_schema=False) @app.get(settings.DOCS_URL, include_in_schema=False)
async def custom_swagger_ui_html() -> HTMLResponse: async def custom_swagger_ui_html() -> HTMLResponse:
return get_swagger_ui_html( return get_swagger_ui_html(
@@ -171,8 +133,6 @@ def register_docs(app: FastAPI) -> None:
swagger_favicon_url=settings.FAVICON_URL, swagger_favicon_url=settings.FAVICON_URL,
) )
custom_swagger_ui_html.__slower_exempt__ = True
@app.get(settings.REDOC_URL, include_in_schema=False) @app.get(settings.REDOC_URL, include_in_schema=False)
async def custom_redoc_html(): async def custom_redoc_html():
return get_redoc_html( return get_redoc_html(
@@ -182,8 +142,6 @@ def register_docs(app: FastAPI) -> None:
redoc_favicon_url=settings.FAVICON_URL, redoc_favicon_url=settings.FAVICON_URL,
) )
custom_redoc_html.__slower_exempt__ = True
def register_frontend(app: FastAPI) -> None: def register_frontend(app: FastAPI) -> None:
if path_conf.FRONTEND_DIST_DIR.exists(): if path_conf.FRONTEND_DIST_DIR.exists():
@@ -31,7 +31,7 @@ async def get_obj_detail_controller(
@DemoRouter.get("/list", summary="分页查询示例", response_model=ResponseSchema[PageResultSchema[DemoOutSchema]]) @DemoRouter.get("/list", summary="分页查询示例", response_model=ResponseSchema[PageResultSchema[DemoOutSchema]])
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))],
page: Annotated[PaginationQueryParam, Query(description="分页参数")], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DemoQueryParam, Query(description="查询参数")], search: Annotated[DemoQueryParam, Query(description="查询参数")],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse: ) -> JSONResponse:
+4 -7
View File
@@ -1,4 +1,3 @@
import asyncio
import json import json
import re import re
from datetime import datetime, time from datetime import datetime, time
@@ -17,7 +16,7 @@ from app.api.v1.module_system.role.model import RoleModel
from app.api.v1.module_system.user.model import UserModel, UserRolesModel from app.api.v1.module_system.user.model import UserModel, UserRolesModel
from app.api.v1.module_system.versions.model import VersionModel from app.api.v1.module_system.versions.model import VersionModel
from app.config.path_conf import SCRIPT_DIR from app.config.path_conf import SCRIPT_DIR
from app.core.database import async_db_session, create_tables from app.core.database import async_db_session, check_db, create_tables, drop_tables
from app.core.logger import logger from app.core.logger import logger
@@ -54,11 +53,9 @@ class InitializeData:
async def init_db(self) -> None: async def init_db(self) -> None:
"""建表并导入种子数据""" """建表并导入种子数据"""
try: await check_db()
await create_tables() # await drop_tables()
except asyncio.exceptions.TimeoutError: await create_tables()
logger.error("❌️ 数据库表结构初始化超时")
raise
async with async_db_session() as session, session.begin(): async with async_db_session() as session, session.begin():
await self.__init_data(session) await self.__init_data(session)
+3 -7
View File
@@ -1,17 +1,13 @@
from app.config.path_conf import BANNER_FILE from app.config.path_conf import BANNER_FILE
def worship(env: str) -> str: def worship() -> str:
"""读取启动 Banner(优先 `banner.txt`,并附带当前环境名)。 """读取启动 Banner(优先 `banner.txt`)。
获取地址:https://patorjk.com/software/taag/#p=testall&f=Fire+Font-k&t=fastapiadmin%0A&x=none&v=4&h=4&w=80&we=false 获取地址:https://patorjk.com/software/taag/#p=testall&f=Fire+Font-k&t=fastapiadmin%0A&x=none&v=4&h=4&w=80&we=false
参数:
- env (str): 当前运行环境标识。
返回: 返回:
- str: banner 文本。 - str: banner 文本。
""" """
if BANNER_FILE.exists(): if BANNER_FILE.exists():
banner = BANNER_FILE.read_text(encoding="utf-8") return BANNER_FILE.read_text(encoding="utf-8")
return f"🚀 当前运行环境: {env}\n{banner}"
return "" return ""
+52 -44
View File
@@ -1,8 +1,10 @@
from datetime import datetime from datetime import datetime
from rich import get_console from rich import box, get_console
from rich.console import Group from rich.console import Group
from rich.panel import Panel from rich.panel import Panel
from rich.rule import Rule
from rich.table import Table
from rich.text import Text from rich.text import Text
from app.config.setting import settings from app.config.setting import settings
@@ -34,63 +36,69 @@ def console_start(
返回: 返回:
- None - None
""" """
env_label = settings.ENVIRONMENT.value if hasattr(settings.ENVIRONMENT, 'value') else settings.ENVIRONMENT
url = f"http://{host}:{port}" url = f"http://{host}:{port}"
base_url = f"{url}{settings.ROOT_PATH}" base_url = f"{url}{settings.ROOT_PATH}"
docs_url = base_url + settings.DOCS_URL docs_url = base_url + settings.DOCS_URL
redoc_url = base_url + settings.REDOC_URL
frontend_url = base_url + settings.WEB_URL frontend_url = base_url + settings.WEB_URL
# 核心服务信息 def _status_text(ready: bool | None) -> str:
service_info = Text() return "✅ 就绪" if ready else "❌ 失败"
service_info.append(f"服务名称 {settings.TITLE} • 优雅 • 简洁 • 高效", style="bold magenta")
service_info.append(f"\n当前版本 v{settings.VERSION}", style="bold green") # 标题
service_info.append(f"\n服务地址 {url}", style="bold blue") title_text = Text(f"\n{settings.TITLE} v{settings.VERSION}", style="bold green")
service_info.append(
f"\n运行环境 {settings.ENVIRONMENT.value if hasattr(settings.ENVIRONMENT, 'value') else settings.ENVIRONMENT}", # 服务信息
style="bold red", info_grid = Table.grid(padding=(0, 1))
) info_grid.add_column(justify="right")
service_info.append( info_grid.add_column()
f"\n重载配置: {'✅ 启动' if reload else '❌ 关闭'}", info_grid.add_row("服务地址", url, style="bold blue")
style="bold italic", info_grid.add_row("运行环境", env_label, style="bold yellow")
) info_grid.add_row("重载配置", "✅ 启动" if reload else "❌ 关闭")
service_info.append(
f"\n调试模式: {'✅ 启动' if settings.DEBUG else '❌ 关闭'}", # 组件状态 — 一行四个,│ 分隔分组
style="bold italic", sep = Text(" │ ", style="dim")
) status_grid = Table.grid(padding=(0, 1))
service_info.append( status_grid.add_column(justify="right")
f"\n{settings.DATABASE_TYPE}: {'✅ 启动' if database_ready else '❌ 关闭'}", status_grid.add_column()
style="bold italic", status_grid.add_column(justify="right")
) status_grid.add_column()
service_info.append( status_grid.add_column(justify="right")
f"\nRedis: {'✅ 启动' if redis_ready else '❌ 关闭'}", status_grid.add_column()
style="bold italic", status_grid.add_column(justify="right")
) status_grid.add_column()
service_info.append( status_grid.add_row(
f"\n调度器: {'✅ 启动' if scheduler_ready else '❌ 关闭'}", "MySQL", _status_text(database_ready),
style="bold italic", sep,
) "Redis", _status_text(redis_ready),
service_info.append( sep,
f"\n限流器: {'✅ 启动' if limiter_ready else '❌ 关闭'}", "调度器", _status_text(scheduler_ready),
style="bold italic", sep,
"限流器", _status_text(limiter_ready),
) )
docs_info = Text() # 文档链接
docs_info.append("📖 文档", style="bold magenta") docs_grid = Table.grid(padding=(0, 1))
docs_info.append(f"\n🔗 Swagger: {docs_url}", style="blue link") docs_grid.add_column(justify="right")
docs_info.append(f"\n🔗 ReDoc: {redoc_url}", style="blue link") docs_grid.add_column()
docs_info.append(f"\n🔗 前端: {frontend_url}", style="blue link") docs_grid.add_row("Swagger", Text(docs_url, style=f"blue link {docs_url}"))
docs_grid.add_row("前端", Text(frontend_url, style=f"blue link {frontend_url}"))
final_content = Group( final_content = Group(
service_info, title_text,
"\n" + "─" * 40, info_grid,
docs_info, Rule(style="dim"),
status_grid,
Rule(style="dim"),
docs_grid,
) )
result = Panel( result = Panel(
renderable=final_content, renderable=final_content,
title="[bold purple]🚀 服务启动完成[/]", title=f"[bold purple]🚀 FastapiAdmin v{settings.VERSION}[/]",
border_style="green", border_style="green",
padding=(1, 2), box=box.HEAVY,
padding=(0, 2),
) )
console.print(result) console.print(result)
+1 -2
View File
@@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi
DEMO_ENABLE = False DEMO_ENABLE = False
# 数据库配置 # 数据库配置
DATABASE_TYPE = "mysql" # mysql、postgres、sqlite、dm(sqlite、dm不支持代码生成) DATABASE_TYPE = "mysql" # mysql、postgres、sqlite
# 数据库配置 # 数据库配置
DATABASE_HOST = "localhost" DATABASE_HOST = "localhost"
@@ -32,7 +32,6 @@ DATABASE_PASSWORD = "your_database_password"
DATABASE_NAME = "fastapiadmin" DATABASE_NAME = "fastapiadmin"
# Redis配置 # Redis配置
REDIS_ENABLE = True
REDIS_HOST = "localhost" REDIS_HOST = "localhost"
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_USER = "" REDIS_USER = ""
+1 -2
View File
@@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi
DEMO_ENABLE = True DEMO_ENABLE = True
# 数据库配置 # 数据库配置
DATABASE_TYPE = "mysql" # mysql、postgres、sqlite、dm(sqlite、dm不支持代码生成) DATABASE_TYPE = "mysql" # mysql、postgres、sqlite
# 数据库配置 # 数据库配置
DATABASE_HOST = "your_database_host" DATABASE_HOST = "your_database_host"
@@ -32,7 +32,6 @@ DATABASE_PASSWORD = "your_database_password"
DATABASE_NAME = "fastapiadmin" DATABASE_NAME = "fastapiadmin"
# Redis配置 # Redis配置
REDIS_ENABLE = True
REDIS_HOST = "your_redis_host" REDIS_HOST = "your_redis_host"
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_USER = '' REDIS_USER = ''
+1 -2
View File
@@ -61,10 +61,9 @@ def run(
os.environ["ENVIRONMENT"] = env.value os.environ["ENVIRONMENT"] = env.value
typer.secho( typer.secho(
message="FastapiAdmin 服务启动", message=f"{worship()}",
fg=typer.colors.GREEN, fg=typer.colors.GREEN,
) )
logger.info(worship(env.value))
# 启动uvicorn服务 # 启动uvicorn服务
uvicorn.run( uvicorn.run(
+3 -3
View File
@@ -925,9 +925,9 @@
"icon": "ri:briefcase-line", "icon": "ri:briefcase-line",
"order": 6, "order": 6,
"permission": "module_platform:workspace:query", "permission": "module_platform:workspace:query",
"route_name": "PlatformWorkspace", "route_name": "TenantWorkspace",
"route_path": "workspace", "route_path": "workspace",
"component_path": "module_platform/tenant/workspace", "component_path": "module_platform/tenant/tenant_workspace",
"status": 0, "status": 0,
"keep_alive": true, "keep_alive": true,
"hidden": false, "hidden": false,
@@ -3217,7 +3217,7 @@
"permission": "module_system:version:query", "permission": "module_system:version:query",
"route_name": "ModuleVersion", "route_name": "ModuleVersion",
"route_path": "version/list", "route_path": "version/list",
"component_path": "module_platform/version/index", "component_path": "module_system/version/index",
"status": 0, "status": 0,
"keep_alive": true, "keep_alive": true,
"hidden": false, "hidden": false,
-2
View File
@@ -27,7 +27,6 @@ _TEST_DB_PATH = tempfile.NamedTemporaryFile(suffix=".db", delete=False).name
os.environ["DATABASE_TYPE"] = "sqlite" os.environ["DATABASE_TYPE"] = "sqlite"
os.environ["DATABASE_NAME"] = _TEST_DB_PATH os.environ["DATABASE_NAME"] = _TEST_DB_PATH
os.environ["REDIS_ENABLE"] = "true"
os.environ["POOL_SIZE"] = "1" os.environ["POOL_SIZE"] = "1"
os.environ["MAX_OVERFLOW"] = "1" os.environ["MAX_OVERFLOW"] = "1"
@@ -35,7 +34,6 @@ from app.config.setting import settings
settings.DATABASE_TYPE = "sqlite" settings.DATABASE_TYPE = "sqlite"
settings.DATABASE_NAME = _TEST_DB_PATH settings.DATABASE_NAME = _TEST_DB_PATH
settings.REDIS_ENABLE = True
settings.POOL_SIZE = 1 settings.POOL_SIZE = 1
settings.MAX_OVERFLOW = 1 settings.MAX_OVERFLOW = 1
settings.CAPTCHA_ENABLE = False # 测试环境关闭验证码 settings.CAPTCHA_ENABLE = False # 测试环境关闭验证码
@@ -1,6 +1,6 @@
import { request } from "@utils"; import { request } from "@utils";
const API_PATH = "/system/token"; const API_PATH = "/system/api_token";
const ApiTokenAPI = { const ApiTokenAPI = {
/** 创建 API Token */ /** 创建 API Token */
+1
View File
@@ -310,6 +310,7 @@
"title": "Notice", "title": "Notice",
"btnRead": "Mark as read", "btnRead": "Mark as read",
"bar": ["Notice", "Message", "Todo"], "bar": ["Notice", "Message", "Todo"],
"empty": "No notifications",
"text": ["No"], "text": ["No"],
"viewAll": "View all" "viewAll": "View all"
}, },
+1
View File
@@ -310,6 +310,7 @@
"title": "通知", "title": "通知",
"btnRead": "标为已读", "btnRead": "标为已读",
"bar": ["通知", "消息", "待办"], "bar": ["通知", "消息", "待办"],
"empty": "暂无通知",
"text": ["暂无"], "text": ["暂无"],
"viewAll": "查看全部" "viewAll": "查看全部"
}, },
@@ -202,7 +202,7 @@ import ApiTokenAPI, {
type ApiTokenTable, type ApiTokenTable,
type ApiTokenCreateForm, type ApiTokenCreateForm,
type ApiTokenRevealSchema, type ApiTokenRevealSchema,
} from "@/api/module_system/api-token"; } from "@/api/module_system/api_token";
import { Plus } from "@element-plus/icons-vue"; import { Plus } from "@element-plus/icons-vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems"; import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";