feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
424
backend/app/api/v1/admin_portal/features.py
Normal file
424
backend/app/api/v1/admin_portal/features.py
Normal file
@@ -0,0 +1,424 @@
|
||||
"""
|
||||
功能开关管理 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()
|
||||
|
||||
Reference in New Issue
Block a user