文章目录

上一篇《让劳务公司在线填表(一):Nextcloud + OnlyOffice 自托管协作实录》记录了自托管方案从选型到上线的全过程。系统真的跑起来之后,新问题也来了:外部填表体验尚可,但维护成本一直压着——授权链固定 30 天到期、编辑器偶发空白要打补丁、账号密码要人管,这些当时都写进了文章,只是真到日常,才知道"自托管一整套办公套件"的维护面有多宽。

于是这次做了个决定:外部填表换到飞书在线表格,内部的读取链路(钉钉表镜像、每日汇总、对账保险丝)原样保留,只把"数据源"从 Nextcloud 文件换成飞书云文档 API。这篇记录换轨的取舍、外部访问模型,把数据读回来这条 API 链路怎么搭、怎么验证,以及最后怎么用"编辑事件"让同步自己跑起来。

飞书加密分享链接的密码访问页
▲ 外部协作者点开链接先过密码页;输入密码后,编辑还需要登录飞书账号

一、为什么换:运维成本也是成本

把三条路摆在一起,取舍其实很清楚:

方案结论原因
继续微信传 Excel维持现状版本混乱、敏感字段在聊天记录里流转,无法审计
自建 Nextcloud + OnlyOffice已上线,后换数据边界最好,但维护重:授权链到期、编辑器补丁、账号管理,外部手机端体验一般
飞书在线表格采用外部"链接 + 密码"即可填,手机端成熟、零运维;代价是数据放第三方

关键不是"哪个功能强",而是这件事该由谁来维护。名单里的信息(姓名、手机号、身份证)敏感,最初选自托管正是为了数据不出域;但它换来的是我们自己维护一整套协作套件。飞书把这层接走之后,外部访问被收窄成"一个文档的一次填写权",我们要维护的东西从"一整套系统"缩到"一个自建应用 + 一段同步脚本"。

💡 取舍:自托管与 SaaS 不是对错题,是数据边界与运维成本之间的交换。当外部协作本身不是主业时,把"能填表"交给成熟产品,把"数据怎么回来"留在自己手里,是更划算的分工。

二、外部访问模型:加密链接 + 密码 + 登录

飞书侧的组织结构很简单:每家公司一个独立表格,互相看不到。分享用"互联网上获得链接的人可编辑",并开启加密分享——链接和密码分开告知,外部点开先过密码页(见上图)。

这里有个必须提前说清的边界:外部编辑必须登录飞书账号(手机号注册即可,不必加入我们的组织);只阅读才可以匿名。对填表人来说这只是一次注册,和之前 Nextcloud 发账号是同一件事,但手机端体验和实时协作是现成产品能力。

三、数据怎么回来:企业自建应用 + 云文档 API

外部怎么填解决了,剩下的问题是:表格在飞书,数据怎么自动回到钉钉表。答案是企业自建应用加云文档 API,链路一共五步:

1
开发者后台创建"企业自建应用",开通云文档只读权限(电子表格读取)
sheets:readonly
↓
2
创建版本并发布(组织管理员自己审核);权限不发布不生效
版本发布
↓
3
在每个文档「… → 更多 → 添加文档应用」把应用加为协作者——这是第二道门
文档授权
↓
4
服务端用 App ID/Secret 换 tenant_access_token,缓存 2 小时复用
访问凭证
↓
5
按范围读取单元格,进入原有的同步链路(钉钉表镜像 / 每日汇总 / 对账)
读范围

第一个坑就在"链接"上:从浏览器地址栏拿到的飞书链接是 /wiki/<node> 形态,它是知识库节点,不是表格本身的 token,直接拿去调表格接口会找不到资源。正确姿势是先换一次:

// 1) wiki 节点 → 真实电子表格 token(之后所有表格接口都用它)
const node = await feishu.api('GET', '/wiki/v2/spaces/get_node',
  { query: { token: nodeToken } });
const sheetToken = node.obj_token;

// 2) 读取范围:默认渲染可区分类型(文本是字符串、数字是 number)
const data = await feishu.api('GET',
  `/sheets/v2/spreadsheets/${sheetToken}/values/${sheetId}!A1:Z200`);
const rows = data.valueRange.values;

凭证管理上只有两条纪律:token 一定要缓存(有效期两小时,别每个请求都换一次);范围读取单次有 10MB 上限,数据量上来要分块或按列裁剪。

四、格式防线:身份证必须是文本

换到飞书并不意味着格式问题自动消失——电子表格默认同样会把长数字当数值处理,18 位身份证一旦被转成数值,后几位直接变成 0,且不可恢复。防线要建在填表之前:

  1. 建表时就把身份证、手机号列设为"文本"格式,再发给外部填写;手机号 11 位数值虽然无损,但带前导零的号码会丢零,同样建议文本;
  2. 验证不看显示,看类型:粘贴一行测试数据后,用 API 回读并检查类型。实测结果——18 位身份证是字符串(完整保留),11 位手机号是数值(该列当时是常规格式);
  3. 日期列用日期格式,读取时按"格式化字符串"取,拿到的就是 yyyy-mm-dd,与下游口径一致。
⚠️ 静默数据事故:数值化的长数字不会报错,只会在某次导出后悄悄少几位。凡是身份证、银行卡号、编号类字段,先设文本、再验证类型,顺序不能反。

五、写进钉钉表:沿用原来的同步纪律

同步脚本的"下半场"完全复用:读取到的数据按"整表重灌"或"固定列增量"写入钉钉表格,然后写后逐值回读对账;空源、解析 0 行等异常一律拒写(保险丝);内容指纹不变则整轮跳过;失败只发一条去重告警。

这次先用一张测试表做链路打样:源表 4 行 × 6 列(姓名/年龄/性别/住址/身份证/手机号),写入钉钉表后回读逐值一致,身份证 18 位完整。触发方式不用定时——见下一节。

六、触发方式:用编辑事件,而不是定时轮询

名单是"随时可能被改"的:白天劳务公司想起来就填两行,晚上也可能补录。定时轮询要么慢(改完还要等下一轮),要么费(缩短间隔就是拿调用量换实时性)。云文档本身支持文件编辑事件,正好拿来代替轮询:

1
给文档订阅事件:调用订阅接口(应用身份,要求应用对该文档有可管理权限),每份文档订阅一次
subscribe
↓
2
开发者后台「事件与回调 → 事件配置」添加「文件编辑」,按提示开通权限
drive.file.edit_v1
↓
3
接收方式选长连接:SDK 与开放平台建 WebSocket 通道,本机就能收,不需要公网 IP/域名/内网穿透
长连接
↓
4
收到事件后防抖 60 秒再执行同步,连续编辑自动合并成一轮;同步完成写后对账
防抖 + 同步

订阅是每份文档一次的动作,一个请求就够;监听侧用官方 SDK 建长连接,核心就两段:

# 给文档订阅事件(应用身份;应用需对文档有可管理权限,每份文档订阅一次)
curl -X POST "https://open.feishu.cn/open-apis/drive/v1/files/${SPREADSHEET_TOKEN}/subscribe?file_type=sheet" \
  -H "Authorization: Bearer ${TENANT_ACCESS_TOKEN}"
// 长连接监听:收到编辑事件 → 防抖 60s → 触发同步(本机即可,无需公网 IP)
const dispatcher = new lark.EventDispatcher({}).register({
  'drive.file.edit_v1': async (data) => {
    const { file_type, file_token } = data.event;
    if (file_type === 'sheet' && file_token === SOURCE_TOKEN) {
      debounce(runSync, 60 * 1000);   // 连续编辑合并成一轮
    }
  },
});
const ws = new lark.WSClient({ appId: APP_ID, appSecret: APP_SECRET });
await ws.start({ eventDispatcher: dispatcher });

实测一轮完整链路:在表格里改一格 → 平台推来编辑事件 → 防抖计时 → 自动读取源表、重灌钉钉表并回读对账;同步脚本本身 2.3 秒跑完,编辑后约一分钟内钉钉表可见。

💡 为什么不做定时:事件驱动把"什么时候同步"交给平台判断,没有编辑就没有调用;防抖则解决"连续编辑"——60 秒内的多次修改合并成一轮,不会把一次填表拆成一串同步。

七、可复用的四步

把这次的链路沉淀成流程,下次换表、换部门照做即可:

  1. 建表并设列格式:文本列(身份证/手机号)与日期列在建表时就定好;
  2. 设分享:"互联网上获得链接的人可编辑" + 加密分享,链接与密码分开发;
  3. 加应用:把自建应用通过"添加文档应用"加进每个文档,权限给"可管理"(订阅事件需要);
  4. 登记并订阅:在同步脚本配置里写一行 {名称, 链接},再对该文档调用一次事件订阅接口,其余全部复用。

八、踩坑清单(可直接复用)

  1. wiki 链接 ≠ 表格 token:先调知识库节点接口换 obj_token,再调表格接口;
  2. 两道门缺一不可:应用权限(开发者后台)和文档授权(添加文档应用),少一道就报权限失败,报错会直接提示去添加;
  3. 长数字列必须文本:建表时设置,粘贴后用 API 回读类型验证,别只看显示;
  4. 访问凭证要缓存:tenant_access_token 两小时有效,复用而不是每请求一换;
  5. 读取范围有上限:单次 10MB,大数据量分块读;
  6. 写后回读对账不能省:这是上一套链路留下的纪律,换数据源后照旧执行;
  7. 触发用事件,不用定时:编辑事件 + 长连接接收,本机即可,不需要公网地址;防抖参数决定"最后一次编辑后等多久";
  8. 「事件配置」和「回调配置」是两页:云文档编辑走事件配置里的「文件编辑」;回调配置是卡片交互这类同步场景,别配错页;
  9. 应用身份订阅要开两个身份的接收权限:官方要求应用与用户身份都开通,只开一边会静默收不到;长连接为集群推送,同一应用保持单实例监听。
✅ 现状:外部填表已切到飞书在线表格(链接 + 密码,手机端可用);内部用自建应用 + 云文档 API 读回钉钉表,并已接上编辑事件(长连接 + 60 秒防抖)——有人改就自动同步,不跑定时。打样链路完整验证通过,自托管那套系统保留为回滚选项。

评 论