mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-08 02:36:07 +00:00
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:
@@ -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="查询节点列表成功")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 —— 子类自动继承"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+1
-2
@@ -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 = ""
|
||||||
|
|||||||
Vendored
+1
-2
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
-1
@@ -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 */
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -310,6 +310,7 @@
|
|||||||
"title": "通知",
|
"title": "通知",
|
||||||
"btnRead": "标为已读",
|
"btnRead": "标为已读",
|
||||||
"bar": ["通知", "消息", "待办"],
|
"bar": ["通知", "消息", "待办"],
|
||||||
|
"empty": "暂无通知",
|
||||||
"text": ["暂无"],
|
"text": ["暂无"],
|
||||||
"viewAll": "查看全部"
|
"viewAll": "查看全部"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -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";
|
||||||
Reference in New Issue
Block a user