feat: 员工同步配置支持多租户
All checks were successful
continuous-integration/drone/push Build is passing

- 后端新增员工同步配置API(获取/保存/测试连接)
- employee_sync_service 从数据库读取配置
- 前端系统设置页面添加"员工同步"Tab
- 支持配置:数据库主机、端口、库名、用户名、密码、表名
- 保留默认配置用于向后兼容
This commit is contained in:
yuliang_guo
2026-01-31 17:01:30 +08:00
parent 8500308919
commit 78e1bb3dc3
3 changed files with 435 additions and 11 deletions

View File

@@ -40,6 +40,17 @@ class DingtalkConfigResponse(BaseModel):
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="是否启用自动同步")
# ============================================
# 辅助函数
# ============================================
@@ -277,6 +288,163 @@ async def update_dingtalk_config(
)
@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),
@@ -295,12 +463,20 @@ async def get_all_settings(
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), # 是否已配置
"configured": bool(dingtalk_corp_id),
},
"employee_sync": {
"enabled": employee_sync_enabled,
"configured": bool(employee_sync_host),
}
}
)