1. 系统设置新增 cleanup 分组(6项配置): - 启用自动清理(开关) - 清理执行时间(默认凌晨3点) - 工单保留天数(默认90天) - 审计日志保留天数(默认180天) - 查询历史保留天数(默认30天) - 定时任务日志保留天数(默认30天) 2. log_cleanup.py: 自动清理服务 - 只清理已完成/已拒绝的工单 - 按保留天数过滤 - 0天=不清理 3. APScheduler cron job: 每天03:00自动清理 4. API endpoints: - POST /settings/cleanup/run — 手动触发清理 - GET /settings/cleanup/preview — 预览待清理数量 5. 前端 PlatformConfigPage: - GROUP_META 新增 cleanup 分组 - CleanupActions 子组件: 预览+手动清理按钮 6. 清空所有历史工单数据(tickets/ticket_approvals/audit_logs/query_histories)
237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
"""系统设置 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()
|
|
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
|