""" 配置管理 API """ import os import json from typing import Optional, List, Dict import pymysql from fastapi import APIRouter, Depends, HTTPException, status, Query from .auth import get_current_admin, get_db_connection, AdminUserInfo from .schemas import ( ConfigTemplateResponse, TenantConfigResponse, TenantConfigCreate, TenantConfigUpdate, TenantConfigGroupResponse, ConfigBatchUpdate, ResponseModel, ) router = APIRouter(prefix="/configs", tags=["配置管理"]) # 配置分组显示名称 CONFIG_GROUP_NAMES = { "database": "数据库配置", "redis": "Redis配置", "security": "安全配置", "coze": "Coze配置", "ai": "AI服务配置", "yanji": "言迹工牌配置", "storage": "文件存储配置", "basic": "基础配置", "dingtalk": "钉钉配置", } def log_operation(cursor, admin: AdminUserInfo, tenant_id: int, tenant_code: str, operation_type: str, resource_type: str, resource_id: int, resource_name: str, old_value: dict = None, new_value: dict = None): """记录操作日志""" cursor.execute( """ INSERT INTO operation_logs (admin_user_id, admin_username, tenant_id, tenant_code, operation_type, resource_type, resource_id, resource_name, old_value, new_value) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, (admin.id, admin.username, tenant_id, tenant_code, operation_type, resource_type, resource_id, resource_name, json.dumps(old_value, ensure_ascii=False) if old_value else None, json.dumps(new_value, ensure_ascii=False) if new_value else None) ) @router.get("/templates", response_model=List[ConfigTemplateResponse], summary="获取配置模板") async def get_config_templates( config_group: Optional[str] = Query(None, description="配置分组筛选"), admin: AdminUserInfo = Depends(get_current_admin), ): """ 获取配置模板列表 配置模板定义了所有可配置项的元数据 """ conn = get_db_connection() try: with conn.cursor() as cursor: if config_group: cursor.execute( """ SELECT * FROM config_templates WHERE config_group = %s ORDER BY sort_order, id """, (config_group,) ) else: cursor.execute( "SELECT * FROM config_templates ORDER BY config_group, sort_order, id" ) rows = cursor.fetchall() result = [] for row in rows: # 解析 options 字段 options = None if row.get("options"): try: options = json.loads(row["options"]) except: pass result.append(ConfigTemplateResponse( id=row["id"], config_group=row["config_group"], config_key=row["config_key"], display_name=row["display_name"], description=row["description"], value_type=row["value_type"], default_value=row["default_value"], is_required=row["is_required"], is_secret=row["is_secret"], options=options, sort_order=row["sort_order"], )) return result finally: conn.close() @router.get("/groups", response_model=List[Dict], summary="获取配置分组列表") async def get_config_groups(admin: AdminUserInfo = Depends(get_current_admin)): """获取配置分组列表""" conn = get_db_connection() try: with conn.cursor() as cursor: cursor.execute( """ SELECT config_group, COUNT(*) as count FROM config_templates GROUP BY config_group ORDER BY config_group """ ) rows = cursor.fetchall() return [ { "group_name": row["config_group"], "group_display_name": CONFIG_GROUP_NAMES.get(row["config_group"], row["config_group"]), "config_count": row["count"], } for row in rows ] finally: conn.close() @router.get("/tenants/{tenant_id}", response_model=List[TenantConfigGroupResponse], summary="获取租户配置") async def get_tenant_configs( tenant_id: int, config_group: Optional[str] = Query(None, description="配置分组筛选"), admin: AdminUserInfo = Depends(get_current_admin), ): """ 获取租户的所有配置 返回按分组整理的配置列表,包含模板信息 """ conn = get_db_connection() try: with conn.cursor() as cursor: # 验证租户存在 cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,)) tenant = cursor.fetchone() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="租户不存在", ) # 查询配置模板和租户配置 group_filter = "AND ct.config_group = %s" if config_group else "" params = [tenant_id, config_group] if config_group else [tenant_id] cursor.execute( f""" SELECT ct.config_group, ct.config_key, ct.display_name, ct.description, ct.value_type, ct.default_value, ct.is_required, ct.is_secret, ct.sort_order, tc.id as config_id, tc.config_value, tc.is_encrypted, tc.created_at, tc.updated_at FROM config_templates ct LEFT JOIN tenant_configs tc ON tc.config_group = ct.config_group AND tc.config_key = ct.config_key AND tc.tenant_id = %s WHERE 1=1 {group_filter} ORDER BY ct.config_group, ct.sort_order, ct.id """, params ) rows = cursor.fetchall() # 按分组整理 groups: Dict[str, List] = {} for row in rows: group = row["config_group"] if group not in groups: groups[group] = [] # 如果是敏感信息且有值,隐藏部分内容 config_value = row["config_value"] if row["is_secret"] and config_value: if len(config_value) > 8: config_value = config_value[:4] + "****" + config_value[-4:] else: config_value = "****" groups[group].append(TenantConfigResponse( id=row["config_id"] or 0, config_group=row["config_group"], config_key=row["config_key"], config_value=config_value if not row["is_secret"] else row["config_value"], value_type=row["value_type"], is_encrypted=row["is_encrypted"] or False, description=row["description"], created_at=row["created_at"] or None, updated_at=row["updated_at"] or None, display_name=row["display_name"], is_required=row["is_required"], is_secret=row["is_secret"], )) return [ TenantConfigGroupResponse( group_name=group, group_display_name=CONFIG_GROUP_NAMES.get(group, group), configs=configs, ) for group, configs in groups.items() ] finally: conn.close() @router.put("/tenants/{tenant_id}/{config_group}/{config_key}", response_model=ResponseModel, summary="更新单个配置") async def update_tenant_config( tenant_id: int, config_group: str, config_key: str, data: TenantConfigUpdate, admin: AdminUserInfo = Depends(get_current_admin), ): """更新租户的单个配置项""" conn = get_db_connection() try: with conn.cursor() as cursor: # 验证租户存在 cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,)) tenant = cursor.fetchone() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="租户不存在", ) # 验证配置模板存在 cursor.execute( """ SELECT value_type, is_secret FROM config_templates WHERE config_group = %s AND config_key = %s """, (config_group, config_key) ) template = cursor.fetchone() if not template: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="无效的配置项", ) # 检查是否已有配置 cursor.execute( """ SELECT id, config_value FROM tenant_configs WHERE tenant_id = %s AND config_group = %s AND config_key = %s """, (tenant_id, config_group, config_key) ) existing = cursor.fetchone() if existing: # 更新 old_value = existing["config_value"] cursor.execute( """ UPDATE tenant_configs SET config_value = %s, is_encrypted = %s WHERE id = %s """, (data.config_value, template["is_secret"], existing["id"]) ) else: # 插入 old_value = None cursor.execute( """ INSERT INTO tenant_configs (tenant_id, config_group, config_key, config_value, value_type, is_encrypted) VALUES (%s, %s, %s, %s, %s, %s) """, (tenant_id, config_group, config_key, data.config_value, template["value_type"], template["is_secret"]) ) # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "update", "config", tenant_id, f"{config_group}.{config_key}", old_value={"value": old_value} if old_value else None, new_value={"value": data.config_value} ) conn.commit() return ResponseModel(message="配置已更新") finally: conn.close() @router.put("/tenants/{tenant_id}/batch", response_model=ResponseModel, summary="批量更新配置") async def batch_update_tenant_configs( tenant_id: int, data: ConfigBatchUpdate, admin: AdminUserInfo = Depends(get_current_admin), ): """批量更新租户配置""" conn = get_db_connection() try: with conn.cursor() as cursor: # 验证租户存在 cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,)) tenant = cursor.fetchone() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="租户不存在", ) updated_count = 0 for config in data.configs: # 获取模板信息 cursor.execute( """ SELECT value_type, is_secret FROM config_templates WHERE config_group = %s AND config_key = %s """, (config.config_group, config.config_key) ) template = cursor.fetchone() if not template: continue # 检查是否已有配置 cursor.execute( """ SELECT id FROM tenant_configs WHERE tenant_id = %s AND config_group = %s AND config_key = %s """, (tenant_id, config.config_group, config.config_key) ) existing = cursor.fetchone() if existing: cursor.execute( """ UPDATE tenant_configs SET config_value = %s, is_encrypted = %s WHERE id = %s """, (config.config_value, template["is_secret"], existing["id"]) ) else: cursor.execute( """ INSERT INTO tenant_configs (tenant_id, config_group, config_key, config_value, value_type, is_encrypted) VALUES (%s, %s, %s, %s, %s, %s) """, (tenant_id, config.config_group, config.config_key, config.config_value, template["value_type"], template["is_secret"]) ) updated_count += 1 # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "batch_update", "config", tenant_id, f"批量更新 {updated_count} 项配置" ) conn.commit() return ResponseModel(message=f"已更新 {updated_count} 项配置") finally: conn.close() @router.delete("/tenants/{tenant_id}/{config_group}/{config_key}", response_model=ResponseModel, summary="删除配置") async def delete_tenant_config( tenant_id: int, config_group: str, config_key: str, admin: AdminUserInfo = Depends(get_current_admin), ): """删除租户的配置项(恢复为默认值)""" conn = get_db_connection() try: with conn.cursor() as cursor: # 验证租户存在 cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,)) tenant = cursor.fetchone() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="租户不存在", ) # 删除配置 cursor.execute( """ DELETE FROM tenant_configs WHERE tenant_id = %s AND config_group = %s AND config_key = %s """, (tenant_id, config_group, config_key) ) if cursor.rowcount == 0: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="配置不存在", ) # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "delete", "config", tenant_id, f"{config_group}.{config_key}" ) conn.commit() return ResponseModel(message="配置已删除,将使用默认值") finally: conn.close() @router.post("/tenants/{tenant_id}/refresh-cache", response_model=ResponseModel, summary="刷新配置缓存") async def refresh_tenant_config_cache( tenant_id: int, admin: AdminUserInfo = Depends(get_current_admin), ): """刷新租户的配置缓存""" conn = get_db_connection() try: with conn.cursor() as cursor: # 获取租户编码 cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,)) tenant = cursor.fetchone() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="租户不存在", ) # 刷新缓存 try: from app.core.config import DynamicConfig import asyncio asyncio.create_task(DynamicConfig.refresh_cache(tenant["code"])) except Exception as e: pass # 缓存刷新失败不影响主流程 return ResponseModel(message="缓存刷新请求已发送") finally: conn.close()