Some checks failed
continuous-integration/drone/push Build is failing
当 feature_switches 表中没有默认记录时,set_feature_switch 函数 现在会使用预定义的默认值创建记录,而不是静默失败
466 lines
16 KiB
Python
466 lines
16 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
|
||
|
||
|
||
class EmployeeSyncConfigUpdate(BaseModel):
|
||
"""员工同步配置更新请求(复用钉钉免密登录配置)"""
|
||
enabled: Optional[bool] = Field(None, description="是否启用自动同步")
|
||
|
||
|
||
# ============================================
|
||
# 辅助函数
|
||
# ============================================
|
||
|
||
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()
|
||
|
||
# 定义功能开关默认值映射
|
||
FEATURE_DEFAULTS = {
|
||
'employee_sync': ('员工同步', 'system', '从钉钉同步员工信息'),
|
||
'dingtalk_login': ('钉钉免密登录', 'system', '允许通过钉钉免密登录'),
|
||
'ai_practice': ('AI 对练', 'feature', 'AI 对练功能'),
|
||
'dual_practice': ('双人对练', 'feature', '双人对练功能'),
|
||
}
|
||
|
||
if default_row:
|
||
feature_name = default_row[0]
|
||
feature_group = default_row[1]
|
||
description = default_row[2]
|
||
elif feature_code in FEATURE_DEFAULTS:
|
||
feature_name, feature_group, description = FEATURE_DEFAULTS[feature_code]
|
||
else:
|
||
# 未知功能码,使用默认值
|
||
feature_name = feature_code
|
||
feature_group = 'system'
|
||
description = f'{feature_code} 功能开关'
|
||
|
||
# 插入租户级配置
|
||
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": feature_name,
|
||
"feature_group": feature_group,
|
||
"is_enabled": 1 if is_enabled else 0,
|
||
"description": description
|
||
}
|
||
)
|
||
|
||
|
||
# ============================================
|
||
# 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("/employee-sync", response_model=ResponseModel)
|
||
async def get_employee_sync_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)
|
||
|
||
# 从 dingtalk 配置组读取(与免密登录共用)
|
||
corp_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_CORP_ID')
|
||
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')
|
||
enabled = await get_feature_switch(db, tenant_id, 'employee_sync')
|
||
dingtalk_enabled = await get_feature_switch(db, tenant_id, 'dingtalk_login')
|
||
|
||
# 检查钉钉配置是否完整
|
||
configured = bool(corp_id and app_key and app_secret)
|
||
|
||
return ResponseModel(
|
||
message="获取成功",
|
||
data={
|
||
"enabled": enabled,
|
||
"configured": configured,
|
||
"dingtalk_enabled": dingtalk_enabled, # 免密登录是否启用
|
||
}
|
||
)
|
||
|
||
|
||
@router.put("/employee-sync", response_model=ResponseModel)
|
||
async def update_employee_sync_config(
|
||
config: EmployeeSyncConfigUpdate,
|
||
current_user: User = Depends(get_current_active_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""
|
||
更新员工同步配置(仅开关,API 凭证复用钉钉免密登录)
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
try:
|
||
if config.enabled is not None:
|
||
await set_feature_switch(db, tenant_id, 'employee_sync', 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.post("/employee-sync/test", response_model=ResponseModel)
|
||
async def test_employee_sync_connection(
|
||
current_user: User = Depends(get_current_active_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""
|
||
测试钉钉 API 连接(复用免密登录配置)
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
# 从 dingtalk 配置组读取(与免密登录共用)
|
||
corp_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_CORP_ID')
|
||
client_id = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_KEY')
|
||
client_secret = await get_system_config(db, tenant_id, 'dingtalk', 'DINGTALK_APP_SECRET')
|
||
|
||
if not all([corp_id, client_id, client_secret]):
|
||
return ResponseModel(
|
||
code=400,
|
||
message="请先在「钉钉免密登录」页签配置 CorpId、AppKey、AppSecret"
|
||
)
|
||
|
||
try:
|
||
from app.services.dingtalk_service import DingTalkService
|
||
|
||
dingtalk = DingTalkService(
|
||
corp_id=corp_id,
|
||
client_id=client_id,
|
||
client_secret=client_secret
|
||
)
|
||
|
||
result = await dingtalk.test_connection()
|
||
|
||
if result["success"]:
|
||
return ResponseModel(
|
||
message=f"连接成功!已获取到组织架构",
|
||
data=result
|
||
)
|
||
else:
|
||
return ResponseModel(
|
||
code=500,
|
||
message=result["message"]
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"测试连接失败: {str(e)}")
|
||
return ResponseModel(
|
||
code=500,
|
||
message=f"连接失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@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')
|
||
|
||
# 员工同步配置状态
|
||
employee_sync_enabled = await get_feature_switch(db, tenant_id, 'employee_sync')
|
||
employee_sync_host = await get_system_config(db, tenant_id, 'employee_sync', 'DB_HOST')
|
||
|
||
return ResponseModel(
|
||
message="获取成功",
|
||
data={
|
||
"dingtalk": {
|
||
"enabled": dingtalk_enabled,
|
||
"configured": bool(dingtalk_corp_id),
|
||
},
|
||
"employee_sync": {
|
||
"enabled": employee_sync_enabled,
|
||
"configured": bool(employee_sync_host),
|
||
}
|
||
}
|
||
)
|