Құжат мазмұны

考勤一套日历五路来源

本文档说明宿舍考勤系统的核心设计:全局唯一的日历裁决函数按五步链判定当天是否考勤,五路打卡来源留痕不覆盖,自动考勤经四层排除生成未归状态。

  • 全局唯一日历函数按学期、调休、假日、周末、兜底五步裁定是否考勤
  • 调休日优先于周末跳过,避免补班日漏统计
  • 五路打卡来源留痕不覆盖,终态取最后一条有效来源
  • 自动考勤每晚23点执行,先问日历再经四层排除生成未归
  • 请假走审批自动排除考勤,暂不考勤独立标注到期恢复

明·如归 · 宿舍管理白皮书 | 第二篇 能力篇

床位不出错,后面所有"智能"才敢往上接。但床位只是一个静态的地基,宿舍里真正每天在动的事情是考勤——几百个年轻人每天夜里回到楼里,谁到了、谁没到、谁不该算没到。多数系统把考勤做成"收打卡",打卡确实收了,但有一个前置问题从来没被干净地回答过:今天到底该不该考勤? 五一调休补班那天算不算?国庆前一天下午放了半天假算不算?寒假最后两天有人提前返校,系统该记他"正常"还是"无记录"?——一个问题的答案必须全校只有一个,否则五路来源各自判断,出来的结果永远对不上。

10.1 为什么"今天算不算"是一个系统设计问题

一个学期里,"今天算不算考勤"的判定依据至少有四层:学期起止范围之内、国家假日和校历调休的精确日期、周末的正常跳过、以及某次临时通知的"本日不考勤"。如果这四层散落在各功能入口——打卡页面自己判、定时任务自己判、闸机对接层自己判——三个地方三套逻辑,改了一处忘改另一处,期末统计出来的人数永远对不上。

考勤系统的设计水平,先不体现在打卡体验上,体现在"判定今天"这件事有没有一个不可绕过的单点。

10.2 单点裁决:五步链,一条路走到底

系统里有一个全局唯一的日历裁决函数,所有需要判断"今天该不该考勤"的代码路径——学生打卡入口、宿管代录入口、每晚的自动考勤任务——统统调它,不许各自造轮子。它的逻辑链只有五步,但顺序是设计过的:

第一步,学期范围。 当前日期不在任何已配置的学期区间内,直接返回"不在考勤时间"。这一步兜住寒暑假、寒假前两周和暑假后两周——那些日子根本不该有考勤记录产生。

第二步,调休日优先。 国庆前的周六被国务院定为补班日,学校配了"调休上班"标记——日历在这一天返回"考勤",覆盖周六的默认跳过。调休优先于周末,是这条链里最不起眼但最容易做错的一环:很多系统把"周末跳过"放在最前面,结果调休补班那天不考勤,全校统计凭空少一天数据。

第三步,明确假日。 国庆、暑假、校定长假——标记了"假日"的日期返回"不考勤"。

第四步,普通周末。 没被前三步命中的周六周日,默认跳过。

第五步,兜底。 走到这里就是普通工作日——考勤。

五步给出一个布尔值和一条原因文字。原因会被返回给调用方——如果学生在假期试图打卡,页面显示的提示句里就嵌着那句原因:"今天是国庆假期,无需打卡"。他看到的拒绝不是"系统故障",是一个说得出理由的判断。

10.3 五路来源:谁报告了"他回来了"

日历决定"今天算",来源决定"这条记录怎么产生"。系统记录五类打卡途径,每条考勤记录都带着来源标签入库:

学生自助打卡(移动端定位打卡或校园 Wi-Fi 触发)、宿舍长查寝代录、宿管巡楼代录、闸机刷卡、人脸识别。前三种是人做出来的判断,后两种是设备给出的事实——人和设备在数据模型上完全平等,都只是一条来源标签不同的记录。

闸机和人脸依赖外部设备接入。设备未接入时这两路来源自然为空,不影响前三路运转;设备接入后,它的通行记录直接作为考勤来源入链——系统接受设备侧识别结果,不替设备下结论,算法在设备端,考勤只负责把"某人某时进了楼"作为一个事实收进来。

10.4 来源不打架:留痕优先于合并

五路来源同时对一个学生做记录时怎么办?系统的原则朴素但正确:来一条记一条,不覆盖。 每条考勤记录带着自己的来源、时间戳和操作者入库。宿舍长九点查寝录了"在寝",学生自己十点在闸机又刷了一次——两条并存,终态取"最后一条有效来源",前一条不删不改。

为什么不能覆盖? 因为出了事故要能回放:"三点半宿舍长说他还在,五点半闸机记录他出了楼"——这两条如果只有一条活着,关键时间线上就有一个断口。留痕不是保守,是替将来那个需要追问的人保留证据。

防代打是这条链上唯一写死的业务规则:学生自助打卡只能为本人床位。 后端按登录身份拦截——从移动端传入他人床位 ID,接口直接返回拒绝,不进入打卡逻辑。这不是权限问题(他确实有"打卡"权限),是身份绑定——"给他人打卡"这件事在学生端不存在,不是被禁止,是从未提供。

10.5 每晚十一点:自动考勤的四个排除

手动考勤的覆盖面永远不够——有人忘刷、有人不想让宿舍长看到自己回来。系统用一个定时任务兜底:每晚 23:00,自动考勤执行一次,给"该在但没有记录"的人生成"未归"状态。

自动考勤不是"给所有人在住名单上盖一条未归"。它先问日历"今天算不算"——不算则整体跳过,日志留原因。算,则进入四层排除:

排除一:今天已有任何来源打卡记录的人。 只要存在——不分来源、不分状态——视为已归,不再追加未归。

排除二:已审批通过的请假且有效期覆盖当天的人。 请假批了三天,中间那天自动考勤不碰他——不需要事先把他加入任何"暂不考勤"名单。这是"请假自动排除"的设计:请假审批通过的那一刻,考勤自动跟着走,两边不需要同步操作。

排除三:暂不考勤名单内且有效期覆盖当天的人。 暂不考勤独立于请假,用于实习、休学、或其他明确标注的例外。它有起止日期,过期自动失效——不是一次添加永久免考。

排除四:不在在住名单的人。 退了床的人不该出现在考勤表上。

走完四层排除剩下的,统一标记"未归"——状态五态之一:无记录、在寝、晚归、未归、假期。自动任务幂等:一人一天一条终态,重复执行不产生双份记录。

10.6 暂不考勤与请假:两条路不走同一扇门

这两件事在多数学校混在一起——"不考勤的人"一张名单拉出来。系统把它们分开了:

请假是审批事件。 有起止时间、有审批链、有原因分类;一旦通过,考勤自动排除对应日期区间,不需要人工再往另一张表加一笔。请假覆盖的时间段之外,考勤正常恢复。

暂不考勤是独立标注。 它不走审批(或走另一条审批),它只声明"这个人在这个时间段内不参与考勤"——实习、休学、交换项目,都是它的典型场景。历史遗留的"晚归""请假"两种暂不考勤类型已标记为仅兼容旧数据,新增不再可选——它们本不该出现在这里。

分开的代价是多维护一张表,好处是:不会出现"假期回来忘了从暂不名单里拉回来"这种事故。 请假有自己的结束时间,到期自动失效;暂不考勤的结束时间到了,当天考勤就恢复。

10.7 状态五档与警报次数

考勤的终态只有五个格子——无记录(当天没有任何来源)、在寝(正常回来)、晚归(过了时间窗才回来)、未归(自动任务生成的最终判断)、假期(日历判定不考勤的日子)。

状态看似简单,但它喂着两套下游逻辑:一是宿管的夜查名单——"未归"的人明天早上该被叫到辅导员那里;二是归寝异常检测——连续 N 天未归、作息突变(他平时十点回,这周突然都十二点后)、同寝连带(一间房三人以上同晚未归),三种异常各自进预警工作台。没有干净的考勤状态,异常检测就是空中楼阁(第二十章详述检测器)。

每条考勤状态记录还带着两个计数器:未归警报次数和宅寝警报次数。它们决定的是第几次该升级惊动——第一次只是站内通知推给宿管,第三次该让辅导员看到,第五次该让学院知道。

10.8 给学校的一件事

考勤系统的检验不需要复杂场景——找一个调休补班的周六,问系统三个问题:打卡入口在那天开放吗?自动考勤在那天执行了吗?执行完的终态和第二天人工巡楼的结果一致吗? 三问都对,日历这个单点才算真正立住了。

日历管住了"该不该算",床位管住了"在哪里算",接下来的问题是:住进去之后的事——从预分配那一天的批次操作,到学期中间一场让人焦头烂额的调宿审批,到毕业那天的退住归档——这条一整年的生命周期,系统接不接得住。