消息推送后台打通
This commit is contained in:
@@ -1,22 +1,41 @@
|
||||
# Webhook 接收器 — 说明
|
||||
|
||||
路径:`pages/mall/delivery/server/webhook-receiver.js`
|
||||
路径:`pages/mall/delivery/webhook-server/webhook-receiver.js`
|
||||
|
||||
目的:接收承运方或 Mock Server 的 HTTP 回调(POST /webhook/express/status),将原始回文写入 `platform_express_event_raw`,并按项目现有映射更新 `platform_express_waybills` 与写入 `platform_express_tracking_events`。
|
||||
|
||||
环境变量(必须/可选):
|
||||
- `SUPA_URL`:Supabase REST 地址(示例 `http://192.168.1.62:18000`)
|
||||
- `SUPA_KEY`:Supabase service_role 或 anon key(用于 REST 写入)
|
||||
- `SUPA_USE_BEARER`(可选):是否附加 `Authorization: Bearer <SUPA_KEY>`,默认 `false`。
|
||||
- 在一些自托管 Supabase/Kong(key-auth)环境中,**只需要** `apikey`;如果误加 Bearer 且 key 不是 JWT,可能出现 `PGRST301`("None of the keys was able to decode the JWT")。
|
||||
- `WEBHOOK_SECRET`(可选):与第三方共享的 HMAC-SHA256 secret,用于校验 `X-Signature`(签名为 hex)
|
||||
- `PORT`(可选):接收器监听端口,默认 `7201`
|
||||
|
||||
配置方式(推荐用配置文件,避免与其他服务端口冲突):
|
||||
- **同目录配置文件(推荐)**:在 `webhook-receiver.js` 同目录放置 `webhook.config.json`,启动时会自动读取。
|
||||
- **显式指定配置文件**:设置 `CONFIG_FILE` / `CONFIG_PATH` 指向你的 `.env` 或 `.json`。
|
||||
- **回退加载**:若未指定 `CONFIG_FILE` 且同目录无 `webhook.config.json`,会尝试读取 `server/.env` 与 `server/config.json`(由 `server/load-config.js` 负责)。
|
||||
|
||||
示例配置文件:仓库内提供 [server/webhook.config.json.example](server/webhook.config.json.example),你可以复制一份到本目录作为 `webhook.config.json` 使用。
|
||||
|
||||
启动(PowerShell):
|
||||
```powershell
|
||||
$env:SUPA_URL='http://192.168.1.62:18000'
|
||||
$env:SUPA_KEY='your_service_role_key'
|
||||
# 可选验签
|
||||
$env:WEBHOOK_SECRET='your-secret'
|
||||
node pages/mall/delivery/server/webhook-receiver.js
|
||||
node pages/mall/delivery/webhook-server/webhook-receiver.js
|
||||
```
|
||||
|
||||
推荐:使用同目录配置文件启动(PowerShell):
|
||||
```powershell
|
||||
# 复制示例并填写真实 SUPA_KEY
|
||||
Copy-Item .\server\webhook.config.json.example .\pages\mall\delivery\webhook-server\webhook.config.json
|
||||
|
||||
node pages/mall/delivery/webhook-server/webhook-receiver.js
|
||||
```
|
||||
|
||||
如果你使用显式 CONFIG_FILE:
|
||||
```powershell
|
||||
$env:CONFIG_FILE=(Resolve-Path .\pages\mall\delivery\webhook-server\webhook.config.json)
|
||||
node pages/mall/delivery/webhook-server/webhook-receiver.js
|
||||
```
|
||||
|
||||
启动(Linux / macOS / WSL):
|
||||
@@ -24,7 +43,30 @@ node pages/mall/delivery/server/webhook-receiver.js
|
||||
export SUPA_URL='http://192.168.1.62:18000'
|
||||
export SUPA_KEY='your_service_role_key'
|
||||
export WEBHOOK_SECRET='your-secret' # optional
|
||||
node pages/mall/delivery/server/webhook-receiver.js
|
||||
node pages/mall/delivery/webhook-server/webhook-receiver.js
|
||||
```
|
||||
|
||||
也可以用配置文件(更适合长期运行):
|
||||
- `server/load-config.js` 会自动尝试加载:`server/.env`、`server/config.json`(以及 `CONFIG_FILE/CONFIG_PATH` 指定的文件),并把其中的键注入到 `process.env`。
|
||||
- 因为接收器已在启动时 `require` 了该加载器,所以你只要把 `SUPA_URL`、`SUPA_KEY` 写进上述文件之一即可。
|
||||
|
||||
如果你不想与 `server/config.json` 共用 `PORT`(避免端口冲突),建议为 webhook 单独准备一个配置文件,然后用 `CONFIG_FILE` 指定:
|
||||
|
||||
```powershell
|
||||
# 复制示例文件并填写真实 SUPA_KEY:
|
||||
Copy-Item .\server\webhook.config.json.example .\server\webhook.config.json
|
||||
|
||||
# 指定配置文件启动(不会影响 push-server 的配置):
|
||||
$env:CONFIG_FILE=(Resolve-Path .\server\webhook.config.json)
|
||||
node pages/mall/delivery/webhook-server/webhook-receiver.js
|
||||
```
|
||||
|
||||
示例:在 `server/.env` 中写入:
|
||||
```env
|
||||
SUPA_URL=http://192.168.1.62:18000
|
||||
SUPA_KEY=your_service_role_key
|
||||
WEBHOOK_SECRET=your-secret
|
||||
PORT=7201
|
||||
```
|
||||
|
||||
测试(curl 模拟第三方推送):
|
||||
@@ -42,6 +84,41 @@ curl -i -X POST http://localhost:7201/webhook/express/status \
|
||||
-d "$BODY"
|
||||
```
|
||||
|
||||
健康检查:
|
||||
- `GET http://localhost:7201/health`(端口以 `PORT` 为准)
|
||||
|
||||
常见问题排查:
|
||||
- 返回 `{ ok:false, message:'waybill not found' }`:说明 webhook 已收到请求,但在 `platform_express_waybills` 中找不到 `tracking_no`(或 `order_no`)匹配的记录。
|
||||
- 返回 `502 supabase unauthorized (check SUPA_KEY/SUPA_URL)`:说明当前 `SUPA_KEY` / `SUPA_URL` 无法通过 Supabase REST 鉴权(常见于 key 填错、已失效、URL 不对)。请换成 Supabase 控制台中的真实 `service_role` key,并重启接收器。
|
||||
|
||||
Windows 下保持“持续监听”(后台运行):
|
||||
> 只要 Node 进程还在,webhook 就会持续监听;如果你关闭终端/窗口或按 `Ctrl+C`,进程结束就不会再监听。
|
||||
|
||||
```powershell
|
||||
# 后台启动并把日志写到文件(推荐)
|
||||
Start-Process node -ArgumentList 'pages/mall/delivery/webhook-server/webhook-receiver.js' \
|
||||
-WorkingDirectory (Get-Location) \
|
||||
-RedirectStandardOutput '.\webhook-receiver.log' \
|
||||
-RedirectStandardError '.\webhook-receiver.err.log' \
|
||||
-PassThru
|
||||
|
||||
# 查看是否在监听(把 7201 换成你的 PORT)
|
||||
netstat -ano | findstr :7201
|
||||
|
||||
# 查看健康检查
|
||||
Invoke-RestMethod -Uri 'http://localhost:7201/health' -Method GET | ConvertTo-Json
|
||||
```
|
||||
|
||||
停止服务(按 PID 结束进程):
|
||||
```powershell
|
||||
# 先用 netstat 找到 LISTENING 的 PID,然后结束它
|
||||
Stop-Process -Id <PID>
|
||||
```
|
||||
|
||||
依赖说明:
|
||||
- 建议使用 Node.js 18+(例如你当前的 Node.js 22),已内置 `fetch`,无需安装 `node-fetch`。
|
||||
- 若使用更老的 Node 且没有 `fetch`,需要安装 `node-fetch`(并保持 CommonJS 兼容)。
|
||||
|
||||
预期:接口返回 200 JSON {ok:true}(若未找到对应运单会返回 {ok:false, message:'waybill not found'})。
|
||||
|
||||
验证写入(查看 Supabase):
|
||||
|
||||
@@ -7,7 +7,7 @@ const URL = `http://localhost:${PORT}/webhook/express/status`
|
||||
const SECRET = process.env.WEBHOOK_SECRET || 'test_secret'
|
||||
|
||||
const payload = {
|
||||
tracking_no: 'LOCALTEST123',
|
||||
tracking_no: 'TEST_YT_20260206_0007',
|
||||
status_code: 'DELIVERED',
|
||||
acceptTime: new Date().toISOString(),
|
||||
remark: 'local test event'
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
// Load configuration into process.env.
|
||||
// Priority:
|
||||
// 1) Real environment variables
|
||||
// 2) CONFIG_FILE/CONFIG_PATH (explicit)
|
||||
// 3) Local file next to this script: webhook.config.json
|
||||
// 4) server/.env / server/config.json via server/load-config.js
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const localConfigPath = path.join(__dirname, 'webhook.config.json')
|
||||
if (!process.env.CONFIG_FILE && !process.env.CONFIG_PATH && fs.existsSync(localConfigPath)) {
|
||||
process.env.CONFIG_FILE = localConfigPath
|
||||
}
|
||||
|
||||
require('../../../../server/load-config')
|
||||
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const fetch = require('node-fetch')
|
||||
const fetch = (globalThis.fetch ? globalThis.fetch.bind(globalThis) : (() => {
|
||||
try {
|
||||
// Fallback for older Node versions where fetch is not available.
|
||||
return require('node-fetch')
|
||||
} catch (e) {
|
||||
throw new Error("No fetch implementation found. Use Node.js 18+ or install 'node-fetch'.")
|
||||
}
|
||||
})())
|
||||
const crypto = require('crypto')
|
||||
|
||||
const PORT = process.env.PORT || 7201
|
||||
@@ -10,11 +33,14 @@ const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '' // optional HMAC secret
|
||||
|
||||
function supaFetch(path, opts = {}) {
|
||||
const url = `${SUPA_URL}/rest/v1/${path}`
|
||||
// Default to apikey only (compatible with self-hosted Supabase/Kong key-auth).
|
||||
// Only attach Authorization: Bearer when explicitly enabled.
|
||||
const headers = Object.assign({}, opts.headers || {}, {
|
||||
apikey: SUPA_KEY,
|
||||
Authorization: `Bearer ${SUPA_KEY}`,
|
||||
Accept: 'application/json'
|
||||
})
|
||||
const sendBearer = (process.env.SUPA_USE_BEARER === 'true')
|
||||
if (sendBearer) headers.Authorization = `Bearer ${SUPA_KEY}`
|
||||
return fetch(url, Object.assign({}, opts, { headers }))
|
||||
}
|
||||
|
||||
@@ -50,22 +76,30 @@ async function findWaybillId(tracking_no, order_no) {
|
||||
try {
|
||||
if (tracking_no) {
|
||||
const r = await supaFetch(`platform_express_waybills?tracking_no=eq.${encodeURIComponent(tracking_no)}`)
|
||||
if (r.ok) {
|
||||
const data = await r.json()
|
||||
if (data && data.length > 0) return data[0].id
|
||||
if (!r.ok) {
|
||||
const txt = await r.text().catch(() => '')
|
||||
const err = new Error(`Supabase query failed (tracking_no): HTTP ${r.status} ${txt}`)
|
||||
err.status = r.status
|
||||
throw err
|
||||
}
|
||||
const data = await r.json()
|
||||
if (data && data.length > 0) return data[0].id
|
||||
}
|
||||
if (order_no) {
|
||||
const r2 = await supaFetch(`platform_express_waybills?order_no=eq.${encodeURIComponent(order_no)}`)
|
||||
if (r2.ok) {
|
||||
const data2 = await r2.json()
|
||||
if (data2 && data2.length > 0) return data2[0].id
|
||||
if (!r2.ok) {
|
||||
const txt2 = await r2.text().catch(() => '')
|
||||
const err2 = new Error(`Supabase query failed (order_no): HTTP ${r2.status} ${txt2}`)
|
||||
err2.status = r2.status
|
||||
throw err2
|
||||
}
|
||||
const data2 = await r2.json()
|
||||
if (data2 && data2.length > 0) return data2[0].id
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
console.warn('findWaybillId error', e)
|
||||
return null
|
||||
console.warn('findWaybillId error', e && e.message ? e.message : e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +174,16 @@ async function start() {
|
||||
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 || '')
|
||||
|
||||
const waybillId = await findWaybillId(tracking_no, order_no)
|
||||
let waybillId = null
|
||||
try {
|
||||
waybillId = await findWaybillId(tracking_no, order_no)
|
||||
} catch (e) {
|
||||
const status = e && e.status ? Number(e.status) : 0
|
||||
if (status === 401 || status === 403) {
|
||||
return res.status(502).json({ ok: false, message: 'supabase unauthorized (check SUPA_KEY/SUPA_URL)' })
|
||||
}
|
||||
return res.status(502).json({ ok: false, message: 'supabase query failed' })
|
||||
}
|
||||
if (!waybillId) {
|
||||
// Waybill not found — respond 200 but inform caller in body.
|
||||
return res.status(200).json({ ok: false, message: 'waybill not found' })
|
||||
|
||||
6
pages/mall/delivery/webhook-server/webhook.config.json
Normal file
6
pages/mall/delivery/webhook-server/webhook.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"SUPA_URL": "http://192.168.1.62:18000",
|
||||
"SUPA_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UtMSIsImlhdCI6MTc2OTY3NjQ5OCwiZXhwIjoxOTI3MzU2NDk4fQ.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
|
||||
"WEBHOOK_SECRET": "",
|
||||
"PORT": "7201"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"SUPA_URL": "http://192.168.1.62:18000",
|
||||
"SUPA_KEY": "PASTE_YOUR_SERVICE_ROLE_KEY_HERE",
|
||||
"WEBHOOK_SECRET": "",
|
||||
"PORT": "7201"
|
||||
}
|
||||
Reference in New Issue
Block a user