Egg.js 对接飞书多维表格 —— 实现注册数据自动上报

0 阅读4分钟

背景

业务上需要将用户注册行为(账号、来源、状态等)自动记录到飞书多维表格中,方便运营团队实时查看注册数据,无需登录后台系统。

本文基于 Egg.js + 飞书开放平台 Node SDK,实现了一个完整的注册数据上报功能。

技术栈

  • Egg.js(Node.js 企业级框架)
  • @larksuiteoapi/node-sdk ^1.60.0(飞书开放平台官方 Node SDK)
  • 飞书多维表格(Bitable)

整体架构

客户端注册请求
    ↓
POST /doc-api/register/report
    ↓
RegisterReportController(参数校验)
    ↓
FeishuBitableService(调用飞书 SDK 写入多维表)
    ↓
飞书多维表格(数据落地)

实现步骤

1. 安装依赖

npm install @larksuiteoapi/node-sdk --save

2. 配置飞书应用凭证

config/config.default.js 中添加飞书多维表配置:

// 飞书多维表配置
feishuBitable: {
  appId: "your_app_id",           // 飞书应用 App ID
  appSecret: "your_app_secret",   // 飞书应用 App Secret
  appToken: "your_app_token",     // 多维表格的 App Token
  tableId: "your_table_id",       // 数据表 ID
  userAccessToken: "your_token",  // 用户访问令牌
},

获取方式:飞书开放平台 创建自建应用,开通多维表格权限后,在 API 调试台获取以上参数。

3. 编写 Service 层 —— 封装飞书 SDK 调用

新建 app/service/feishuBitable.js

const Service = require("egg").Service;
const lark = require("@larksuiteoapi/node-sdk");

class FeishuBitableService extends Service {
  // 创建飞书客户端
  createClient() {
    const { appId, appSecret } = this.ctx.app.config.feishuBitable;
    return new lark.Client({ appId, appSecret });
  }

  // 向多维表添加注册记录
  async addRegisterRecord(data) {
    const { ctx, logger } = this;
    const config = ctx.app.config.feishuBitable;

    try {
      const client = this.createClient();

      // 构建字段数据
      const fields = {
        "注册账号": data.account,
        "注册时间": Date.now(),
        "utmurl": data.utmurl || "",
        "注册前向地址": data.cl_referrer || "",
        "code": data.code,
        "成功/失败原因": data.fail_reason || "成功",
      };

      const result = await client.bitable.v1.appTableRecord.create(
        {
          path: {
            app_token: config.appToken,
            table_id: config.tableId,
          },
          data: { fields },
        },
        lark.withUserAccessToken(config.userAccessToken),
      );

      logger.info("[FeishuBitable] 添加注册记录成功", {
        account: data.account,
        recordId: result?.data?.record?.record_id,
      });

      return { success: true, data: result.data };
    } catch (error) {
      logger.error("[FeishuBitable] 添加注册记录失败", {
        account: data.account,
        error: error.message,
        response: error.response?.data,
      });
      return { success: false, error: error.message };
    }
  }
}

module.exports = FeishuBitableService;

4. 编写 Controller 层 —— 接口入口与参数校验

新建 app/controller/user/registerReport.js

const Controller = require("egg").Controller;

class RegisterReportController extends Controller {
  async report() {
    const { ctx, logger } = this;
    const { account, utmurl, cl_referrer, code, fail_reason } = ctx.request.body;

    // 参数校验
    if (!account) {
      ctx.body = { code: "000001", message: "注册账号不能为空" };
      return;
    }
    if (!code) {
      ctx.body = { code: "000001", message: "code不能为空" };
      return;
    }

    try {
      const result = await ctx.service.feishuBitable.addRegisterRecord({
        account, utmurl, cl_referrer, code, fail_reason,
      });

      if (!result.success) {
        ctx.body = { code: "000002", message: "上报失败" };
        return;
      }

      ctx.body = { code: "000000", message: "成功", data: result.data };
    } catch (error) {
      logger.error("[RegisterReport] 上报异常", error);
      ctx.body = { code: "000500", message: "网络错误" };
    }
  }
}

module.exports = RegisterReportController;

5. 注册路由

app/router/router_user.js 中添加:

router.post("/doc-api/register/report", controller.user.registerReport.report);

接口文档

POST /doc-api/register/report

请求参数(JSON Body):

字段类型必填说明
accountstring注册账号(手机号)
codestring状态码,如 "000000"
utmurlstringutm 来源 URL
cl_referrerstring注册前向地址(Referrer)
fail_reasonstring失败原因,不传默认为 "成功"

请求示例:

{
  "account": "18330825660",
  "utmurl": "https://www.baidu.com",
  "cl_referrer": "https://www.google.com",
  "code": "000000",
  "fail_reason": "成功"
}

响应示例:

{
  "code": "000000",
  "message": "成功",
  "data": {
    "record": {
      "record_id": "recxxxxxx",
      "fields": { ... }
    }
  }
}

踩坑记录

Map 不能作为飞书 SDK 的 fields 参数

飞书官方示例代码使用了 new Map() 来构建 fields:

// 官方示例(存在问题)
data: {
  fields: new Map([
    ['注册账号', '18330825660'],
    ['code', '000000'],
  ]),
}

但实际使用中,这会导致写入空记录。 原因是 SDK 内部通过 JSON.stringify 序列化请求体,而 Map 无法被正确序列化:

JSON.stringify(new Map([["key", "value"]]))
// 输出: "{}"    ← 数据全部丢失!

解决方案: 使用普通对象替代 Map:

// 正确写法
data: {
  fields: {
    "注册账号": "18330825660",
    "code": "000000",
  },
}

多维表格字段名必须精确匹配

fields 中的 key 必须和飞书多维表格中的列名完全一致(包括中文、标点、空格),否则该字段会被静默忽略,不会报错。

涉及文件清单

文件操作说明
package.json修改新增 @larksuiteoapi/node-sdk 依赖
config/config.default.js修改添加飞书多维表配置项
config/config.local.js修改添加本地环境飞书配置 + MySQL charset
app/service/feishuBitable.js新增飞书多维表 Service 封装
app/controller/user/registerReport.js新增注册上报 Controller
app/router/router_user.js修改注册上报路由

多维表格字段映射

多维表列名数据来源说明
注册账号account用户手机号
注册时间Date.now()接口调用时的时间戳(毫秒)
utmurlutmurl广告追踪来源
注册前向地址cl_referrer用户注册前的页面地址
codecode注册结果状态码
成功/失败原因fail_reason注册结果描述

关注前端求生录微信公众号,了解更多 微信图片_20260427132414_18_737.jpg