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