Files
012-kaopeilian/backend/app/api/v1/admin_portal/features.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

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