第一版代码我写得特别直,每个采样点跟阈值比一下,越界就记一条告警:
// 第一版:逐点判越限
function checkViolation(samples: number[], limit: number): string[] {
const alarms: string[] = []
for (let i = 0; i < samples.length; i++) {
if (samples[i] > limit) {
alarms.push(`样本 ${i} 越限:${samples[i]}℃`)
}
}
return alarms
}
这段逻辑在实验室里跑没问题,接上真车数据当天就露馅了。沪A·C271 装的是生鲜 8.4t,目标 -18℃,跑上海到杭州,路上温度记录不连续:进隧道丢信号、进出仓要开门、制冷机偶发重启,曲线会整段断掉。断的那 40 分钟里舱温去了哪儿,逐点判定看不见——它只对"有数据的点"负责。于是界面"异常 1"只亮了一个,货舱里可能已有奶制品悄悄超过 8℃ 又降回来。这里有个坑:没有越限告警,不等于货没问题,很多时候只是没有记录。
问题落在两件事上:越限怎么判,断档之后怎么恢复出结论。前者简单,后者才是关键。
三种货三套规则
冷链车不是一种货一种温度。这辆车是生鲜,苏E·H119 装的是疫苗 2.1t,浙B·K804 是乳品 6.7t。冷冻、冷藏、疫苗的允许区间完全不一样,疫苗那辆显示 3.8℃ 还在 2~8 之间,它被标"关注"是因为断档,温度本身没问题。所以我先把货品类型和判定规则建出来,规则里除了温度上下限,还塞进两个跟"断档"强相关的字段:
export enum CargoKind {
Frozen = 'frozen',
Chilled = 'chilled',
Vaccine = 'vaccine'
}
export interface TempSample {
ts: number
temp: number
door: boolean
}
export interface CargoRule {
low: number
high: number
maxGapMs: number
recoverTolerance: number
}
const RULES: Record<CargoKind, CargoRule> = {
[CargoKind.Frozen]: { low: -25, high: -12, maxGapMs: 20 * 60 * 1000, recoverTolerance: 300 },
[CargoKind.Chilled]: { low: 0, high: 8, maxGapMs: 15 * 60 * 1000, recoverTolerance: 180 },
[CargoKind.Vaccine]: { low: 2, high: 8, maxGapMs: 10 * 60 * 1000, recoverTolerance: 120 }
}
三套规则摊开就是一张表,界面货舱状态卡片的绿黄红,查的就是这张表:
| 货品 | 允许区间 | 断档容忍 | 恢复容忍 | 结论标色 |
|---|---|---|---|---|
| 冷冻生鲜 | -25 ~ -12℃ | 20 分钟 | 300 秒 | 正常 / 关注 / 异常 |
| 冷藏乳品 | 0 ~ 8℃ | 15 分钟 | 180 秒 | 正常 / 关注 / 异常 |
| 疫苗 | 2 ~ 8℃ | 10 分钟 | 120 秒 | 正常 / 关注 / 异常 |
maxGapMs 是断档时长的容忍上限,疫苗最严,10 分钟;冷冻生鲜放宽到 20 分钟。recoverTolerance 是恢复期允许的越限秒数——制冷机重启那几秒舱温会抖一下,不能因为抖这一下就把货判死刑。这两个字段是后来补的,严格判越限会误杀,完全不判断档会漏放。
越限判定带上恢复窗口
判越限本身不复杂,复杂的是要不要把越限记成告警。我把连续越限和瞬时抖动分开:温度冲出上限,先看持续了多久,低于 recoverTolerance 的算恢复抖动,超过才算真实越限。门打开的那几秒舱温必然波动,这种采样点要单独标记,不能混进告警吓人:
export interface Violation {
startTs: number
endTs: number
seconds: number
}
function detectViolations(samples: TempSample[], rule: CargoRule): Violation[] {
const out: Violation[] = []
let start = -1
for (const s of samples) {
const outOfRange = s.temp < rule.low || s.temp > rule.high
if (outOfRange && !s.door) {
if (start < 0) {
start = s.ts
}
} else if (start >= 0) {
const seconds = Math.round((s.ts - start) / 1000)
if (seconds >= rule.recoverTolerance) {
out.push({ startTs: start, endTs: s.ts, seconds })
}
start = -1
}
}
return out
}
门打开导致的越限直接跳过,冷气外泄这个波动是预期的,记个开门次数就行,界面里"开门事件 12 次"就来自这个字段。真正要命的是关门后温度回不来。

断档才是主角
越限判定写完后,重心挪到断档识别。两个相邻采样点的时间差超过 maxGapMs,中间这段就是盲区,断档前后的温度值和时长决定了这段盲区能不能接受:
export interface TempGap {
durationMs: number
before: number
after: number
}
function findGaps(samples: TempSample[], rule: CargoRule): TempGap[] {
const gaps: TempGap[] = []
for (let i = 1; i < samples.length; i++) {
const dt = samples[i].ts - samples[i - 1].ts
if (dt > rule.maxGapMs) {
gaps.push({
durationMs: dt,
before: samples[i - 1].temp,
after: samples[i].temp
})
}
}
return gaps
}
function judgeGap(gap: TempGap, rule: CargoRule): string {
const inRange = (t: number): boolean => t >= rule.low && t <= rule.high
if (inRange(gap.before) && inRange(gap.after) && gap.durationMs <= rule.maxGapMs * 2) {
return 'ok'
}
if (gap.after - gap.before > 2) {
return `risky: 恢复后抬升 ${(gap.after - gap.before).toFixed(1)}℃`
}
return `reject: 断档 ${Math.round(gap.durationMs / 60000)} 分钟`
}
逻辑分三档。前后温度都在区间内、时长又在两倍容忍上限以内,算 ok——这是最常见的情况,比如过隧道丢 12 分钟信号,前后是 -18.3℃ 和 -18.1℃,货没事。第二档是恢复后温度比断档前抬升超过 2℃,说明盲区里大概率发生过一次升温,比如制冷机重启失败了一小会儿,这个标 risky。第三档是时长超了上限或者恢复后仍越界,直接 reject。这三档对应界面里"正常 / 关注 / 异常"三个颜色。
本地落盘再合并结论
断档多半是信号问题造成的,采样数据得先落盘。我用 @ohos.data.preferences 存最近 6 小时的曲线,界面右上角"温度记录 · 最近 6 小时"就从这里画:
import preferences from '@ohos.data.preferences'
async function persistSamples(key: string, samples: TempSample[]): Promise<void> {
const store = await preferences.getPreferences(getContext(this), 'coldchain')
await store.put(key, JSON.stringify(samples))
await store.flush()
}
收尾时把越限和断档两份结论合成一份货品判定。越限有一条真实的,或者断档判出 risky/reject 的,这辆车就进告警队列;只有恢复抖动和短断档的,维持正常。沪A·C271 判下来是正常,疫苗那辆因为中途断档 14 分钟被判了关注,跟界面里"异常 1"对得上。
合成这步落成一个不到十行的函数,两条判定线在这里拧成一句结论:
function mergeVerdict(violations: Violation[], gaps: TempGap[], rule: CargoRule): string {
// 真实越限:持续秒数够上恢复容忍的才算,抖一下的不算
const hard = violations.some((v) => v.seconds >= rule.recoverTolerance)
const gapVerdicts = gaps.map((g) => judgeGap(g, rule))
if (hard || gapVerdicts.some((v) => v.startsWith('reject'))) {
return '异常'
}
if (gapVerdicts.some((v) => v.startsWith('risky'))) {
return '关注'
}
return '正常'
}
沪A·C271 跑下来全是 ok 加两次恢复抖动,返回值"正常";疫苗那辆恢复后温度抬升过了 2℃ 线,judgeGap 出 risky,落到"关注"。货到港敢不敢收货,看的就是这个字符串。
结论能合成了,能不能交给真实车队还得另说。
交给真实车队之前,四种边界情况我逐个戳过:
- 采样整段消失,时长超过容忍上限两倍:
judgeGap里durationMs <= maxGapMs * 2这条兜不住,前后温度再正常也判 reject——盲区太长,前后值说明不了中间发生过什么。 - 开门引起的越限:
door为 true 的采样点在detectViolations里直接跳过,只累计开门次数,界面"开门事件 12 次"就是这个计数器,不能拿它吓人。 - 恢复后温度抬升超过 2℃:制冷机重启失败过的典型痕迹,盲区里大概率热过一轮,判 risky,需要人工翻曲线确认。
- 设备重启或校时导致时间戳回退:相邻两点
ts差为负,findGaps会算出负时长。我在录入前先把dt <= 0的点剔掉,不然一个负 gap 能把整段判定带飞。
为什么疫苗只忍 10 分钟
疫苗那行阈值最苛刻,不是随手写的。疫苗的 2~8℃ 是活性要求,温度曲线在区间外停够 120 秒,效价影响就开始不可逆;生鲜冻品靠的是整箱热惯性,-12℃ 以上多待十几分钟也只是表面化冻,所以冷冻放到 20 分钟、300 秒。maxGapMs 的两倍判定是同一套逻辑:隧道里丢 12 分钟信号,前后是 -18.3℃ 和 -18.1℃,热惯性没被破坏,算 ok;超过两倍容忍度,说明制冷状态已经不可信,宁可错杀。乳品那行居中,15 分钟、180 秒,跟着酸奶和鲜奶的货架期走。三行数字在货主合同里都有对应条款,合同没写的按最严那行兜底。
整个模块的结构长这样:
ColdChain/
├─ entry/src/main/ets/
│ ├─ pages/Index.ets # 车辆列表、温控曲线、货舱状态
│ ├─ features/ColdChainController.ets # 规则表、越限与断档判定
│ ├─ features/GapJudge.ets # 断档识别与货品结论合并
│ └─ entryability/EntryAbility.ets # 页面入口
├─ entry/src/main/resources/ # 色板 #102C3D / #167B9B / #58D7E5
└─ build-profile.json5

屏幕左侧是那三辆在途车,右边是选中车辆的 6 小时温度曲线和货舱状态。曲线中段被我刻意留了缺口,就是那次隧道丢信号,判定逻辑读到缺口后先查前后温度、再算时长,才给结论。
写完整套判定,我最大的感受是:冷链监控里真正要命的是"没有数据"这件事本身。逐点判越限只回答了"记录到的点合不合格",断档恢复才回答了"没记录到的点还能不能信"。这两条合起来,才是一辆冷链车到港之后敢不敢收货的依据。
本文只描述端侧的数据判定思路,不构成冷链运输合规标准或货损责任认定依据。真实业务里温度上限、断档容忍时长要按《药品经营质量管理规范》和货主合同逐单确认,疫苗、生鲜等强监管货品接收前仍以温度记录仪导出数据和现场验收为准。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Dovis5884/article/details/170336886




