backend-design.md 7.9 KB

企微 Webhook 接收服务 — 后端设计文档

一、项目概述

本服务是社群运营系统的企业微信回调数据接收层。企微平台在群事件发生时通过 HTTP POST 推送 JSON 数据到本服务,服务解析后存入 Parse Server(底层为 PostgreSQL)。

核心职责:接收回调 → 解析事件 → 存储群数据 + 文本消息。


二、技术选型

组件 选型 原因
运行时 Node.js + TypeScript 前后端统一语言栈,类型安全
HTTP 框架 Express 4.x 轻量,中间件生态成熟
数据库 PostgreSQL 通过 Parse Server 托管
数据库访问 Parse SDK (parse/node) 直接操作 Parse Object,无需手写 SQL
包管理器 pnpm 节省磁盘空间,安装速度快
开发运行 tsx 直接运行 TypeScript,无需编译步骤

三、项目目录结构

backend/
├── .env                              # 环境变量
├── package.json                      # 依赖声明 (pnpm)
├── tsconfig.json                     # TypeScript 配置 (ESNext module)
├── docs/                             # 设计文档(本目录)
└── src/
    ├── index.ts                      # 总启动器
    └── apps/
        └── pc/
            ├── app.ts                # PC 端 Express 装配层
            ├── health/
            │   └── server.ts         # 服务启动入口 + Schema 初始化
            └── qiwe/                 # 企微 Webhook 模块
                ├── routes/
                │   └── webhook.routes.ts       # 路由定义
                ├── controllers/
                │   └── webhook.controller.ts   # 请求处理(解析 → 响应 → 异步处理)
                ├── services/
                │   └── webhook.service.ts      # 业务逻辑(事件分发 + Parse CRUD)
                └── models/
                    ├── parse-client.ts         # Parse SDK 初始化
                    └── schema-setup.ts         # 建表脚本(启动时自动执行)

分层职责

请求 → routes(路由映射)
       → controllers(参数校验、响应、异步调度)
         → services(业务编排、Parse 读写)
           → models/parse-client(数据库连接)
  • routes: 只做 URL → controller 的映射,不写任何逻辑
  • controllers: 校验请求体、立即返回 HTTP 响应、调 service 异步处理
  • services: 根据 cmd + msgType 分发事件、执行 upsert、处理 base64 解码等业务逻辑
  • models: Parse SDK 初始化、Schema 管理,不含业务逻辑

四、启动流程

src/index.ts
  │
  ├─ 1. import 'dotenv/config'        → 加载 .env 到 process.env
  ├─ 2. import parse-client.ts        → Parse.initialize()(副作用执行)
  └─ 3. import health/server.ts       → 启动 Express
         │
         ├─ ensureSchemas()           → 幂等建表(GroupChat / GroupMember / Message)
         └─ app.listen(3101)          → 监听端口

五、Webhook 接口

端点

POST /api/qiwe/webhook
Content-Type: application/json

请求体格式

{
  "code": 0,
  "msg": "成功",
  "data": [
    {
      "guid": "设备标识",
      "userId": "企微userId",
      "cmd": 15000,
      "msgType": 0,
      "msgServerId": 1002001,
      "msgUniqueIdentifier": "唯一消息ID",
      "senderId": 168885000001,
      "fromRoomId": 123456789,
      "timestamp": 1759064100,
      "msgData": { "content": "消息内容", "atList": [] },
      "base64RawData": "base64编码的原始数据"
    }
  ]
}

响应

无论处理结果如何,均在收到请求后立即返回 HTTP 200

{ "success": true, "data": null, "error": null }

这是强制性设计:企微平台要求回调接口在 3 秒内返回 200,否则判定超时并丢弃消息。实际数据处理在响应之后异步执行。


六、事件处理分发逻辑

processWebhookEvent(event) 根据 cmdmsgType 分发:

cmd msgType 处理器 行为
15000 0 / 2 handleTextMessage() 文本消息入库(去重)
15000 1001 handleGroupNameChange() 更新群名称
15000 1002 handleMemberAdd() 新增群成员
15000 1003 handleMemberRemove() 移除群成员(标记 left)
15000 1005 handleMemberQuit() 成员主动退群(标记 left)
15000 1006 handleGroupCreate() 创建群 + 初始化成员列表
15000 1022 日志 群主转让(仅记录)
15000 1023 handleGroupDismiss() 群解散(标记 dismissed)
15000 1043 日志 管理员变动(仅记录)
15000 2002 日志 删除聊天(仅记录)
15000 2055 日志 清空聊天(仅记录)
15500 任意 日志 系统消息(联系人/标签变动)
11016 日志 账号状态变化(登录/离线/顶号)
20000 日志 API 异步消息

设计原则

  • cmd=15000 是核心入口:普通消息 + 群事件共用
  • 仅文本入消息库msgType=0 或 2 才写入 Message 表,图片/视频/文件等暂不存储
  • 群事件写相关表:1001~1043 写 GroupChat / GroupMember
  • 15500 系统消息暂不入库:联系人变动、标签变动等需求后续按需开启
  • 未匹配事件只记日志:便于排查,不丢数据

七、关键设计决策

7.1 为什么 controller 先响应再处理?

因为企微平台硬性要求 3 秒内返回 200。如果在 controller 里 await 所有数据库操作完成才响应,网络抖动或数据库慢查询可能导致超时,消息被平台丢弃。

采用"先响应,后处理":收到 body 后立刻 res.json(200),然后 for 循环异步处理每条事件。这样即使某条数据处理耗时较长,也不影响响应速度。

7.2 为什么使用 Upsert 而不是 Insert?

GroupChatGroupMember 使用 upsert 模式(查→在则更新/不在则创建):

  • 群创建(1006)事件可能重复推送
  • 群名变更(1001)需要在已有记录上更新 roomName
  • 成员可能退出后重新加入,需要恢复 status=active

直接 insert 会导致主键冲突或重复数据。upsert 保证了数据的一致性和幂等性。

7.3 为什么用 Parse SDK 而不是直接 SQL?

  • Parse 自动管理 objectIdcreatedAtupdatedAt
  • 无需手写建表语句、迁移脚本
  • 无需管理数据库连接池
  • Parse Dashboard 可直接查看/编辑数据
  • 后续需要复杂查询时,可通过云函数写 SQL

7.4 为什么要解码 base64?

企微平台将成员列表等数据用 base64 编码传输,原因有三:

  1. JSON 安全:避免原始数据中的特殊字符破坏 JSON 结构
  2. 兼容 Protobuf:企微底层使用 protobuf 序列化,base64 是标准传输格式
  3. 数据完整性:防止 HTTP 传输过程中因字符集转换导致数据损坏

解码后的格式是分号分隔的 userId 列表,如 "168885000001;168885000002"


八、环境变量

变量 默认值 说明
NODE_ENV development 运行环境
PC_PORT 3101 PC 端服务端口
PARSE_APP_ID lami-ai Parse 应用 ID
PARSE_MASTER_KEY 5s1gfOasPqx9JKsA Parse Master Key(服务端使用)
PARSE_SERVER_URL https://server.sh-lami.com/parse Parse Server 地址

九、启动命令

pnpm dev      # 开发模式(tsx watch,文件变更自动重启)
pnpm start    # 生产模式

十、相关文档