站长头像

清酒Blog

岁岁平,岁岁安,岁岁平安!

文章 评论 标签
19 23 8

工时记录:为"一趟一趟"的工作造的时间账本

作者头像 作者头像

清酒 / 08-30 / 0 阅读 / 编辑

工时记录:为"一趟一趟"的工作造的时间账本

摘要:有一类工作的计量单位不是"小时",而是"趟":几点出车、几点到站、走了哪条线、哪个站接的车。市面上的考勤工具全都按"上下班打卡"设计,对这种工作形态无能为力。所以我自己做了一个。这篇讲讲它的形态,更讲讲它为了"数据绝不能丢"付出过的代价。

一、问题:时间不是一整块,是一趟一趟的

考勤软件的默认假设是:你九点上班,六点下班,中间是连续的一块时间。

但很多真实工作不是这样的。跑运输的、跑线的、倒班的,他们的一天是离散的:出公寓、到库接车、到站接车、退乘换站、入库收车……每一段有自己的起止时刻和地点,任何一段漏记,这一趟的账就不完整,月底结算就说不清。

通用工具应付不了这种结构——它们的世界里只有"上班"和"下班"两个锚点;表格软件倒是灵活,但要求人在手机上跟 Excel 搏斗,隧道里、库房门口、深夜的驾驶室里,谁也做不到。

工时记录 App 就是为这个缝隙做的:它的一切都围绕"趟"这个最小单位设计。

二、六步打卡:把一条流程钉进界面

一趟完整的出勤被拆成六个关键节点,App 按流程顺序引导记录——出寓、接车库接、站接、退乘站换、入库、退勤。每一步一键打上当时的时间,不用手填;一趟走到了哪一步、卡了多久,列表上一目了然。

围绕"趟"的周边功能也全部按这个场景长的:

  • 站名管理。常跑的站点建成字典,打卡时选择而不是输入;站名支持跨设备同步,换新手机不用重建;
  • 十五个字段一趟全记录。出寓时间、接车库接、站接、退乘站换、入库、退勤,加备注、金额等辅助字段,一趟的所有维度都在,而且 App 端和服务端的字段定义逐字对齐——两头说同样的语言,同步才不会"意思走样";
  • 趟次计算器。选两趟时间,自动算间隔时长,核对班表、复盘休息时间用;- 统计页。按月看趟数和时长,还有一行"开通以来累计 N 趟 · 工作 X 小时"——这一行是刻意设计的:记录越久,它越有分量。有人用它回顾一年,有人用它跟家人解释自己这一年是怎么过的;
  • 深色模式全量适配。夜班的人会懂这个细节的分量——凌晨两点看手机,白花花的界面是一种暴力。

三、一次真实的数据事故

这个 App 立项时我写给自己一句话,后来成了整个项目最高优先级的约束:

功能可以有缺陷,但记录的趟、时间、站名,绝不能丢、不能重复、不能被覆盖。

这句话不是凭空写的。早期版本出过一次真实事故:某天打卡的时间没能上云,另一台设备的旧副本又同步回来盖在本地,最后云端出现重复趟。对普通应用这是个小 bug,对工时记录这是天塌——用户拿这个数据对账、算钱,一趟重复、一趟丢失,都是真金白银。

那次之后我把整条同步链路推倒重做,并且立了新的开发规矩:凡是碰数据链路的改动,必须先跑一组专门的回归用例——修改检出、推送、合并、删除墓碑,一个都不能少。新增功能可以缓,新增场景不行。

从那以后这条链路再没丢过一条记录。但我很快发现,"没丢"和"知道没丢"之间还有很远的距离——这就引出了下面这个更吓人的故事。

四、同步在生产环境坏着,而没有人知道

有一段时间,我在后台例行体检时顺手验证了一下云同步接口——结果发现工时数据的推送请求全部在服务端报错,一个能落库的都没有。

更让人后背发凉的是另一件事:这个问题已经存在了一段时间,而用户毫无感知。为什么?因为 App 的同步有本地兜底——推不上云就先留在本地,下次再试,界面上一切都好。兜底机制设计的初衷是保护数据,但它的副作用是把服务端的故障完美地掩盖了。 数据没丢,只是永远上不了云;换设备的用户会突然发现自己的历史是空的,而那时候距离最后一次成功同步,可能已经过去很久。

根因挖出来很荒诞:服务端对一个字段的校验规则引用了一个不存在的验证规则名,任何包含该字段的请求一律报错。它之所以一直没被发现,是因为那条代码路径恰好被前面的兜底"保护"着。

这个案子给我的教训比那次数据事故更深:

  • 兜底是双刃剑——它保护用户,也掩护故障。所有"静默失败"的地方,都必须有另一双眼睛在盯着;
  • 可靠性不能靠"用起来没发现问题",要靠主动的、端到端的验证——App 好好的不代表链路是通的;
  • 自那以后,云同步的推送、拉取、幂等、墓碑每条路径都进了自动化回归,后台的体检命令也会验同步链路的活性。

现在这个 App 的同步链路是我所有产品里验证最密的一条。它配得上这个地位——毕竟它是用户的工资条。

五、同步协议的六条规矩

重做后的同步协议有六条核心设计,后来被原样复用到了我后面三个产品里:

  1. 本地先行。所有记录先落本地磁盘,网络状况与记录动作完全解耦——进隧道照样打卡,出来自动补推。离线是常态,不是异常;
  2. 防抖合并。改动后不立刻发请求,短暂静默后合并推送。狂点十下,最多两个请求;省流量也省电;
  3. 幂等去重。每条记录带唯一标识,服务端同一条数据推一万次也只存一次。"同一条数据重复进入"是回归测试的必备用例;
  4. 墓碑语义。删除记墓碑,已删除的数据不接受旧设备的迟到修改,删除语义永远优先;
  5. 增量游标。服务端在查询之前捕获时钟,游标续传——时钟捕获晚于查询的话,查询和计时之间的修改会被永久跳过,这是又一个真实的窗口期竞态案例;
  6. 合并而不是覆盖。多台设备各自记的站名做并集合并,谁加的都算数。

每一条背后都有一次教训。协议不是设计出来的,是赔偿出来的。

六、一个"定义了但没人调用"的函数

再讲一个同步链路上的小案子,因为它太典型了。

站名跨设备同步,服务端的拉取接口是写好了的——可代码审查时发现,App 里定义了这个调用,但全工程没有任何地方调用它。同步只剩"本地全量覆盖云端"一个方向。后果组合起来很惊悚:新设备登录后本地站名是空的,首次同步就把云端站名整个清空;而任何一台设备永远拉不到其他设备新加的站名。

修复并不复杂,难的是确立检查方式:现在每次大改之后都会做一遍"调用面核对"——每个接口函数,谁调用、在哪调用、什么时机调用,对不上号的就地处理。一个没人调用的函数,不是无害的,是定时炸弹。

七、后台:给管理者看的另一面

App 之外配了一个管理后台:使用人数、总趟数、今日出勤、进行中未退勤、最新版本、累计下载,六张指标卡一屏看全;近七日出勤趋势图和每个人的趟次明细单独成卡,明细的字段命名和 App 里逐字对齐——后台看到什么,就是当事人记了什么,没有二次翻译,也就没有二次失真。

八、功能全景与技术栈

功能全景:

  • 记录——六步打卡流程、一趟十五个字段全维度、站名字典与跨设备并集同步;
  • 同步——离线先行、防抖合并、幂等去重、墓碑语义、增量游标续传、同步状态可视化;
  • 工具——趟次间隔计算器、月度统计、"开通以来累计"行、深色模式全量适配、记住登录邮箱;
  • 后台——六张指标卡、近七日出勤趋势、按用户分组的趟次明细(字段与 App 逐字对齐);
  • 可靠性——数据链路专项回归用例、端到端同步活性验证、应用内自更新。

技术栈:

层选型
运行环境Linux + Nginx + PHP 8.2
后端自研 KX-Verse 底盘(Laravel 12)
数据库/缓存MySQL / 文件缓存 + 全页缓存
App 端uni-app(Vue 3),云打包分发
同步协议自研:幂等 + 墓碑 + 增量游标 + 服务端裁决
登录QQ 互联原生登录

九、写在最后

工时记录目前迭代到 1.5 版本,支持 QQ 一键登录、应用内自更新、记住登录邮箱,整套服务跑在我自己的云端底座上——每日加密备份、分钟级巡检,数据链路的每一环都在自动化的注视之下。

它是我四个产品里最"朴素"的一个:没有照片、没有 OCR、没有花哨的图表。但它也是被最严苛标准检验的一个——因为我始终记得那个立项目的下午定下的排序:

给一个司机省下的每一分钟都是小事,弄丢他的一趟记录是大事。工具的价值排序里,可靠永远排在漂亮前面。

这个 App 存在的意义,就是证明这句话。