- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
481 lines
17 KiB
Python
481 lines
17 KiB
Python
"""
|
|
配置管理 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()
|
|
|