""" WeChat Work (企业微信) Integration API - 企业微信集成接口 功能: 1. 配置企业微信应用凭证 2. 验证 URL 和处理事件 3. 发送消息到企业微信群 4. 查询集成状态 """ from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from ..database import get_db from ..dependencies.auth import get_current_user from ..dependencies.tenant import get_tenant_id from ..models import User from ..services.auth import has_any_role from ..services.wecom_integration import get_wecom_service router = APIRouter(prefix="/wecom", tags=["WeChat Work Integration"]) # ==================== Request/Response Models ==================== class WeComConfigRequest(BaseModel): """企业微信配置请求""" corp_id: str = Field(..., description="企业 ID") agent_id: str = Field(..., description="应用 ID") app_secret: str = Field(..., description="应用 Secret") token: str = Field(..., description="回调验证 Token") encoding_aes_key: str = Field(..., description="消息加密密钥") class WeComSendMessageRequest(BaseModel): """发送消息请求""" chat_id: str = Field(..., description="群聊 ID") message: str = Field(..., description="消息内容") # ==================== Configuration Endpoints ==================== @router.post("/config") async def save_wecom_config( request: WeComConfigRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id) ): """ 保存企业微信配置 配置信息说明: - corp_id: 企业 ID,从企业微信管理后台获取 - agent_id: 应用 ID - app_secret: 应用 Secret - token: 事件订阅验证 Token - encoding_aes_key: 消息加密密钥 获取步骤: 1. 访问 https://work.weixin.qq.com/ 2. 进入应用管理 -> 自建应用 3. 在「开发配置」获取 Agent ID 和 Secret 4. 在「接收消息」配置 Token 和 EncodingAESKey """ if not has_any_role(str(current_user.role or ""), ["ADMIN", "BOSS"]): raise HTTPException(status_code=403, detail="只有管理员可以配置企业微信集成") try: service = get_wecom_service(db) await service.save_config( corp_id=request.corp_id, agent_id=request.agent_id, app_secret=request.app_secret, token=request.token, encoding_aes_key=request.encoding_aes_key, tenant_id=tenant_id ) return { 'status': 'success', 'message': '企业微信配置已保存', 'next_steps': [ '1. 在企业微信管理平台配置接收消息 URL', f'2. URL: https://your-domain.com/api/wecom/webhook', '3. 点击验证按钮完成 URL 验证' ] } except Exception as e: raise HTTPException( status_code=500, detail=f"保存配置失败: {str(e)}" ) @router.get("/config") async def get_wecom_config( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id) ): """获取企业微信配置状态(不返回敏感信息)""" try: service = get_wecom_service(db) await service.load_config(tenant_id) return { 'is_configured': service.is_configured(), 'corp_id': service.corp_id if service.is_configured() else None, 'agent_id': service.agent_id if service.is_configured() else None, 'has_token': bool(service.token), 'has_encoding_key': bool(service.encoding_aes_key) } except Exception as e: raise HTTPException( status_code=500, detail=f"获取配置失败: {str(e)}" ) @router.delete("/config") async def delete_wecom_config( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id) ): """删除企业微信配置""" from ..models import Integration from sqlalchemy.future import select if not has_any_role(str(current_user.role or ""), ["ADMIN", "BOSS"]): raise HTTPException(status_code=403, detail="只有管理员可以删除配置") try: result = await db.execute( select(Integration).where( Integration.tenant_id == tenant_id, Integration.provider == 'wecom' ) ) integration = result.scalar_one_or_none() if integration: integration.is_active = False await db.commit() return {'status': 'success', 'message': '企业微信配置已删除'} except Exception as e: raise HTTPException( status_code=500, detail=f"删除配置失败: {str(e)}" ) # ==================== Webhook Endpoints ==================== @router.get("/webhook") async def verify_webhook_url( msg_signature: str, timestamp: str, nonce: str, echostr: str, db: AsyncSession = Depends(get_db) ): """ 验证企业微信事件订阅 URL 企业微信会发送 GET 请求来验证 URL 有效性 """ try: service = get_wecom_service(db) await service.load_config(tenant_id=1) if not service.is_configured(): raise HTTPException( status_code=400, detail="请先配置企业微信" ) decrypted_echostr = service.verify_url(msg_signature, timestamp, nonce, echostr) return { "errcode": 0, "errmsg": "ok", "echostr": decrypted_echostr } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/webhook") async def handle_wecom_event( request: Request, db: AsyncSession = Depends(get_db) ): """ 处理企业微信事件推送 企业微信会发送 POST 请求推送事件 需要验证签名确保请求来自企业微信 """ try: # 获取请求头 msg_signature = request.headers.get('msg_signature', '') timestamp = request.headers.get('timestamp', '') nonce = request.headers.get('nonce', '') # 读取请求体 body_bytes = await request.body() body = body_bytes.decode('utf-8') # 加载企业微信配置 service = get_wecom_service(db) await service.load_config(tenant_id=1) # 验证签名 if not service.verify_callback(msg_signature, timestamp, nonce, body): raise HTTPException(status_code=401, detail="签名验证失败") # 解析事件 from ..services.wecom_integration import WeChatWorkIntegrationService decrypted = service._decrypt(body) event_data = json.loads(decrypted) if decrypted else {} # 处理事件 result = await service.handle_message_event(event_data) return { "errcode": 0, "errmsg": "ok" } except HTTPException: raise except Exception as e: print(f"[WeCom] Webhook error: {e}") # 企业微信要求返回成功状态,避免重复推送 return {'errcode': 0, 'errmsg': 'ok'} # ==================== Message Endpoints ==================== @router.post("/send-message") async def send_wecom_message( request: WeComSendMessageRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id) ): """ 发送文本消息到企业微信群 用于发送简报、通知等 """ try: service = get_wecom_service(db) await service.load_config(tenant_id) result = await service.send_message_to_group( chat_id=request.chat_id, message=request.message ) return { 'status': 'success', 'result': result } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException( status_code=500, detail=f"发送消息失败: {str(e)}" ) # ==================== Test Endpoints ==================== @router.post("/test-connection") async def test_wecom_connection( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id) ): """测试企业微信连接是否正常""" try: service = get_wecom_service(db) await service.load_config(tenant_id) if not service.is_configured(): return { 'status': 'error', 'message': '企业微信未配置' } # 尝试获取 access_token 测试连接 try: access_token = await service._get_access_token() return { 'status': 'success', 'message': '企业微信连接正常', 'has_token': bool(access_token) } except Exception as token_error: return { 'status': 'error', 'message': f'获取 Token 失败: {str(token_error)}', 'hint': '请检查 Corp ID、Agent ID 和 App Secret 是否正确' } except Exception as e: raise HTTPException( status_code=500, detail=f"测试连接失败: {str(e)}" )