"""配置管理 API""" import asyncio import json import shutil from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from ..database import get_db from ..models import BuildConfig from ..schemas import BuildConfigUpdate from ..config import CONFIG_JSON_PATH, DEFAULT_MAX_CONCURRENT_BUILDS, DEFAULT_BUILD_DIR_RETENTION_HOURS router = APIRouter(prefix="/api/config", tags=["config"]) _config_lock = asyncio.Lock() # 默认上传配置 DEFAULT_UPLOAD = { "mode": "oss", "oss": { "access_key_id": "", "access_key_secret": "", "endpoint": "oss-cn-beijing.aliyuncs.com", "bucket_name": "", "base_url": "", }, "webdav": { "server_url": "", "username": "", "password": "", "base_path": "/ios-builds", "public_url": "", }, "dingtalk": { "enabled": False, "webhook_url": "", "secret": "", }, } # 默认服务器环境配置 DEFAULT_SERVERS = { "测试环境": { "api": "https://api3-dev.readoor.cn", "assDom": "applinks:dev-data1.readoor.cn", "universalLink": "https://dev-data1.readoor.cn" }, "正式环境": { "api": "https://api3.readoor.cn", "assDom": "applinks:data1.readoor.cn", "universalLink": "https://data1.readoor.cn" }, "华师大环境": { "api": "https://api3.ecnupress.com.cn", "assDom": "applinks:data1.ecnupress.com.cn", "universalLink": "https://data1.ecnupress.com.cn" }, "外教环境": { "api": "https://weread-api3.sflep.com/api3", "assDom": "applinks:wereadossda.sflep.com", "universalLink": "https://wereadossda.sflep.com" } } def load_config() -> dict: """读取 config.json""" if not CONFIG_JSON_PATH.exists(): return {"apps": {}, "schemes": {}, "servers": DEFAULT_SERVERS, "branches": ["main"], "upload": DEFAULT_UPLOAD} with open(CONFIG_JSON_PATH, "r", encoding="utf-8") as f: config = json.load(f) # 确保必要字段存在 if "servers" not in config: config["servers"] = DEFAULT_SERVERS if "branches" not in config: config["branches"] = ["main"] if "upload" not in config: config["upload"] = DEFAULT_UPLOAD return config def save_config(config: dict): """保存 config.json(自动备份)""" if CONFIG_JSON_PATH.exists(): backup_path = CONFIG_JSON_PATH.with_suffix(".json.bak") shutil.copy2(CONFIG_JSON_PATH, backup_path) with open(CONFIG_JSON_PATH, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) def get_build_config_value(db: Session, key: str, default=None) -> str: """获取打包配置值""" config = db.query(BuildConfig).filter(BuildConfig.key == key).first() return config.value if config else default @router.get("") async def get_full_config(): """获取完整 config.json""" return load_config() @router.put("") async def update_full_config(config: dict): """更新整个 config.json""" async with _config_lock: save_config(config) return {"message": "配置已更新"} @router.get("/apps") async def get_apps(): """获取 apps 配置""" config = load_config() return config.get("apps", {}) @router.post("/apps") async def create_app(app: dict): """新增 app""" async with _config_lock: config = load_config() apps = config.get("apps", {}) # 自动生成 ID numeric_keys = [int(k) for k in apps.keys() if k.isdigit()] new_id = str(max(numeric_keys) + 1) if numeric_keys else "1" apps[new_id] = app config["apps"] = apps save_config(config) return {"id": new_id, "message": "App 已创建"} @router.put("/apps/{app_id}") async def update_app(app_id: str, app: dict): """更新指定 app""" async with _config_lock: config = load_config() apps = config.get("apps", {}) if app_id not in apps: raise HTTPException(status_code=404, detail="App 不存在") apps[app_id] = app config["apps"] = apps save_config(config) return {"message": "App 已更新"} @router.delete("/apps/{app_id}") async def delete_app(app_id: str): """删除指定 app""" async with _config_lock: config = load_config() apps = config.get("apps", {}) if app_id not in apps: raise HTTPException(status_code=404, detail="App 不存在") del apps[app_id] config["apps"] = apps save_config(config) return {"message": "App 已删除"} @router.get("/schemes") async def get_schemes(): """获取 schemes 配置""" config = load_config() return config.get("schemes", {}) @router.post("/schemes") async def create_scheme(scheme: dict): """新增 scheme""" async with _config_lock: config = load_config() schemes = config.get("schemes", {}) numeric_keys = [int(k) for k in schemes.keys() if k.isdigit()] new_id = str(max(numeric_keys) + 1) if numeric_keys else "1" schemes[new_id] = scheme config["schemes"] = schemes save_config(config) return {"id": new_id, "message": "Scheme 已创建"} @router.put("/schemes/{scheme_id}") async def update_scheme(scheme_id: str, scheme: dict): """更新指定 scheme""" async with _config_lock: config = load_config() schemes = config.get("schemes", {}) if scheme_id not in schemes: raise HTTPException(status_code=404, detail="Scheme 不存在") schemes[scheme_id] = scheme config["schemes"] = schemes save_config(config) return {"message": "Scheme 已更新"} @router.delete("/schemes/{scheme_id}") async def delete_scheme(scheme_id: str): """删除指定 scheme""" async with _config_lock: config = load_config() schemes = config.get("schemes", {}) if scheme_id not in schemes: raise HTTPException(status_code=404, detail="Scheme 不存在") del schemes[scheme_id] config["schemes"] = schemes save_config(config) return {"message": "Scheme 已删除"} # 分支管理 API @router.get("/branches") async def get_branches(): """获取分支列表""" config = load_config() return config.get("branches", ["main"]) @router.post("/branches") async def add_branch(data: dict): """新增分支""" name = data.get("name", "").strip() if not name: raise HTTPException(status_code=400, detail="分支名称不能为空") async with _config_lock: config = load_config() branches = config.get("branches", ["main"]) if name in branches: raise HTTPException(status_code=400, detail="分支已存在") branches.append(name) config["branches"] = branches save_config(config) return {"message": "分支已添加"} @router.delete("/branches/{branch_name}") async def delete_branch(branch_name: str): """删除分支""" async with _config_lock: config = load_config() branches = config.get("branches", ["main"]) if branch_name not in branches: raise HTTPException(status_code=404, detail="分支不存在") branches.remove(branch_name) config["branches"] = branches save_config(config) return {"message": "分支已删除"} @router.get("/build") async def get_build_settings(db: Session = Depends(get_db)): """获取打包设置""" return { "max_concurrent_builds": int(get_build_config_value(db, "max_concurrent_builds", DEFAULT_MAX_CONCURRENT_BUILDS)), "build_dir_retention_hours": int(get_build_config_value(db, "build_dir_retention_hours", DEFAULT_BUILD_DIR_RETENTION_HOURS)), "build_base_dir": get_build_config_value(db, "build_base_dir", "/Users/shen/Documents"), } @router.put("/build") async def update_build_settings(settings: BuildConfigUpdate, db: Session = Depends(get_db)): """更新打包设置""" if settings.max_concurrent_builds is not None: config = db.query(BuildConfig).filter(BuildConfig.key == "max_concurrent_builds").first() if config: config.value = str(settings.max_concurrent_builds) else: db.add(BuildConfig(key="max_concurrent_builds", value=str(settings.max_concurrent_builds))) if settings.build_dir_retention_hours is not None: config = db.query(BuildConfig).filter(BuildConfig.key == "build_dir_retention_hours").first() if config: config.value = str(settings.build_dir_retention_hours) else: db.add(BuildConfig(key="build_dir_retention_hours", value=str(settings.build_dir_retention_hours))) if settings.build_base_dir is not None: config = db.query(BuildConfig).filter(BuildConfig.key == "build_base_dir").first() if config: config.value = settings.build_base_dir else: db.add(BuildConfig(key="build_base_dir", value=settings.build_base_dir)) db.commit() return {"message": "打包设置已更新"} # 上传配置 API @router.get("/upload") async def get_upload_config(): """获取上传配置""" config = load_config() return config.get("upload", DEFAULT_UPLOAD) @router.put("/upload") async def update_upload_config(upload_data: dict): """更新上传配置""" async with _config_lock: config = load_config() config["upload"] = upload_data save_config(config) return {"message": "上传配置已更新"} # 服务器环境管理 API @router.get("/servers") async def get_servers(): """获取服务器环境配置""" config = load_config() return config.get("servers", DEFAULT_SERVERS) @router.post("/servers") async def create_server(server_data: dict): """新增服务器环境""" async with _config_lock: config = load_config() servers = config.get("servers", DEFAULT_SERVERS) name = server_data.get("name", "").strip() if not name: raise HTTPException(status_code=400, detail="环境名称不能为空") if name in servers: raise HTTPException(status_code=400, detail="环境名称已存在") servers[name] = { "api": server_data.get("api", ""), "assDom": server_data.get("assDom", ""), "universalLink": server_data.get("universalLink", ""), } config["servers"] = servers save_config(config) return {"message": "服务器环境已创建"} @router.put("/servers/{server_name}") async def update_server(server_name: str, server_data: dict): """更新服务器环境""" async with _config_lock: config = load_config() servers = config.get("servers", DEFAULT_SERVERS) if server_name not in servers: raise HTTPException(status_code=404, detail="环境不存在") # 如果名称变了,需要删除旧的 new_name = server_data.get("name", server_name).strip() if new_name != server_name: if new_name in servers: raise HTTPException(status_code=400, detail="新环境名称已存在") servers[new_name] = servers.pop(server_name) else: new_name = server_name servers[new_name] = { "api": server_data.get("api", ""), "assDom": server_data.get("assDom", ""), "universalLink": server_data.get("universalLink", ""), } config["servers"] = servers save_config(config) return {"message": "服务器环境已更新"} @router.delete("/servers/{server_name}") async def delete_server(server_name: str): """删除服务器环境""" async with _config_lock: config = load_config() servers = config.get("servers", DEFAULT_SERVERS) if server_name not in servers: raise HTTPException(status_code=404, detail="环境不存在") # 检查是否有 App 在使用此环境 apps = config.get("apps", {}) for app in apps.values(): if app.get("server") == server_name: raise HTTPException(status_code=400, detail=f"无法删除:有 App 正在使用此环境") del servers[server_name] config["servers"] = servers save_config(config) return {"message": "服务器环境已删除"}