feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
480
backend/app/api/v1/admin_portal/configs.py
Normal file
480
backend/app/api/v1/admin_portal/configs.py
Normal file
@@ -0,0 +1,480 @@
|
||||
"""
|
||||
配置管理 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": "基础配置",
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user