mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
refactor: 统一初始化数据文件存放路径并清理冗余脚本数据
1. 将所有初始化数据文件从app/scripts/data迁移至backend/sql/data目录 2. 更新path_conf.py中的SCRIPT_DIR配置指向新路径 3. 移除app/scripts/data下的冗余初始化数据文件 4. 清理权限缓存相关的进程级缓存逻辑 5. 调整部分代码导入顺序和冗余依赖 6. 简化用户菜单获取逻辑,移除菜单树缓存 7. 调整套餐菜单设置接口,移除redis参数传递
This commit is contained in:
@@ -30,10 +30,10 @@ from app.core.base_schema import AuthSchema, PageResultSchema
|
|||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
from app.core.logger import logger
|
from app.core.logger import logger
|
||||||
from app.utils.common_util import CamelCaseUtil, compute_menu_route_first_segment, search_to_dict
|
from app.utils.common_util import CamelCaseUtil, compute_menu_route_first_segment, search_to_dict
|
||||||
from .gen_util import GenUtils
|
|
||||||
from .jinja2_template_util import Jinja2TemplateUtil
|
|
||||||
|
|
||||||
from .crud import GenTableColumnCRUD, GenTableCRUD
|
from .crud import GenTableColumnCRUD, GenTableCRUD
|
||||||
|
from .gen_util import GenUtils
|
||||||
|
from .jinja2_template_util import Jinja2TemplateUtil
|
||||||
from .schema import (
|
from .schema import (
|
||||||
GenSyncColumnChange,
|
GenSyncColumnChange,
|
||||||
GenSyncPreviewSchema,
|
GenSyncPreviewSchema,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi_cache import FastAPICache
|
from fastapi_cache import FastAPICache
|
||||||
from fastapi_cache.decorator import cache
|
from fastapi_cache.decorator import cache
|
||||||
@@ -113,11 +113,10 @@ async def get_menus_controller(
|
|||||||
|
|
||||||
@PackageRouter.post("/menus/{package_id}/set", summary="设置套餐菜单", response_model=ResponseSchema)
|
@PackageRouter.post("/menus/{package_id}/set", summary="设置套餐菜单", response_model=ResponseSchema)
|
||||||
async def set_menus_controller(
|
async def set_menus_controller(
|
||||||
request: Request,
|
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:update"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
package_id: Annotated[int, Path(description="套餐ID", ge=1)],
|
package_id: Annotated[int, Path(description="套餐ID", ge=1)],
|
||||||
data: Annotated[PackageMenuSetSchema, Body(description="菜单列表")],
|
data: Annotated[PackageMenuSetSchema, Body(description="菜单列表")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await PackageService(auth, db).set_menus(package_id=package_id, data=data, redis=request.app.state.redis)
|
await PackageService(auth, db).set_menus(package_id=package_id, data=data)
|
||||||
return SuccessResponse(msg="设置成功")
|
return SuccessResponse(msg="设置成功")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from redis.asyncio.client import Redis
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -10,8 +9,6 @@ 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, require_superadmin
|
||||||
from app.core.logger import logger
|
from app.core.logger import logger
|
||||||
from app.core.dependencies import _package_menu_cache
|
|
||||||
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
|
||||||
|
|
||||||
from .crud import PackageCRUD
|
from .crud import PackageCRUD
|
||||||
@@ -142,41 +139,12 @@ class PackageService:
|
|||||||
result = await self.db.execute(stmt)
|
result = await self.db.execute(stmt)
|
||||||
return [row[0] for row in result.all()]
|
return [row[0] for row in result.all()]
|
||||||
|
|
||||||
async def set_menus(self, package_id: int, data: PackageMenuSetSchema, redis: Redis | None = None) -> None:
|
async def set_menus(self, package_id: int, data: PackageMenuSetSchema) -> None:
|
||||||
await self.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id))
|
await self.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id))
|
||||||
for menu_id in data.menu_ids:
|
for menu_id in data.menu_ids:
|
||||||
self.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id))
|
self.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id))
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
logger.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}")
|
logger.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}")
|
||||||
# 失效缓存:使所有 worker / 进程 立刻重新查询套餐菜单(最长 60s 旧值生效 -> 立即生效)
|
|
||||||
await self._invalidate_package_menu_cache(package_id=package_id, redis=redis)
|
|
||||||
|
|
||||||
async def _invalidate_package_menu_cache(self, package_id: int, redis: Redis | None = None) -> None:
|
|
||||||
"""套餐菜单变更后失效缓存(进程级 + 跨进程广播)。
|
|
||||||
|
|
||||||
进程级:清空本进程的 ``_package_menu_cache`` 中所有引用此套餐的租户(这里用全局清空)。
|
|
||||||
跨进程:通过 Redis pub/sub 通知所有 worker 进程清空各自的进程缓存。
|
|
||||||
|
|
||||||
缓存具体的 location 在 ``_get_cached_tenant_menu_ids`` (dependencies.py)。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
_package_menu_cache.clear()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if redis is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
|
|
||||||
await RedisCURD(redis).publish(
|
|
||||||
"cache:invalidate:package_menus",
|
|
||||||
json.dumps({"package_id": package_id}),
|
|
||||||
)
|
|
||||||
logger.info(f"已广播套餐[{package_id}]菜单缓存失效")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"广播缓存失效失败(仅影响跨 worker 延迟生效): {e!s}")
|
|
||||||
|
|
||||||
async def get_package_menu_ids(self, package_id: int) -> list[int]:
|
async def get_package_menu_ids(self, package_id: int) -> list[int]:
|
||||||
stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id)
|
stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id)
|
||||||
|
|||||||
@@ -1052,7 +1052,7 @@ class TenantService:
|
|||||||
|
|
||||||
amount = pkg.price
|
amount = pkg.price
|
||||||
|
|
||||||
from app.api.v1.module_platform.order.service import _generate_order_no, PaymentService
|
from app.api.v1.module_platform.order.service import PaymentService, _generate_order_no
|
||||||
|
|
||||||
order = await OrderCRUD(auth, db).create(
|
order = await OrderCRUD(auth, db).create(
|
||||||
OrderCreateInternalSchema(
|
OrderCreateInternalSchema(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from .schema import (
|
|||||||
ParamsUpdateSchema,
|
ParamsUpdateSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ParamsService:
|
class ParamsService:
|
||||||
"""参数管理服务
|
"""参数管理服务
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import json
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
from fastapi_cache import FastAPICache
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.api.v1.module_platform.menu.crud import MenuCRUD
|
from app.api.v1.module_platform.menu.crud import MenuCRUD
|
||||||
@@ -205,17 +203,7 @@ class UserService:
|
|||||||
user_dict.tenant_by = CommonSchema(id=user.tenant_by.id, name=user.tenant_by.name, status=user.tenant_by.status)
|
user_dict.tenant_by = CommonSchema(id=user.tenant_by.id, name=user.tenant_by.name, status=user.tenant_by.status)
|
||||||
user_dict.is_impersonate = self.auth.is_impersonate
|
user_dict.is_impersonate = self.auth.is_impersonate
|
||||||
|
|
||||||
# 菜单树缓存(菜单变更极低频,Redis 60s 过期)
|
|
||||||
_pc_only = {"client": "pc"}
|
_pc_only = {"client": "pc"}
|
||||||
_MENU_TREE_TTL = 60
|
|
||||||
_prefix = FastAPICache.get_prefix()
|
|
||||||
cache_key = f"{_prefix}:menu:menu_tree:{self.auth.user.tenant_id or 'platform'}" if self.auth.user.is_superuser else f"{_prefix}:menu:menu_tree:user:{self.auth.user.id}"
|
|
||||||
_backend = FastAPICache.get_backend()
|
|
||||||
cached = await _backend.get(cache_key)
|
|
||||||
if cached:
|
|
||||||
user_dict.menus = json.loads(cached.decode())
|
|
||||||
return user_dict
|
|
||||||
|
|
||||||
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"}
|
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(
|
||||||
@@ -242,9 +230,8 @@ class UserService:
|
|||||||
if menu_ids
|
if menu_ids
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
menu_tree = 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])]
|
||||||
await _backend.set(cache_key, json.dumps(menu_tree).encode(), expire=_MENU_TREE_TTL)
|
user_dict.menus = menu_tree
|
||||||
user_dict.menus = [MenuTreeOutSchema(**item) for item in menu_tree]
|
|
||||||
return user_dict
|
return user_dict
|
||||||
|
|
||||||
async def update_current_info(self, data: CurrentUserUpdateSchema) -> UserOutSchema:
|
async def update_current_info(self, data: CurrentUserUpdateSchema) -> UserOutSchema:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ INVOICE_DIR = STATIC_DIR / "invoice"
|
|||||||
ENV_DIR = BASE_DIR / "env"
|
ENV_DIR = BASE_DIR / "env"
|
||||||
|
|
||||||
# 初始化脚本
|
# 初始化脚本
|
||||||
SCRIPT_DIR: Path = BASE_DIR / "app" / "scripts" / "data"
|
SCRIPT_DIR: Path = BASE_DIR / "sql" / "data"
|
||||||
|
|
||||||
# 模版文件配置(统一管理代码生成模板 + HTML 模板)
|
# 模版文件配置(统一管理代码生成模板 + HTML 模板)
|
||||||
TEMPLATE_DIR: Path = BASE_DIR / "templates"
|
TEMPLATE_DIR: Path = BASE_DIR / "templates"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -17,12 +16,8 @@ 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.core.request_context import RequestContext
|
from app.core.request_context import RequestContext
|
||||||
|
|
||||||
from app.core.security import OAuth2Schema, decode_access_token
|
from app.core.security import OAuth2Schema, decode_access_token
|
||||||
|
|
||||||
# 套餐可用菜单 ID 进程级缓存(package/service.py 也引用,共享同一 dict)
|
|
||||||
_package_menu_cache: dict[int, tuple[float, list[int]]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def db_getter() -> AsyncGenerator[AsyncSession, None]:
|
async def db_getter() -> AsyncGenerator[AsyncSession, None]:
|
||||||
"""数据库会话 — 请求级生命周期管理。
|
"""数据库会话 — 请求级生命周期管理。
|
||||||
@@ -248,15 +243,9 @@ class AuthPermission:
|
|||||||
raise CustomException(msg="无权限操作", code=RET.FORBIDDEN.code, status_code=403)
|
raise CustomException(msg="无权限操作", code=RET.FORBIDDEN.code, status_code=403)
|
||||||
|
|
||||||
if user.tenant_id:
|
if user.tenant_id:
|
||||||
# 获取租户可用菜单 ID(带 60s 进程级缓存)
|
from app.api.v1.module_platform.package.service import PackageService
|
||||||
cached = _package_menu_cache.get(user.tenant_id)
|
result = await PackageService(auth, db).get_tenant_available_menu_ids(user.tenant_id)
|
||||||
if cached and time.time() - cached[0] < 60:
|
allowed_ids = set[int](result)
|
||||||
allowed_ids = set[int](cached[1])
|
|
||||||
else:
|
|
||||||
from app.api.v1.module_platform.package.service import PackageService
|
|
||||||
result = await PackageService(auth, db).get_tenant_available_menu_ids(user.tenant_id)
|
|
||||||
_package_menu_cache[user.tenant_id] = (time.time(), result)
|
|
||||||
allowed_ids = set[int](result)
|
|
||||||
user_permissions = {p for p, mid in auth.permissions_with_menu.items() if mid in allowed_ids}
|
user_permissions = {p for p, mid in auth.permissions_with_menu.items() if mid in allowed_ids}
|
||||||
|
|
||||||
if not any(perm in user_permissions for perm in self.permissions):
|
if not any(perm in user_permissions for perm in self.permissions):
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from redis.asyncio.client import Redis
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.middleware.gzip import GZipMiddleware
|
from starlette.middleware.gzip import GZipMiddleware
|
||||||
@@ -13,10 +15,6 @@ from starlette.requests import Request
|
|||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from starlette.types import ASGIApp
|
from starlette.types import ASGIApp
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from redis.asyncio.client import Redis
|
|
||||||
|
|
||||||
from app.common.enums import RedisInitKeyConfig
|
from app.common.enums import RedisInitKeyConfig
|
||||||
from app.common.response import ErrorResponse
|
from app.common.response import ErrorResponse
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
@@ -27,7 +25,6 @@ from app.core.request_context import RequestContext, clear_current_tenant, reset
|
|||||||
from app.core.security import decode_access_token
|
from app.core.security import decode_access_token
|
||||||
from app.utils.ip_local_util import get_client_ip
|
from app.utils.ip_local_util import get_client_ip
|
||||||
|
|
||||||
|
|
||||||
# ── 中间件配置(Redis 缓存读取) ──────────────────────────────
|
# ── 中间件配置(Redis 缓存读取) ──────────────────────────────
|
||||||
# 中间件高频读取的 sys_param 配置键集合
|
# 中间件高频读取的 sys_param 配置键集合
|
||||||
MIDDLEWARE_CONFIG_KEYS: tuple[str, ...] = (
|
MIDDLEWARE_CONFIG_KEYS: tuple[str, ...] = (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
Reference in New Issue
Block a user