文章目录
上一篇《让劳务公司在线填表(一):Nextcloud + OnlyOffice 自托管协作实录》记录了自托管方案从选型到上线的全过程。系统真的跑起来之后,新问题也来了:外部填表体验尚可,但维护成本一直压着——授权链固定 30 天到期、编辑器偶发空白要打补丁、账号密码要人管,这些当时都写进了文章,只是真到日常,才知道"自托管一整套办公套件"的维护面有多宽。
于是这次做了个决定:外部填表换到飞书在线表格,内部的读取链路(钉钉表镜像、每日汇总、对账保险丝)原样保留,只把"数据源"从 Nextcloud 文件换成飞书云文档 API。这篇记录换轨的取舍、外部访问模型,把数据读回来这条 API 链路怎么搭、怎么验证,以及最后怎么用"编辑事件"让同步自己跑起来。
一、为什么换:运维成本也是成本
把三条路摆在一起,取舍其实很清楚:
| 方案 | 结论 | 原因 |
|---|---|---|
| 继续微信传 Excel | 维持现状 | 版本混乱、敏感字段在聊天记录里流转,无法审计 |
| 自建 Nextcloud + OnlyOffice | 已上线,后换 | 数据边界最好,但维护重:授权链到期、编辑器补丁、账号管理,外部手机端体验一般 |
| 飞书在线表格 | 采用 | 外部"链接 + 密码"即可填,手机端成熟、零运维;代价是数据放第三方 |
关键不是"哪个功能强",而是这件事该由谁来维护。名单里的信息(姓名、手机号、身份证)敏感,最初选自托管正是为了数据不出域;但它换来的是我们自己维护一整套协作套件。飞书把这层接走之后,外部访问被收窄成"一个文档的一次填写权",我们要维护的东西从"一整套系统"缩到"一个自建应用 + 一段同步脚本"。
二、外部访问模型:加密链接 + 密码 + 登录
飞书侧的组织结构很简单:每家公司一个独立表格,互相看不到。分享用"互联网上获得链接的人可编辑",并开启加密分享——链接和密码分开告知,外部点开先过密码页(见上图)。
这里有个必须提前说清的边界:外部编辑必须登录飞书账号(手机号注册即可,不必加入我们的组织);只阅读才可以匿名。对填表人来说这只是一次注册,和之前 Nextcloud 发账号是同一件事,但手机端体验和实时协作是现成产品能力。
- 权限收窄:外部只给"可编辑",不给"可管理",改不了分享范围;表头行设"保护范围",编辑者动不了列名与结构;
- 隔离方式:一家一个文件、一条链接、一个密码,公司之间的隔离天然成立;
- 诚实说明:飞书表格没有自建方案那种服务端硬拦截(禁删、禁改名),所以"数据安全"靠三件事兜底——一司一表、表头保护、以及内部同步链路自带的对账与备份。
三、数据怎么回来:企业自建应用 + 云文档 API
外部怎么填解决了,剩下的问题是:表格在飞书,数据怎么自动回到钉钉表。答案是企业自建应用加云文档 API,链路一共五步:
第一个坑就在"链接"上:从浏览器地址栏拿到的飞书链接是 /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,且不可恢复。防线要建在填表之前:
- 建表时就把身份证、手机号列设为"文本"格式,再发给外部填写;手机号 11 位数值虽然无损,但带前导零的号码会丢零,同样建议文本;
- 验证不看显示,看类型:粘贴一行测试数据后,用 API 回读并检查类型。实测结果——18 位身份证是字符串(完整保留),11 位手机号是数值(该列当时是常规格式);
- 日期列用日期格式,读取时按"格式化字符串"取,拿到的就是
yyyy-mm-dd,与下游口径一致。
五、写进钉钉表:沿用原来的同步纪律
同步脚本的"下半场"完全复用:读取到的数据按"整表重灌"或"固定列增量"写入钉钉表格,然后写后逐值回读对账;空源、解析 0 行等异常一律拒写(保险丝);内容指纹不变则整轮跳过;失败只发一条去重告警。
这次先用一张测试表做链路打样:源表 4 行 × 6 列(姓名/年龄/性别/住址/身份证/手机号),写入钉钉表后回读逐值一致,身份证 18 位完整。触发方式不用定时——见下一节。
六、触发方式:用编辑事件,而不是定时轮询
名单是"随时可能被改"的:白天劳务公司想起来就填两行,晚上也可能补录。定时轮询要么慢(改完还要等下一轮),要么费(缩短间隔就是拿调用量换实时性)。云文档本身支持文件编辑事件,正好拿来代替轮询:
订阅是每份文档一次的动作,一个请求就够;监听侧用官方 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 秒跑完,编辑后约一分钟内钉钉表可见。
七、可复用的四步
把这次的链路沉淀成流程,下次换表、换部门照做即可:
- 建表并设列格式:文本列(身份证/手机号)与日期列在建表时就定好;
- 设分享:"互联网上获得链接的人可编辑" + 加密分享,链接与密码分开发;
- 加应用:把自建应用通过"添加文档应用"加进每个文档,权限给"可管理"(订阅事件需要);
- 登记并订阅:在同步脚本配置里写一行
{名称, 链接},再对该文档调用一次事件订阅接口,其余全部复用。
八、踩坑清单(可直接复用)
- wiki 链接 ≠ 表格 token:先调知识库节点接口换
obj_token,再调表格接口; - 两道门缺一不可:应用权限(开发者后台)和文档授权(添加文档应用),少一道就报权限失败,报错会直接提示去添加;
- 长数字列必须文本:建表时设置,粘贴后用 API 回读类型验证,别只看显示;
- 访问凭证要缓存:tenant_access_token 两小时有效,复用而不是每请求一换;
- 读取范围有上限:单次 10MB,大数据量分块读;
- 写后回读对账不能省:这是上一套链路留下的纪律,换数据源后照旧执行;
- 触发用事件,不用定时:编辑事件 + 长连接接收,本机即可,不需要公网地址;防抖参数决定"最后一次编辑后等多久";
- 「事件配置」和「回调配置」是两页:云文档编辑走事件配置里的「文件编辑」;回调配置是卡片交互这类同步场景,别配错页;
- 应用身份订阅要开两个身份的接收权限:官方要求应用与用户身份都开通,只开一边会静默收不到;长连接为集群推送,同一应用保持单实例监听。
评 论