feat: 员工同步改为钉钉开放API方式
All checks were successful
continuous-integration/drone/push Build is passing

- 新增 dingtalk_service.py 调用钉钉开放API
- 支持获取 Access Token、部门列表、员工列表
- employee_sync_service 改为从钉钉API获取员工
- 前端配置界面支持配置 CorpId、ClientId、ClientSecret
- 移除外部数据库表依赖
This commit is contained in:
yuliang_guo
2026-01-31 17:25:44 +08:00
parent cabc3c3442
commit 7be1ac1787
4 changed files with 441 additions and 145 deletions

View File

@@ -1,6 +1,6 @@
"""
员工同步服务
外部钉钉员工表同步员工数据到考培练系统
钉钉开放 API 同步员工数据到考培练系统
"""
from typing import List, Dict, Any, Optional, Tuple
@@ -23,115 +23,79 @@ logger = get_logger(__name__)
class EmployeeSyncService:
"""员工同步服务"""
# 默认外部数据库连接配置(向后兼容,从环境变量读取)
DEFAULT_TABLE_NAME = "v_钉钉员工表"
def __init__(self, db: AsyncSession, tenant_id: int = 1):
self.db = db
self.tenant_id = tenant_id
self.external_engine = None
self.table_name = self.DEFAULT_TABLE_NAME
self._db_url = None
self._dingtalk_config = None
async def _get_table_name_from_db(self) -> str:
"""从数据库获取员工表名配置"""
async def _get_dingtalk_config(self) -> Dict[str, str]:
"""从数据库获取钉钉 API 配置"""
if self._dingtalk_config:
return self._dingtalk_config
try:
result = await self.db.execute(
text("""
SELECT config_value
SELECT config_key, config_value
FROM tenant_configs
WHERE tenant_id = :tenant_id
AND config_group = 'employee_sync'
AND config_key = 'TABLE_NAME'
AND config_group = 'employee_sync'
"""),
{"tenant_id": self.tenant_id}
)
row = result.fetchone()
return row[0] if row else self.DEFAULT_TABLE_NAME
except Exception:
return self.DEFAULT_TABLE_NAME
rows = result.fetchall()
config = {}
for key, value in rows:
config[key] = value
self._dingtalk_config = config
return config
except Exception as e:
logger.error(f"获取钉钉配置失败: {e}")
return {}
def _get_db_url_from_env(self) -> str:
"""从环境变量获取数据库连接URL"""
import os
# 优先使用环境变量中的完整URL
db_url = os.environ.get('EMPLOYEE_SYNC_DB_URL', '')
if db_url:
return db_url
# 向后兼容:如果没有配置环境变量,使用默认值
logger.warning("EMPLOYEE_SYNC_DB_URL 环境变量未配置,使用默认数据源")
return "mysql+aiomysql://neuron_new:NWxGM6CQoMLKyEszXhfuLBIIo1QbeK@120.77.144.233:29613/neuron_new?charset=utf8mb4"
async def __aenter__(self):
"""异步上下文管理器入口"""
self._db_url = self._get_db_url_from_env()
self.table_name = await self._get_table_name_from_db()
self.external_engine = create_async_engine(
self._db_url,
echo=False,
pool_pre_ping=True,
pool_recycle=3600
)
# 预加载钉钉配置
await self._get_dingtalk_config()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
if self.external_engine:
await self.external_engine.dispose()
pass
async def fetch_employees_from_dingtalk(self) -> List[Dict[str, Any]]:
"""
从钉钉员工表获取在职员工数据
从钉钉 API 获取在职员工数据
Returns:
员工数据列表
"""
logger.info(f"开始从员工表 {self.table_name} 获取数据...")
config = await self._get_dingtalk_config()
query = f"""
SELECT
员工姓名,
手机号,
邮箱,
所属部门,
职位,
工号,
是否领导,
是否在职,
钉钉用户ID,
入职日期,
工作地点
FROM {self.table_name}
WHERE 是否在职 = 1
ORDER BY 员工姓名
"""
corp_id = config.get('CORP_ID')
client_id = config.get('CLIENT_ID')
client_secret = config.get('CLIENT_SECRET')
async with self.external_engine.connect() as conn:
result = await conn.execute(text(query))
rows = result.fetchall()
employees = []
for row in rows:
employees.append({
'full_name': row[0],
'phone': row[1],
'email': row[2],
'department': row[3],
'position': row[4],
'employee_no': row[5],
'is_leader': bool(row[6]),
'is_active': bool(row[7]),
'dingtalk_id': row[8],
'join_date': row[9],
'work_location': row[10]
})
logger.info(f"获取到 {len(employees)} 条在职员工数据")
return employees
if not all([corp_id, client_id, client_secret]):
raise Exception("钉钉 API 配置不完整,请先配置 CorpId、ClientId、ClientSecret")
from app.services.dingtalk_service import DingTalkService
dingtalk = DingTalkService(
corp_id=corp_id,
client_id=client_id,
client_secret=client_secret
)
employees = await dingtalk.get_all_employees()
# 过滤在职员工
active_employees = [emp for emp in employees if emp.get('is_active', True)]
logger.info(f"获取到 {len(active_employees)} 条在职员工数据")
return active_employees
def generate_email(self, phone: str, original_email: Optional[str]) -> Optional[str]:
"""