feat(phase1): core/ 基础设施迁移,旧路径改垫片

- core/: database/redis_client/jwt/password/response/exceptions/security/logger/deps/middleware/app
- create_app 迁移到 core/app.py,中间件注册顺序与异常处理器原样保留
- extensions/ utils/ apps/ 旧文件全部改为 re-export 垫片,import 路径保持兼容
- 新增 core 单元测试(jwt/password/response/request 共 12 个)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-07 10:02:01 +08:00
co-authored by Claude
parent fe1d191e55
commit db4ee85db5
22 changed files with 1476 additions and 364 deletions
+285
View File
@@ -0,0 +1,285 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
# | 开源协议: Apache-2.0
# +======================================================================
# 由 apps/middleware/ 合并迁移而来。
"""核心中间件与依赖:认证、鉴权、演示拦截、数据库会话、操作日志、CORS。"""
import asyncio
import json
import logging
import time
from functools import wraps
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
import config
from config.app import CORS_ALLOWED_ORIGINS
from core import response as R
from core.database import SessionLocal, _db_ctx
from core.exceptions import AuthorizationException
from core.jwt import get_access_token, is_token_blacklisted, parse_payload
from core.security import get_user_id, get_username
logger = logging.getLogger(__name__)
# ============================================================
# 中间件注册
# ============================================================
def register_middleware(app: FastAPI):
"""
请求响应拦截 hook(当前为空实现,可自定义业务逻辑)。
"""
@app.middleware("http")
async def logger_request(request: Request, call_next):
response = await call_next(request)
return response
def register_cros(app: FastAPI):
"""注册 CORS 与 ProxyHeaders 中间件。"""
origins_str = CORS_ALLOWED_ORIGINS.strip()
if origins_str:
origins = [o.strip() for o in origins_str.split(',') if o.strip()]
allow_credentials = True
allow_headers = [
"Authorization", "Content-Type", "Accept",
"X-Requested-With", "X-Process-Time",
]
else:
origins = ["*"]
allow_credentials = False
allow_headers = ["*"]
app.add_middleware(ProxyHeadersMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=allow_credentials,
allow_methods=['OPTIONS', 'GET', 'POST', 'DELETE', 'PUT'],
allow_headers=allow_headers,
max_age=3600
)
# ============================================================
# 登录认证(router 级依赖,挂载在 v1 上)
# ============================================================
async def login_required(request: Request):
"""登录验证依赖:提取并校验 Token,将用户信息写入 request.state。"""
success, access_token, msg = get_access_token(request)
if not success:
raise AuthorizationException(code=401, msg=msg)
if await is_token_blacklisted(access_token):
raise AuthorizationException(code=401, msg="登录已注销")
result = parse_payload(access_token)
if result['code'] != 0:
raise AuthorizationException(code=401, msg=result['msg'])
data = result['data']
request.state.user_id = int(data.get('userId', 0))
request.state.username = data.get('username', '')
request.state.realname = data.get('realname', '')
# ============================================================
# 节点权限鉴权(装饰器)
# ============================================================
def _extract_request(*args, **kwargs):
"""从函数参数中提取 Request 对象"""
req = kwargs.get('request')
if req is not None:
return req
for arg in args:
if isinstance(arg, Request):
return arg
return None
def permission_required(permission: str):
"""节点权限鉴权装饰器,userId==1(管理员)直接放行。"""
def decorator(func):
def _check(request):
"""同步权限校验(直查DB,无Redis依赖),返回 R.failed 或 None(通过)"""
userId = get_user_id(request)
if userId == 1:
return None
# 懒加载业务 service,避免 core → apps 顶层依赖(迁移完成后指向 modules)
from apps.services import menu
permission_list = menu.get_permissions_list_sync(userId)
if permission not in permission_list:
return R.failed("权限不足")
return None
if asyncio.iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
request = _extract_request(*args, **kwargs)
denied = await asyncio.to_thread(_check, request)
if denied:
return denied
return await func(*args, **kwargs)
return async_wrapper
@wraps(func)
def sync_wrapper(*args, **kwargs):
request = _extract_request(*args, **kwargs)
denied = _check(request)
if denied:
return denied
return func(*args, **kwargs)
return sync_wrapper
return decorator
# ============================================================
# 演示模式拦截(装饰器)
# ============================================================
def check_demo(func):
"""演示模式拦截装饰器,放在 @permission_required 之后、路由函数之前。"""
if asyncio.iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
if config.FASTAPI_DEMO:
return R.failed("演示环境,暂无操作权限")
return await func(*args, **kwargs)
return async_wrapper
@wraps(func)
def sync_wrapper(*args, **kwargs):
if config.FASTAPI_DEMO:
return R.failed("演示环境,暂无操作权限")
return func(*args, **kwargs)
return sync_wrapper
# ============================================================
# 数据库会话中间件
# ============================================================
def _is_business_success(response) -> bool:
"""检查响应体中业务状态码是否为成功(code == 0)。"""
try:
body = getattr(response, 'body', None)
if body:
data = json.loads(body)
return data.get('code') == 0
except Exception:
pass
return 200 <= response.status_code < 300
async def db_session_middleware(request: Request, call_next):
"""请求级 DB 会话:写操作按业务成功与否 commit/rollback,finally 关闭。"""
db = SessionLocal()
_db_ctx.set(db)
try:
response = await call_next(request)
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
if _is_business_success(response):
db.commit()
else:
db.rollback()
return response
except Exception:
db.rollback()
raise
finally:
db.close()
# ============================================================
# 操作日志中间件
# ============================================================
# 不记录操作日志的路径前缀
_EXCLUDE_PATHS = (
'/login',
'/captcha',
'/upload',
'/loginlog',
'/operationlog',
'/index/menu',
'/index/user',
'/index/logout',
)
def _should_log(request: Request) -> bool:
"""判断当前请求是否需要记录操作日志"""
if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'):
return False
path = request.url.path
for prefix in _EXCLUDE_PATHS:
if prefix in path:
return False
return True
def _get_route_summary(request: Request) -> str:
"""从路由获取操作标题(summary)"""
try:
route = request.scope.get('route')
if route and hasattr(route, 'summary') and route.summary:
return route.summary
except Exception:
pass
return request.method + ' ' + request.url.path
async def operation_log_middleware(request: Request, call_next):
"""操作日志中间件:自动拦截写请求并记录操作日志。"""
if not _should_log(request):
return await call_next(request)
start_time = time.time()
status = 0
error_msg = ''
try:
response = await call_next(request)
try:
body = getattr(response, 'body', None)
if body:
data = json.loads(body)
if data.get('code', 0) != 0:
status = 1
error_msg = data.get('msg', '')
except Exception:
pass
return response
except Exception as e:
status = 1
error_msg = str(e)
raise
finally:
try:
consume_time = int((time.time() - start_time) * 1000)
user_id = get_user_id(request)
if user_id and user_id > 0:
# 懒加载业务 service,避免 core → apps 顶层依赖(迁移完成后指向 modules)
from apps.services import operation_log
operation_log.record_operation_log(
request,
title=_get_route_summary(request),
method=get_username(request),
consume_time=consume_time,
status=status,
error=error_msg,
)
except Exception as e:
logger.error(f"操作日志中间件异常: {str(e)}")