feat(boot-notification): 添加超时处理和日志记录以增强引导通知的持久性

This commit is contained in:
2026-03-15 04:23:13 +08:00
parent 216a8e118d
commit 8a537e80e3
2 changed files with 73 additions and 43 deletions

View File

@@ -9,6 +9,8 @@ export const useDrizzle = () => {
if (!pgPoolInstance || !drizzleInstance) { if (!pgPoolInstance || !drizzleInstance) {
pgPoolInstance = new Pool({ pgPoolInstance = new Pool({
connectionString: process.env.DATABASE_CONNECTION_STRING, connectionString: process.env.DATABASE_CONNECTION_STRING,
connectionTimeoutMillis: 3000,
idleTimeoutMillis: 10000,
}) })
drizzleInstance = drizzle({ client: pgPoolInstance, schema }) drizzleInstance = drizzle({ client: pgPoolInstance, schema })
} }

View File

@@ -8,58 +8,86 @@ import type {
} from '../types.ts' } from '../types.ts'
const DEFAULT_HEARTBEAT_INTERVAL = 60 const DEFAULT_HEARTBEAT_INTERVAL = 60
const BOOT_NOTIFICATION_DB_TIMEOUT_MS = 3000
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string) {
return Promise.race<T>([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
}),
])
}
export async function handleBootNotification( export async function handleBootNotification(
payload: BootNotificationRequest, payload: BootNotificationRequest,
ctx: OcppConnectionContext, ctx: OcppConnectionContext,
): Promise<BootNotificationResponse> { ): Promise<BootNotificationResponse> {
const db = useDrizzle() const db = useDrizzle()
const currentTime = dayjs().toISOString()
const [cp] = await db console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} received`)
.insert(chargePoint)
.values({
id: crypto.randomUUID(),
chargePointIdentifier: ctx.chargePointIdentifier,
chargePointVendor: payload.chargePointVendor,
chargePointModel: payload.chargePointModel,
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
firmwareVersion: payload.firmwareVersion ?? null,
iccid: payload.iccid ?? null,
imsi: payload.imsi ?? null,
meterType: payload.meterType ?? null,
meterSerialNumber: payload.meterSerialNumber ?? null,
// New, unknown devices start as Pending — admin must manually accept them
registrationStatus: 'Pending',
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
lastBootNotificationAt: dayjs().toDate(),
})
.onConflictDoUpdate({
target: chargePoint.chargePointIdentifier,
set: {
chargePointVendor: payload.chargePointVendor,
chargePointModel: payload.chargePointModel,
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
firmwareVersion: payload.firmwareVersion ?? null,
iccid: payload.iccid ?? null,
imsi: payload.imsi ?? null,
meterType: payload.meterType ?? null,
meterSerialNumber: payload.meterSerialNumber ?? null,
// Do NOT override registrationStatus — preserve whatever the admin set
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
lastBootNotificationAt: dayjs().toDate(),
updatedAt: dayjs().toDate(),
},
})
.returning()
const status = cp.registrationStatus try {
ctx.isRegistered = status === 'Accepted' const [cp] = await withTimeout(
db
.insert(chargePoint)
.values({
id: crypto.randomUUID(),
chargePointIdentifier: ctx.chargePointIdentifier,
chargePointVendor: payload.chargePointVendor,
chargePointModel: payload.chargePointModel,
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
firmwareVersion: payload.firmwareVersion ?? null,
iccid: payload.iccid ?? null,
imsi: payload.imsi ?? null,
meterType: payload.meterType ?? null,
meterSerialNumber: payload.meterSerialNumber ?? null,
// New, unknown devices start as Pending — admin must manually accept them
registrationStatus: 'Pending',
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
lastBootNotificationAt: dayjs().toDate(),
})
.onConflictDoUpdate({
target: chargePoint.chargePointIdentifier,
set: {
chargePointVendor: payload.chargePointVendor,
chargePointModel: payload.chargePointModel,
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
firmwareVersion: payload.firmwareVersion ?? null,
iccid: payload.iccid ?? null,
imsi: payload.imsi ?? null,
meterType: payload.meterType ?? null,
meterSerialNumber: payload.meterSerialNumber ?? null,
// Do NOT override registrationStatus — preserve whatever the admin set
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
lastBootNotificationAt: dayjs().toDate(),
updatedAt: dayjs().toDate(),
},
})
.returning(),
BOOT_NOTIFICATION_DB_TIMEOUT_MS,
`BootNotification persistence for ${ctx.chargePointIdentifier}`,
)
console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} status=${status}`) const status = cp.registrationStatus
ctx.isRegistered = status === 'Accepted'
return { console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} status=${status}`)
currentTime: dayjs().toISOString(),
interval: DEFAULT_HEARTBEAT_INTERVAL, return {
status, currentTime,
interval: DEFAULT_HEARTBEAT_INTERVAL,
status,
}
} catch (error) {
ctx.isRegistered = false
console.error(`[OCPP] BootNotification ${ctx.chargePointIdentifier} persistence failed:`, error)
return {
currentTime,
interval: DEFAULT_HEARTBEAT_INTERVAL,
status: 'Pending',
}
} }
} }