Files
banban/banban-mini/src/services/system-banner.ts
2026-05-09 16:50:42 +08:00

176 lines
5.4 KiB
TypeScript

import Taro from '@tarojs/taro'
import { Binding, getBindings } from './binding'
import { DeviceAlarmItem, getDeviceAlarms } from './device'
const LAST_SEEN_ALARM_MAP_KEY = 'systemBannerLastSeenAlarmMap'
const DEVICE_ALARM_FETCH_LIMIT = 20
const LAST_POLL_TIME_KEY = 'systemBannerLastPollTime'
type LastSeenAlarmMap = Record<string, number>
export interface SystemBannerNotification {
key: string
title: string
summary: string
detail: string
timeLabel: string
route: string
alarmId: number
deviceId: string
}
function readLastSeenAlarmMap(): LastSeenAlarmMap {
const storedValue = Taro.getStorageSync(LAST_SEEN_ALARM_MAP_KEY)
if (!storedValue || typeof storedValue !== 'object') return {}
return storedValue as LastSeenAlarmMap
}
function writeLastSeenAlarmMap(map: LastSeenAlarmMap) {
Taro.setStorageSync(LAST_SEEN_ALARM_MAP_KEY, map)
}
function readLastPollTime(): number {
const value = Number(Taro.getStorageSync(LAST_POLL_TIME_KEY) || 0)
return Number.isFinite(value) ? value : 0
}
function writeLastPollTime(value: number) {
Taro.setStorageSync(LAST_POLL_TIME_KEY, value)
}
function setDeviceAlarmSeen(deviceId: string, alarmId: number, map: LastSeenAlarmMap) {
map[deviceId] = alarmId
}
function hasDeviceAlarmBaseline(deviceId: string, map: LastSeenAlarmMap): boolean {
return Object.prototype.hasOwnProperty.call(map, deviceId)
}
function getAlarmChildLabel(alarm: DeviceAlarmItem, fallbackChildName?: string | null): string {
const childName = String(alarm.child_name || '').trim()
if (childName) return childName
const fallbackName = String(fallbackChildName || '').trim()
if (fallbackName) return fallbackName
if (alarm.child_id) return `儿童 ${alarm.child_id}`
return '当前孩子'
}
function getAlarmTypeLabel(sourceMsgId?: string | null): string {
const normalizedValue = String(sourceMsgId || '').trim()
if (normalizedValue === '010') return '紧急告警'
if (!normalizedValue) return '设备提醒'
return normalizedValue
}
function formatBannerTime(value?: string | null): string {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
return `${hour}:${minute}`
}
function buildDeviceAlarmNotification(
alarm: DeviceAlarmItem,
fallbackChildName?: string | null
): SystemBannerNotification {
const childLabel = getAlarmChildLabel(alarm, fallbackChildName)
const alarmTypeLabel = getAlarmTypeLabel(alarm.source_msg_id)
return {
key: `device-alarm-${alarm.device_id}-${alarm.alarm_id}`,
title: '系统消息',
summary: `${childLabel} 的伴伴触发了${alarmTypeLabel}`,
detail: `设备 ${alarm.device_id}`,
timeLabel: formatBannerTime(alarm.created_at),
route: '/pages/device/index',
alarmId: alarm.alarm_id,
deviceId: alarm.device_id,
}
}
function sortAlarmsByCreatedAt(left: DeviceAlarmItem, right: DeviceAlarmItem): number {
const leftTime = new Date(left.created_at).getTime()
const rightTime = new Date(right.created_at).getTime()
return leftTime - rightTime
}
export async function pollSystemBannerNotifications(): Promise<SystemBannerNotification[]> {
const bindingResponse = await getBindings(undefined, 100)
const bindings = bindingResponse.items || []
if (bindings.length === 0) return []
const lastSeenAlarmMap = readLastSeenAlarmMap()
const previousPollTime = readLastPollTime()
const currentPollTime = Date.now()
let hasMapChanged = false
const alarmResults = await Promise.allSettled(
bindings.map(async (binding) => ({
binding,
response: await getDeviceAlarms(binding.device_id, DEVICE_ALARM_FETCH_LIMIT),
}))
)
const nextNotifications: Array<{ alarm: DeviceAlarmItem; binding: Binding }> = []
alarmResults.forEach((result) => {
if (result.status !== 'fulfilled') return
const { binding, response } = result.value
const deviceId = String(binding.device_id || '').trim()
if (!deviceId) return
const alarms = response.items || []
const latestAlarmId = alarms[0]?.alarm_id || 0
const hasBaseline = hasDeviceAlarmBaseline(deviceId, lastSeenAlarmMap)
const knownAlarmId = Number(lastSeenAlarmMap[deviceId] || 0)
// Establish a baseline on first poll so old history does not flood the user.
if (!hasBaseline) {
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
return
}
if (latestAlarmId < knownAlarmId) {
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
return
}
if (latestAlarmId === knownAlarmId) {
return
}
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
alarms
.filter((alarm) => {
if (alarm.alarm_id <= knownAlarmId) return false
if (previousPollTime <= 0) return true
const createdAt = new Date(alarm.created_at).getTime()
if (Number.isNaN(createdAt)) return true
return createdAt >= previousPollTime
})
.forEach((alarm) => {
nextNotifications.push({ alarm, binding })
})
})
if (hasMapChanged) {
writeLastSeenAlarmMap(lastSeenAlarmMap)
}
writeLastPollTime(currentPollTime)
return nextNotifications
.sort((left, right) => sortAlarmsByCreatedAt(left.alarm, right.alarm))
.map(({ alarm, binding }) => buildDeviceAlarmNotification(alarm, binding.child_name))
}