"""系统设置 API — GET/PUT 分组设置,自动初始化种子数据。""" from typing import List, Dict from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel from app.core.database import get_db from app.core.security import require_role from app.models.user import User from app.models.system_setting import SystemSetting from app.schemas.system_setting import SettingItem, SettingUpdateItem from app.services.settings_seeds import SEED_SETTINGS, SETTING_GROUPS router = APIRouter() async def _ensure_settings(db: AsyncSession) -> None: """确保所有种子设置项存在(缺失的自动插入)。""" result = await db.execute(select(SystemSetting.key)) existing = {row[0] for row in result.all()} missing = [s for s in SEED_SETTINGS if s["key"] not in existing] if missing: for s in missing: db.add(SystemSetting(**s)) await db.commit() @router.get("", response_model=Dict[str, List[SettingItem]]) async def get_settings( db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """获取所有系统设置,按分组返回。""" await _ensure_settings(db) result = await db.execute( select(SystemSetting).order_by(SystemSetting.group, SystemSetting.sort_order) ) settings = result.scalars().all() grouped: Dict[str, List[SettingItem]] = {} for s in settings: group = s.group if group not in grouped: grouped[group] = [] grouped[group].append(SettingItem( key=s.key, value=s.value, value_type=s.value_type, group=s.group, label=s.label, description=s.description, default_value=s.default_value, sort_order=s.sort_order, )) return grouped @router.get("/groups") async def get_setting_groups( db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """获取设置分组信息。""" await _ensure_settings(db) return SETTING_GROUPS @router.put("") async def update_settings( items: List[SettingUpdateItem], db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """批量更新系统设置。""" await _ensure_settings(db) updated = [] for item in items: result = await db.execute( select(SystemSetting).where(SystemSetting.key == item.key) ) setting = result.scalar_one_or_none() if not setting: continue # 类型校验 if setting.value_type == "boolean" and item.value not in ("true", "false"): raise HTTPException(400, f"设置 '{setting.label}' 需要布尔值 (true/false)") if setting.value_type == "integer": try: int(item.value) except ValueError: raise HTTPException(400, f"设置 '{setting.label}' 需要数字") # 范围校验 if setting.key == "query_max_limit": v = int(item.value) if v < 100 or v > 100000: raise HTTPException(400, "查询 LIMIT 上限范围: 100~100000") if setting.key == "query_timeout": v = int(item.value) if v < 5 or v > 300: raise HTTPException(400, "查询超时范围: 5~300 秒") if setting.key == "query_max_length": v = int(item.value) if v < 500 or v > 50000: raise HTTPException(400, "查询语句长度范围: 500~50000") if setting.key == "security_password_min_length": v = int(item.value) if v < 4 or v > 32: raise HTTPException(400, "密码最小长度范围: 4~32") if setting.key == "security_max_login_attempts": v = int(item.value) if v < 0 or v > 20: raise HTTPException(400, "最大登录失败次数范围: 0~20") setting.value = item.value updated.append(setting.key) await db.commit() # 刷新 query_service 的配置缓存,使新设置立即生效 try: from app.services.query_service import refresh_config await refresh_config() except Exception: pass return {"updated": updated} # ── 供其他模块调用的工具函数 ── async def get_setting_value(db: AsyncSession, key: str, fallback=None): """获取设置值的实际类型(给其他模块用)。""" result = await db.execute( select(SystemSetting).where(SystemSetting.key == key) ) setting = result.scalar_one_or_none() if setting: return setting.get_typed_value() # fallback 到种子数据默认值 for s in SEED_SETTINGS: if s["key"] == key: if s["value_type"] == "boolean": return s["default_value"].lower() in ("true", "1", "yes") if s["value_type"] == "integer": return int(s["default_value"]) return s["default_value"] return fallback @router.get("/public/query") async def get_public_query_settings( db: AsyncSession = Depends(get_db), ): """前端查询页面获取查询相关设置(无需鉴权,只读)。""" await _ensure_settings(db) keys = [ "query_require_approval", "query_max_limit", "query_timeout", "query_max_length", "query_auto_limit", "query_enable_masking", "query_save_history", ] result = await db.execute( select(SystemSetting).where(SystemSetting.key.in_(keys)) ) settings = result.scalars().all() return {s.key: s.get_typed_value() for s in settings} @router.post("/cleanup/run") async def run_manual_cleanup( current_user: User = Depends(require_role("admin")), ): """管理员手动触发日志清理。""" from app.services.log_cleanup import run_cleanup stats = await run_cleanup() return {"message": "清理完成", "stats": stats} @router.get("/cleanup/preview") async def preview_cleanup( db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """预览将被清理的记录数量(不实际删除)。""" from datetime import datetime, timedelta, timezone as tz from sqlalchemy import func as sa_func await _ensure_settings(db) async def _get_int(key: str) -> int: result = await db.execute(select(SystemSetting).where(SystemSetting.key == key)) s = result.scalar_one_or_none() return int(s.value) if s else 0 preview = {} # 工单 ticket_days = await _get_int('cleanup_ticket_days') if ticket_days > 0: cutoff = datetime.now(tz.utc) - timedelta(days=ticket_days) from app.models.ticket import Ticket result = await db.execute( select(sa_func.count(Ticket.id)).where( Ticket.status.in_(['success', 'failed', 'rejected']), Ticket.create_time < cutoff, ) ) preview['tickets'] = result.scalar() # 审计日志 audit_days = await _get_int('cleanup_audit_log_days') if audit_days > 0: cutoff = datetime.now(tz.utc) - timedelta(days=audit_days) from app.models.audit_log import AuditLog result = await db.execute( select(sa_func.count(AuditLog.id)).where(AuditLog.created_at < cutoff) ) preview['audit_logs'] = result.scalar() # 查询历史 query_days = await _get_int('cleanup_query_history_days') if query_days > 0: cutoff = datetime.now(tz.utc) - timedelta(days=query_days) from app.models.query_history import QueryHistory result = await db.execute( select(sa_func.count(QueryHistory.id)).where(QueryHistory.created_at < cutoff) ) preview['query_histories'] = result.scalar() # 定时任务日志 task_log_days = await _get_int('cleanup_scheduled_task_log_days') if task_log_days > 0: cutoff = datetime.now(tz.utc) - timedelta(days=task_log_days) from app.models.scheduled_task import ScheduledTaskLog result = await db.execute( select(sa_func.count(ScheduledTaskLog.id)).where(ScheduledTaskLog.started_at < cutoff) ) preview['scheduled_task_logs'] = result.scalar() return preview