328 lines
10 KiB
Python
328 lines
10 KiB
Python
"""
|
||
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)
|
||
can_view_secrets = has_any_role(str(current_user.role or ""), ["ADMIN", "BOSS"])
|
||
|
||
return {
|
||
'is_configured': service.is_configured(),
|
||
'corp_id': service.corp_id,
|
||
'agent_id': service.agent_id,
|
||
'app_secret': service.app_secret if can_view_secrets else None,
|
||
'has_token': bool(service.token),
|
||
'has_encoding_key': bool(service.encoding_aes_key),
|
||
'token_preview': f"{service.token[:4]}...{service.token[-4:]}" if service.token and len(service.token) > 8 else (service.token or None),
|
||
'encoding_aes_key_preview': f"{service.encoding_aes_key[:4]}...{service.encoding_aes_key[-4:]}" if service.encoding_aes_key and len(service.encoding_aes_key) > 8 else (service.encoding_aes_key or None),
|
||
'token': service.token if can_view_secrets else None,
|
||
'encoding_aes_key': service.encoding_aes_key if can_view_secrets else None,
|
||
}
|
||
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.type == '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")
|
||
@router.get("/webhook/", include_in_schema=False)
|
||
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")
|
||
@router.post("/webhook/", include_in_schema=False)
|
||
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)}"
|
||
)
|