""" 功能开关管理 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 ( FeatureSwitchCreate, FeatureSwitchUpdate, FeatureSwitchResponse, FeatureSwitchGroupResponse, ResponseModel, ) router = APIRouter(prefix="/features", tags=["功能开关"]) # 功能分组显示名称 FEATURE_GROUP_NAMES = { "exam": "考试模块", "practice": "陪练模块", "broadcast": "播课模块", "course": "课程模块", "yanji": "智能工牌模块", "auth": "认证模块", } 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("/defaults", response_model=List[FeatureSwitchGroupResponse], summary="获取默认功能开关") async def get_default_features( admin: AdminUserInfo = Depends(get_current_admin), ): """获取全局默认的功能开关配置""" conn = get_db_connection() try: with conn.cursor() as cursor: cursor.execute( """ SELECT * FROM feature_switches WHERE tenant_id IS NULL ORDER BY feature_group, id """ ) rows = cursor.fetchall() # 按分组整理 groups: Dict[str, List] = {} for row in rows: group = row["feature_group"] or "other" if group not in groups: groups[group] = [] config = None if row.get("config"): try: config = json.loads(row["config"]) except: pass groups[group].append(FeatureSwitchResponse( id=row["id"], tenant_id=row["tenant_id"], feature_code=row["feature_code"], feature_name=row["feature_name"], feature_group=row["feature_group"], is_enabled=row["is_enabled"], config=config, description=row["description"], created_at=row["created_at"], updated_at=row["updated_at"], )) return [ FeatureSwitchGroupResponse( group_name=group, group_display_name=FEATURE_GROUP_NAMES.get(group, group), features=features, ) for group, features in groups.items() ] finally: conn.close() @router.get("/tenants/{tenant_id}", response_model=List[FeatureSwitchGroupResponse], summary="获取租户功能开关") async def get_tenant_features( 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="租户不存在", ) # 获取默认配置 cursor.execute( """ SELECT * FROM feature_switches WHERE tenant_id IS NULL ORDER BY feature_group, id """ ) default_rows = cursor.fetchall() # 获取租户配置 cursor.execute( """ SELECT * FROM feature_switches WHERE tenant_id = %s """, (tenant_id,) ) tenant_rows = cursor.fetchall() # 合并配置 tenant_features = {row["feature_code"]: row for row in tenant_rows} groups: Dict[str, List] = {} for row in default_rows: group = row["feature_group"] or "other" if group not in groups: groups[group] = [] # 使用租户配置覆盖默认配置 effective_row = tenant_features.get(row["feature_code"], row) config = None if effective_row.get("config"): try: config = json.loads(effective_row["config"]) except: pass groups[group].append(FeatureSwitchResponse( id=effective_row["id"], tenant_id=effective_row["tenant_id"], feature_code=effective_row["feature_code"], feature_name=effective_row["feature_name"], feature_group=effective_row["feature_group"], is_enabled=effective_row["is_enabled"], config=config, description=effective_row["description"], created_at=effective_row["created_at"], updated_at=effective_row["updated_at"], )) return [ FeatureSwitchGroupResponse( group_name=group, group_display_name=FEATURE_GROUP_NAMES.get(group, group), features=features, ) for group, features in groups.items() ] finally: conn.close() @router.put("/tenants/{tenant_id}/{feature_code}", response_model=ResponseModel, summary="更新租户功能开关") async def update_tenant_feature( tenant_id: int, feature_code: str, data: FeatureSwitchUpdate, 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 * FROM feature_switches WHERE tenant_id IS NULL AND feature_code = %s """, (feature_code,) ) default_feature = cursor.fetchone() if not default_feature: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="无效的功能编码", ) # 检查租户是否已有配置 cursor.execute( """ SELECT id, is_enabled FROM feature_switches WHERE tenant_id = %s AND feature_code = %s """, (tenant_id, feature_code) ) existing = cursor.fetchone() if existing: # 更新 old_enabled = existing["is_enabled"] update_fields = [] update_values = [] if data.is_enabled is not None: update_fields.append("is_enabled = %s") update_values.append(data.is_enabled) if data.config is not None: update_fields.append("config = %s") update_values.append(json.dumps(data.config)) if update_fields: update_values.append(existing["id"]) cursor.execute( f"UPDATE feature_switches SET {', '.join(update_fields)} WHERE id = %s", update_values ) else: # 创建租户配置 old_enabled = default_feature["is_enabled"] cursor.execute( """ INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, config, description) VALUES (%s, %s, %s, %s, %s, %s, %s) """, (tenant_id, feature_code, default_feature["feature_name"], default_feature["feature_group"], data.is_enabled if data.is_enabled is not None else default_feature["is_enabled"], json.dumps(data.config) if data.config else default_feature["config"], default_feature["description"]) ) # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "update", "feature", tenant_id, feature_code, old_value={"is_enabled": old_enabled}, new_value={"is_enabled": data.is_enabled, "config": data.config} ) conn.commit() status_text = "启用" if data.is_enabled else "禁用" return ResponseModel(message=f"功能 {default_feature['feature_name']} 已{status_text}") finally: conn.close() @router.delete("/tenants/{tenant_id}/{feature_code}", response_model=ResponseModel, summary="重置租户功能开关") async def reset_tenant_feature( tenant_id: int, feature_code: 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 feature_switches WHERE tenant_id = %s AND feature_code = %s """, (tenant_id, feature_code) ) if cursor.rowcount == 0: return ResponseModel(message="功能配置已是默认值") # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "reset", "feature", tenant_id, feature_code ) conn.commit() return ResponseModel(message="功能配置已重置为默认值") finally: conn.close() @router.post("/tenants/{tenant_id}/batch", response_model=ResponseModel, summary="批量更新功能开关") async def batch_update_tenant_features( tenant_id: int, features: List[Dict], admin: AdminUserInfo = Depends(get_current_admin), ): """ 批量更新租户的功能开关 请求体格式: [ {"feature_code": "exam_module", "is_enabled": true}, {"feature_code": "practice_voice", "is_enabled": false} ] """ 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 feature in features: feature_code = feature.get("feature_code") is_enabled = feature.get("is_enabled") if not feature_code or is_enabled is None: continue # 获取默认配置 cursor.execute( """ SELECT * FROM feature_switches WHERE tenant_id IS NULL AND feature_code = %s """, (feature_code,) ) default_feature = cursor.fetchone() if not default_feature: continue # 检查租户是否已有配置 cursor.execute( """ SELECT id FROM feature_switches WHERE tenant_id = %s AND feature_code = %s """, (tenant_id, feature_code) ) existing = cursor.fetchone() if existing: cursor.execute( "UPDATE feature_switches SET is_enabled = %s WHERE id = %s", (is_enabled, existing["id"]) ) else: cursor.execute( """ INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, description) VALUES (%s, %s, %s, %s, %s, %s) """, (tenant_id, feature_code, default_feature["feature_name"], default_feature["feature_group"], is_enabled, default_feature["description"]) ) updated_count += 1 # 记录操作日志 log_operation( cursor, admin, tenant_id, tenant["code"], "batch_update", "feature", tenant_id, f"批量更新 {updated_count} 项功能开关" ) conn.commit() return ResponseModel(message=f"已更新 {updated_count} 项功能开关") finally: conn.close()