178 lines
6.3 KiB
Python
178 lines
6.3 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
飞书同步 API - 组织架构与用户同步管理接口
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.base_schema import ResponseModel
|
|
from app.database import get_db
|
|
from core.feishu_sync.schema import (
|
|
DeptTreeRequest,
|
|
FeishuSyncConfigUpdate,
|
|
TestConnectionRequest,
|
|
)
|
|
from core.feishu_sync.service import FeishuSyncService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/feishu-sync", tags=["飞书组织同步"])
|
|
|
|
|
|
@router.post("/test-connection", response_model=ResponseModel, summary="连接测试")
|
|
async def test_connection(data: TestConnectionRequest):
|
|
try:
|
|
result = await FeishuSyncService.test_connection(
|
|
app_id=data.app_id,
|
|
app_secret=data.app_secret,
|
|
)
|
|
return ResponseModel(data=result, message="连接成功")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"连接失败: {str(e)}")
|
|
|
|
|
|
@router.get("/config", summary="获取同步配置")
|
|
async def get_config():
|
|
from core.system_config.service import SystemConfigService
|
|
return await SystemConfigService.get_group_config("sync_feishu", mask_secrets=True)
|
|
|
|
|
|
@router.put("/config", response_model=ResponseModel, summary="保存同步配置")
|
|
async def update_config(
|
|
data: FeishuSyncConfigUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
from core.system_config.service import SystemConfigService
|
|
configs = {k: v for k, v in data.model_dump().items() if v is not None}
|
|
updated = await SystemConfigService.update_group_config(db, "sync_feishu", configs)
|
|
return ResponseModel(data=updated, message="保存成功")
|
|
|
|
|
|
@router.post("/sync/dept", response_model=ResponseModel, summary="同步组织架构")
|
|
async def sync_departments(db: AsyncSession = Depends(get_db)):
|
|
try:
|
|
result = await FeishuSyncService.sync_departments(db)
|
|
return ResponseModel(data=result, message="组织架构同步完成")
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"部门同步失败: {e}")
|
|
raise HTTPException(status_code=500, detail=f"同步失败: {str(e)}")
|
|
|
|
|
|
@router.post("/sync/user", response_model=ResponseModel, summary="同步用户")
|
|
async def sync_users(db: AsyncSession = Depends(get_db)):
|
|
try:
|
|
result = await FeishuSyncService.sync_users(db)
|
|
return ResponseModel(data=result, message="用户同步完成")
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"用户同步失败: {e}")
|
|
raise HTTPException(status_code=500, detail=f"同步失败: {str(e)}")
|
|
|
|
|
|
@router.get("/stats", summary="获取同步统计")
|
|
async def get_sync_stats(db: AsyncSession = Depends(get_db)):
|
|
stats = await FeishuSyncService.get_sync_stats(db)
|
|
return stats
|
|
|
|
|
|
@router.get("/logs", summary="获取同步日志")
|
|
async def get_sync_logs(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await FeishuSyncService.get_sync_logs(db, page=page, page_size=page_size)
|
|
|
|
|
|
@router.post("/dept-tree", summary="获取飞书部门树")
|
|
async def get_dept_tree(data: DeptTreeRequest):
|
|
try:
|
|
tree = await FeishuSyncService.get_feishu_dept_tree(
|
|
app_id=data.app_id,
|
|
app_secret=data.app_secret,
|
|
)
|
|
return tree
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"获取部门树失败: {str(e)}")
|
|
|
|
|
|
# ==================== 回调相关接口 ====================
|
|
|
|
|
|
@router.post("/callback", summary="飞书事件回调")
|
|
async def feishu_callback(request: Request):
|
|
"""
|
|
飞书事件订阅回调端点(无需认证)
|
|
|
|
处理两种场景:
|
|
1. URL 验证: {"type":"url_verification","challenge":"xxx","token":"xxx"}
|
|
2. 事件推送: {"encrypt":"xxx"} 或明文 v2.0 事件
|
|
签名通过 Header: X-Lark-Signature, X-Lark-Request-Timestamp, X-Lark-Request-Nonce
|
|
"""
|
|
from core.feishu_sync.callback_handler import FeishuCallbackHandler
|
|
|
|
try:
|
|
crypto = await FeishuCallbackHandler.get_crypto()
|
|
except ValueError as e:
|
|
logger.error(f"回调配置不完整: {e}")
|
|
raise HTTPException(status_code=500, detail="回调配置不完整")
|
|
|
|
raw_body = await request.body()
|
|
body_str = raw_body.decode("utf-8")
|
|
|
|
try:
|
|
body = json.loads(body_str)
|
|
except json.JSONDecodeError:
|
|
raise HTTPException(status_code=400, detail="无效的 JSON 请求体")
|
|
|
|
# URL 验证阶段飞书可能不带签名头,先尝试处理
|
|
if body.get("type") == "url_verification":
|
|
event_body = crypto.decrypt_event(body)
|
|
return crypto.handle_url_verification(event_body)
|
|
|
|
# 事件推送必须验证签名
|
|
signature = request.headers.get("X-Lark-Signature", "")
|
|
timestamp = request.headers.get("X-Lark-Request-Timestamp", "")
|
|
nonce = request.headers.get("X-Lark-Request-Nonce", "")
|
|
|
|
if not all([signature, timestamp, nonce]):
|
|
raise HTTPException(status_code=403, detail="缺少签名验证头")
|
|
|
|
if not crypto.verify_signature(timestamp, nonce, body_str, signature):
|
|
raise HTTPException(status_code=403, detail="签名验证失败")
|
|
|
|
event_body = crypto.decrypt_event(body)
|
|
|
|
event_type = event_body.get("header", {}).get("event_type", "")
|
|
logger.info(f"收到飞书回调事件: {event_type}")
|
|
|
|
asyncio.create_task(_safe_handle_event(event_body))
|
|
|
|
return {"code": 0, "msg": "ok"}
|
|
|
|
|
|
async def _safe_handle_event(event_body: dict) -> None:
|
|
"""安全地处理回调事件"""
|
|
from core.feishu_sync.callback_handler import FeishuCallbackHandler
|
|
try:
|
|
await FeishuCallbackHandler.handle_event(event_body)
|
|
except Exception as e:
|
|
logger.error(f"处理回调事件失败: {e}", exc_info=True)
|
|
|
|
|
|
@router.get("/callback/status", response_model=ResponseModel, summary="查询回调状态")
|
|
async def get_callback_status():
|
|
try:
|
|
result = await FeishuSyncService.get_callback_status()
|
|
return ResponseModel(data=result, message="查询成功")
|
|
except Exception as e:
|
|
return ResponseModel(data={"registered": False}, message="未注册或查询失败")
|