Files
012-kaopeilian/backend/app/api/v1/admin_portal/configs.py
yuliang_guo 662947cd06
Some checks failed
continuous-integration/drone/push Build is failing
feat: 添加钉钉扫码登录功能
- 后端:钉钉 OAuth 认证服务
- 后端:系统设置 API(钉钉配置)
- 前端:登录页钉钉扫码入口
- 前端:系统设置页面
- 数据库迁移脚本
2026-01-29 14:40:00 +08:00

482 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": "基础配置",
"dingtalk": "钉钉配置",
}
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()