雾非雾の迷惘头像
温度曲线断了一段,冷链监控怎样判断货还能不能要封面图

温度曲线断了一段,冷链监控怎样判断货还能不能要

第一版代码我写得特别直,每个采样点跟阈值比一下,越界就记一条告警:

// 第一版:逐点判越限
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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--