feifei 异步通知
This commit is contained in:
@@ -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]'
|
||||
}
|
||||
Reference in New Issue
Block a user