refactor: 平台应用异步四层架构(接口/业务/领域/基础设施)

- 基础设施层 async:SQLAlchemy 异步引擎/会话、33 Repository async 化、models/security/seed 迁入 infrastructure、新增 cache.py(redis.asyncio) 与 oss.py(aioboto3)
- 接口层:routers 迁 api/routers 并全 async,dependencies 迁 api/dependencies(get_db/get_current_user async)
- 依赖:sqlalchemy[asyncio]/aiosqlite/asyncmy/redis/aioboto3;config 异步 URL + Redis/OSS 配置
- 删除废弃:旧同步 db/dependencies/repositories/storage
- 验证:平台 19 路由 + 培训 48 路由全注册;/health /auth/login /auth/me /admin/tasks /notifications 等接口 async 可用
This commit is contained in:
2026-08-23 23:52:58 +08:00
parent 079035ad65
commit 980a2db6d9
29 changed files with 3327 additions and 323 deletions
+108
View File
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
"""接口层依赖:数据库句柄与当前登录用户(JWT + RBAC,全异步)。
``Database`` 实例由 ``main.py`` 在启动时创建并挂在 ``app.state.db`` 上,
路由通过 ``Depends(get_db)`` 取用;测试时可替换为临时目录实例。
"""
from __future__ import annotations
from fastapi import Depends, HTTPException, Request
from .. import config
from ..infrastructure.repositories import Database
from ..jwt import decode_access_token
BEARER_PREFIX = "Bearer "
def extract_bearer_token(request: Request) -> str:
"""从 Authorization 头提取 Bearer token,没有则返回空串。"""
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith(BEARER_PREFIX):
return auth_header[len(BEARER_PREFIX):].strip()
return ""
def get_db(request: Request) -> Database:
return request.app.state.db
async def _resolve_identity(user: dict, identity_id: str | None, db: Database) -> dict:
"""按令牌携带的 identity_id 装配当前身份;缺失则回退到 users.role 单角色。
身份即权限来源:从 user_identities 取 role/sub_role/org/region
并据此计算 permissions 与数据范围(全异步,角色变更即时生效)。
"""
if identity_id:
ident = await db.identities.get_for_user(identity_id, user["id"])
if ident and ident.get("status") == "active":
user["identity_id"] = ident["id"]
user["port"] = ident["port"]
user["role"] = ident["role"]
user["sub_role"] = ident.get("sub_role")
user["org_id"] = ident.get("org_id")
user["region_id"] = ident.get("region_id")
user["identity_name"] = ident.get("name", "")
user["permissions"] = await db.roles.permissions_for(
user.get("role", "opc_member"), user.get("sub_role"),
)
user["scope_region_ids"] = await db.regions.visible_region_ids(user.get("region_id"))
user["scope_level"] = await db.regions.level(user.get("region_id"))
return user
async def get_current_user(
request: Request,
db: Database = Depends(get_db),
) -> dict:
"""校验调用方 JWT,返回当前用户记录(含 role/权限/数据范围)。"""
token = extract_bearer_token(request)
if not token:
raise HTTPException(status_code=401, detail="No token provided")
payload = decode_access_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid or expired token")
user_id = payload.get("sub")
if not await db.tokens.session_valid(
payload.get("jti", ""),
user_id,
payload.get("ver", 0),
):
raise HTTPException(status_code=401, detail="Invalid or expired token")
user = await db.users.get_by_id(user_id)
if user is None or user.get("status") != "active":
raise HTTPException(status_code=401, detail="Invalid or expired token")
return await _resolve_identity(user, payload.get("identity_id"), db)
async def optional_current_user(
request: Request,
db: Database = Depends(get_db),
) -> dict | None:
"""可选登录:有有效 JWT 则返回用户,否则返回 None(不报错)。"""
token = extract_bearer_token(request)
if not token:
return None
payload = decode_access_token(token)
if payload is None:
return None
if not await db.tokens.session_valid(
payload.get("jti", ""), payload.get("sub", ""), payload.get("ver", 0),
):
return None
user = await db.users.get_by_id(payload.get("sub"))
if user is None or user.get("status") != "active":
return None
return await _resolve_identity(user, payload.get("identity_id"), db)
def require_port(user: dict = Depends(get_current_user)) -> dict:
"""按端口隔离的资源(如智能体):必须已解析出端口身份,否则 403。"""
if not user.get("port"):
raise HTTPException(status_code=403, detail="请先选择身份/端口")
return user