· engineering· 约 25 分钟精读
端侧离线登记册的检索与筛选工程:分面计数、空态归因与选择集边界
现场要做的其实只有两个动作:把手里这件设备找出来,把某一批设备一起看一遍。本文以密旋科技 TagWorks 与 SlingGuard 两套离线台账为样本,把“哪些设备显示在屏幕上”这条链路逐段拆开:状态如何被压成单值枚举、分面数字算的是什么范围、空列表到底该归因给谁、勾选集为什么可以越出可见集,以及没有索引时这笔计算账落在哪里。
一、现场要的只有两个动作
在地下配电间做便携式电器的定期检测,或者在港口堆场做吊索具的定期检验,作业人员手上真正要完成的事情只有两个:把手里这件东西在台账里找出来;把某一批东西一起看一遍。前者是检索,后者是筛选。
这两件事都不发生在服务器上。地下室没有信号,堆场边缘也没有,台账就在设备里。密旋科技的两套现场工具各自回答了这两个动作的一部分:TagWorks 面向电气安全,按 AS/NZS 3760 的检测周期维护便携式电器台账;SlingGuard 面向吊装索具,按 LOLER 1998 与 OSHA 29 CFR 1910.184 维护索具的检验与放行记录。两者都是端侧离线:数据落本机,没有账号,也没有远端索引。
本文只处理一个很窄的问题:屏幕上究竟该显示哪些行。这条链路看起来只是“把不匹配的滤掉”,但真正落到代码里,它牵出四件事——状态怎么算、计数算的是什么范围、空列表怎么解释、勾选与可见之间是什么关系。
二、两条链路的形状不一样
TagWorks 的筛选是一个四选一的分面:全部、需要关注、三十天内到期、合规。它没有检索框。要找一件具体设备,路径是先按分面缩小范围,再滚动,或者直接扫标签进详情页。站点是第二条路径:仪表盘按站点聚合,点进去看到的是该站点的只读清单。
SlingGuard 的筛选是六个状态分面加一个自由检索框。检索覆盖四个字段:标签号、名称、位置、出厂编号。两个条件取交集:文本命中,且状态相等。
| 维度 | TagWorks | SlingGuard |
|---|---|---|
| 筛选谓词来源 | 单一状态分面,四选一 | 状态分面六选一 |
| 自由检索 | 无检索框 | 四个字段,任一包含即命中 |
| 组合方式 | 分面,或站点下钻(二者不同时生效) | 检索与分面取交集 |
| 分面计数 | 不显示计数,仪表盘另有汇总 | 胶囊上显示计数 |
| 列表顺序 | 登记顺序,新件追加在末尾 | 登记顺序,新件插入在头部 |
| 站点 | 结构化下钻入口 | 仅作为一个可检索字段 |
先把两条链路画成一张图。左侧是共同的四段:登记册快照、文本谓词、状态分面、可见集;右侧是这条链路上四处容易说错的地方。
这张表里最容易被忽略的是最后一行。TagWorks 的站点清单来自资产里的自由文本字段,去掉空值、排序后成为导航入口;SlingGuard 没有站点导航,位置只是字符串。于是“同一批设备”在两套工具里的定义并不相同:前者靠字符串相等,后者靠字符串包含。
三、状态是单值枚举,不是布尔集合
分面能够成立,前提是每件资产在任一时刻只属于一个状态。两套工具都把状态做成单值枚举,并各自写了一条优先级链。
TagWorks 的枚举有六个取值:逾期、三十天内到期、合规、失败、不完整、未初检。判定顺序是固定的:最新一次检验若判为失败,直接落“失败”,与日期无关;其次,若最新记录既没有建立 PASS 也不算判失败,落“不完整”;再次,若从未检验过,落“未初检”;最后才按到期日分三种——已过期为逾期,三十天以内为临期,其余为合规。
SlingGuard 的枚举也有六个取值:在用、临期、逾期、隔离、报废、未检验。顺序同样固定:手工报废标记或存在报废记录,落“报废”;存在未释放且证据完整的隔离记录,或者存在手工隔离标记,落“隔离”;从未检验过,落“未检验”;其后按下一次检验到期日分三种。这里有一处值得单独说明:后来的一次 PASS 不会自动清掉已经记录的隔离,解除隔离必须走一条独立的、与隔离记录显式关联的放行记录。
| 状态值 | 判定条件(按优先级从上到下) | 归属分面 |
|---|---|---|
| 失败 | 最新检验未通过 | 需要关注 |
| 不完整 | 最新记录未建立 PASS,也未判失败 | 需要关注 |
| 未初检 | 台账内无任何检验记录 | 需要关注 |
| 逾期 | 到期日已过 | 需要关注 |
| 三十天内到期 | 到期日在三十天窗口内 | 三十天内到期 |
| 合规 | 到期日在窗口之外 | 合规 |
这张表说明一件事:分面是状态的投影,不是另一个独立事实。TagWorks 把四个值合成一个“需要关注”,所以点进去会看到四种不同颜色的行内徽章;SlingGuard 是一对一分面,点进去只有一种。两者都不算错,但它们对“这一批”的定义粒度不同——前者的分面回答“哪些要处理”,后者的分面回答“哪些属于同一类”。
四、分面数字算的是什么范围
SlingGuard 的分面胶囊上带数字。数字的算法很直接:对每一个状态,遍历全部资产求一次状态,再计数。这里有三处后果。
第一,数字是全量口径,不随检索词收缩。输入框里已经有关键词时,胶囊上的数字仍然是整个台账的计数。操作者看到“逾期 12”,点进去只看到两行,因为另外十行被检索词挡住了。
第二,计数为零的分面不渲染。某个状态曾经存在、如今归零,胶囊直接消失。这省了空间,代价是操作者无法通过“点进去看到空列表”来确认这一类真的为零——空列表本身也是一种证据,而它被优化掉了。
第三,计数在每次视图求值时重算,且每个分面各遍历一遍。六个分面就是六遍遍历,同一件资产的状态在一次求值里被重复计算六次。
TagWorks 没有胶囊计数,但仪表盘顶部的四个数字来自同一套状态函数:资产总数、合规数、三十天内到期数、需要关注数(失败、不完整、逾期、未初检之和)。它与筛选分面口径一致。一致这一点很实际:屏幕上的“需要关注 7”与点进分面后数出来的行数不会打架,复核时不需要解释第二套算法。
五、空列表该归因给谁
空列表至少有三种来路:台账本来就是空的;分面命中了零条;检索词命中了零条。三种来路对操作者意味着完全不同的下一步,所以空态文案必须能被分辨。
TagWorks 的空态分两支:台账为空时提示“还没有资产,先添加第一台设备”;台账非空但当前无匹配时提示“没有资产符合这个筛选条件”。分支键是台账是否为空,识别的是第一种来路;第二种来路被合并处理,站点下钻为空时也落在这句通用文案上,不会指出是哪个站点为空。
SlingGuard 的空态也分两支,但分支键是检索词是否为空:检索词为空时提示“点加号登记第一件索具”,检索词非空时提示“没有设备符合你的检索”。问题在于,第二个可空维度(选中的分面)没有进入判断。
| 场景 | TagWorks 文案 | SlingGuard 文案 | 归因 |
|---|---|---|---|
| 台账为空,无筛选 | 还没有资产 | 点加号登记第一件 | 准确 |
| 台账非空,分面无命中 | 没有资产符合筛选 | 点加号登记第一件 | 归因不足 |
| 台账非空,检索无命中 | 不适用 | 没有设备符合检索 | 准确 |
| 分面已选但计数在两次求值之间归零 | 没有资产符合筛选 | 点加号登记第一件 | 归因不足 |
需要说清楚的是,SlingGuard 的第三行并不是常规路径:胶囊只在计数大于零时出现,所以“选了分面却看到空列表”通常要等到状态在两次求值之间发生变化——比如跨过午夜,一件设备的到期判定翻转了。这是归因粒度的问题,不是显示错误。但它暴露了一个结构:空态文案的判定维度与可空维度没有对齐,两个可空条件只判了一个。
六、选择集可以越出可见集
SlingGuard 的批量检验依赖一个勾选集合。这个集合独立于检索词与分面:切换分面、输入关键词、清空关键词,都不会改变已经勾选的项。底部的按钮写着“批量检验(N)”,这个 N 是勾选数,不是当前可见行数。退出批量模式才会清空勾选;批量模式下侧滑删除不执行;删除一件资产会同时清掉它的检验记录与放行记录。
于是会出现这样一幕:操作者先在“逾期”分面里勾了五件,再切到“全部”、输入另一个词,屏幕上只剩两行,底部按钮仍写着“批量检验(5)”。这是刻意的取舍——勾选是操作者的意图,不应被视角变化抹掉——但它要求操作者能看见自己勾了什么,而当前没有一份“已选清单”视图。选择集与可见集分离是合理的,分离之后缺少一个可查看的交集窗口,才是缺口。
两处侧滑删除用的都是同一个模式:偏移量作用在“当场重新求值出来的数组”上,而不是“上一帧渲染的那个数组”。标签按日期算出的状态在跨过午夜时会翻转,两帧之间数组的成员与顺序都可能变化,此时按索引删除有可能落到相邻行。这不是高频路径,但它说明按索引定位行、按标识符删除资产这两件事需要分开:索引是渲染的产物,资产标识符才是稳定键。
七、没有索引的代价
两套存储都是内存数组加 JSON 整体快照落盘:没有索引,没有查询引擎,状态也不是存储字段。每次显示都要由记录现算——最新一次检验是哪一条、有没有未解决的失败、复检与放行是否构成了有效的解除链。
以 TagWorks 为例,单件资产的状态要反复触及记录表:一次取最新检验记录,一次检查未解决的失败(这一次还要把该资产的历史排序),而判定时又要再取一次最新结论,内部再各扫一遍。按代码结构推算,单件资产约四遍全表触及。SlingGuard 更重一些:报废判定、隔离判定、有效放行集合各扫一遍,再回落到日期分三种。
| 环节 | 计算口径 | 量级 |
|---|---|---|
| TagWorks 一次分面筛选 | 400 件 × 1600 条记录 × 约 4 遍 | 约 256 万次元素访问 |
| TagWorks 可见行重算 | 惰性列表,只算可见的十余行 | 约 8 万次 |
| SlingGuard 分面胶囊 | 6 个分面 × 400 件 × 1600 条 × 约 4 遍 | 约 1536 万次 |
| SlingGuard 选中分面再过滤 | 400 件 × 1600 条 × 约 4 遍 | 约 256 万次 |
这张表里的数字是按代码结构推出来的量级,不是 Instruments 采样值;列表是惰性的,行内重算只发生在可见行,所以真正的成本集中在“全量遍历”那两层。结论不是“慢”,而是一个比例:分面计数的代价是选中过滤的六倍,而它在每次键入时都会重跑一遍。
可落地的收敛并不涉及语义变更:一次求值里先算一张“资产标识符到状态”的表,让分面计数、选中过滤、行内徽章都查这张表;状态只与记录集合有关,记录变动时递增一个版本号即可让表失效。这一步把六遍扫描压成一遍,且不改变任何一条判定规则。
八、三个顺序同时存在
屏幕上的顺序是登记顺序:TagWorks 把新资产追加到末尾,SlingGuard 把新资产插到头部。两者都没有排序控件,也没有“按到期日排列”这个选项。
纸面上的顺序是另一套:导出登记册时,TagWorks 按站点与名称拼起来的键排序,SlingGuard 按标签号排序。
报告里的顺序是第三套:两份 PDF 都按站点或位置分组,空值归入一个合成的“未分配”分组,组内再排一次序。
三套顺序并存,对复核对话的影响很直接:口头说“第 12 行”没有任何一处能对齐。可行的口径是以标签号为准,或者把排序做成可见可选项——但在端侧,任何新增的排序维度都要同时写进导出与报告,否则又会多出第四套顺序。
拼接排序键还有两个细节。TagWorks 直接用站点字符串加名称字符串作为比较键,中间没有分隔符,站点“AB”配名称“C”与站点“A”配名称“BC”会得到同一个键;这不会丢数据,但会让这几行的相对顺序退化为不稳定。另一处是命名碰撞:PDF 把空站点合成一个名为“未分配”的分组,如果台账里真有一个站点叫这个名字,两部分内容会落进同一节。两处都不严重,但都属于“导出件与屏幕不完全同构”的范畴,写进交付说明比留给人去猜要好。
九、检索面与归一化
SlingGuard 的检索覆盖四个字段,其余字段一概不参与:制造商、设备类型、链条等级、额定载荷数值、检验备注、证书编号都不在匹配范围内。这意味着搜一个品牌名能不能命中,取决于它有没有被写进名称字段;搜“80 级”不会命中链条等级字段;搜一句缺陷描述不会命中检验备注。
| 可检索 | 不可检索 |
|---|---|
| 标签号 | 制造商 |
| 名称 | 设备类型 |
| 位置 | 链条等级 |
| 出厂编号 | 额定载荷数值、证书编号、检验备注 |
匹配用的接口是大小写不敏感且区域感知的包含判断。它的文档承诺只有这两点,没有承诺变音符、全半角、连字符或空白的归一。所以“SL-101”与“SL101”互不命中,全角写法的标签号同样不命中。对扫码入场的设备这不是问题——扫出来的字符串与台账里存的是同一串;对手工键入的设备,这就是一个每天都会遇到的小摩擦。
TagWorks 走了另一条路:它有归一化,但只做在入口。扫码得到的字符串会去掉首尾空白、按竖线切分、取出标识字段,再与台账比对;新增与判重都经过这一层。可是筛选侧的站点比较是字符串相等,没有去空白,也没有大小写折叠;站点清单直接取原始字符串去空后排序。结果是“Yard A”与“yard a”算两个站点,只含空格的站点会变成一个看不见名字的入口。
于是两套工具各缺一半:一套把归一化做在入口却没做在检索,另一套做了检索却没做归一化。补法并不复杂——让检索与分面共用同一个归一化函数(大小写、首尾空白、连字符、全半角),把归一化后的值作为比较键,原始字符串留给显示与导出。这样既不会改变台账里的既有记录,也不会让检索结果依赖于操作者的输入习惯。
十、能力边界
第一条,检索不命中不等于台账里没有这件设备,只说明那四个字段里没有包含这个词。二者在界面上长得很像,在责任上完全不同。
第二条,筛选不改变任何判定。可见集是显示层的东西;到期、失败、隔离、放行的结论各自由本机规则独立计算,与当前选中哪个分面无关。把可见集当成结论集会得出错误的合规判断。
第三条,分面数字不是可见集的行数。前者是全量口径,后者是检索与分面取交集之后的结果;两者在有关键词时必然不同。
第四条,选择集可以越出可见集。这是有意为之,但需要一条让操作者看得见的通道;没有这条通道,批量操作的对象就只能靠记忆。
第五条,无索引意味着规模有上限。四百件资产、一千六百条记录还在可接受区间;到数千件、上万条记录时,每次键入重算六遍分面会成为可感知的延迟。端侧离线用“数据不外流”换来的,就是这类必须自己承担的工程账——没有远端索引可以借,也没有服务端缓存可以加。
十一、结语
把“显示哪些行”这件事拆开之后,能落成条款的东西并不多:状态是单值枚举且有固定优先级;分面是状态的投影,计数有明确的作用域;空态要按可空维度给归因;勾选集与可见集是两个集合,允许分离但要能查看;没有索引时,成本集中在全量遍历那一层,收敛方式是先算一张状态表。
这些都不涉及新能力,只是把已有的东西说得更准。密旋科技在这两套工具里选择了端侧离线,代价与收益都写在同一个地方:数据不出设备,于是所有与数据规模相关的工程问题也一并留在设备上。把这条链路的形状交代清楚,比给它加一个更好看的分面更有用。
参考文献
[1] Standards Australia / Standards New Zealand. AS/NZS 3760:2022 — In-service safety inspection and testing of electrical equipment. https://www.standards.org.au/standards-catalogue/sa-snz/electrotechnology/el-036
[2] UK Legislation. Lifting Operations and Lifting Equipment Regulations 1998 (LOLER), SI 1998/2307. https://www.legislation.gov.uk/uksi/1998/2307/contents
[3] U.S. Occupational Safety and Health Administration. 29 CFR 1910.184 — Slings. https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.184
[4] Apple Developer Documentation. NSString — localizedCaseInsensitiveContains. https://developer.apple.com/documentation/foundation/nsstring/localizedcaseinsensitivecontains(_:)
[5] Apple Developer Documentation. NSString — localizedStandardContains. https://developer.apple.com/documentation/foundation/nsstring/localizedstandardcontains(_:)
[6] Apple Developer Documentation. SwiftUI — searchable(text:placement:prompt:). https://developer.apple.com/documentation/swiftui/view/searchable(text:placement:prompt:)
[7] ISO. ISO 55000:2014 — Asset management: Overview, principles and terminology. https://www.iso.org/standard/55088.html
[8] W3C. Web Content Accessibility Guidelines (WCAG) 2.2. https://www.w3.org/TR/WCAG22/