给一个客户做考勤汇总,每天凌晨从企业微信拉前一天的打卡记录,写进 MySQL,再给 HR 导出 Excel。本地测了三天都对,上线第一天早上,HR 发来一句话:所有人上班时间都晚了 8 小时。
1. 时间戳被转了两次
接口返回的 checkin_time 是 Unix 时间戳,本身没有时区。我们脚本里用 PHP 的 date() 格式化,服务器时区设的是 Asia/Shanghai,这一步没问题。问题是前面还有同事写的一层「统一时间处理」,把时间戳当成字符串又按 UTC 解析了一次,等于多绕了一圈,最后整体偏移。
改法很土:全链路只认时间戳,格式化只在最后导出那一步做,中间不许再转。顺手在数据库里加了一列 raw_ts 存原始值,出事能倒回去看。
2. access_token 每次都重新取
第一版图省事,每个请求都调一次拿 token 的接口。跑 200 多人的数据要几十次请求,有天触发了频率限制,整批失败。改成存 Redis,过期 7000 秒,官方给 7200,留点余量,快到期再刷。
钉钉那边的考勤接口我们早半年接过,坑不一样,频率限制倒是差不多。
3. 分页 cursor 用错了
打卡接口按 cursor 翻页,第一版把上一页最后一条记录的时间当 cursor 传回去,结果边界那条被重复拉了一次,考勤表里多了几条重复。改成老老实实用响应里返回的 next_cursor,并且在写入时按 user_id 加 checkin_time 建了唯一索引,重复的直接忽略。
4. 2000 行一条条插
2000 多行记录,一开始循环单条 INSERT,跑了 20 多秒。改成拼成每 500 行一批的批量 INSERT,降到 1 秒出头。没什么技术含量,就是别忘了。
5. 没考虑补卡
接口里除了正常打卡还有补卡记录,字段一样但来源不同。第一版没区分,把补卡当成当天第二次打卡算进去了。后来加了个 source 字段,导出时按 HR 的规则单独列一列。
收尾
那天上午把 8 小时改回来,重跑了一次全量,HR 那边重新导出。表格发出去之前我们拿三个人的原始时间戳手算了一遍,对着看,没再偏。
后来这份脚本的注释第一行写着:时间只转一次,在最后。

