seaislee1209 2b990f06fb
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m18s
Build and Deploy Web / build-and-deploy (push) Successful in 53s
feat: 合同金额权限控制 + 内置角色权限自动同步
- 新增 project:view_contract 权限,仅超级管理员可查看合同金额
- 项目详情、仪表盘、结算页、创建项目表单均受权限保护
- 启动时自动同步内置角色权限,新增权限无需手动更新数据库

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 12:10:02 +08:00

194 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
from sqlalchemy.orm.attributes import flag_modified
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}")
elif existing.is_system:
# 同步内置角色:补充代码中新增的权限
current = set(existing.permissions or [])
defined = set(role_def["permissions"])
missing = defined - current
if missing:
existing.permissions = list(current | missing)
flag_modified(existing, "permissions")
print(f"[SYNC] added permissions to {role_name}: {missing}")
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()