消息推送
This commit is contained in:
@@ -26,9 +26,11 @@ const fetch = (globalThis.fetch ? globalThis.fetch.bind(globalThis) : (() => {
|
||||
})())
|
||||
const crypto = require('crypto')
|
||||
|
||||
const PORT = process.env.PORT || 7201
|
||||
// 支持服务专用端口,避免与 push-server 共用 server/config.json 时发生端口冲突
|
||||
const PORT = process.env.WEBHOOK_PORT || process.env.PORT || 7201
|
||||
const SUPA_URL = (process.env.SUPA_URL || process.env.SUPA_URL_OVERRIDE || '').replace(/\/$/, '')
|
||||
const SUPA_KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || ''
|
||||
// Prefer service role key when present (server-side), to avoid RLS issues.
|
||||
const SUPA_KEY = process.env.SERVICE_ROLE_KEY || process.env.SUPA_KEY || ''
|
||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '' // optional HMAC secret
|
||||
|
||||
function supaFetch(path, opts = {}) {
|
||||
@@ -51,6 +53,21 @@ function computeSignature(bodyText, ts) {
|
||||
return h.digest('hex')
|
||||
}
|
||||
|
||||
function stableEventDedupeKey({ tracking_no, carrier, status_code, event_time, event_code, event_text }) {
|
||||
// 目的:让 webhook 重复回调(同一事件)不会重复入库,从而避免重复入队/重复通知。
|
||||
// 注意:优先使用稳定字段组合;不要使用 Date.now() 这类易变字段。
|
||||
const base = JSON.stringify({
|
||||
tracking_no: tracking_no || null,
|
||||
carrier: carrier || null,
|
||||
status_code: status_code || null,
|
||||
event_time: event_time || null,
|
||||
event_code: event_code || null,
|
||||
event_text: (event_text || '').trim().slice(0, 200)
|
||||
})
|
||||
const hex = crypto.createHash('sha256').update(base).digest('hex')
|
||||
return 'WH_' + hex.slice(0, 32)
|
||||
}
|
||||
|
||||
async function upsertRaw(payload, tracking_no, carrier, signature_valid) {
|
||||
try {
|
||||
const body = {
|
||||
@@ -72,10 +89,10 @@ async function upsertRaw(payload, tracking_no, carrier, signature_valid) {
|
||||
}
|
||||
}
|
||||
|
||||
async function findWaybillId(tracking_no, order_no) {
|
||||
async function findWaybill(tracking_no, order_no) {
|
||||
try {
|
||||
if (tracking_no) {
|
||||
const r = await supaFetch(`platform_express_waybills?tracking_no=eq.${encodeURIComponent(tracking_no)}`)
|
||||
const r = await supaFetch(`platform_express_waybills?tracking_no=eq.${encodeURIComponent(tracking_no)}&select=id,carrier,tracking_no,order_no,order_id`)
|
||||
if (!r.ok) {
|
||||
const txt = await r.text().catch(() => '')
|
||||
const err = new Error(`Supabase query failed (tracking_no): HTTP ${r.status} ${txt}`)
|
||||
@@ -83,10 +100,10 @@ async function findWaybillId(tracking_no, order_no) {
|
||||
throw err
|
||||
}
|
||||
const data = await r.json()
|
||||
if (data && data.length > 0) return data[0].id
|
||||
if (data && data.length > 0) return data[0]
|
||||
}
|
||||
if (order_no) {
|
||||
const r2 = await supaFetch(`platform_express_waybills?order_no=eq.${encodeURIComponent(order_no)}`)
|
||||
const r2 = await supaFetch(`platform_express_waybills?order_no=eq.${encodeURIComponent(order_no)}&select=id,carrier,tracking_no,order_no,order_id`)
|
||||
if (!r2.ok) {
|
||||
const txt2 = await r2.text().catch(() => '')
|
||||
const err2 = new Error(`Supabase query failed (order_no): HTTP ${r2.status} ${txt2}`)
|
||||
@@ -94,11 +111,11 @@ async function findWaybillId(tracking_no, order_no) {
|
||||
throw err2
|
||||
}
|
||||
const data2 = await r2.json()
|
||||
if (data2 && data2.length > 0) return data2[0].id
|
||||
if (data2 && data2.length > 0) return data2[0]
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
console.warn('findWaybillId error', e && e.message ? e.message : e)
|
||||
console.warn('findWaybill error', e && e.message ? e.message : e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -135,12 +152,23 @@ async function updateWaybill(id, status_code, text) {
|
||||
|
||||
async function insertEvent(event) {
|
||||
try {
|
||||
await supaFetch('platform_express_tracking_events', {
|
||||
// 幂等插入:基于 (waybill_id, dedupe_key) 的唯一约束忽略重复。
|
||||
const resp = await supaFetch('platform_express_tracking_events?on_conflict=waybill_id,dedupe_key', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json', Prefer: 'resolution=ignore-duplicates' },
|
||||
body: JSON.stringify(event)
|
||||
})
|
||||
} catch (e) { console.warn('insertEvent error', e) }
|
||||
|
||||
if (!resp.ok) {
|
||||
const txt = await resp.text().catch(() => '')
|
||||
console.warn('insertEvent failed:', `HTTP ${resp.status}`, txt)
|
||||
return { ok: false, status: resp.status, body: txt }
|
||||
}
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
console.warn('insertEvent error', e)
|
||||
return { ok: false, status: 0, body: (e && e.message) ? e.message : String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
@@ -164,19 +192,24 @@ async function start() {
|
||||
sigValid = calc === String(sig)
|
||||
}
|
||||
|
||||
// persist raw
|
||||
await upsertRaw(req.body || {}, req.body && (req.body.mailNo || req.body.tracking_no), req.body && req.body.carrier, sigValid)
|
||||
// persist raw (best-effort)
|
||||
await upsertRaw(
|
||||
req.body || {},
|
||||
req.body && (req.body.mailNo || req.body.tracking_no),
|
||||
req.body && (req.body.carrier || req.body.company),
|
||||
sigValid
|
||||
)
|
||||
|
||||
// find waybill
|
||||
const tracking_no = req.body && (req.body.mailNo || req.body.tracking_no)
|
||||
const order_no = req.body && (req.body.txLogisticId || req.body.order_no)
|
||||
const carrier = req.body && req.body.carrier ? req.body.carrier : (req.body && req.body.company || null)
|
||||
const carrierIn = req.body && (req.body.carrier || req.body.company || null)
|
||||
const event_code = req.body && (req.body.infoContent || req.body.status_code || req.body.event_code)
|
||||
const event_text = req.body && (req.body.remark || req.body.event_text || '')
|
||||
|
||||
let waybillId = null
|
||||
let waybill = null
|
||||
try {
|
||||
waybillId = await findWaybillId(tracking_no, order_no)
|
||||
waybill = await findWaybill(tracking_no, order_no)
|
||||
} catch (e) {
|
||||
const status = e && e.status ? Number(e.status) : 0
|
||||
if (status === 401 || status === 403) {
|
||||
@@ -184,11 +217,14 @@ async function start() {
|
||||
}
|
||||
return res.status(502).json({ ok: false, message: 'supabase query failed' })
|
||||
}
|
||||
if (!waybillId) {
|
||||
if (!waybill || !waybill.id) {
|
||||
// Waybill not found — respond 200 but inform caller in body.
|
||||
return res.status(200).json({ ok: false, message: 'waybill not found' })
|
||||
}
|
||||
|
||||
const waybillId = waybill.id
|
||||
const carrier = carrierIn || waybill.carrier || null
|
||||
|
||||
const status_code = mapStatus(event_code)
|
||||
|
||||
// update waybill
|
||||
@@ -205,18 +241,26 @@ async function start() {
|
||||
}
|
||||
|
||||
// insert event
|
||||
const received_at = new Date().toISOString()
|
||||
const dedupe_key = stableEventDedupeKey({ tracking_no, carrier, status_code, event_time, event_code, event_text })
|
||||
const eventPayload = {
|
||||
waybill_id: waybillId,
|
||||
carrier: carrier,
|
||||
tracking_no: tracking_no || null,
|
||||
tracking_no: tracking_no || waybill.tracking_no || null,
|
||||
received_at,
|
||||
source: 'webhook',
|
||||
event_id: dedupe_key,
|
||||
event_time: event_time,
|
||||
event_code: event_code || 'UNKNOWN',
|
||||
event_text: event_text || '',
|
||||
status_code: status_code,
|
||||
raw_payload: req.body || {},
|
||||
dedupe_key: 'WEBHOOK_' + Date.now()
|
||||
dedupe_key
|
||||
}
|
||||
const ins = await insertEvent(eventPayload)
|
||||
if (!ins || ins.ok !== true) {
|
||||
return res.status(200).json({ ok: false, message: 'insert tracking_event failed (see webhook-receiver logs)' })
|
||||
}
|
||||
await insertEvent(eventPayload)
|
||||
|
||||
return res.json({ ok: true })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user