消息推送

This commit is contained in:
not-like-juvenile
2026-03-10 16:39:50 +08:00
parent ee9fabd806
commit e67016a6f4
18 changed files with 1176 additions and 49 deletions

View File

@@ -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 })
})