chore: initial commit — leaudit-platform project skeleton
17-table PostgreSQL schema with full Chinese column comments, FastAPI project structure (admin/common/modules), DSL rule files, and schema migration scripts.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""FastAPI 应用工厂。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from fastapi_admin.config import APP_NAME, APP_CORS_ORIGINS
|
||||
from fastapi_admin.config._loader import _find_project_root
|
||||
|
||||
# 确保项目根在 sys.path
|
||||
_PROJECT_ROOT = _find_project_root()
|
||||
if str(_PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_PROJECT_ROOT))
|
||||
|
||||
# fastapi_modules 目录加入路径(使 importlib 能找到各模块)
|
||||
_FASTMOD = _PROJECT_ROOT / "fastapi_modules"
|
||||
_FASTMOD_LEAUDIT = _FASTMOD / "fastapi_leaudit"
|
||||
if str(_FASTMOD) not in sys.path:
|
||||
sys.path.insert(0, str(_FASTMOD))
|
||||
if str(_FASTMOD_LEAUDIT) not in sys.path:
|
||||
sys.path.insert(0, str(_FASTMOD_LEAUDIT))
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""创建并配置 FastAPI 应用。"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger("APP").info(f"{APP_NAME} starting...")
|
||||
yield
|
||||
logging.getLogger("APP").info(f"{APP_NAME} shutting down...")
|
||||
|
||||
app = FastAPI(
|
||||
title=APP_NAME,
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
docs_url="/api/docs",
|
||||
redoc_url=None,
|
||||
)
|
||||
|
||||
# CORS
|
||||
origins = [o.strip() for o in APP_CORS_ORIGINS.split(",") if o.strip()]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册控制器
|
||||
from fastapi_admin.bootstrap_parts.controllers import register_controllers
|
||||
register_controllers(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""控制器注册器 —— 扫描 controller_packages 并注册路由。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
|
||||
controller_packages = [
|
||||
"fastapi_modules.fastapi_leaudit.controllers",
|
||||
]
|
||||
|
||||
|
||||
def register_controllers(app: FastAPI) -> None:
|
||||
"""扫描所有控制器包,注册 BaseController 子类的路由。"""
|
||||
for package_name in controller_packages:
|
||||
try:
|
||||
pkg = importlib.import_module(package_name)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
package_routers: dict[str, APIRouter] = {}
|
||||
_collect_package_routers(pkg, str(Path(pkg.__file__ or "").parent), package_name, package_routers)
|
||||
_register_from_package(pkg, package_name, package_routers, app)
|
||||
|
||||
|
||||
def _collect_package_routers(
|
||||
pkg: Any, pkg_dir: str, pkg_name: str, routers: dict[str, APIRouter],
|
||||
) -> None:
|
||||
"""收集所有包级 router(从 __init__.py)。"""
|
||||
if hasattr(pkg, "router") and isinstance(pkg.router, APIRouter):
|
||||
routers[pkg_name] = pkg.router
|
||||
|
||||
if not hasattr(pkg, "__path__"):
|
||||
return
|
||||
|
||||
for _, sub_name, is_pkg in pkgutil.iter_modules([pkg_dir]):
|
||||
if sub_name.startswith("_"):
|
||||
continue
|
||||
sub_full = f"{pkg_name}.{sub_name}"
|
||||
if is_pkg:
|
||||
try:
|
||||
sub_pkg = importlib.import_module(sub_full)
|
||||
sub_dir = str(Path(pkg_dir) / sub_name)
|
||||
_collect_package_routers(sub_pkg, sub_dir, sub_full, routers)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _register_from_package(
|
||||
pkg: Any, pkg_name: str, package_routers: dict[str, APIRouter], app: FastAPI,
|
||||
) -> None:
|
||||
"""从包中注册 BaseController 子类。"""
|
||||
from fastapi_common.fastapi_common_web.controller import BaseController
|
||||
|
||||
if not hasattr(pkg, "__path__"):
|
||||
return
|
||||
|
||||
pkg_dir = str(Path(pkg.__file__ or "").parent)
|
||||
|
||||
for _, module_name, _ in pkgutil.iter_modules([pkg_dir]):
|
||||
if module_name.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
mod = importlib.import_module(f"{pkg_name}.{module_name}")
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
for _, obj in vars(mod).items():
|
||||
if (
|
||||
isinstance(obj, type)
|
||||
and issubclass(obj, BaseController)
|
||||
and obj is not BaseController
|
||||
):
|
||||
instance = obj()
|
||||
target_router = _resolve_target_router(pkg_name, module_name, package_routers)
|
||||
app.include_router(instance.router, prefix="/api", dependencies=target_router.dependencies)
|
||||
|
||||
|
||||
def _resolve_target_router(
|
||||
pkg_name: str, module_name: str, package_routers: dict[str, APIRouter],
|
||||
) -> APIRouter:
|
||||
"""沿包路径向上查找最近的包级 router。"""
|
||||
parts = pkg_name.split(".")
|
||||
for i in range(len(parts), 0, -1):
|
||||
parent = ".".join(parts[:i])
|
||||
if parent in package_routers:
|
||||
return package_routers[parent]
|
||||
return APIRouter()
|
||||
@@ -0,0 +1,59 @@
|
||||
"""配置模块。
|
||||
|
||||
所有 Settings 实例的字段和 @property 会被自动导出为模块级变量。
|
||||
业务代码直接导入:
|
||||
|
||||
from fastapi_admin.config import APP_PORT, ASYNCPG_DATABASE_URL
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._loader import load_config as _load_config
|
||||
|
||||
# 优先加载 TOML → os.environ(必须在 Settings 实例化之前)
|
||||
_load_config()
|
||||
|
||||
from ._settings import app, jwt, db, redis, oss, llm, vlm, ocr, leaudit as _leaudit # noqa: E402
|
||||
|
||||
|
||||
def _export_settings(instance: object, prefix: str = "") -> dict[str, object]:
|
||||
"""将 Settings 实例的所有字段和 @property 导出为模块级变量。"""
|
||||
result: dict[str, object] = {}
|
||||
for key in dir(type(instance)):
|
||||
if key.startswith("_"):
|
||||
continue
|
||||
value = getattr(instance, key, None)
|
||||
if callable(value) and not isinstance(value, property):
|
||||
continue
|
||||
if isinstance(value, property):
|
||||
value = value.__get__(instance)
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
_APP = _export_settings(app)
|
||||
_JWT = _export_settings(jwt)
|
||||
_DB = _export_settings(db)
|
||||
_REDIS = _export_settings(redis)
|
||||
_OSS = _export_settings(oss)
|
||||
_LLM = _export_settings(llm)
|
||||
_VLM = _export_settings(vlm)
|
||||
_OCR = _export_settings(ocr)
|
||||
_LEAUDIT = _export_settings(_leaudit)
|
||||
|
||||
# 将所有变量注入当前模块的全局命名空间
|
||||
_ALL = {}
|
||||
_ALL.update(_APP)
|
||||
_ALL.update(_JWT)
|
||||
_ALL.update(_DB)
|
||||
_ALL.update(_REDIS)
|
||||
_ALL.update(_OSS)
|
||||
_ALL.update(_LLM)
|
||||
_ALL.update(_VLM)
|
||||
_ALL.update(_OCR)
|
||||
_ALL.update(_LEAUDIT)
|
||||
|
||||
globals().update(_ALL)
|
||||
|
||||
# 常量
|
||||
ROOT_PATH = __import__("pathlib").Path(__file__).resolve().parents[2]
|
||||
@@ -0,0 +1,55 @@
|
||||
"""类型存根 —— 为 IDE 提供动态导出变量的类型信息。"""
|
||||
|
||||
# APP
|
||||
APP_NAME: str
|
||||
APP_HOST: str
|
||||
APP_PORT: int
|
||||
APP_CORS_ORIGINS: str
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY: str
|
||||
JWT_ACCESS_TOKEN_EXPIRE_HOURS: int
|
||||
JWT_ALGORITHM: str
|
||||
|
||||
# DB
|
||||
DB_HOST: str
|
||||
DB_PORT: int
|
||||
DB_NAME: str
|
||||
DB_USER: str
|
||||
DB_PASSWORD: str
|
||||
ASYNCPG_DATABASE_URL: str
|
||||
|
||||
# Redis
|
||||
REDIS_HOST: str
|
||||
REDIS_PORT: int
|
||||
REDIS_DB: int
|
||||
REDIS_PASSWORD: str
|
||||
|
||||
# OSS
|
||||
OSS_ENDPOINT: str
|
||||
OSS_ACCESS_KEY: str
|
||||
OSS_SECRET_KEY: str
|
||||
OSS_BUCKET: str
|
||||
OSS_REGION: str
|
||||
|
||||
# LLM
|
||||
LLM_BASE_URL: str
|
||||
LLM_MODEL: str
|
||||
LLM_API_KEY: str
|
||||
|
||||
# VLM
|
||||
VLM_BASE_URL: str
|
||||
VLM_MODEL: str
|
||||
|
||||
# OCR
|
||||
OCR_BASE_URL: str
|
||||
OCR_TIMEOUT: int
|
||||
|
||||
# LEAUDIT
|
||||
LEAUDIT_RULES_DIR: str
|
||||
LEAUDIT_RESCUE_MODE: str
|
||||
LEAUDIT_LLM_MAX_CONCURRENCY: int
|
||||
LEAUDIT_VLM_MAX_CONCURRENCY: int
|
||||
|
||||
# 常量
|
||||
ROOT_PATH: object
|
||||
@@ -0,0 +1,68 @@
|
||||
"""配置加载器 —— 读取 TOML 文件并注入 os.environ。
|
||||
|
||||
加载顺序(后覆盖前):
|
||||
1. app.toml — 基础配置
|
||||
2. app.{APP_ENV}.toml — 环境差异
|
||||
3. app.ai.toml — AI 专用(最高 TOML 优先级)
|
||||
4. 已有环境变量 — 不覆盖(最高优先级)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _find_project_root() -> Path:
|
||||
"""查找项目根目录(包含 app.toml 的目录)。"""
|
||||
return _ROOT
|
||||
|
||||
|
||||
def _load_toml(path: Path) -> dict:
|
||||
"""读取 TOML 文件并展平 [SECTION].KEY → SECTION_KEY。"""
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
import tomli as tomllib
|
||||
|
||||
with open(path, "rb") as fh:
|
||||
data = tomllib.load(fh)
|
||||
|
||||
flat: dict[str, str] = {}
|
||||
for section, values in data.items():
|
||||
if not isinstance(values, dict):
|
||||
continue
|
||||
for key, val in values.items():
|
||||
env_key = f"{section.upper()}_{key.upper()}"
|
||||
if isinstance(val, list):
|
||||
flat[env_key] = ",".join(str(v) for v in val)
|
||||
elif isinstance(val, bool):
|
||||
flat[env_key] = str(val).lower()
|
||||
elif val is not None:
|
||||
flat[env_key] = str(val)
|
||||
return flat
|
||||
|
||||
|
||||
def load_config() -> None:
|
||||
"""加载所有 TOML 配置到 os.environ(不覆盖已有环境变量)。"""
|
||||
root = _find_project_root()
|
||||
|
||||
env = os.getenv("APP_ENV", "development")
|
||||
toml_files = [
|
||||
root / "app.toml",
|
||||
root / f"app.{env}.toml",
|
||||
root / "app.ai.toml",
|
||||
]
|
||||
|
||||
for toml_path in toml_files:
|
||||
if not toml_path.exists():
|
||||
continue
|
||||
for key, value in _load_toml(toml_path).items():
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
# 确保项目根在 sys.path 中
|
||||
if str(root) not in sys.path:
|
||||
sys.path.insert(0, str(root))
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Pydantic Settings 定义。
|
||||
|
||||
每个 Settings 类对应 app.toml 中的一个 [SECTION]。
|
||||
字段名 = SECTION_KEY(TOML 展平后的环境变量名)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class _Base(BaseSettings):
|
||||
"""所有 Settings 的基类。"""
|
||||
model_config = {"env_file": None, "extra": "ignore"}
|
||||
|
||||
|
||||
class AppSettings(_Base):
|
||||
"""应用基础配置 [APP]。"""
|
||||
APP_NAME: str = "LeAudit Platform"
|
||||
APP_HOST: str = "0.0.0.0"
|
||||
APP_PORT: int = 8000
|
||||
APP_CORS_ORIGINS: str = "*"
|
||||
|
||||
|
||||
class JwtSettings(_Base):
|
||||
"""JWT 配置 [JWT]。"""
|
||||
JWT_SECRET_KEY: str = ""
|
||||
JWT_ACCESS_TOKEN_EXPIRE_HOURS: int = 24
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
|
||||
|
||||
class DbSettings(_Base):
|
||||
"""数据库配置 [DB]。"""
|
||||
DB_HOST: str = "localhost"
|
||||
DB_PORT: int = 5432
|
||||
DB_NAME: str = "leaudit"
|
||||
DB_USER: str = "postgres"
|
||||
DB_PASSWORD: str = ""
|
||||
|
||||
@property
|
||||
def ASYNCPG_DATABASE_URL(self) -> str:
|
||||
"""动态构建 asyncpg 连接 URL。"""
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASSWORD}"
|
||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
)
|
||||
|
||||
|
||||
class RedisSettings(_Base):
|
||||
"""Redis 配置 [REDIS]。"""
|
||||
REDIS_HOST: str = "localhost"
|
||||
REDIS_PORT: int = 6379
|
||||
REDIS_DB: int = 0
|
||||
REDIS_PASSWORD: str = ""
|
||||
|
||||
|
||||
class OssSettings(_Base):
|
||||
"""OSS 对象存储配置 [OSS]。"""
|
||||
OSS_ENDPOINT: str = ""
|
||||
OSS_ACCESS_KEY: str = ""
|
||||
OSS_SECRET_KEY: str = ""
|
||||
OSS_BUCKET: str = "leaudit"
|
||||
OSS_REGION: str = ""
|
||||
|
||||
|
||||
class LlmSettings(_Base):
|
||||
"""LLM 配置 [LLM]。"""
|
||||
LLM_BASE_URL: str = ""
|
||||
LLM_MODEL: str = ""
|
||||
LLM_API_KEY: str = ""
|
||||
|
||||
|
||||
class VlmSettings(_Base):
|
||||
"""VLM 配置 [VLM]。"""
|
||||
VLM_BASE_URL: str = ""
|
||||
VLM_MODEL: str = ""
|
||||
|
||||
|
||||
class OcrSettings(_Base):
|
||||
"""OCR 配置 [OCR]。"""
|
||||
OCR_BASE_URL: str = ""
|
||||
OCR_TIMEOUT: int = 300
|
||||
|
||||
|
||||
class LeauditSettings(_Base):
|
||||
"""LeAudit 引擎配置 [LEAUDIT]。"""
|
||||
LEAUDIT_RULES_DIR: str = "rules"
|
||||
LEAUDIT_RESCUE_MODE: str = "auto"
|
||||
LEAUDIT_LLM_MAX_CONCURRENCY: int = 5
|
||||
LEAUDIT_VLM_MAX_CONCURRENCY: int = 3
|
||||
|
||||
|
||||
# 实例化所有 Settings
|
||||
app = AppSettings()
|
||||
jwt = JwtSettings()
|
||||
db = DbSettings()
|
||||
redis = RedisSettings()
|
||||
oss = OssSettings()
|
||||
llm = LlmSettings()
|
||||
vlm = VlmSettings()
|
||||
ocr = OcrSettings()
|
||||
leaudit = LeauditSettings()
|
||||
Reference in New Issue
Block a user