本服务是社群运营系统的企业微信回调数据接收层。企微平台在群事件发生时通过 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(数据库连接)
cmd + msgType 分发事件、执行 upsert、处理 base64 解码等业务逻辑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) → 监听端口
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) 根据 cmd 和 msgType 分发:
| 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 异步消息 |
msgType=0 或 2 才写入 Message 表,图片/视频/文件等暂不存储因为企微平台硬性要求 3 秒内返回 200。如果在 controller 里 await 所有数据库操作完成才响应,网络抖动或数据库慢查询可能导致超时,消息被平台丢弃。
采用"先响应,后处理":收到 body 后立刻 res.json(200),然后 for 循环异步处理每条事件。这样即使某条数据处理耗时较长,也不影响响应速度。
GroupChat 和 GroupMember 使用 upsert 模式(查→在则更新/不在则创建):
直接 insert 会导致主键冲突或重复数据。upsert 保证了数据的一致性和幂等性。
objectId、createdAt、updatedAt企微平台将成员列表等数据用 base64 编码传输,原因有三:
解码后的格式是分号分隔的 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 # 生产模式