· compliance· 约 21 分钟精读
三类检验、两个到期日:SlingGuard 检验类型与设备周期的口径分离
标准把检验分成使用前检查、定期检查与彻底检验三层,SlingGuard 把它们压进一个三成员枚举,到期日却只由一个设备周期字段推算。本文按源码核对类型、周期、记录到期日与状态判定四条线,并把 30 天预警窗口在四个档位上的占比摆在同一张表里。
港口堆场的吊装库房里,班组每天开工前要做一次使用前检查,每六个月要把吊具交给合资格人员做一次彻底检验。两件事都写进同一本台账。于是常出现这样的画面:一条三吨扁平吊带的记录刚录完,PDF 证据页上写着「Scheduled next date: 2027-04-30」,而资产详情页顶部写着「Next Due: 2027-01-21」。两个日期都由这次操作产生,都不是伪造,但它们回答的不是同一个问题。
本文核对 SlingGuard 里与「多久算到期」有关的四条线:检验类型的枚举、设备级的周期字段、记录级的到期日字段,以及状态机的判定入口。结论按源码逐行核对。涉及的主要文件是 InspectionRecord.swift(258 行)、RiggingAsset.swift(206 行)、RiggingStore.swift(215 行)、InspectionEntryView.swift(242 行)、BatchInspectionView.swift(225 行)、ReportExporter.swift(294 行)与 CSVExporter.swift(58 行)。
一、标准把检验分成三层,应用把前两层压进一个枚举
LOLER 1998 第 9 条第(3)款把在役检验分成两类动作。第(a)项要求彻底检验:升降人员的设备与吊装附件至少每 6 个月一次,其他起重设备至少每 12 个月一次,或者按检验方案执行;第(b)项要求在两次彻底检验之间,由合资格人员按适当间隔进行检查。HSE 的说明把它拆得更细:使用者做的使用前检查、按周或按月按季做的定期检查、以及每年一到两次的彻底检验。三者的产物不同,只有彻底检验产生一份带「下次彻底检验到期日」的书面报告。
北美文本的分层类似而措辞不同。OSHA 29 CFR 1910.184(d) 要求每班使用前由指定合资格人员检查吊索和所有连接件;ASME B30.9 把检验分成初始检验、频繁检验与定期检验三级,只有定期检验要求书面记录,周期按使用工况分为每年(正常)、每月至每季(严酷),以及由合资格人员确定(特殊)。
| 标准中的层级 | 典型间隔 | 是否要求书面记录 | SlingGuard 枚举成员 |
|---|---|---|---|
| 使用前检查 | 每班或每日 | 否 | frequentPreUse |
| 定期彻底检验(吊装附件) | 6 个月 | 是 | thoroughExamination6Mo |
| 定期彻底检验(其他设备) | 12 个月 | 是 | annual12Mo |
| 按检验方案执行 | 方案自定 | 是 | 无对应成员 |
表格最后一行值得注意。LOLER 第 9 条第(3)款第(a)项第(iii)目允许按检验方案执行,SlingGuard 没有对应类型,只在记录里留了一个自由文本字段,标签是「Procedure / scheme reference」。这个字段会写进证据、会进 PDF、也会进 CSV,但它不参与任何日期计算。方案说九个月,应用仍按设备周期字段算。
二、三个成员与两个默认值
InspectionType 只有三个成员,rawValue 是长英文字符串,例如 6-Month Thorough Examination (LOLER/OSHA);displayName 是界面上显示的三档短名。把显示名与存储值分开是对的:前者可以随界面语言调整,后者是落盘格式,一旦发布就应冻结。三个成员的展示名分别是「Six-month examination」「Annual examination」「Pre-use check」。
两个录入界面的默认值都是六个月彻底检验。单件录入的状态初值是 .thoroughExamination6Mo,批量录入也是同一个初值。这意味着在一个全部是六个月的库房里,默认值恰好正确;在一个以十二个月设备为主的库房里,每录一条都要手动改一次类型。批量录入时这个成本被放大:一次选中二十件,二十条记录共用同一个类型初值,改一次即可,但如果库房是混合周期,批量界面就会把同一个类型写给周期不同的资产。
三、一次检验写两个字段,但只有一个字段参与算术
单件录入的到期日由一段只有两行的计算属性给出:
private var qualifyingNextDueDate: Date {
Calendar.current.date(byAdding: .month, value: asset.inspectionIntervalMonths, to: date) ?? date
}属性名里带 qualifying,读起来像是「合格检验才算出到期日」。实际的计算里没有任何限定:asset.inspectionIntervalMonths 是设备字段,date 是表单上的记录日期,除这两个输入之外没有第三个。写成式子就是
其中周期 取自资产,记录日期 取自表单,检验类型不出现在式子里。批量录入的投影函数写法相同,只是对每件被选中的资产各算一次。InspectionRecord 的构造器在 nextDueDate 缺省时回落到记录日期加 6 个月。三处都不读 inspectionType。
把三种类型与四个周期档位交叉,得到十二种组合,记录里写下的日期只有四种可能:
| 选择的检验类型 | 周期 1 个月 | 周期 3 个月 | 周期 6 个月 | 周期 12 个月 |
|---|---|---|---|---|
| 六个月彻底检验 | 记录日期加 1 月 | 加 3 月 | 加 6 月 | 加 12 月 |
| 十二个月定期检验 | 记录日期加 1 月 | 加 3 月 | 加 6 月 | 加 12 月 |
| 使用前检查 | 记录日期加 1 月 | 加 3 月 | 加 6 月 | 加 12 月 |
同列三行完全一样,这正说明类型不参与算术。最容易读出来的矛盾是:给一台 6 个月周期的吊带选「十二个月定期检验」,记录里写下的下次到期是 6 个月后;给一台 12 个月周期的主吊环选「六个月彻底检验」,写下的是 12 个月后。操作者刚在同一个表单里选过类型,日期却不认它。
四、唯一被读取的一位
addInspectionRecord 里有一行条件:只有当记录的检验类型不是使用前检查时,才把记录日期写回资产的 lastInspectionDate。这是类型在整个判定链上唯一的用处。这个取舍本身符合标准——使用前检查不是彻底检验,不应重置六个月的法定周期。问题在于它只做了一半:记录这一侧仍然写了一个 nextDueDate,并且这个日期会被 PDF 与 CSV 当作「计划下次到期」输出。
以真机里的 SL-101 为例。它的周期档位是 6 个月,状态行写着下次到期 2027-01-21,按周期反推,最近一次彻底检验在 2026-07-21:
| 量 | 值 | 来源 |
|---|---|---|
| 资产级最近彻底检验 | 2026-07-21 | 上一次彻底检验记录 |
| 资产级下次到期 | 2027-01-21 | 最近彻底检验加 6 个月 |
| 当前状态(2026-10-30) | In Service,距到期 83 天 | 当前时间早于下次到期 |
| 2026-10-30 的使用前检查 | 记录到期 2027-04-30 | 记录日期加 6 个月 |
两个日期相差 99 天。前者决定状态,后者只被打印。同一次操作在同一份 PDF 里留下两个互不相干的日期,读者没有任何线索知道该信哪一个。更麻烦的是,这两个日期都写在证据页上,而证据页的标题是「Inspection Record Evidence」——它看起来像在陈述一个事实,实际混了一个推算值。
五、状态机只认资产级时钟
状态判定有六态,优先级固定:报废、隔离、未检验、逾期、即将到期、在用。判定分两步走。第一步读记录里的结论位:已报废或存在报废结论的记录进入报废,手动隔离或存在未被完整放行记录抵消的隔离记录进入隔离。第二步才是日期判定,而这一步只读资产级字段:lastInspectionDate 为空进入未检验;当前时间晚于下次到期进入逾期;到期日落在未来 30 天内进入即将到期;其余为在用。
nextInspectionDue 的基期是「最近彻底检验日期,没有则用投用日期」。这个兜底值带来一处口径落差:一台从未检验过的新设备,状态是未检验(第 3 条先返回),但 PDF 汇总表的「Scheduled due」列仍会打印「投用日期加周期」的日期。这个日期是推算值,不是法定到期日,报告没有标注它的性质。读到这一行的人很容易把它当成「这台设备的下次检验日」,而它其实只是「如果它从未被检验,按投用日推算会在哪天到期」。
还有一处命名与取值不一致。PDF 汇总表的列头是「Last record」,取值来自 asset.lastInspectionDate,而它排除使用前检查。明细页则逐条列出所有记录,包括使用前检查。于是一台昨天刚做过使用前检查的设备,汇总表的「最后记录」可能停在五个月前,而明细页第一行是昨天。两处都叫「记录」,指的不是同一批记录。
顺带说明,RiggingStore 里有两个为此写好的辅助方法:latestRecord(for:) 取任意最近记录,latestQualifyingExamination(for:) 排除使用前检查。全仓库没有任何调用点。判定链走的是资产字段,这两个方法没有被接进去。它们是这段设计意图留下的痕迹:有人想过把「最近记录」与「最近一次合格彻底检验」分开表达,最后没有接上。同一份代码里,两个语义不同的概念只剩一个字段承载。
六、30 天预警窗口与四个周期档位
dueSoon 的窗口是 30 天,写死在状态函数里,与周期无关。新建资产时可以选 1、3、6、12 个月四档,于是同一个 30 天在不同档位上代表的含义差别很大:
| 周期档位 | 平均周期天数 | 30 天窗口占比 | 每周期处于「在用」的天数 |
|---|---|---|---|
| 1 个月 | 30.4 | 98.7% | 不足 1 天 |
| 3 个月 | 91.2 | 32.9% | 约 61 天 |
| 6 个月 | 182.4 | 16.4% | 约 152 天 |
| 12 个月 | 364.8 | 8.2% | 约 335 天 |
占比的定义很简单,把 30 天窗口除以周期长度即可:
其中 是周期档位(月), 取每月 30.4 天的名义长度。代进四档得到 98.7%、32.9%、16.4% 与 8.2%。
第一行的后果是实际的。在港口与海上工况常用的 1 个月档位下,一个周期里只有不到一天不是「即将到期」:31 天的月份里,检验当天是在用,第二天起就进入预警区间,直到到期。仪表盘的六态分布因此失去分辨力——从「刚检完」到「还有三天到期」之间没有任何状态差异。
再往上一层,汇总里的合规率用「在用加即将到期」除以「总数减报废」计算。这个口径可以叫「未逾期率」,本身没有问题;但在 1 个月档位下它恒等于 100%,除非真的逾期。数字不会报错,只是不再提供信息。
七、日历月加法:181 到 184 天
到期日用的是日历月加法,不是天数加法。这符合法条的表述——「每 6 个月」指日历月,而不是 182.5 天。代价是同一个「6 个月」在不同起点上长度不同:
- 6 个月跨度在 181 到 184 天之间,2020 至 2030 年的均值为 182.61 天;
- 12 个月跨度是 365 或 366 天;
- 月杪会被截断:2026-08-31 加 6 个月得到 2027-02-28,跨度 181 天;从 2027-02-28 再加 6 个月得到 2027-08-28。若每次都按到期日续期,锚点会逐年前移,三年后比原始日期早三天。
这不是缺陷,而是取舍:日历月加法保证每次间隔不超过 6 个日历月,换成固定天数反而会在某些年份越过上限。真正需要补的是说明——报告里的日期按日历月推进,遇到月杪取当月最后一天。当前 PDF 只打印一个日期,没有任何关于推进规则的说明。
八、录入面的三处值域缺口
第一,检验日期没有上下界。单件与批量两处日期选择器都只声明了显示日期,没有范围约束。操作者可以选一个未来的日期。未来日期会经 addInspectionRecord 写进 lastInspectionDate,把下次到期整体后移——状态等于被人工续期。应用不会提示这条记录来自未来。
第二,补录会把到期日往回拉。addInspectionRecord 无条件用记录日期覆盖 lastInspectionDate,没有取最大值。补录一条去年的纸质记录,到期日随之下移,一台在用设备可能立刻变成逾期。这个行为对「把历史记录补齐」是有用的,对「不小心选了去年」是有害的,两者在界面上没有区分。
第三,周期字段创建后不可改。RiggingStore 有 updateAsset,但全仓库没有调用点,界面也没有编辑入口。选错档位的唯一补救是删除资产重建,而 deleteAssets 会连带删除该资产的全部记录与放行记录。CSV 导入侧同样只有新增:遇到重复的 Tag ID 直接跳过,不会更新已有资产的周期。导入解析写的是「整数转换失败则回落 6」,缺失或非法回落 6 个月,0 与负数照单全收,没有值域校验。一个手工改过的 CSV 里写下 0,到期日等于记录日期,资产立刻逾期;写下负数,到期日在过去。
九、四条改动
第一,让类型与周期对齐,或者明确声明类型不参与算术。两种都可以,现状是最差的第三种:界面让人以为选了类型,实际只改了标签。最小改动是在选中「十二个月定期检验」时按 12 个月计算,或在类型与设备周期不一致时提示并要求确认。
第二,使用前检查不写记录级到期日。把字段改成可选,或在写入时置空;PDF 明细对这类记录打印「不适用」,CSV 输出空列。这样两条时钟就只剩一条,预使用记录只保留「谁在什么时候看过」这一件事。
第三,报告口径对齐。「Last record」改成「Last thorough examination」,或改成真正的最近记录日期;「Scheduled due」对未检验资产标注为「按投用日期推算」。这两处都只是标签与取值的一致性,不涉及判定逻辑。
第四,预警窗口按周期比例取值。把固定的 30 天改为周期的两成与 30 天取小:1 个月档位取 6 天,12 个月档位仍取 30 天。这样「即将到期」在任何档位都表示周期的最后两成,而不是在短周期上覆盖整个周期。
十、能力边界
- 应用不判断标准适用性。录入页的脚注写明它不选择也不校验适用的检验标准。类型与周期都是操作者填的,应用只负责记录与算术。
- 到期判定只用本机记录。没有外部证书链,没有联网核验,也没有第三方审核轨迹。
- 记录确认是本地开关加署名文本,不是密码学签名,也不绑定证书有效期。
- 记录日期可回溯填写,应用不做单调性校验。
- 删除资产会删除其全部记录与放行记录,历史不保留。
- 日历运算依赖系统时区与日历设置,跨时区补录可能得到不同的日期边界。
十一、结语
三层检验、两条时钟、一个 30 天常数。这三件事单独看都合理,放在一起就产生了本文列出的那些错位:类型是标签,周期是设备属性,到期日是记录字段,而判定只读其中一个。把标签与算术对齐,比再加一个检验类型更有用。
对一个在现场使用的合规工具来说,最要紧的不是算得多复杂,而是同一个问题只有一个答案。当一条记录同时携带两个到期日、而界面对两者的称呼又不够区分时,使用者只能靠记忆判断哪个算数。这跟记性无关,跟字段有关。
参考资料
- [1] The Lifting Operations and Lifting Equipment Regulations 1998, Regulation 9. https://www.legislation.gov.uk/uksi/1998/2307/regulation/9
- [2] Health and Safety Executive. Thorough examinations and inspections of lifting equipment. https://www.hse.gov.uk/work-equipment-machinery/thorough-examinations-lifting-equipment.htm
- [3] OSHA. 29 CFR 1910.184 — Slings. https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.184
- [4] ASME. B30.9 Slings. https://www.asme.org/codes-standards/find-codes-standards/b30-9-slings
- [5] Apple. Calendar.date(byAdding:value:to:wrappingComponents:). https://developer.apple.com/documentation/foundation/calendar
- [6] Apple. DatePicker. https://developer.apple.com/documentation/swiftui/datepicker