feifei 异步通知

This commit is contained in:
yml2213
2026-07-07 17:09:55 +08:00
parent 2471df0086
commit c2c168f18e
6 changed files with 279 additions and 1 deletions
@@ -0,0 +1,24 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { signKuaishouFeifeiPayload } from './http-client.js'
import { verifyKuaishouFeifeiNotifySignature } from './notify-service.js'
test('verifyKuaishouFeifeiNotifySignature 使用原始 body 校验 feifei 通知签名', () => {
const input = {
appKey: 'mk_app_key',
appSecret: 'app-secret',
timestamp: '1783411548',
rawBody: '{"event":"order.status_changed","order":{"platform_order_no":"DT56bf8f1727a7"}}',
}
const sign = signKuaishouFeifeiPayload({
appKey: input.appKey,
appSecret: input.appSecret,
timestamp: input.timestamp,
body: input.rawBody,
})
const invalidSign = `${sign.slice(0, -1)}${sign.endsWith('0') ? '1' : '0'}`
assert.equal(verifyKuaishouFeifeiNotifySignature({ ...input, sign }), true)
assert.equal(verifyKuaishouFeifeiNotifySignature({ ...input, sign: invalidSign }), false)
})
@@ -0,0 +1,152 @@
import crypto from 'node:crypto'
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { findKuaishouFeifeiTaskByOrder } from '../../../repositories/task-repo.js'
import { syncKuaishouFeifeiTaskStatus } from '../../fulfillment/kuaishou-feifei/index.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { assertKuaishouFeifeiConfig } from './config.js'
import { signKuaishouFeifeiPayload } from './http-client.js'
import { mapKuaishouFeifeiOrder } from './order-service.js'
type JsonObject = Record<string, any>
type NotifyHeaders = Record<string, string | string[] | undefined>
export async function handleKuaishouFeifeiNotify(input: {
body: unknown
headers: NotifyHeaders
rawBody?: string | undefined
}) {
const config = assertKuaishouFeifeiConfig()
const rawBody = String(input.rawBody || '')
const appKey = normalizeHeader(input.headers['x-app-key'])
const timestamp = normalizeHeader(input.headers['x-timestamp'])
const sign = normalizeHeader(input.headers['x-sign'])
if (!rawBody) {
throw createHttpError('kuaishou-feifei 通知原始请求体缺失', {
statusCode: 400,
errorCode: 'kuaishou_feifei_notify_raw_body_missing',
})
}
if (!appKey || appKey !== config.appKey) {
throw createHttpError('kuaishou-feifei 通知 App Key 无效', {
statusCode: 401,
errorCode: 'kuaishou_feifei_notify_app_key_invalid',
})
}
if (!timestamp || !sign) {
throw createHttpError('kuaishou-feifei 通知签名参数缺失', {
statusCode: 401,
errorCode: 'kuaishou_feifei_notify_sign_missing',
})
}
if (!verifyKuaishouFeifeiNotifySignature({
appKey,
appSecret: config.appSecret,
timestamp,
rawBody,
sign,
})) {
throw createHttpError('kuaishou-feifei 通知验签失败', {
statusCode: 401,
errorCode: 'kuaishou_feifei_notify_sign_invalid',
})
}
const source = isPlainObject(input.body) ? input.body : {}
const event = String(source.event || '').trim()
const order = mapKuaishouFeifeiOrder(source.order)
if (!order.platformOrderNo && !order.orderNo) {
throw createHttpError('kuaishou-feifei 通知订单号缺失', {
statusCode: 400,
errorCode: 'kuaishou_feifei_notify_order_no_missing',
})
}
const task = await findKuaishouFeifeiTaskByOrder({
platformOrderNo: order.platformOrderNo,
orderNo: order.orderNo,
})
if (!task) {
throw createHttpError('kuaishou-feifei 通知未匹配到本地任务', {
statusCode: 404,
errorCode: 'kuaishou_feifei_notify_task_not_found',
context: {
platformOrderNo: order.platformOrderNo,
orderNo: order.orderNo,
},
})
}
const now = nowIso()
await createTaskEvent(
task.id,
'kuaishou_feifei_notify_received',
{
event,
platformOrderNo: order.platformOrderNo,
orderNo: order.orderNo,
productCode: order.productCode,
rechargeStatus: order.rechargeStatus,
rechargeStatusLabel: order.rechargeStatusLabel,
raw: order.raw,
},
now,
)
const updatedTask = await syncKuaishouFeifeiTaskStatus(task)
return {
ok: true,
event,
taskId: task.id,
platformOrderNo: order.platformOrderNo,
orderNo: order.orderNo,
rechargeStatus: order.rechargeStatus,
rechargeStatusLabel: order.rechargeStatusLabel,
taskStatus: updatedTask.task_status,
deliveryStatus: updatedTask.delivery_status,
}
}
export function verifyKuaishouFeifeiNotifySignature(input: {
appKey: string
appSecret: string
timestamp: string
rawBody: string
sign: string
}) {
const expected = signKuaishouFeifeiPayload({
appKey: input.appKey,
appSecret: input.appSecret,
timestamp: input.timestamp,
body: input.rawBody,
})
return timingSafeEqualString(expected, String(input.sign || '').trim().toLowerCase())
}
function normalizeHeader(value: string | string[] | undefined) {
return Array.isArray(value) ? String(value[0] || '').trim() : String(value || '').trim()
}
function timingSafeEqualString(left: string, right: string) {
const leftBuffer = Buffer.from(left, 'utf8')
const rightBuffer = Buffer.from(right, 'utf8')
if (leftBuffer.length !== rightBuffer.length) {
return false
}
return crypto.timingSafeEqual(leftBuffer, rightBuffer)
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}