- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
425 lines
15 KiB
Python
425 lines
15 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 (
|
|
FeatureSwitchCreate,
|
|
FeatureSwitchUpdate,
|
|
FeatureSwitchResponse,
|
|
FeatureSwitchGroupResponse,
|
|
ResponseModel,
|
|
)
|
|
|
|
router = APIRouter(prefix="/features", tags=["功能开关"])
|
|
|
|
# 功能分组显示名称
|
|
FEATURE_GROUP_NAMES = {
|
|
"exam": "考试模块",
|
|
"practice": "陪练模块",
|
|
"broadcast": "播课模块",
|
|
"course": "课程模块",
|
|
"yanji": "智能工牌模块",
|
|
}
|
|
|
|
|
|
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("/defaults", response_model=List[FeatureSwitchGroupResponse], summary="获取默认功能开关")
|
|
async def get_default_features(
|
|
admin: AdminUserInfo = Depends(get_current_admin),
|
|
):
|
|
"""获取全局默认的功能开关配置"""
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"""
|
|
SELECT * FROM feature_switches
|
|
WHERE tenant_id IS NULL
|
|
ORDER BY feature_group, id
|
|
"""
|
|
)
|
|
rows = cursor.fetchall()
|
|
|
|
# 按分组整理
|
|
groups: Dict[str, List] = {}
|
|
for row in rows:
|
|
group = row["feature_group"] or "other"
|
|
if group not in groups:
|
|
groups[group] = []
|
|
|
|
config = None
|
|
if row.get("config"):
|
|
try:
|
|
config = json.loads(row["config"])
|
|
except:
|
|
pass
|
|
|
|
groups[group].append(FeatureSwitchResponse(
|
|
id=row["id"],
|
|
tenant_id=row["tenant_id"],
|
|
feature_code=row["feature_code"],
|
|
feature_name=row["feature_name"],
|
|
feature_group=row["feature_group"],
|
|
is_enabled=row["is_enabled"],
|
|
config=config,
|
|
description=row["description"],
|
|
created_at=row["created_at"],
|
|
updated_at=row["updated_at"],
|
|
))
|
|
|
|
return [
|
|
FeatureSwitchGroupResponse(
|
|
group_name=group,
|
|
group_display_name=FEATURE_GROUP_NAMES.get(group, group),
|
|
features=features,
|
|
)
|
|
for group, features in groups.items()
|
|
]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}", response_model=List[FeatureSwitchGroupResponse], summary="获取租户功能开关")
|
|
async def get_tenant_features(
|
|
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="租户不存在",
|
|
)
|
|
|
|
# 获取默认配置
|
|
cursor.execute(
|
|
"""
|
|
SELECT * FROM feature_switches
|
|
WHERE tenant_id IS NULL
|
|
ORDER BY feature_group, id
|
|
"""
|
|
)
|
|
default_rows = cursor.fetchall()
|
|
|
|
# 获取租户配置
|
|
cursor.execute(
|
|
"""
|
|
SELECT * FROM feature_switches
|
|
WHERE tenant_id = %s
|
|
""",
|
|
(tenant_id,)
|
|
)
|
|
tenant_rows = cursor.fetchall()
|
|
|
|
# 合并配置
|
|
tenant_features = {row["feature_code"]: row for row in tenant_rows}
|
|
|
|
groups: Dict[str, List] = {}
|
|
for row in default_rows:
|
|
group = row["feature_group"] or "other"
|
|
if group not in groups:
|
|
groups[group] = []
|
|
|
|
# 使用租户配置覆盖默认配置
|
|
effective_row = tenant_features.get(row["feature_code"], row)
|
|
|
|
config = None
|
|
if effective_row.get("config"):
|
|
try:
|
|
config = json.loads(effective_row["config"])
|
|
except:
|
|
pass
|
|
|
|
groups[group].append(FeatureSwitchResponse(
|
|
id=effective_row["id"],
|
|
tenant_id=effective_row["tenant_id"],
|
|
feature_code=effective_row["feature_code"],
|
|
feature_name=effective_row["feature_name"],
|
|
feature_group=effective_row["feature_group"],
|
|
is_enabled=effective_row["is_enabled"],
|
|
config=config,
|
|
description=effective_row["description"],
|
|
created_at=effective_row["created_at"],
|
|
updated_at=effective_row["updated_at"],
|
|
))
|
|
|
|
return [
|
|
FeatureSwitchGroupResponse(
|
|
group_name=group,
|
|
group_display_name=FEATURE_GROUP_NAMES.get(group, group),
|
|
features=features,
|
|
)
|
|
for group, features in groups.items()
|
|
]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/{feature_code}", response_model=ResponseModel, summary="更新租户功能开关")
|
|
async def update_tenant_feature(
|
|
tenant_id: int,
|
|
feature_code: str,
|
|
data: FeatureSwitchUpdate,
|
|
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 * FROM feature_switches
|
|
WHERE tenant_id IS NULL AND feature_code = %s
|
|
""",
|
|
(feature_code,)
|
|
)
|
|
default_feature = cursor.fetchone()
|
|
|
|
if not default_feature:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="无效的功能编码",
|
|
)
|
|
|
|
# 检查租户是否已有配置
|
|
cursor.execute(
|
|
"""
|
|
SELECT id, is_enabled FROM feature_switches
|
|
WHERE tenant_id = %s AND feature_code = %s
|
|
""",
|
|
(tenant_id, feature_code)
|
|
)
|
|
existing = cursor.fetchone()
|
|
|
|
if existing:
|
|
# 更新
|
|
old_enabled = existing["is_enabled"]
|
|
|
|
update_fields = []
|
|
update_values = []
|
|
|
|
if data.is_enabled is not None:
|
|
update_fields.append("is_enabled = %s")
|
|
update_values.append(data.is_enabled)
|
|
|
|
if data.config is not None:
|
|
update_fields.append("config = %s")
|
|
update_values.append(json.dumps(data.config))
|
|
|
|
if update_fields:
|
|
update_values.append(existing["id"])
|
|
cursor.execute(
|
|
f"UPDATE feature_switches SET {', '.join(update_fields)} WHERE id = %s",
|
|
update_values
|
|
)
|
|
else:
|
|
# 创建租户配置
|
|
old_enabled = default_feature["is_enabled"]
|
|
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO feature_switches
|
|
(tenant_id, feature_code, feature_name, feature_group, is_enabled, config, description)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
""",
|
|
(tenant_id, feature_code, default_feature["feature_name"],
|
|
default_feature["feature_group"],
|
|
data.is_enabled if data.is_enabled is not None else default_feature["is_enabled"],
|
|
json.dumps(data.config) if data.config else default_feature["config"],
|
|
default_feature["description"])
|
|
)
|
|
|
|
# 记录操作日志
|
|
log_operation(
|
|
cursor, admin, tenant_id, tenant["code"],
|
|
"update", "feature", tenant_id, feature_code,
|
|
old_value={"is_enabled": old_enabled},
|
|
new_value={"is_enabled": data.is_enabled, "config": data.config}
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
status_text = "启用" if data.is_enabled else "禁用"
|
|
return ResponseModel(message=f"功能 {default_feature['feature_name']} 已{status_text}")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.delete("/tenants/{tenant_id}/{feature_code}", response_model=ResponseModel, summary="重置租户功能开关")
|
|
async def reset_tenant_feature(
|
|
tenant_id: int,
|
|
feature_code: 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 feature_switches
|
|
WHERE tenant_id = %s AND feature_code = %s
|
|
""",
|
|
(tenant_id, feature_code)
|
|
)
|
|
|
|
if cursor.rowcount == 0:
|
|
return ResponseModel(message="功能配置已是默认值")
|
|
|
|
# 记录操作日志
|
|
log_operation(
|
|
cursor, admin, tenant_id, tenant["code"],
|
|
"reset", "feature", tenant_id, feature_code
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
return ResponseModel(message="功能配置已重置为默认值")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/batch", response_model=ResponseModel, summary="批量更新功能开关")
|
|
async def batch_update_tenant_features(
|
|
tenant_id: int,
|
|
features: List[Dict],
|
|
admin: AdminUserInfo = Depends(get_current_admin),
|
|
):
|
|
"""
|
|
批量更新租户的功能开关
|
|
|
|
请求体格式:
|
|
[
|
|
{"feature_code": "exam_module", "is_enabled": true},
|
|
{"feature_code": "practice_voice", "is_enabled": false}
|
|
]
|
|
"""
|
|
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 feature in features:
|
|
feature_code = feature.get("feature_code")
|
|
is_enabled = feature.get("is_enabled")
|
|
|
|
if not feature_code or is_enabled is None:
|
|
continue
|
|
|
|
# 获取默认配置
|
|
cursor.execute(
|
|
"""
|
|
SELECT * FROM feature_switches
|
|
WHERE tenant_id IS NULL AND feature_code = %s
|
|
""",
|
|
(feature_code,)
|
|
)
|
|
default_feature = cursor.fetchone()
|
|
|
|
if not default_feature:
|
|
continue
|
|
|
|
# 检查租户是否已有配置
|
|
cursor.execute(
|
|
"""
|
|
SELECT id FROM feature_switches
|
|
WHERE tenant_id = %s AND feature_code = %s
|
|
""",
|
|
(tenant_id, feature_code)
|
|
)
|
|
existing = cursor.fetchone()
|
|
|
|
if existing:
|
|
cursor.execute(
|
|
"UPDATE feature_switches SET is_enabled = %s WHERE id = %s",
|
|
(is_enabled, existing["id"])
|
|
)
|
|
else:
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO feature_switches
|
|
(tenant_id, feature_code, feature_name, feature_group, is_enabled, description)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
""",
|
|
(tenant_id, feature_code, default_feature["feature_name"],
|
|
default_feature["feature_group"], is_enabled, default_feature["description"])
|
|
)
|
|
|
|
updated_count += 1
|
|
|
|
# 记录操作日志
|
|
log_operation(
|
|
cursor, admin, tenant_id, tenant["code"],
|
|
"batch_update", "feature", tenant_id, f"批量更新 {updated_count} 项功能开关"
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
return ResponseModel(message=f"已更新 {updated_count} 项功能开关")
|
|
finally:
|
|
conn.close()
|
|
|