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:
zhangtao
2026-07-13 01:38:51 +08:00
parent cf88ab8897
commit 8d0e76c694
23 changed files with 16 additions and 75 deletions
@@ -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:
+1 -1
View File
@@ -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"
+3 -14
View File
@@ -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):
+2 -5
View File
@@ -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 -1
View File
@@ -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