Some checks failed
continuous-integration/drone/push Build is failing
1. 课程学习进度追踪
- 新增 UserCourseProgress 和 UserMaterialProgress 模型
- 新增 /api/v1/progress/* 进度追踪 API
- 更新 admin.py 使用真实课程完成率数据
2. 路由权限检查完善
- 新增前端 permissionChecker.ts 权限检查工具
- 更新 router/guard.ts 实现团队和课程权限验证
- 新增后端 permission_service.py
3. AI 陪练音频转文本
- 新增 speech_recognition.py 语音识别服务
- 新增 /api/v1/speech/* API
- 更新 ai-practice-coze.vue 支持语音输入
4. 双人对练报告生成
- 更新 practice_room_service.py 添加报告生成功能
- 新增 /rooms/{room_code}/report API
- 更新 duo-practice-report.vue 调用真实 API
5. 学习提醒推送
- 新增 notification_service.py 通知服务
- 新增 scheduler_service.py 定时任务服务
- 支持钉钉、企微、站内消息推送
6. 智能学习推荐
- 新增 recommendation_service.py 推荐服务
- 新增 /api/v1/recommendations/* API
- 支持错题、能力、进度、热门多维度推荐
7. 安全问题修复
- DEBUG 默认值改为 False
- 添加 SECRET_KEY 安全警告
- 新增 check_security_settings() 检查函数
8. 证书 PDF 生成
- 更新 certificate_service.py 添加 PDF 生成
- 添加 weasyprint、Pillow、qrcode 依赖
- 更新下载 API 支持 PDF 和 PNG 格式
307 lines
10 KiB
Python
307 lines
10 KiB
Python
"""
|
||
系统设置 API
|
||
|
||
供企业管理员(admin角色)配置系统级别的设置,如钉钉免密登录等
|
||
"""
|
||
|
||
from typing import Optional, Dict, Any
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from sqlalchemy import text
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.core.deps import get_current_active_user, get_db
|
||
from app.core.logger import logger
|
||
from app.models.user import User
|
||
from app.schemas.base import ResponseModel
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
# ============================================
|
||
# Schema 定义
|
||
# ============================================
|
||
|
||
class DingtalkConfigUpdate(BaseModel):
|
||
"""钉钉配置更新请求"""
|
||
app_key: Optional[str] = Field(None, description="钉钉AppKey")
|
||
app_secret: Optional[str] = Field(None, description="钉钉AppSecret")
|
||
agent_id: Optional[str] = Field(None, description="钉钉AgentId")
|
||
corp_id: Optional[str] = Field(None, description="钉钉CorpId")
|
||
enabled: Optional[bool] = Field(None, description="是否启用钉钉免密登录")
|
||
|
||
|
||
class DingtalkConfigResponse(BaseModel):
|
||
"""钉钉配置响应"""
|
||
app_key: Optional[str] = None
|
||
app_secret_masked: Optional[str] = None # 脱敏显示
|
||
agent_id: Optional[str] = None
|
||
corp_id: Optional[str] = None
|
||
enabled: bool = False
|
||
|
||
|
||
# ============================================
|
||
# 辅助函数
|
||
# ============================================
|
||
|
||
def check_admin_permission(user: User):
|
||
"""检查是否为管理员"""
|
||
if user.role != 'admin':
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="需要管理员权限"
|
||
)
|
||
|
||
|
||
async def get_or_create_tenant_id(db: AsyncSession) -> int:
|
||
"""获取或创建默认租户ID(简化版,假设单租户)"""
|
||
# 对于考培练系统,简化处理,使用固定的租户ID=1
|
||
return 1
|
||
|
||
|
||
async def get_system_config(db: AsyncSession, tenant_id: int, config_group: str, config_key: str) -> Optional[str]:
|
||
"""获取系统配置值"""
|
||
result = await db.execute(
|
||
text("""
|
||
SELECT config_value FROM tenant_configs
|
||
WHERE tenant_id = :tenant_id AND config_group = :config_group AND config_key = :config_key
|
||
"""),
|
||
{"tenant_id": tenant_id, "config_group": config_group, "config_key": config_key}
|
||
)
|
||
row = result.fetchone()
|
||
return row[0] if row else None
|
||
|
||
|
||
async def set_system_config(db: AsyncSession, tenant_id: int, config_group: str, config_key: str, config_value: str):
|
||
"""设置系统配置值"""
|
||
# 检查是否已存在
|
||
existing = await get_system_config(db, tenant_id, config_group, config_key)
|
||
|
||
if existing is not None:
|
||
# 更新
|
||
await db.execute(
|
||
text("""
|
||
UPDATE tenant_configs
|
||
SET config_value = :config_value
|
||
WHERE tenant_id = :tenant_id AND config_group = :config_group AND config_key = :config_key
|
||
"""),
|
||
{"tenant_id": tenant_id, "config_group": config_group, "config_key": config_key, "config_value": config_value}
|
||
)
|
||
else:
|
||
# 插入
|
||
await db.execute(
|
||
text("""
|
||
INSERT INTO tenant_configs (tenant_id, config_group, config_key, config_value, value_type, is_encrypted)
|
||
VALUES (:tenant_id, :config_group, :config_key, :config_value, 'string', :is_encrypted)
|
||
"""),
|
||
{
|
||
"tenant_id": tenant_id,
|
||
"config_group": config_group,
|
||
"config_key": config_key,
|
||
"config_value": config_value,
|
||
"is_encrypted": 1 if config_key == 'DINGTALK_APP_SECRET' else 0
|
||
}
|
||
)
|
||
|
||
|
||
async def get_feature_switch(db: AsyncSession, tenant_id: int, feature_code: str) -> bool:
|
||
"""获取功能开关状态"""
|
||
# 先查租户级别
|
||
result = await db.execute(
|
||
text("""
|
||
SELECT is_enabled FROM feature_switches
|
||
WHERE feature_code = :feature_code AND tenant_id = :tenant_id
|
||
"""),
|
||
{"tenant_id": tenant_id, "feature_code": feature_code}
|
||
)
|
||
row = result.fetchone()
|
||
if row:
|
||
return bool(row[0])
|
||
|
||
# 再查默认值
|
||
result = await db.execute(
|
||
text("""
|
||
SELECT is_enabled FROM feature_switches
|
||
WHERE feature_code = :feature_code AND tenant_id IS NULL
|
||
"""),
|
||
{"feature_code": feature_code}
|
||
)
|
||
row = result.fetchone()
|
||
return bool(row[0]) if row else False
|
||
|
||
|
||
async def set_feature_switch(db: AsyncSession, tenant_id: int, feature_code: str, is_enabled: bool):
|
||
"""设置功能开关状态"""
|
||
# 检查是否已存在租户级配置
|
||
result = await db.execute(
|
||
text("""
|
||
SELECT id FROM feature_switches
|
||
WHERE feature_code = :feature_code AND tenant_id = :tenant_id
|
||
"""),
|
||
{"tenant_id": tenant_id, "feature_code": feature_code}
|
||
)
|
||
row = result.fetchone()
|
||
|
||
if row:
|
||
# 更新
|
||
await db.execute(
|
||
text("""
|
||
UPDATE feature_switches
|
||
SET is_enabled = :is_enabled
|
||
WHERE tenant_id = :tenant_id AND feature_code = :feature_code
|
||
"""),
|
||
{"tenant_id": tenant_id, "feature_code": feature_code, "is_enabled": 1 if is_enabled else 0}
|
||
)
|
||
else:
|
||
# 获取默认配置信息
|
||
result = await db.execute(
|
||
text("""
|
||
SELECT feature_name, feature_group, description FROM feature_switches
|
||
WHERE feature_code = :feature_code AND tenant_id IS NULL
|
||
"""),
|
||
{"feature_code": feature_code}
|
||
)
|
||
default_row = result.fetchone()
|
||
|
||
if default_row:
|
||
# 插入租户级配置
|
||
await db.execute(
|
||
text("""
|
||
INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, description)
|
||
VALUES (:tenant_id, :feature_code, :feature_name, :feature_group, :is_enabled, :description)
|
||
"""),
|
||
{
|
||
"tenant_id": tenant_id,
|
||
"feature_code": feature_code,
|
||
"feature_name": default_row[0],
|
||
"feature_group": default_row[1],
|
||
"is_enabled": 1 if is_enabled else 0,
|
||
"description": default_row[2]
|
||
}
|
||
)
|
||
|
||
|
||
# ============================================
|
||
# API 端点
|
||
# ============================================
|
||
|
||
@router.get("/dingtalk", response_model=ResponseModel)
|
||
async def get_dingtalk_config(
|
||
current_user: User = Depends(get_current_active_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""
|
||
获取钉钉配置
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
# 获取配置
|
||
app_key = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_KEY')
|
||
app_secret = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_SECRET')
|
||
agent_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_AGENT_ID')
|
||
corp_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_CORP_ID')
|
||
enabled = await get_feature_switch(db, tenant_id, 'dingtalk_login')
|
||
|
||
# 脱敏处理 app_secret
|
||
app_secret_masked = None
|
||
if app_secret:
|
||
if len(app_secret) > 8:
|
||
app_secret_masked = app_secret[:4] + '****' + app_secret[-4:]
|
||
else:
|
||
app_secret_masked = '****'
|
||
|
||
return ResponseModel(
|
||
message="获取成功",
|
||
data={
|
||
"app_key": app_key,
|
||
"app_secret_masked": app_secret_masked,
|
||
"agent_id": agent_id,
|
||
"corp_id": corp_id,
|
||
"enabled": enabled,
|
||
}
|
||
)
|
||
|
||
|
||
@router.put("/dingtalk", response_model=ResponseModel)
|
||
async def update_dingtalk_config(
|
||
config: DingtalkConfigUpdate,
|
||
current_user: User = Depends(get_current_active_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""
|
||
更新钉钉配置
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
try:
|
||
# 更新配置
|
||
if config.app_key is not None:
|
||
await set_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_KEY', config.app_key)
|
||
|
||
if config.app_secret is not None:
|
||
await set_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_SECRET', config.app_secret)
|
||
|
||
if config.agent_id is not None:
|
||
await set_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_AGENT_ID', config.agent_id)
|
||
|
||
if config.corp_id is not None:
|
||
await set_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_CORP_ID', config.corp_id)
|
||
|
||
if config.enabled is not None:
|
||
await set_feature_switch(db, tenant_id, 'dingtalk_login', config.enabled)
|
||
|
||
await db.commit()
|
||
|
||
logger.info(
|
||
"钉钉配置已更新",
|
||
user_id=current_user.id,
|
||
username=current_user.username,
|
||
)
|
||
|
||
return ResponseModel(message="配置已保存")
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"更新钉钉配置失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="保存配置失败"
|
||
)
|
||
|
||
|
||
@router.get("/all", response_model=ResponseModel)
|
||
async def get_all_settings(
|
||
current_user: User = Depends(get_current_active_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""
|
||
获取所有系统设置概览
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
# 钉钉配置状态
|
||
dingtalk_enabled = await get_feature_switch(db, tenant_id, 'dingtalk_login')
|
||
dingtalk_corp_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_CORP_ID')
|
||
|
||
return ResponseModel(
|
||
message="获取成功",
|
||
data={
|
||
"dingtalk": {
|
||
"enabled": dingtalk_enabled,
|
||
"configured": bool(dingtalk_corp_id), # 是否已配置
|
||
}
|
||
}
|
||
)
|