Files
012-kaopeilian/backend/app/api/v1/endpoints/employee_sync.py
yuliang_guo c97a09de35
Some checks failed
continuous-integration/drone/push Build is failing
feat: 添加通讯录自动同步功能
- 添加 APScheduler 依赖
- 创建定时任务调度模块 scheduler.py
- 增量同步:每30分钟执行
- 完整同步:每天凌晨2点执行
- 添加定时任务管理 API
- 支持环境变量配置同步参数
2026-01-29 15:19:20 +08:00

329 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
员工同步API接口
提供从钉钉员工表同步员工数据的功能
"""
from typing import Any, Dict
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.logger import get_logger
from app.core.deps import get_current_user, get_db
from app.services.employee_sync_service import EmployeeSyncService
from app.models.user import User
logger = get_logger(__name__)
router = APIRouter()
@router.post("/sync", summary="执行员工同步")
async def sync_employees(
*,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
从钉钉员工表同步在职员工数据到考培练系统
权限要求: 仅管理员可执行
同步内容:
- 创建用户账号(用户名=手机号,初始密码=123456
- 创建部门团队
- 创建岗位并关联用户
- 设置领导为团队负责人
Returns:
同步结果统计
"""
# 权限检查:仅管理员可执行
if current_user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以执行员工同步"
)
logger.info(f"管理员 {current_user.username} 开始执行员工同步")
try:
async with EmployeeSyncService(db) as sync_service:
stats = await sync_service.sync_employees()
return {
"success": True,
"message": "员工同步完成",
"data": stats
}
except Exception as e:
logger.error(f"员工同步失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"员工同步失败: {str(e)}"
)
@router.get("/preview", summary="预览待同步员工数据")
async def preview_sync_data(
*,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
预览待同步的员工数据(不执行实际同步)
权限要求: 仅管理员可查看
Returns:
预览数据,包括员工列表、部门列表、岗位列表等
"""
# 权限检查:仅管理员可查看
if current_user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以预览员工数据"
)
logger.info(f"管理员 {current_user.username} 预览员工同步数据")
try:
async with EmployeeSyncService(db) as sync_service:
preview_data = await sync_service.preview_sync_data()
return {
"success": True,
"message": "预览数据获取成功",
"data": preview_data
}
except Exception as e:
logger.error(f"预览数据获取失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"预览数据获取失败: {str(e)}"
)
@router.post("/incremental-sync", summary="增量同步员工")
async def incremental_sync_employees(
*,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
增量同步钉钉员工数据
功能说明:
- 新增:钉钉有但系统没有的员工
- 删除:系统有但钉钉没有的员工(物理删除)
- 跳过:两边都存在的员工(不修改任何信息)
权限要求: 管理员admin 或 manager可执行
Returns:
同步结果统计
"""
# 权限检查:管理员或经理可执行
if current_user.role not in ['admin', 'manager']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以执行员工同步"
)
logger.info(f"用户 {current_user.username} ({current_user.role}) 开始执行增量员工同步")
try:
async with EmployeeSyncService(db) as sync_service:
stats = await sync_service.incremental_sync_employees()
return {
"success": True,
"message": "增量同步完成",
"data": {
"added_count": stats['added_count'],
"deleted_count": stats['deleted_count'],
"skipped_count": stats['skipped_count'],
"added_users": stats['added_users'],
"deleted_users": stats['deleted_users'],
"errors": stats['errors'],
"duration": stats['duration']
}
}
except Exception as e:
logger.error(f"增量同步失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"增量同步失败: {str(e)}"
)
@router.get("/status", summary="查询同步状态")
async def get_sync_status(
*,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
查询当前系统的用户、团队、岗位统计信息
Returns:
统计信息
"""
from sqlalchemy import select, func
from app.models.user import User, Team
from app.models.position import Position
try:
# 统计用户数量
user_count_stmt = select(func.count(User.id)).where(User.is_deleted == False)
user_result = await db.execute(user_count_stmt)
total_users = user_result.scalar()
# 统计各角色用户数量
admin_count_stmt = select(func.count(User.id)).where(
User.is_deleted == False,
User.role == 'admin'
)
admin_result = await db.execute(admin_count_stmt)
admin_count = admin_result.scalar()
manager_count_stmt = select(func.count(User.id)).where(
User.is_deleted == False,
User.role == 'manager'
)
manager_result = await db.execute(manager_count_stmt)
manager_count = manager_result.scalar()
trainee_count_stmt = select(func.count(User.id)).where(
User.is_deleted == False,
User.role == 'trainee'
)
trainee_result = await db.execute(trainee_count_stmt)
trainee_count = trainee_result.scalar()
# 统计团队数量
team_count_stmt = select(func.count(Team.id)).where(Team.is_deleted == False)
team_result = await db.execute(team_count_stmt)
total_teams = team_result.scalar()
# 统计岗位数量
position_count_stmt = select(func.count(Position.id)).where(Position.is_deleted == False)
position_result = await db.execute(position_count_stmt)
total_positions = position_result.scalar()
return {
"success": True,
"data": {
"users": {
"total": total_users,
"admin": admin_count,
"manager": manager_count,
"trainee": trainee_count
},
"teams": total_teams,
"positions": total_positions
}
}
except Exception as e:
logger.error(f"查询统计信息失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"查询统计信息失败: {str(e)}"
)
@router.get("/scheduler/jobs", summary="查看定时同步任务")
async def get_scheduler_jobs(
*,
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
查看定时同步任务列表
权限要求: 仅管理员可查看
Returns:
定时任务列表,包含下次执行时间等信息
"""
if current_user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以查看定时任务"
)
try:
from app.core.scheduler import scheduler_manager
jobs = scheduler_manager.get_jobs()
return {
"success": True,
"message": "获取定时任务成功",
"data": {
"auto_sync_enabled": scheduler_manager.AUTO_SYNC_ENABLED,
"incremental_sync_interval_minutes": scheduler_manager.INCREMENTAL_SYNC_INTERVAL_MINUTES,
"full_sync_hour": scheduler_manager.FULL_SYNC_HOUR,
"jobs": jobs
}
}
except Exception as e:
logger.error(f"获取定时任务失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"获取定时任务失败: {str(e)}"
)
@router.post("/scheduler/trigger/{job_id}", summary="手动触发定时任务")
async def trigger_scheduler_job(
*,
job_id: str,
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
手动触发指定的定时任务
权限要求: 仅管理员可执行
Args:
job_id: 任务ID (employee_incremental_sync 或 employee_full_sync)
Returns:
任务执行结果
"""
if current_user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以触发定时任务"
)
logger.info(f"管理员 {current_user.username} 手动触发定时任务: {job_id}")
try:
from app.core.scheduler import scheduler_manager
result = await scheduler_manager.trigger_job(job_id)
return {
"success": True,
"message": f"任务 {job_id} 执行完成",
"data": result
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
logger.error(f"触发定时任务失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"触发定时任务失败: {str(e)}"
)