All checks were successful
continuous-integration/drone/push Build is passing
- 后端新增员工同步配置API(获取/保存/测试连接) - employee_sync_service 从数据库读取配置 - 前端系统设置页面添加"员工同步"Tab - 支持配置:数据库主机、端口、库名、用户名、密码、表名 - 保留默认配置用于向后兼容
483 lines
17 KiB
Python
483 lines
17 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):
|
||
"""员工同步配置更新请求"""
|
||
db_host: Optional[str] = Field(None, description="数据库主机")
|
||
db_port: Optional[int] = Field(None, description="数据库端口")
|
||
db_name: Optional[str] = Field(None, description="数据库名")
|
||
db_user: Optional[str] = Field(None, description="数据库用户名")
|
||
db_password: Optional[str] = Field(None, description="数据库密码")
|
||
table_name: Optional[str] = Field(None, description="员工表/视图名称")
|
||
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()
|
||
|
||
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("/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)
|
||
|
||
# 获取配置
|
||
db_host = await get_system_config(db, tenant_id, 'employee_sync', 'DB_HOST')
|
||
db_port = await get_system_config(db, tenant_id, 'employee_sync', 'DB_PORT')
|
||
db_name = await get_system_config(db, tenant_id, 'employee_sync', 'DB_NAME')
|
||
db_user = await get_system_config(db, tenant_id, 'employee_sync', 'DB_USER')
|
||
db_password = await get_system_config(db, tenant_id, 'employee_sync', 'DB_PASSWORD')
|
||
table_name = await get_system_config(db, tenant_id, 'employee_sync', 'TABLE_NAME')
|
||
enabled = await get_feature_switch(db, tenant_id, 'employee_sync')
|
||
|
||
# 脱敏处理密码
|
||
password_masked = None
|
||
if db_password:
|
||
if len(db_password) > 4:
|
||
password_masked = '****' + db_password[-2:]
|
||
else:
|
||
password_masked = '****'
|
||
|
||
return ResponseModel(
|
||
message="获取成功",
|
||
data={
|
||
"db_host": db_host,
|
||
"db_port": int(db_port) if db_port else None,
|
||
"db_name": db_name,
|
||
"db_user": db_user,
|
||
"db_password_masked": password_masked,
|
||
"table_name": table_name or "v_钉钉员工表",
|
||
"enabled": 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:
|
||
"""
|
||
更新员工同步配置
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
try:
|
||
if config.db_host is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'DB_HOST', config.db_host)
|
||
|
||
if config.db_port is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'DB_PORT', str(config.db_port))
|
||
|
||
if config.db_name is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'DB_NAME', config.db_name)
|
||
|
||
if config.db_user is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'DB_USER', config.db_user)
|
||
|
||
if config.db_password is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'DB_PASSWORD', config.db_password)
|
||
|
||
if config.table_name is not None:
|
||
await set_system_config(db, tenant_id, 'employee_sync', 'TABLE_NAME', config.table_name)
|
||
|
||
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:
|
||
"""
|
||
测试员工同步数据库连接
|
||
|
||
仅限管理员访问
|
||
"""
|
||
check_admin_permission(current_user)
|
||
|
||
tenant_id = await get_or_create_tenant_id(db)
|
||
|
||
# 获取配置
|
||
db_host = await get_system_config(db, tenant_id, 'employee_sync', 'DB_HOST')
|
||
db_port = await get_system_config(db, tenant_id, 'employee_sync', 'DB_PORT')
|
||
db_name = await get_system_config(db, tenant_id, 'employee_sync', 'DB_NAME')
|
||
db_user = await get_system_config(db, tenant_id, 'employee_sync', 'DB_USER')
|
||
db_password = await get_system_config(db, tenant_id, 'employee_sync', 'DB_PASSWORD')
|
||
table_name = await get_system_config(db, tenant_id, 'employee_sync', 'TABLE_NAME') or "v_钉钉员工表"
|
||
|
||
if not all([db_host, db_port, db_name, db_user, db_password]):
|
||
return ResponseModel(
|
||
code=400,
|
||
message="配置不完整,请先填写所有数据库连接信息"
|
||
)
|
||
|
||
try:
|
||
from sqlalchemy.ext.asyncio import create_async_engine
|
||
|
||
# 构建连接URL
|
||
db_url = f"mysql+aiomysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}?charset=utf8mb4"
|
||
|
||
engine = create_async_engine(db_url, echo=False, pool_pre_ping=True)
|
||
|
||
async with engine.connect() as conn:
|
||
# 测试查询员工表
|
||
result = await conn.execute(text(f"SELECT COUNT(*) FROM {table_name}"))
|
||
count = result.scalar()
|
||
|
||
await engine.dispose()
|
||
|
||
return ResponseModel(
|
||
message=f"连接成功!员工表共有 {count} 条记录",
|
||
data={"employee_count": count}
|
||
)
|
||
|
||
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),
|
||
}
|
||
}
|
||
)
|