文档

Webhooks API

SmartCV Webhooks API 文档,了解如何接收实时事件通知

更新时间: 2024/12/30

Webhooks 允许您的应用程序接收 SmartCV 中发生的事件的实时通知,而无需持续轮询我们的 API。

🔔 概述

当特定事件在 SmartCV 中发生时,我们会向您配置的端点发送 HTTP POST 请求。这样您可以:

  • 实时同步数据
  • 自动触发工作流程
  • 监控重要活动
  • 集成第三方服务

📋 支持的事件

简历相关事件

事件类型描述
resume.created简历创建
resume.updated简历更新
resume.deleted简历删除
resume.shared简历分享
resume.exported简历导出

用户相关事件

事件类型描述
user.registered用户注册
user.subscription_changed订阅状态变化
user.profile_updated用户资料更新

AI 相关事件

事件类型描述
ai.analysis_completedAI 分析完成
ai.content_generatedAI 内容生成完成

🔧 配置 Webhooks

通过 API 配置

端点: POST /api/webhooks

请求体:

{
  "url": "https://yourapp.com/webhooks/smartcv",
  "events": ["resume.created", "resume.updated", "ai.analysis_completed"],
  "secret": "your_webhook_secret",
  "active": true
}

响应:

{
  "success": true,
  "data": {
    "id": "webhook_123",
    "url": "https://yourapp.com/webhooks/smartcv",
    "events": ["resume.created", "resume.updated", "ai.analysis_completed"],
    "secret": "your_webhook_secret",
    "active": true,
    "createdAt": "2024-12-30T10:00:00Z"
  }
}

管理 Webhooks

// 获取 Webhooks 列表
async function getWebhooks() {
  const response = await fetch('/api/webhooks', {
    credentials: 'include'
  })
  return response.json()
}
 
// 更新 Webhook
async function updateWebhook(webhookId, updates) {
  const response = await fetch(`/api/webhooks/${webhookId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    credentials: 'include',
    body: JSON.stringify(updates)
  })
  return response.json()
}
 
// 删除 Webhook
async function deleteWebhook(webhookId) {
  const response = await fetch(`/api/webhooks/${webhookId}`, {
    method: 'DELETE',
    credentials: 'include'
  })
  return response.json()
}

📨 Webhook 载荷

载荷结构

所有 Webhook 请求都包含以下结构:

{
  "id": "event_123",
  "type": "resume.created",
  "created": "2024-12-30T10:00:00Z",
  "data": {
    /* 事件特定数据 */
  },
  "api_version": "2024-12-30"
}

事件示例

resume.created

{
  "id": "event_123",
  "type": "resume.created",
  "created": "2024-12-30T10:00:00Z",
  "data": {
    "resume": {
      "id": "resume_456",
      "title": "软件工程师简历",
      "status": "DRAFT",
      "user_id": "user_789",
      "template_id": "template_123",
      "created_at": "2024-12-30T10:00:00Z"
    }
  },
  "api_version": "2024-12-30"
}

ai.analysis_completed

{
  "id": "event_124",
  "type": "ai.analysis_completed",
  "created": "2024-12-30T10:05:00Z",
  "data": {
    "analysis": {
      "resume_id": "resume_456",
      "overall_score": 85,
      "keyword_match_score": 78,
      "format_score": 92,
      "content_score": 88,
      "suggestions_count": 5
    }
  },
  "api_version": "2024-12-30"
}

user.subscription_changed

{
  "id": "event_125",
  "type": "user.subscription_changed",
  "created": "2024-12-30T10:10:00Z",
  "data": {
    "user": {
      "id": "user_789",
      "email": "[email protected]"
    },
    "subscription": {
      "previous_type": "FREE",
      "current_type": "PREMIUM",
      "changed_at": "2024-12-30T10:10:00Z"
    }
  },
  "api_version": "2024-12-30"
}

🔐 安全验证

签名验证

SmartCV 使用 HMAC SHA256 签名来确保 Webhook 的安全性。

请求头:

X-SmartCV-Signature: sha256=<signature>
X-SmartCV-Timestamp: <timestamp>

验证示例

Node.js

const crypto = require('crypto')
 
function verifyWebhookSignature(payload, signature, secret, timestamp) {
  // 检查时间戳(防止重放攻击)
  const currentTime = Math.floor(Date.now() / 1000)
  const webhookTime = parseInt(timestamp)
 
  if (Math.abs(currentTime - webhookTime) > 300) {
    // 5分钟内有效
    throw new Error('Webhook timestamp too old')
  }
 
  // 验证签名
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + payload)
    .digest('hex')
 
  const actualSignature = signature.replace('sha256=', '')
 
  if (!crypto.timingSafeEqual(Buffer.from(expectedSignature, 'hex'), Buffer.from(actualSignature, 'hex'))) {
    throw new Error('Invalid webhook signature')
  }
 
  return true
}
 
// Express 中间件示例
app.post('/webhooks/smartcv', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body
  const signature = req.headers['x-smartcv-signature']
  const timestamp = req.headers['x-smartcv-timestamp']
 
  try {
    verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET, timestamp)
 
    const event = JSON.parse(payload)
    handleWebhookEvent(event)
 
    res.status(200).send('OK')
  } catch (error) {
    console.error('Webhook verification failed:', error)
    res.status(400).send('Invalid webhook')
  }
})

Python

import hmac
import hashlib
import time
import json
 
def verify_webhook_signature(payload, signature, secret, timestamp):
    # 检查时间戳
    current_time = int(time.time())
    webhook_time = int(timestamp)
 
    if abs(current_time - webhook_time) > 300:  # 5分钟内有效
        raise ValueError("Webhook timestamp too old")
 
    # 验证签名
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        f"{timestamp}.{payload}".encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
 
    actual_signature = signature.replace('sha256=', '')
 
    if not hmac.compare_digest(expected_signature, actual_signature):
        raise ValueError("Invalid webhook signature")
 
    return True
 
# Flask 示例
from flask import Flask, request, jsonify
 
@app.route('/webhooks/smartcv', methods=['POST'])
def handle_webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-SmartCV-Signature')
    timestamp = request.headers.get('X-SmartCV-Timestamp')
 
    try:
        verify_webhook_signature(payload, signature, WEBHOOK_SECRET, timestamp)
 
        event = json.loads(payload)
        handle_webhook_event(event)
 
        return jsonify({'status': 'success'}), 200
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

📝 事件处理示例

完整的 Webhook 处理器

class SmartCVWebhookHandler {
  constructor(secret) {
    this.secret = secret
  }
 
  async handleWebhook(payload, signature, timestamp) {
    // 验证签名
    this.verifySignature(payload, signature, timestamp)
 
    const event = JSON.parse(payload)
 
    // 根据事件类型分发处理
    switch (event.type) {
      case 'resume.created':
        await this.handleResumeCreated(event.data)
        break
      case 'resume.updated':
        await this.handleResumeUpdated(event.data)
        break
      case 'ai.analysis_completed':
        await this.handleAnalysisCompleted(event.data)
        break
      case 'user.subscription_changed':
        await this.handleSubscriptionChanged(event.data)
        break
      default:
        console.log(`Unhandled event type: ${event.type}`)
    }
  }
 
  async handleResumeCreated(data) {
    const { resume } = data
    console.log(`New resume created: ${resume.id}`)
 
    // 同步到本地数据库
    await this.syncResumeToDatabase(resume)
 
    // 发送通知
    await this.sendNotification({
      type: 'resume_created',
      message: `Resume "${resume.title}" has been created`,
      userId: resume.user_id
    })
  }
 
  async handleAnalysisCompleted(data) {
    const { analysis } = data
    console.log(`Analysis completed for resume: ${analysis.resume_id}`)
 
    // 更新分析结果
    await this.updateAnalysisResults(analysis)
 
    // 如果评分较低,发送改进建议
    if (analysis.overall_score < 70) {
      await this.sendImprovementSuggestions(analysis)
    }
  }
 
  async handleSubscriptionChanged(data) {
    const { user, subscription } = data
    console.log(
      `User ${user.id} subscription changed from ${subscription.previous_type} to ${subscription.current_type}`
    )
 
    // 更新用户权限
    await this.updateUserPermissions(user.id, subscription.current_type)
 
    // 发送欢迎邮件(如果是升级)
    if (subscription.current_type === 'PREMIUM' && subscription.previous_type === 'FREE') {
      await this.sendWelcomePremiumEmail(user.email)
    }
  }
 
  verifySignature(payload, signature, timestamp) {
    const crypto = require('crypto')
 
    const currentTime = Math.floor(Date.now() / 1000)
    const webhookTime = parseInt(timestamp)
 
    if (Math.abs(currentTime - webhookTime) > 300) {
      throw new Error('Webhook timestamp too old')
    }
 
    const expectedSignature = crypto
      .createHmac('sha256', this.secret)
      .update(timestamp + '.' + payload)
      .digest('hex')
 
    const actualSignature = signature.replace('sha256=', '')
 
    if (!crypto.timingSafeEqual(Buffer.from(expectedSignature, 'hex'), Buffer.from(actualSignature, 'hex'))) {
      throw new Error('Invalid webhook signature')
    }
  }
}
 
// 使用示例
const webhookHandler = new SmartCVWebhookHandler(process.env.WEBHOOK_SECRET)
 
app.post('/webhooks/smartcv', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    await webhookHandler.handleWebhook(req.body, req.headers['x-smartcv-signature'], req.headers['x-smartcv-timestamp'])
    res.status(200).send('OK')
  } catch (error) {
    console.error('Webhook error:', error)
    res.status(400).send('Error processing webhook')
  }
})

🔄 重试机制

重试策略

SmartCV 使用指数退避策略进行重试:

  • 重试次数: 最多 3 次
  • 重试间隔: 1s, 2s, 4s
  • 超时时间: 每次请求 30 秒超时

成功标准

以下 HTTP 状态码被视为成功:

  • 200 - OK
  • 201 - Created
  • 202 - Accepted
  • 204 - No Content

🧪 测试 Webhooks

测试端点

您可以使用以下工具测试 Webhook:

  1. ngrok
  • 创建本地隧道
  1. Webhook.site - 在线测试工具
  2. RequestBin - 请求收集器

手动触发测试

// 发送测试 Webhook
async function sendTestWebhook(webhookId) {
  const response = await fetch(`/api/webhooks/${webhookId}/test`, {
    method: 'POST',
    credentials: 'include'
  })
 
  return response.json()
}

📊 监控和调试

Webhook 日志

您可以查看 Webhook 的发送日志:

async function getWebhookLogs(webhookId, limit = 50) {
  const response = await fetch(`/api/webhooks/${webhookId}/logs?limit=${limit}`, {
    credentials: 'include'
  })
 
  return response.json()
}

日志响应示例

{
  "success": true,
  "data": [
    {
      "id": "delivery_123",
      "event_id": "event_456",
      "event_type": "resume.created",
      "url": "https://yourapp.com/webhooks/smartcv",
      "status_code": 200,
      "response_time": 145,
      "attempts": 1,
      "delivered_at": "2024-12-30T10:00:00Z"
    },
    {
      "id": "delivery_124",
      "event_id": "event_457",
      "event_type": "ai.analysis_completed",
      "url": "https://yourapp.com/webhooks/smartcv",
      "status_code": 500,
      "response_time": 30000,
      "attempts": 3,
      "last_attempt_at": "2024-12-30T10:05:00Z",
      "next_retry_at": null,
      "error": "Connection timeout"
    }
  ]
}

❌ 故障排除

常见问题

  1. 签名验证失败

    • 检查密钥是否正确
    • 确保使用原始请求体计算签名
    • 检查时间戳是否在有效范围内
  2. 重复事件

    • 实现幂等性处理
    • 使用事件 ID 去重
  3. 连接超时

    • 优化端点响应时间
    • 异步处理长时间操作

最佳实践

  • 快速响应: 在 30 秒内响应
  • 幂等处理: 同一事件可能被发送多次
  • 错误处理: 正确返回错误状态码
  • 日志记录: 记录所有 Webhook 事件以便调试

下一步

💡 示例代码

浏览完整的代码示例和最佳实践

🔐 认证系统

了解如何进行用户认证和授权