diff --git a/backend/config.py b/backend/config.py index b4e5c18..4d0cfc3 100644 --- a/backend/config.py +++ b/backend/config.py @@ -4,8 +4,16 @@ from dotenv import load_dotenv load_dotenv() -# 数据库 -DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./airlabs.db") +# 数据库:优先从 DB_HOST 等环境变量拼接 MySQL URL,否则回退到 DATABASE_URL / SQLite +DB_HOST = os.getenv("DB_HOST", "") +if DB_HOST: + DB_USER = os.getenv("DB_USER", "airlabs_manage") + DB_PASSWORD = os.getenv("DB_PASSWORD", "") + DB_PORT = os.getenv("DB_PORT", "3306") + DB_NAME = os.getenv("DB_NAME", "airlabs_manage") + DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4" +else: + DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./airlabs.db") # JWT 认证 SECRET_KEY = os.getenv("SECRET_KEY", "airlabs-project-secret-key-change-in-production") diff --git a/backend/database.py b/backend/database.py index 3c26832..ac84902 100644 --- a/backend/database.py +++ b/backend/database.py @@ -3,7 +3,9 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base from config import DATABASE_URL -engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +# check_same_thread 仅 SQLite 需要 +connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {} +engine = create_engine(DATABASE_URL, connect_args=connect_args) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/backend/main.py b/backend/main.py index 8364f7d..033320f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -21,6 +21,17 @@ 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 @@ -144,6 +155,16 @@ def init_roles_and_admin(): 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(): diff --git a/backend/migrate_sqlite_to_mysql.py b/backend/migrate_sqlite_to_mysql.py new file mode 100644 index 0000000..257cf34 --- /dev/null +++ b/backend/migrate_sqlite_to_mysql.py @@ -0,0 +1,77 @@ +"""一次性脚本:将 SQLite 数据迁移到 MySQL""" +import sqlite3 +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from config import DATABASE_URL + +SQLITE_PATH = "airlabs.db" + +# 按外键依赖顺序排列 +TABLES = [ + "roles", + "users", + "projects", + "project_milestones", + "submissions", + "submission_history", + "ai_tool_costs", + "ai_tool_cost_allocations", + "outsource_costs", + "cost_overrides", + "overhead_costs", +] + + +def migrate(): + if not DATABASE_URL.startswith("mysql"): + print("ERROR: DATABASE_URL 不是 MySQL,请检查 .env 配置") + return + + # 连接 SQLite + sqlite_conn = sqlite3.connect(SQLITE_PATH) + sqlite_conn.row_factory = sqlite3.Row + + # 连接 MySQL — 先建表 + from database import Base, engine + import models # noqa: F401 — 确保所有模型已注册 + Base.metadata.create_all(bind=engine) + print("[OK] MySQL tables created") + + mysql_engine = engine + Session = sessionmaker(bind=mysql_engine) + session = Session() + + try: + for table in TABLES: + rows = sqlite_conn.execute(f"SELECT * FROM {table}").fetchall() + if not rows: + print(f" {table}: 0 rows (skip)") + continue + + cols = rows[0].keys() + col_list = ", ".join(cols) + param_list = ", ".join(f":{c}" for c in cols) + insert_sql = text(f"INSERT INTO {table} ({col_list}) VALUES ({param_list})") + + # 清空目标表(避免重复运行冲突) + session.execute(text(f"DELETE FROM {table}")) + + for row in rows: + data = dict(row) + session.execute(insert_sql, data) + + session.commit() + print(f" {table}: {len(rows)} rows migrated") + + print("\n[DONE] Migration complete!") + except Exception as e: + session.rollback() + print(f"\n[ERROR] Migration failed: {e}") + raise + finally: + session.close() + sqlite_conn.close() + + +if __name__ == "__main__": + migrate() diff --git a/backend/requirements.txt b/backend/requirements.txt index 2568228..8473afe 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ openai httpx apscheduler python-dotenv +pymysql diff --git a/k8s/backend-deployment-prod.yaml b/k8s/backend-deployment-prod.yaml index 94c48ab..25e9a1d 100644 --- a/k8s/backend-deployment-prod.yaml +++ b/k8s/backend-deployment-prod.yaml @@ -1,15 +1,3 @@ -# SQLite 持久化存储 -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: airlabs-manage-sqlite-data -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi ---- apiVersion: apps/v1 kind: Deployment metadata: @@ -33,18 +21,23 @@ spec: ports: - containerPort: 8000 env: - # SQLite 存储在持久卷中 - - name: DATABASE_URL - value: "sqlite:////app/data/airlabs.db" - # 生产环境 JWT 密钥(部署前请修改) + # Database (阿里云 RDS MySQL) + - name: DB_HOST + value: "rm-7xv1uaw910558p1788o.mysql.rds.aliyuncs.com" + - name: DB_NAME + value: "airlabs-manage" + - name: DB_USER + value: "airlabs_manage" + - name: DB_PASSWORD + value: "Airlabs-manage123" + - name: DB_PORT + value: "3306" + # 生产环境 JWT 密钥 - name: SECRET_KEY value: "Ui5-xEvtAhKRDtlXKzDfd7TElsVZFUhakff0qcjn8jU" # CORS 允许的域名 - name: CORS_ORIGINS value: "https://airlabs-manage-web.airlabs.art" - volumeMounts: - - name: sqlite-data - mountPath: /app/data resources: requests: memory: "128Mi" @@ -52,10 +45,6 @@ spec: limits: memory: "512Mi" cpu: "500m" - volumes: - - name: sqlite-data - persistentVolumeClaim: - claimName: airlabs-manage-sqlite-data --- apiVersion: v1 kind: Service