新增快手电子凭证后台工具
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiLong,
|
||||
normalizeOpenApiPositiveInteger,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryAvailableEticketInput = {
|
||||
id?: unknown
|
||||
code?: unknown
|
||||
num?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryCheckAvailableInput = {
|
||||
sellerId?: unknown
|
||||
buyerId?: unknown
|
||||
orderId?: unknown
|
||||
eticketType?: unknown
|
||||
bizTypeCode?: unknown
|
||||
etickets?: unknown
|
||||
eTicketList?: unknown
|
||||
}
|
||||
|
||||
export function checkKuaishouIndustryEticketAvailable(
|
||||
input: KuaishouIndustryCheckAvailableInput = {},
|
||||
) {
|
||||
const eticketType = String(input.eticketType || input.bizTypeCode || '').trim()
|
||||
const etickets = normalizeAvailableEtickets(input.etickets || input.eTicketList)
|
||||
const sellerId = normalizeOpenApiLong(input.sellerId)
|
||||
const bizParams = pickDefinedBizParams({
|
||||
buyerId: normalizeOpenApiLong(input.buyerId),
|
||||
eticketType,
|
||||
etickets,
|
||||
orderId: normalizeOpenApiLong(input.orderId),
|
||||
sellerId,
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.virtual.eticket.checkavailable',
|
||||
path: '/open/virtual/eticket/checkavailable',
|
||||
bizParams,
|
||||
...(sellerId ? { sellerId: String(sellerId) } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeAvailableEtickets(value: unknown): JsonObject[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => normalizeAvailableEticket(item))
|
||||
.filter((item): item is JsonObject => Boolean(item))
|
||||
}
|
||||
|
||||
function normalizeAvailableEticket(value: unknown): JsonObject | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = value as KuaishouIndustryAvailableEticketInput
|
||||
const id = String(current.id || '').trim()
|
||||
const code = String(current.code || '').trim()
|
||||
const num = normalizeOpenApiPositiveInteger(current.num, 1)
|
||||
|
||||
if (!id && !code) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pickDefinedBizParams({ id, code, num })
|
||||
}
|
||||
+53
-147
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type ConsumeCallbackInput = {
|
||||
oid: string
|
||||
@@ -33,38 +26,9 @@ type ConsumeCallbackInput = {
|
||||
consumePoiId?: number
|
||||
}
|
||||
|
||||
export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', '行业电子凭证配置未启用,跳过核销回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logWarn('[kuaishou-industry/consume-callback]', 'accessToken 刷新失败,无法发起核销回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起核销回调'
|
||||
logWarn('[kuaishou-industry/consume-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
export async function consumeCallback(
|
||||
input: ConsumeCallbackInput,
|
||||
): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
etickets: input.etickets.map((e) => {
|
||||
@@ -90,121 +54,63 @@ export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ su
|
||||
if (input.seriallNum) bizParams.seriallNum = input.seriallNum
|
||||
if (input.consumePoiId != null) bizParams.consumePoiId = input.consumePoiId
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.consume',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/consume`
|
||||
|
||||
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
||||
url,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
status: input.status,
|
||||
consumeType: input.consumeType,
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.consume',
|
||||
path: '/integration/callback/virtual/eticket/consume',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
||||
...request,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
status: input.status,
|
||||
consumeType: input.consumeType,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logInfo('[kuaishou-industry/consume-callback]', '行业电子凭证配置未启用,跳过核销回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logWarn('[kuaishou-industry/consume-callback]', 'accessToken 刷新失败,无法发起核销回调', {
|
||||
error: message,
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `核销回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
response: json,
|
||||
})
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调异常 oid=${input.oid}`, resolveCallbackErrorDetail(err))
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起核销回调'
|
||||
logWarn('[kuaishou-industry/consume-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
if (result.success) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `核销回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logWarn(
|
||||
'[kuaishou-industry/consume-callback]',
|
||||
`核销回调异常 oid=${input.oid}`,
|
||||
result.errorDetail || { error: result.error },
|
||||
)
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
response: result.response || null,
|
||||
})
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
return {
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { assertKuaishouIndustryConfig } from './config.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
type SignMethod = 'MD5' | 'HMAC_SHA256'
|
||||
export type SignMethod = 'MD5' | 'HMAC_SHA256'
|
||||
|
||||
export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) {
|
||||
const entries = Object.entries(params)
|
||||
|
||||
+53
-147
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type DestroyCallbackInput = {
|
||||
oid: string
|
||||
@@ -23,38 +16,9 @@ type DestroyCallbackInput = {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export async function destroyCallback(input: DestroyCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', '行业电子凭证配置未启用,跳过销毁回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logWarn('[kuaishou-industry/destroy-callback]', 'accessToken 刷新失败,无法发起销毁回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起销毁回调'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
export async function destroyCallback(
|
||||
input: DestroyCallbackInput,
|
||||
): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
reason: input.reason,
|
||||
@@ -73,121 +37,63 @@ export async function destroyCallback(input: DestroyCallbackInput): Promise<{ su
|
||||
if (input.ext) bizParams.ext = input.ext
|
||||
if (input.token) bizParams.token = input.token
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.destroy',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/destroy`
|
||||
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
||||
url,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
reason: input.reason,
|
||||
hasToken: Boolean(input.token),
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.destroy',
|
||||
path: '/integration/callback/virtual/eticket/destroy',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
||||
...request,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
reason: input.reason,
|
||||
hasToken: Boolean(input.token),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', '行业电子凭证配置未启用,跳过销毁回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', 'accessToken 刷新失败,无法发起销毁回调', {
|
||||
error: message,
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `销毁回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
response: json,
|
||||
})
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调异常 oid=${input.oid}`, resolveCallbackErrorDetail(err))
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起销毁回调'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
if (result.success) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `销毁回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logWarn(
|
||||
'[kuaishou-industry/destroy-callback]',
|
||||
`销毁回调异常 oid=${input.oid}`,
|
||||
result.errorDetail || { error: result.error },
|
||||
)
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
response: result.response || null,
|
||||
})
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
return {
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { signKuaishouIndustryPayload, type SignMethod } from './crypto.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
export type JsonObject = Record<string, any>
|
||||
|
||||
type KuaishouIndustryOpenApiCallInput = {
|
||||
apiMethod: string
|
||||
path: string
|
||||
bizParams: JsonObject
|
||||
httpMethod?: 'GET' | 'POST'
|
||||
sellerId?: string
|
||||
signMethod?: SignMethod
|
||||
onRequest?: (request: JsonObject) => void
|
||||
}
|
||||
|
||||
export type KuaishouIndustryOpenApiCallResult = {
|
||||
success: boolean
|
||||
response?: JsonObject
|
||||
error?: string
|
||||
request?: JsonObject
|
||||
durationMs?: number
|
||||
httpStatus?: number
|
||||
skippedReason?: 'disabled' | 'missing_access_token' | 'token_error'
|
||||
errorDetail?: JsonObject
|
||||
}
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
|
||||
export async function requestKuaishouIndustryOpenApi(
|
||||
input: KuaishouIndustryOpenApiCallInput,
|
||||
): Promise<KuaishouIndustryOpenApiCallResult> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
return { success: true, skippedReason: 'disabled' }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
skippedReason: 'token_error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorDetail: resolveKuaishouOpenApiErrorDetail(error),
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
skippedReason: 'missing_access_token',
|
||||
error: 'accessToken 未配置',
|
||||
}
|
||||
}
|
||||
|
||||
const signMethod = input.signMethod || 'MD5'
|
||||
const paramStr = JSON.stringify(input.bizParams)
|
||||
const signParams: JsonObject = {
|
||||
method: input.apiMethod,
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: config.version || '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod,
|
||||
param: paramStr,
|
||||
}
|
||||
signParams.sign = signKuaishouIndustryPayload(signParams, signMethod, config)
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const httpMethod = input.httpMethod || 'POST'
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}${normalizeOpenApiPath(input.path)}`
|
||||
const requestLog = buildKuaishouOpenApiRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams: input.bizParams,
|
||||
httpMethod,
|
||||
})
|
||||
|
||||
input.onRequest?.(requestLog)
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = httpMethod === 'GET'
|
||||
? await fetch(`${url}?${body.toString()}`, { method: 'GET' })
|
||||
: await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch {
|
||||
json = { raw: text }
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
return {
|
||||
success: res.ok && Number(json.result) === 1,
|
||||
response: json,
|
||||
request: requestLog,
|
||||
durationMs,
|
||||
httpStatus: res.status,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request: requestLog,
|
||||
errorDetail: resolveKuaishouOpenApiErrorDetail(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildKuaishouOpenApiRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
httpMethod,
|
||||
}: {
|
||||
url: string
|
||||
signParams: JsonObject
|
||||
bizParams: JsonObject
|
||||
httpMethod?: 'GET' | 'POST'
|
||||
}): JsonObject {
|
||||
return {
|
||||
url,
|
||||
method: httpMethod || 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
apiMethod: signParams.method,
|
||||
appkey: signParams.appkey,
|
||||
access_token: signParams.access_token,
|
||||
version: signParams.version,
|
||||
timestamp: signParams.timestamp,
|
||||
signMethod: signParams.signMethod,
|
||||
sign: signParams.sign,
|
||||
param: bizParams,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') ||
|
||||
DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
export function resolveKuaishouOpenApiErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function normalizeOpenApiPath(value: unknown): string {
|
||||
const path = String(value || '').trim()
|
||||
if (!path) {
|
||||
return '/'
|
||||
}
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export type JsonObject = Record<string, any>
|
||||
|
||||
export function pickDefinedBizParams(input: JsonObject): JsonObject {
|
||||
const output: JsonObject = {}
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
continue
|
||||
}
|
||||
|
||||
output[key] = value
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function normalizeOpenApiLong(value: unknown): string | number {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(text)) {
|
||||
return text
|
||||
}
|
||||
|
||||
const parsed = Number(text)
|
||||
return Number.isSafeInteger(parsed) ? parsed : text
|
||||
}
|
||||
|
||||
export function normalizeOpenApiInteger(value: unknown, fallback = 0): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizeOpenApiPositiveInteger(value: unknown, fallback = 1): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizeStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiInteger,
|
||||
normalizeOpenApiLong,
|
||||
normalizeOpenApiPositiveInteger,
|
||||
normalizeStringList,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryRefundListInput = {
|
||||
sellerId?: string
|
||||
beginTime?: unknown
|
||||
endTime?: unknown
|
||||
type?: unknown
|
||||
pageSize?: unknown
|
||||
currentPage?: unknown
|
||||
sort?: unknown
|
||||
queryType?: unknown
|
||||
negotiateStatus?: unknown
|
||||
pcursor?: unknown
|
||||
status?: unknown
|
||||
option?: JsonObject
|
||||
orderId?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryRefundApproveInput = {
|
||||
sellerId?: string
|
||||
refundId?: unknown
|
||||
desc?: unknown
|
||||
refundAmount?: unknown
|
||||
status?: unknown
|
||||
negotiateStatus?: unknown
|
||||
refundHandingWay?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryRefundDisagreeInput = {
|
||||
sellerId?: string
|
||||
refundId?: unknown
|
||||
sellerDisagreeReason?: unknown
|
||||
sellerDisagreeDesc?: unknown
|
||||
sellerDisagreeImages?: unknown
|
||||
status?: unknown
|
||||
negotiateStatus?: unknown
|
||||
}
|
||||
|
||||
export function listKuaishouIndustryRefunds(input: KuaishouIndustryRefundListInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
beginTime: normalizeOpenApiLong(input.beginTime),
|
||||
endTime: normalizeOpenApiLong(input.endTime),
|
||||
type: normalizeOpenApiInteger(input.type, 8),
|
||||
pageSize: normalizeOpenApiPositiveInteger(input.pageSize, 50),
|
||||
currentPage: normalizeOpenApiPositiveInteger(input.currentPage, 1),
|
||||
sort: input.sort === undefined || input.sort === '' ? undefined : normalizeOpenApiInteger(input.sort, 1),
|
||||
queryType: input.queryType === undefined || input.queryType === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.queryType, 1),
|
||||
negotiateStatus: input.negotiateStatus === undefined || input.negotiateStatus === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.negotiateStatus, 0),
|
||||
pcursor: String(input.pcursor ?? ''),
|
||||
status: input.status === undefined || input.status === '' ? undefined : normalizeOpenApiInteger(input.status, 0),
|
||||
option: input.option && typeof input.option === 'object' ? input.option : undefined,
|
||||
orderId: normalizeOpenApiLong(input.orderId),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.pcursor.list',
|
||||
path: '/open/seller/order/refund/pcursor/list',
|
||||
httpMethod: 'GET',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function approveKuaishouIndustryRefund(input: KuaishouIndustryRefundApproveInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
refundId: normalizeOpenApiLong(input.refundId),
|
||||
desc: String(input.desc ?? '').trim(),
|
||||
refundAmount: normalizeOpenApiLong(input.refundAmount),
|
||||
status: input.status === undefined || input.status === '' ? undefined : normalizeOpenApiInteger(input.status, 0),
|
||||
negotiateStatus: input.negotiateStatus === undefined || input.negotiateStatus === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.negotiateStatus, 0),
|
||||
refundHandingWay: input.refundHandingWay === undefined || input.refundHandingWay === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.refundHandingWay, 0),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.approve',
|
||||
path: '/open/seller/order/refund/approve',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function disagreeKuaishouIndustryRefund(input: KuaishouIndustryRefundDisagreeInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
refundId: normalizeOpenApiLong(input.refundId),
|
||||
sellerDisagreeReason: normalizeOpenApiInteger(input.sellerDisagreeReason, 100),
|
||||
sellerDisagreeDesc: String(input.sellerDisagreeDesc ?? '').trim(),
|
||||
sellerDisagreeImages: normalizeStringList(input.sellerDisagreeImages),
|
||||
status: normalizeOpenApiInteger(input.status, 10),
|
||||
negotiateStatus: normalizeOpenApiInteger(input.negotiateStatus, 1),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.disagree.refund',
|
||||
path: '/open/seller/order/refund/disagree/refund',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiPositiveInteger,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryReverseEticketInput = {
|
||||
id?: unknown
|
||||
code?: unknown
|
||||
num?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryReverseCallbackInput = {
|
||||
sellerId?: string
|
||||
oid?: unknown
|
||||
eticketType?: unknown
|
||||
etickets?: unknown
|
||||
serialNum?: unknown
|
||||
reason?: unknown
|
||||
ext?: unknown
|
||||
token?: unknown
|
||||
}
|
||||
|
||||
export function reverseKuaishouIndustryCallback(input: KuaishouIndustryReverseCallbackInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
oid: String(input.oid || '').trim(),
|
||||
eticketType: String(input.eticketType || '').trim(),
|
||||
etickets: normalizeReverseEtickets(input.etickets),
|
||||
serialNum: String(input.serialNum || '').trim(),
|
||||
reason: String(input.reason || '').trim(),
|
||||
ext: normalizeReverseExt(input.ext),
|
||||
token: String(input.token || '').trim(),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.reverse',
|
||||
path: '/integration/callback/virtual/eticket/reverse',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeReverseEtickets(value: unknown): JsonObject[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => normalizeReverseEticket(item))
|
||||
.filter((item): item is JsonObject => Boolean(item))
|
||||
}
|
||||
|
||||
function normalizeReverseEticket(value: unknown): JsonObject | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = value as KuaishouIndustryReverseEticketInput
|
||||
const id = String(current.id || '').trim()
|
||||
const code = String(current.code || '').trim()
|
||||
const num = normalizeOpenApiPositiveInteger(current.num, 1)
|
||||
|
||||
if (!id && !code) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pickDefinedBizParams({ id, code, num })
|
||||
}
|
||||
|
||||
function normalizeReverseExt(value: unknown): unknown {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim()
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logIntegration } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type SendCallbackInput = {
|
||||
oid: string
|
||||
@@ -29,37 +22,6 @@ type SendCallbackInput = {
|
||||
}
|
||||
|
||||
export async function sendCallback(input: SendCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', '行业电子凭证配置未启用,跳过发货回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logIntegration('[kuaishou-industry/send-callback]', 'accessToken 刷新失败,无法发起发货回调', {
|
||||
error: message,
|
||||
}, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起发货回调'
|
||||
logIntegration('[kuaishou-industry/send-callback]', message, undefined, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
sendType: input.sendType,
|
||||
@@ -83,153 +45,70 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success:
|
||||
if (input.expressCode) bizParams.expressCode = input.expressCode
|
||||
if (input.expressNo) bizParams.expressNo = input.expressNo
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.send',
|
||||
path: '/integration/callback/virtual/eticket/send',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
const requestLog = {
|
||||
sellerId: input.sellerId || '',
|
||||
...request,
|
||||
}
|
||||
logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog)
|
||||
},
|
||||
})
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.send',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logIntegration('[kuaishou-industry/send-callback]', '行业电子凭证配置未启用,跳过发货回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/send`
|
||||
const requestLog = {
|
||||
sellerId: input.sellerId || '',
|
||||
...buildSendCallbackRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
}),
|
||||
}
|
||||
|
||||
logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog)
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
request: requestLog,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
request: requestLog,
|
||||
response: json,
|
||||
}, { level: 'warn' })
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调异常 oid=${input.oid}`, {
|
||||
...resolveCallbackErrorDetail(err),
|
||||
request: requestLog,
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logIntegration('[kuaishou-industry/send-callback]', 'accessToken 刷新失败,无法发起发货回调', {
|
||||
error: message,
|
||||
}, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
function buildSendCallbackRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
}: {
|
||||
url: string
|
||||
signParams: JsonObject
|
||||
bizParams: JsonObject
|
||||
}): JsonObject {
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起发货回调'
|
||||
logIntegration('[kuaishou-industry/send-callback]', message, undefined, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const requestLog = result.request
|
||||
? {
|
||||
sellerId: input.sellerId || '',
|
||||
...result.request,
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (result.success) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
request: requestLog,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调异常 oid=${input.oid}`, {
|
||||
...(result.errorDetail || { error: result.error }),
|
||||
request: requestLog,
|
||||
}, { level: 'warn' })
|
||||
} else {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
request: requestLog,
|
||||
response: result.response || null,
|
||||
}, { level: 'warn' })
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
method: 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
apiMethod: signParams.method,
|
||||
appkey: signParams.appkey,
|
||||
access_token: signParams.access_token,
|
||||
version: signParams.version,
|
||||
timestamp: signParams.timestamp,
|
||||
signMethod: signParams.signMethod,
|
||||
sign: signParams.sign,
|
||||
param: bizParams,
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user