feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View 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()