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:
wren
2026-04-27 16:48:22 +08:00
commit 535d97a70c
142 changed files with 25219 additions and 0 deletions
View File
+65
View File
@@ -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()
+59
View File
@@ -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]
+55
View File
@@ -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
+68
View File
@@ -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))
+102
View File
@@ -0,0 +1,102 @@
"""Pydantic Settings 定义。
每个 Settings 类对应 app.toml 中的一个 [SECTION]。
字段名 = SECTION_KEYTOML 展平后的环境变量名)。
"""
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()