"""AirLabs Project —— 主入口""" from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from database import engine, Base from models import ( User, Role, PhaseGroup, BUILTIN_ROLES, COST_PERM_MIGRATION, Project, ProjectMilestone, DEFAULT_MILESTONES ) from auth import hash_password import os import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 创建所有表 Base.metadata.create_all(bind=engine) # ── 手动迁移:为旧 SQLite 数据库补充 role_id 列 ── from config import DATABASE_URL as _db_url if _db_url.startswith("sqlite"): import sqlalchemy with engine.connect() as conn: cols = [r[1] for r in conn.execute(sqlalchemy.text("PRAGMA table_info('users')"))] if "role_id" not in cols: conn.execute(sqlalchemy.text("ALTER TABLE users ADD COLUMN role_id INTEGER")) conn.commit() logger.info("[MIGRATE] added column users.role_id") app = FastAPI(title="AirLabs Project", version="1.0.0") # CORS from config import CORS_ORIGINS app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 注册路由 from routers.auth import router as auth_router from routers.users import router as users_router from routers.projects import router as projects_router from routers.submissions import router as submissions_router from routers.costs import router as costs_router from routers.dashboard import router as dashboard_router from routers.roles import router as roles_router try: from routers.reports import router as reports_router except ImportError as e: reports_router = None logging.warning(f"[路由] reports 模块加载失败: {e}") app.include_router(auth_router) app.include_router(users_router) app.include_router(projects_router) app.include_router(submissions_router) app.include_router(costs_router) app.include_router(dashboard_router) app.include_router(roles_router) if reports_router: app.include_router(reports_router) # 前端静态文件 frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist") if os.path.exists(frontend_dir): app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets") @app.get("/{full_path:path}") async def serve_frontend(full_path: str): file_path = os.path.join(frontend_dir, full_path) if os.path.isfile(file_path): return FileResponse(file_path) return FileResponse(os.path.join(frontend_dir, "index.html")) @app.on_event("startup") async def start_scheduler(): """启动定时任务调度器""" try: from services.scheduler_service import setup_scheduler setup_scheduler() except Exception as e: logger.warning(f"[定时任务] 启动失败(不影响核心功能): {e}") @app.on_event("shutdown") async def stop_scheduler(): """关闭定时任务调度器""" try: from services.scheduler_service import scheduler scheduler.shutdown(wait=False) logger.info("[定时任务] 已关闭") except Exception: pass @app.on_event("startup") def init_roles_and_admin(): """首次启动时创建内置角色和默认管理员""" from database import SessionLocal db = SessionLocal() try: # 初始化内置角色 for role_name, role_def in BUILTIN_ROLES.items(): existing = db.query(Role).filter(Role.name == role_name).first() if not existing: role = Role( name=role_name, description=role_def["description"], permissions=role_def["permissions"], is_system=1, ) db.add(role) print(f"[OK] created role: {role_name}") db.commit() # 迁移旧成本权限 → 细分权限 old_cost_perms = set(COST_PERM_MIGRATION.keys()) for role in db.query(Role).all(): perms = list(role.permissions or []) changed = False for old_perm, new_perms in COST_PERM_MIGRATION.items(): if old_perm in perms: perms.remove(old_perm) for np in new_perms: if np not in perms: perms.append(np) changed = True if changed: role.permissions = perms print(f"[MIGRATE] upgraded cost permissions for role: {role.name}") db.commit() # 为已有项目补充默认里程碑 for proj in db.query(Project).all(): has_ms = db.query(ProjectMilestone).filter( ProjectMilestone.project_id == proj.id ).first() if not has_ms: for ms in DEFAULT_MILESTONES: db.add(ProjectMilestone( project_id=proj.id, name=ms["name"], phase=PhaseGroup(ms["phase"]), sort_order=ms.get("sort_order", 0), )) print(f"[MIGRATE] added default milestones for project: {proj.name}") db.commit() # 迁移:为旧用户补充默认 role_id(成员角色) member_role = db.query(Role).filter(Role.name == "成员").first() if member_role: orphans = db.query(User).filter(User.role_id.is_(None)).all() for u in orphans: u.role_id = member_role.id print(f"[MIGRATE] assigned default role '成员' to user: {u.username}") if orphans: db.commit() # 创建默认管理员(关联超级管理员角色) admin_role = db.query(Role).filter(Role.name == "超级管理员").first() if admin_role and not db.query(User).filter(User.username == "admin").first(): owner = User( username="admin", password_hash=hash_password("admin123"), name="管理员", phase_group=PhaseGroup.PRODUCTION, role_id=admin_role.id, monthly_salary=0, ) db.add(owner) db.commit() print("[OK] default admin created: admin / admin123") finally: db.close()