refactor: mock清理+安全配置+通用组件+菜单优化

P0安全: SECRET_KEY/ADMIN_PASSWORD/CORS环境变量化, seed仅admin

P1Mock: mock.ts 301->88行, 删8数据mock+5generateMock, net-1300行

P2组件: useToast/useFetchList+ConfirmDialog/FormDialog

P3菜单: menuSections/menuItems->constants/menu.ts

P4API: NotificationsPage/WorkflowPage重写+RolesPage演示

测试: 1050passed, tsc clean
This commit is contained in:
logan committed 2026-05-24 02:42:29 +08:00
1 parent ec3a03cf38
commit 8b921c6c83
33 files changed
+1734 -2006

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
"""系统设置 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}