113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from sqlalchemy import text
|
|
|
|
from app.api.v1.router import api_router
|
|
from app.core.config import settings
|
|
from app.core.exceptions import register_exception_handlers
|
|
from app.crud.user import ensure_admin_exists
|
|
from app.db.base import Base
|
|
from app.db.session import SessionLocal, engine
|
|
from app.services.fee_seed import seed_demo_fees
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
# Ensure models are imported so metadata is populated
|
|
from app.models import user as user_model # noqa: F401
|
|
|
|
if settings.ENV == "development":
|
|
async with engine.begin() as conn:
|
|
await _ensure_legacy_primary_keys(conn)
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
async with SessionLocal() as session:
|
|
await ensure_admin_exists(session)
|
|
await seed_demo_fees(session)
|
|
yield
|
|
|
|
|
|
async def _ensure_legacy_primary_keys(conn) -> None:
|
|
# Fix legacy databases where tables with an "id" column were created without a PK.
|
|
result = await conn.execute(
|
|
text(
|
|
"""
|
|
SELECT c.relname AS table_name
|
|
FROM pg_class c
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE n.nspname = 'public'
|
|
AND c.relkind = 'r'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM pg_attribute a
|
|
WHERE a.attrelid = c.oid
|
|
AND a.attname = 'id'
|
|
AND a.attisdropped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint con
|
|
WHERE con.conrelid = c.oid
|
|
AND con.contype = 'p'
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
tables = [row.table_name for row in result.fetchall()]
|
|
for table_name in tables:
|
|
await conn.execute(
|
|
text(f'ALTER TABLE "{table_name}" ADD CONSTRAINT "{table_name}_pkey" PRIMARY KEY (id)')
|
|
)
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
app = FastAPI(
|
|
title="CTMS 后端 API",
|
|
description="临床试验项目管理系统后端接口文档",
|
|
version="0.1.0",
|
|
debug=settings.ENV == "development",
|
|
lifespan=lifespan,
|
|
openapi_tags=[
|
|
{"name": "auth", "description": "认证与登录"},
|
|
{"name": "users", "description": "用户管理"},
|
|
{"name": "studies", "description": "项目管理"},
|
|
{"name": "sites", "description": "中心管理"},
|
|
{"name": "study-members", "description": "项目成员"},
|
|
{"name": "attachments", "description": "通用附件"},
|
|
{"name": "audit-logs", "description": "审计日志"},
|
|
{"name": "dashboard", "description": "项目总览与统计"},
|
|
{"name": "subjects", "description": "参与者"},
|
|
{"name": "visits", "description": "访视"},
|
|
{"name": "aes", "description": "不良事件"},
|
|
{"name": "finance", "description": "费用管理"},
|
|
{"name": "faq-categories", "description": "FAQ 分类"},
|
|
{"name": "faqs", "description": "FAQ 条目"},
|
|
{"name": "health", "description": "健康检查"},
|
|
],
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
register_exception_handlers(app)
|
|
|
|
@app.get(
|
|
"/health",
|
|
tags=["health"],
|
|
summary="健康检查",
|
|
description="返回服务存活状态。",
|
|
)
|
|
async def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
app.include_router(api_router, prefix="/api/v1")
|
|
return app
|
|
|
|
|
|
app = create_app()
|