feat: migrate ORM base to sqlmodel-ext, add file viewers and WOPI integration

- Migrate SQLModel base classes, mixins, and database management to
  external sqlmodel-ext package; remove sqlmodels/base/, sqlmodels/mixin/,
  and sqlmodels/database.py
- Add file viewer/editor system with WOPI protocol support for
  collaborative editing (OnlyOffice, Collabora)
- Add enterprise edition license verification module (ee/)
- Add Dockerfile multi-stage build with Cython compilation support
- Add new dependencies: sqlmodel-ext, cryptography, whatthepatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 14:23:17 +08:00
parent 53b757de7a
commit eac0766e79
74 changed files with 4819 additions and 4837 deletions

61
main.py
View File

@@ -1,16 +1,40 @@
from pathlib import Path
from typing import NoReturn
from fastapi import FastAPI, Request
from loguru import logger as l
from utils.conf import appmeta
from utils.http.http_exceptions import raise_internal_error
from utils.lifespan import lifespan
from routers import router
from service.redis import RedisManager
from sqlmodels.database_connection import DatabaseManager
from sqlmodels.migration import migration
from utils import JWT
from routers import router
from service.redis import RedisManager
from loguru import logger as l
from utils.conf import appmeta
from utils.http.http_exceptions import raise_internal_error
from utils.lifespan import lifespan
# 尝试加载企业版功能
try:
from ee import init_ee
from ee.license import LicenseError
async def _init_ee_and_routes() -> None:
try:
await init_ee()
except LicenseError as exc:
l.critical(f"许可证验证失败: {exc}")
raise SystemExit(1) from exc
from ee.routers import ee_router
from routers.api.v1 import router as v1_router
v1_router.include_router(ee_router)
lifespan.add_startup(_init_ee_and_routes)
except ImportError:
l.info("以 Community 版本运行")
STATICS_DIR: Path = (Path(__file__).parent / "statics").resolve()
"""前端静态文件目录(由 Docker 构建时复制)"""
async def _init_db() -> None:
"""初始化数据库连接引擎"""
@@ -64,6 +88,31 @@ async def handle_unexpected_exceptions(
# 挂载路由
app.include_router(router)
# 挂载前端静态文件(仅当 statics/ 目录存在时,即 Docker 部署环境)
if STATICS_DIR.is_dir():
from starlette.staticfiles import StaticFiles
from fastapi.responses import FileResponse
_assets_dir: Path = STATICS_DIR / "assets"
if _assets_dir.is_dir():
app.mount("/assets", StaticFiles(directory=_assets_dir), name="assets")
@app.get("/{path:path}")
async def spa_fallback(path: str) -> FileResponse:
"""
SPA fallback 路由
优先级API 路由 > /assets 静态挂载 > 此 catch-all 路由。
若请求路径对应 statics/ 下的真实文件则直接返回,否则返回 index.html。
"""
file_path: Path = (STATICS_DIR / path).resolve()
# 防止路径穿越
if file_path.is_relative_to(STATICS_DIR) and path and file_path.is_file():
return FileResponse(file_path)
return FileResponse(STATICS_DIR / "index.html")
l.info(f"前端静态文件已挂载: {STATICS_DIR}")
# 防止直接运行 main.py
if __name__ == "__main__":
l.error("请用 fastapi ['dev', 'run'] 命令启动服务")