Files
ai-agent-admin/backend-fastapi/core/wecom_sync/callback_handler.py
T
2026-06-08 18:14:59 +08:00

310 lines
11 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.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
企业微信回调事件处理器
处理企业微信事件订阅推送的增量变更事件:
- 部门:create_party / update_party / delete_party
- 成员:create_user / update_user / delete_user
"""
import logging
from typing import Any, Dict, List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config_manager import config_manager
from app.database import AsyncSessionLocal
from core.dept.model import Dept
from core.user.model import User
from core.user.service import UserService
from core.wecom_sync.client import WecomClient
from core.wecom_sync.crypto import WecomCrypto
logger = logging.getLogger(__name__)
DEPT_EVENTS = {"create_party", "update_party", "delete_party"}
USER_EVENTS = {"create_user", "update_user", "delete_user"}
class WecomCallbackHandler:
"""企业微信回调事件处理器"""
@classmethod
async def get_crypto(cls) -> WecomCrypto:
"""从配置获取加解密实例"""
config = await config_manager.get_group("sync_wecom")
token = config.get("callback_token")
aes_key = config.get("callback_aes_key")
corp_id = config.get("corp_id")
if not all([token, aes_key, corp_id]):
raise ValueError("企业微信回调配置不完整(callback_token / callback_aes_key / corp_id")
return WecomCrypto(token=token, aes_key=aes_key, corp_id=corp_id)
@classmethod
async def get_client(cls) -> WecomClient:
"""从配置获取企业微信客户端"""
config = await config_manager.get_group("sync_wecom")
corp_id = config.get("corp_id")
corp_secret = config.get("corp_secret")
if not corp_id or not corp_secret:
raise ValueError("企业微信同步凭证未配置")
return WecomClient(corp_id=corp_id, corp_secret=corp_secret)
@classmethod
async def handle_event(cls, event_type: str, event_data: Dict[str, Any]) -> None:
"""分发事件到对应处理方法"""
config = await config_manager.get_group("sync_wecom")
enable_dept = config.get("enable_dept_event") == "true"
enable_user = config.get("enable_user_event") == "true"
if event_type in DEPT_EVENTS and enable_dept:
await cls._handle_dept_event(event_type, event_data)
elif event_type in USER_EVENTS and enable_user:
await cls._handle_user_event(event_type, event_data)
else:
logger.info(f"忽略未处理的事件: {event_type}")
# ==================== 部门事件 ====================
@classmethod
async def _handle_dept_event(cls, event_type: str, event_data: Dict[str, Any]) -> None:
"""处理部门变更事件"""
dept_id = event_data.get("Id")
if not dept_id:
logger.warning(f"部门事件缺少 Id: {event_data}")
return
client = await cls.get_client()
async with AsyncSessionLocal() as db:
try:
if event_type == "delete_party":
await cls._remove_dept(db, int(dept_id))
else:
await cls._upsert_dept(db, client, int(dept_id))
except Exception as e:
logger.error(f"处理部门事件失败 dept_id={dept_id}: {e}")
await db.commit()
@classmethod
async def _upsert_dept(cls, db: AsyncSession, client: WecomClient, wecom_dept_id: int) -> None:
"""创建或更新单个部门"""
detail = await client.get_dept_detail(wecom_dept_id)
name = detail.get("name", "")
wecom_parent_id = detail.get("parentid")
result = await db.execute(
select(Dept).where(
Dept.wecom_dept_id == str(wecom_dept_id),
Dept.is_deleted == False, # noqa: E712
)
)
local_dept = result.scalar_one_or_none()
parent_id = None
level = 0
path = "/"
if wecom_parent_id:
parent_result = await db.execute(
select(Dept).where(
Dept.wecom_dept_id == str(wecom_parent_id),
Dept.is_deleted == False, # noqa: E712
)
)
parent_dept = parent_result.scalar_one_or_none()
if parent_dept:
parent_id = parent_dept.id
level = parent_dept.level + 1
path = f"{parent_dept.path or '/'}{parent_dept.id}/"
if local_dept:
local_dept.name = name
local_dept.parent_id = parent_id
local_dept.level = level
local_dept.path = path
else:
local_dept = Dept(
name=name,
wecom_dept_id=str(wecom_dept_id),
parent_id=parent_id,
level=level,
path=path,
dept_type="department",
status=True,
)
db.add(local_dept)
await db.flush()
logger.info(f"部门同步成功: {name} (wecom_dept_id={wecom_dept_id})")
@classmethod
async def _remove_dept(cls, db: AsyncSession, wecom_dept_id: int) -> None:
"""软删除部门"""
result = await db.execute(
select(Dept).where(
Dept.wecom_dept_id == str(wecom_dept_id),
Dept.is_deleted == False, # noqa: E712
)
)
local_dept = result.scalar_one_or_none()
if local_dept:
local_dept.is_deleted = True
logger.info(f"部门已删除: {local_dept.name} (wecom_dept_id={wecom_dept_id})")
# ==================== 用户事件 ====================
@classmethod
async def _handle_user_event(cls, event_type: str, event_data: Dict[str, Any]) -> None:
"""处理用户变更事件"""
userid = event_data.get("UserID")
if not userid:
logger.warning(f"用户事件缺少 UserID: {event_data}")
return
new_userid = event_data.get("NewUserID")
client = await cls.get_client()
async with AsyncSessionLocal() as db:
dept_result = await db.execute(
select(Dept).where(
Dept.wecom_dept_id.isnot(None),
Dept.is_deleted == False, # noqa: E712
)
)
local_depts = dept_result.scalars().all()
wecom_dept_map = {dept.wecom_dept_id: dept.id for dept in local_depts}
try:
if event_type == "delete_user":
await cls._deactivate_user(db, userid)
elif event_type == "update_user" and new_userid:
await cls._update_userid(db, userid, new_userid)
await cls._upsert_user(db, client, new_userid, wecom_dept_map)
else:
await cls._upsert_user(db, client, userid, wecom_dept_map)
except Exception as e:
logger.error(f"处理用户事件失败 userid={userid}: {e}")
await db.commit()
@classmethod
async def _upsert_user(
cls,
db: AsyncSession,
client: WecomClient,
wecom_userid: str,
wecom_dept_map: Dict[str, str],
) -> None:
"""创建或更新单个用户"""
import secrets
import string
detail = await client.get_user_detail(wecom_userid)
name = detail.get("name", "")
mobile = detail.get("mobile", "")
email = detail.get("email", "")
status = detail.get("status", 1)
dept_ids = detail.get("department", [])
local_dept_id = None
main_department = detail.get("main_department")
if main_department and str(main_department) in wecom_dept_map:
local_dept_id = wecom_dept_map[str(main_department)]
else:
for did in dept_ids:
mapped = wecom_dept_map.get(str(did))
if mapped:
local_dept_id = mapped
break
# 按 wecom_userid 匹配
result = await db.execute(
select(User).where(
User.wecom_userid == wecom_userid,
User.is_deleted == False, # noqa: E712
)
)
local_user = result.scalar_one_or_none()
# 按手机号匹配
if not local_user and mobile:
result = await db.execute(
select(User).where(
User.mobile == mobile,
User.is_deleted == False, # noqa: E712
)
)
local_user = result.scalar_one_or_none()
active = status == 1
if local_user:
local_user.name = name
if mobile:
local_user.mobile = mobile
if email:
local_user.email = email
if local_dept_id:
local_user.dept_id = local_dept_id
local_user.wecom_userid = wecom_userid
local_user.user_status = 1 if active else 0
local_user.is_active = active
else:
username = mobile or f"wc_{wecom_userid}"
existing = await db.execute(select(User).where(User.username == username))
if existing.scalar_one_or_none():
username = f"wc_{wecom_userid}"
chars = string.ascii_letters + string.digits + "!@#$%"
password = "".join(secrets.choice(chars) for _ in range(16))
local_user = User(
username=username,
password=UserService.hash_password(password),
name=name,
mobile=mobile or None,
email=email or None,
dept_id=local_dept_id,
wecom_userid=wecom_userid,
user_type=1,
user_status=1 if active else 0,
is_active=active,
)
db.add(local_user)
await db.flush()
logger.info(f"用户同步成功: {name} (wecom_userid={wecom_userid})")
@classmethod
async def _update_userid(cls, db: AsyncSession, old_userid: str, new_userid: str) -> None:
"""处理 userid 变更"""
result = await db.execute(
select(User).where(
User.wecom_userid == old_userid,
User.is_deleted == False, # noqa: E712
)
)
local_user = result.scalar_one_or_none()
if local_user:
local_user.wecom_userid = new_userid
await db.flush()
logger.info(f"用户 userid 已更新: {old_userid} -> {new_userid}")
@classmethod
async def _deactivate_user(cls, db: AsyncSession, wecom_userid: str) -> None:
"""用户离职:禁用用户"""
result = await db.execute(
select(User).where(
User.wecom_userid == wecom_userid,
User.is_deleted == False, # noqa: E712
)
)
local_user = result.scalar_one_or_none()
if local_user:
local_user.user_status = 0
local_user.is_active = False
logger.info(f"用户已禁用: {local_user.name} (wecom_userid={wecom_userid})")