- 后端:钉钉 OAuth 认证服务 - 后端:系统设置 API(钉钉配置) - 前端:登录页钉钉扫码入口 - 前端:系统设置页面 - 数据库迁移脚本
This commit is contained in:
306
backend/app/api/v1/system_settings.py
Normal file
306
backend/app/api/v1/system_settings.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
系统设置 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), # 是否已配置
|
||||
}
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user