# 处理事件

通过事件订阅功能，应用可以及时接收飞书中资源数据的变化，并根据变化情况做对应的业务处理，详情参见[事件概述](https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM)。在应用内订阅事件时，还需要在本地服务端建立与应用的连接，以便接收事件数据。服务端 SDK 封装了长连接方式，可以快速建立数据通道处理事件；你也可以选择自建 HTTP 服务器处理事件。两种方式介绍如下：

订阅方式 | 介绍
---|---
使用长连接接收事件 | 该方式是飞书 SDK 内提供的能力，你可以通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道（你的服务器需要能够访问公网）。后续当应用订阅的事件发生时，开放平台会通过该通道向你的服务器发送消息。相较于传统的 Webhook 模式，长连接模式大大降低了接入成本，将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下：测试阶段无需使用内网穿透工具，通过长连接模式在本地开发环境中即可接收事件回调。SDK 内封装了鉴权逻辑，只在建连时进行鉴权，后续事件推送均为明文数据，无需再处理解密和验签逻辑。只需保证运行环境具备访问公网能力即可，无需提供公网 IP 或域名。无需部署防火墙和配置白名单。
将事件发送至开发者服务器 | 传统的 Webhook 模式，该方式需要你提供用于接收事件消息的服务器公网地址。后续当应用订阅的事件发生时，开放平台会向服务器的公网地址发送 HTTP POST 请求，请求内包含事件数据。

## （推荐）方式一：使用长连接接收事件

如果事件订阅方式需要选择 **使用长连接接收事件**，则需要先使用 SDK 建立与应用的连接。本章节提供建立长连接的示例代码与代码解析，通过 SDK 建立长连接之后，你才能在应用的事件订阅方式中保存 **使用长连接接收事件** 方式。关于应用内配置事件订阅方式的介绍，参考[配置事件订阅方式](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case)。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/c5a72c50a0d1caf40bdc8718c911b8cd_MHMSb9cxef.png?height=580&lazyload=true&maxWidth=550&width=1224)

### 注意事项

在开始配置之前，你需要确保已了解以下注意事项：
- 目前长连接模式仅支持企业自建应用。
- 与 **将事件发送至开发者服务器** 方式（即自建服务器方式）的要求相同，长连接模式下接收到消息后，需要在 3 秒内处理完成，否则会触发超时重推机制。
- 每个应用最多建立 50 个连接（在配置长连接时，每初始化一个 client 就是一个连接）。
- 长连接模式的消息推送为 **集群模式**，不支持广播，即如果同一应用部署了多个客户端（client），那么只有其中随机一个客户端会收到消息。

### 长连接代码

```go
package main
import (
   "context"
   "fmt"
   larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
   larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
   "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
   larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
   larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
)
func main() {
   // 注册事件回调，OnP2MessageReceiveV1 为接收消息 v2.0；OnCustomizedEvent 内的 message 为接收消息 v1.0。
   eventHandler := dispatcher.NewEventDispatcher("", "").
      OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
         fmt.Printf("[ OnP2MessageReceiveV1 access ], data: %s\n", larkcore.Prettify(event))
         return nil
      }).
      OnCustomizedEvent("这里填入你要自定义订阅的 event 的 key，例如 out_approval", func(ctx context.Context, event *larkevent.EventReq) error {
         fmt.Printf("[ OnCustomizedEvent access ], type: message, data: %s\n", string(event.Body))
         return nil
      })
   // 创建Client
   cli := larkws.NewClient("YOUR_APP_ID", "YOUR_APP_SECRET",
      larkws.WithEventHandler(eventHandler),
      larkws.WithLogLevel(larkcore.LogLevelDebug),
   )
   // 启动客户端
   err := cli.Start(context.Background())
   if err != nil {
      panic(err)
   }
}
```

代码实现说明：
1. 通过 dispatcher.NewEventDispatcher() 初始化事件处理器（eventHandler），注意**两个参数必须填空字符串**。
1. 通过 eventHandler 的 OnXXXX() 方法处理不同的事件。上述示例中分别监听了「接收消息 v1.0」和「接收消息 v2.0」两个事件。

**说明**：开放平台提供的事件包括 v1.0 和 v2.0 两个版本。两个版本的结构不同。在处理事件时，可通过[事件列表](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list)文档或在开发者后台[添加事件](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/subscription-event-case)时，获取事件的类型与版本信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4dbc1cd97d95f1af949354ce3c350461_4PX46ts1Hs.png?height=380&lazyload=true&maxWidth=650&width=1666)

如果是 v1.0 事件，则注册服务器时需要查找并使用 OnCustomizedEvent 方法；如果是 v2.0 事件，则注册服务器时需要查找并使用 onP2xxxx 开头的方法。
例如，使用 `onP2MessageReceiveV1` 注册接收消息事件回调时，`P2`便对应的是 v2.0 版本事件结构。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/8bbadcadbadcf70bb9cb4c44afea25cd_z7bPrzvx1l.png?height=286&lazyload=true&maxWidth=650&width=1598)

1. 通过 larkws.NewClient() 初始化长连接客户端，必填参数为应用的 APP_ID 和 APP_SECRET，需登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** > **应用凭证** 区域获取。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_NouE7PqEkQ.png?height=524&lazyload=true&maxWidth=750&width=3594)
1. 可选参数传入 eventHandler，同时可设置日志级别。
1. 通过 cli.Start() 启动客户端，如连接成功，控制台会打印 "connected to wss://xxxxx"，主线程将阻塞，直到进程结束。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/b1b3d6b4d3a724fa7408f5c71a11d19e_ysuov6r3Bn.png?height=338&lazyload=true&maxWidth=750&width=2286)

## 方式二：将事件发送至开发者服务器

如果事件订阅方式选择 **将事件发送至开发者服务器**，则需要设置事件请求地址，后续在事件发生时，开放平台会向该地址发送包含事件 JSON 数据的 HTTP POST 请求。为此你需启动一个 HTTP 服务器接收并处理事件。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/2b355c6bb9405407832987a20a3c1c9b_b94HR233cB.png?height=629&lazyload=true&maxWidth=650&width=962)

### 选择一：使用 Go SDK 原生 HTTP 服务器处理事件

订阅事件后，你可以使用下面代码，对飞书开放平台推送的事件进行处理。以下代码示例基于 Go SDK 原生的 HTTP 服务器，启动一个 HTTP 服务器：
```go
package main
import (
    "context"
    "fmt"
    "net/http"
    "github.com/larksuite/oapi-sdk-go/v3/core"
    "github.com/larksuite/oapi-sdk-go/v3/core/httpserverext"
    "github.com/larksuite/oapi-sdk-go/v3/event"
    "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
    "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
)
func main() {
    // 注册消息处理器
    // 用于签名验证和消息解密，默认可以传递为空串。但如果你在开发者后台 > 事件与回调 > 加密策略中开启了加密，则必须传递 Encrypt Key 和 Verification Token
    handler := dispatcher.NewEventDispatcher("verificationToken", "eventEncryptKey")
    handler = handler.OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
       // 处理消息 event，这里简单打印消息的内容
       fmt.Println(larkcore.Prettify(event))
       fmt.Println(event.RequestId())
       return nil
    }).OnCustomizedEvent("这里填入你要自定义订阅的 event 的 key，例如 out_approval", func(ctx context.Context, event *larkevent.EventReq) error {
       // 原生消息体
       fmt.Println(string(event.Body))
       fmt.Println(larkcore.Prettify(event.Header))
       fmt.Println(larkcore.Prettify(event.RequestURI))
       fmt.Println(event.RequestId())
       // 处理消息
       cipherEventJsonStr, err := handler.ParseReq(ctx, event)
       if err != nil {
          //  错误处理
          return err
       }
       plainEventJsonStr, err := handler.DecryptEvent(ctx, cipherEventJsonStr)
       if err != nil {
          //  错误处理
          return err
       }
       // 处理解密后的 消息体
       fmt.Println(plainEventJsonStr)
       return nil
    })
    // 注册 http 路由
    http.HandleFunc("/webhook/event", httpserverext.NewEventHandlerFunc(handler, larkevent.WithLogLevel(larkcore.LogLevelDebug)))
    // 启动 http 服务
    err := http.ListenAndServe(":9999", nil)
    if err != nil {
       panic(err)
    }
}
```
其中需要注意：EventDispatcher.newBuilder 方法的参数用于签名验证和消息解密，默认可以传递为空串。但如果你在[开发者后台](https://open.feishu.cn/app) > **事件与回调** > **加密策略**中开启了加密，则必须传递 Encrypt Key 和 Verification Token。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/529c6adc9193d752eac5f87e4cb0b90e_XaHLy4WSDD.png?height=773&lazyload=true&maxWidth=750&width=1495)

了解更多事件订阅示例，参考 [GitHub 代码仓库](https://github.com/larksuite/oapi-sdk-go/blob/v3_main/sample/event/event.go)。

### 选择二：集成 Gin 框架处理事件

如果你当前应用使用的是 Gin Web 框架，并且不希望使用 Go SDK 的原生 HTTP 服务器，你可以参考以下集成步骤，将当前应用的 Gin 服务与 SDK 进行集成。
1. 安装集成包。

把 Go SDK 集成已有的 Gin 框架，需要先引入 [oapi-sdk-gin](https://github.com/larksuite/oapi-sdk-gin) 集成包。

```bash
      go get -u github.com/larksuite/oapi-sdk-gin
      ```
1. 进行代码集成。集成示例如下：
    ```go
    import (
            "context"
            "fmt"

"github.com/gin-gonic/gin"
             "github.com/larksuite/oapi-sdk-gin"
             "github.com/larksuite/oapi-sdk-go/v3/card"
             "github.com/larksuite/oapi-sdk-go/v3/core"
             "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
             "github.com/larksuite/oapi-sdk-go/v3/service/contact/v3"
             "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
    )

func main() {
            // 注册消息处理器
            handler := dispatcher.NewEventDispatcher("verificationToken", "eventEncryptKey").OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
                    fmt.Println(larkcore.Prettify(event))
                    fmt.Println(event.RequestId())
                    return nil
            }).OnP2MessageReadV1(func(ctx context.Context, event *larkim.P2MessageReadV1) error {
                    fmt.Println(larkcore.Prettify(event))
                    fmt.Println(event.RequestId())
                    return nil
            }).OnP2UserCreatedV3(func(ctx context.Context, event *larkcontact.P2UserCreatedV3) error {
                    fmt.Println(larkcore.Prettify(event))
                    fmt.Println(event.RequestId())
                    return nil
            })

...

// 在已有 Gin 实例上注册消息处理路由
            gin.POST("/webhook/event", sdkginext.NewEventHandlerFunc(handler))
    }
    ```

### 选择三：集成 hertz 框架处理事件

详情请参见[集成 hertz 框架](https://github.com/hertz-contrib/lark-hertz)。

## （可选）在消息处理器内向对应用户发送消息

在 HTTP 服务器启动后，你还可以进一步补充代码逻辑，实现接收事件后，向用户发送消息的效果。
对于商店应用的 ISV 开发者，当需要在消息处理器内向对应企业或组织的用户发送消息时，需要先从事件回调数据中获取企业或组织 key，然后使用如下方式调用消息 API 进行消息发送。

```go
import (
        "context"
        "fmt"
        lark "github.com/larksuite/oapi-sdk-go/v3"
        "net/http"
        "os"
        "github.com/larksuite/oapi-sdk-go/v3/core"
        "github.com/larksuite/oapi-sdk-go/v3/core/httpserverext"
        "github.com/larksuite/oapi-sdk-go/v3/event"
        "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
        "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
)
func main() {
        // 创建 API Client。你需在此传入你的应用的实际 App ID 和 App Secret
        var appID, appSecret = os.Getenv("APP_ID"), os.Getenv("APP_SECRET")
        var cli = lark.NewClient(appID, appSecret, lark.WithLogReqAtDebug(true), lark.WithLogLevel(larkcore.LogLevelDebug))
        // 注册消息处理器
        handler := dispatcher.NewEventDispatcher("verificationToken", "eventEncryptKey").OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
                // 处理消息 event，这里简单打印消息的内容
                fmt.Println(larkcore.Prettify(event))
                fmt.Println(event.RequestId())
                // 获取企业或组织 key 并发送消息
                tenantKey := event.TenantKey()
                // 商店应用给指定企业或组织的用户发送消息
                resp, err := cli.Im.Message.Create(context.Background(), larkim.NewCreateMessageReqBuilder().
                        ReceiveIdType(larkim.ReceiveIdTypeOpenId).
                        Body(larkim.NewCreateMessageReqBodyBuilder().
                                MsgType(larkim.MsgTypePost).
                                ReceiveId("ou_c245b0a7dff2725cfa2fb104f8babcef").
                                Content("text").
                                Build()).
                        Build(), larkcore.WithTenantKey(tenantKey))
                // 发送结果处理，resp,err
                fmt.Println(resp, err)
                return nil
        })
        // 注册 http 路由
        http.HandleFunc("/webhook/event", httpserverext.NewEventHandlerFunc(handler, larkevent.WithLogLevel(larkcore.LogLevelDebug)))
        // 启动 http 服务
        err := http.ListenAndServe(":9999", nil)
        if err != nil {
                panic(err)
        }
}
```

