利益相关:我是《班妥了》的开发者。本文不是把"做个日历"包装成技术难题,而是复盘真实实现里最容易算错、丢历史和不同步的部分。
排班应用第一眼很像 CRUD:定义几个班次、生成一个月历、允许用户修改某一天。真正写下去才发现,它同时碰到了日期算法、跨日时间、历史状态、关系型数据和鸿蒙 Form 卡片生命周期。下面选四个对同类应用也有参考价值的坑。
1. 循环排班不能只存"当前模板"
最直观的做法,是保存一个起始日和一个序列,例如"白白夜夜休休",再用日期差取模:
ts
function cycleIndex(days: number, length: number): number {
return ((days % length) + length) % length;
}
这里必须用正模,否则查看起始日前的日期时,负数余数会把索引带出数组。
更大的坑是:用户今天切换了一套循环,不代表过去的班表也应该被重算。因此我最终没有只保存"当前循环",而是保存循环分段:
startKey:这段循环从哪天开始;endKey:哪天结束,空值代表当前仍生效;templateIndex:使用哪套循环模板。
关闭循环时只封存当前分段;重新开启时创建新分段。这样月度和年度统计不会因为一次改模板,把几个月前的数据全部改写。
单日换班则是另一层数据:人工覆盖优先于循环。用户点"恢复循环"时,只删除当天覆盖记录,不需要重写后面的每一天。这种分层比把整个月 42 个格子都持久化更稳,也更容易解释。
2. 夜班不是同一天内的两个时间点
如果班次是 20:00 到次日 08:00,直接做 end - start 会得到负数。实现时需要先判断结束分钟是否小于或等于开始分钟;若是,就给结束时间加 24 小时,再扣除休息分钟。
提醒也有相同问题:
- 班前提醒按"开始时间 - 提前量"计算;
- 下班前提醒如果跨午夜,触发日应落到第二天;
- 排班一旦临时调整,未来提醒必须重算。
鸿蒙侧使用系统代理提醒后,还要考虑单应用提醒配额。我这里会规划未来 31 天,但按触发时间只注册最近 30 条;首条注册失败就终止本轮,避免权限或额度未就绪时连续制造失败调用。
3. 本地优先不等于把所有数据塞进一个 JSON
《班妥了》不要求登录,核心数据保存在本地关系型数据库 rotaday.db。早期把设置放 JSON 很方便,但团队成员、逐日排班和长期单日覆盖持续增长后,继续堆 JSON 会让更新、迁移和统计都变得笨重。
目前主要拆成几张表:
shift_assignment:个人单日覆盖,日期为主键;team_member:团队成员;team_assignment:日期 + 成员复合主键;app_setting:主题、循环分段、提醒等轻量设置;form_instance:已添加的桌面卡片实例。
一次保存会放在事务里:设置、个人排班、成员与团队排班任何一步失败都回滚。版本迁移时则先读取旧设置和旧关系表,再升级到新 schema,避免"升级成功但历史班表消失"这种最伤信任的问题。
4. 桌面卡片最怕"主页面是新的,卡片还是旧的"
鸿蒙版提供 2×2 和 2×4 两种今日班次卡片。这里我踩到的关键点有两个。
第一,FormExtensionAbility 创建卡片时,先同步返回一份可理解的默认数据,再异步读取数据库并更新。否则添加卡片过程等数据库,容易触发超时。
第二,主页面保存排班后,不只更新内存状态,还要读取已注册的 formId,统一调用 updateForm。失效的 formId 会从本地表清理,避免以后每次保存都重复报错。
卡片计算今日班次时复用与主页面相同的规则:个人版按"单日覆盖 > 循环",团队版读取标记为"我"的成员实际排班。业务规则只保留一份,才能避免 App 显示白班、桌面卡片却显示夜班。
从技术边界反推产品边界
这些实现最后落在《班妥了》的两种模式里:个人版负责循环排班、临时改单日、三类提醒和月度/年度统计;团队版负责成员、岗位和逐日排班,让负责人从月历里快速看出当天谁在岗。它不是考勤、薪资或劳动合规一体化 HR 系统,定位仍然是轻量排班工具。
目前有鸿蒙版本和微信小程序版本。鸿蒙版适合长期使用,支持桌面卡片,免费、无广告、无需登录,本地保存;小程序不需要安装,适合临时查班或帮家人排一段时间。
如果你也做过日历、值班、提醒或 Form 卡片类应用,欢迎交流你遇到过的日期边界。最后放一张当前的个人排班界面:
