feat: 实现快手电子凭证三个出站回调接口
- 新增 send-callback-service.ts: 卡券发码回调 (eticket/send) - 新增 consume-callback-service.ts: 核销回调 (eticket/consume) - 新增 destroy-callback-service.ts: 销毁回调 (eticket/destroy) - 三个回调均在对应入站接口处理成功后异步触发(fire-and-forget) - 共享 sendCallbackEnabled 开关控制,默认关闭 - 新增 accessToken 配置项(OAuth获取) - 新增 scripts/curl-send-callback.ts: 手动测试 curl 生成脚本 - docker-compose 添加 KUASHOU_INDUSTRY_ACCESS_TOKEN / SEND_CALLBACK_ENABLED 环境变量
This commit is contained in:
@@ -18,6 +18,8 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustryRun
|
||||
appSecret: String(config.appSecret || '').trim(),
|
||||
signSecret: String(config.signSecret || '').trim(),
|
||||
messageSecret: String(config.messageSecret || '').trim(),
|
||||
accessToken: String(config.accessToken || '').trim(),
|
||||
sendCallbackEnabled: Boolean(config.sendCallbackEnabled),
|
||||
provider: String(config.provider || KUISHOU_INDUSTRY_PROVIDER).trim() || KUISHOU_INDUSTRY_PROVIDER,
|
||||
platform: String(config.platform || KUISHOU_INDUSTRY_PLATFORM).trim() || KUISHOU_INDUSTRY_PLATFORM,
|
||||
shopId: String(config.shopId || KUISHOU_INDUSTRY_PROVIDER).trim() || KUISHOU_INDUSTRY_PROVIDER,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const KUAISHOU_OPEN_API = 'https://openapi.kuaixiaodian.com'
|
||||
|
||||
type ConsumeCallbackInput = {
|
||||
oid: string
|
||||
etickets: Array<{
|
||||
id: string
|
||||
code?: string
|
||||
num: number
|
||||
goodsValue?: number
|
||||
status?: string
|
||||
}>
|
||||
status: string
|
||||
consumeType: string
|
||||
consumeTime?: number
|
||||
storeName?: string
|
||||
storeAddress?: string
|
||||
expressNo?: string
|
||||
expressCode?: string
|
||||
appointmentTime?: string
|
||||
eticketType?: string
|
||||
ext?: string
|
||||
token: string
|
||||
seriallNum?: string
|
||||
consumePoiId?: number
|
||||
}
|
||||
|
||||
export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.sendCallbackEnabled) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', '回调未启用,跳过')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
logWarn('[kuaishou-industry/consume-callback]', 'accessToken 未配置,跳过')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
etickets: input.etickets.map((e) => {
|
||||
const item: JsonObject = { id: e.id, num: e.num }
|
||||
if (e.code) item.code = e.code
|
||||
if (e.goodsValue != null) item.goodsValue = e.goodsValue
|
||||
if (e.status) item.status = e.status
|
||||
return item
|
||||
}),
|
||||
status: input.status,
|
||||
consumeType: input.consumeType,
|
||||
token: input.token,
|
||||
}
|
||||
|
||||
if (input.consumeTime != null) bizParams.consumeTime = input.consumeTime
|
||||
if (input.storeName) bizParams.storeName = input.storeName
|
||||
if (input.storeAddress) bizParams.storeAddress = input.storeAddress
|
||||
if (input.expressNo) bizParams.expressNo = input.expressNo
|
||||
if (input.expressCode) bizParams.expressCode = input.expressCode
|
||||
if (input.appointmentTime) bizParams.appointmentTime = input.appointmentTime
|
||||
if (input.eticketType) bizParams.eticketType = input.eticketType
|
||||
if (input.ext) bizParams.ext = input.ext
|
||||
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 = `${KUAISHOU_OPEN_API}/integration/callback/virtual/eticket/consume`
|
||||
|
||||
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
||||
url,
|
||||
oid: input.oid,
|
||||
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(),
|
||||
})
|
||||
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}`, {
|
||||
error: message,
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
|
||||
import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js'
|
||||
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||
import { logWarn } from '../../../utils/logger.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
KUISHOU_INDUSTRY_PLATFORM,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
buildIndustrySuccessResponse,
|
||||
buildIndustryErrorResponse,
|
||||
} from './response.js'
|
||||
import { consumeCallback } from './consume-callback-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
@@ -116,8 +118,40 @@ export async function handleConsumeCode(rawBody: JsonObject = {}) {
|
||||
return buildIndustryErrorResponse(4012005, `未找到匹配的卡券: ${params.etickets.map((e) => e.id).join(',')}`)
|
||||
}
|
||||
|
||||
fireConsumeCallback({
|
||||
oid: normalizedOid,
|
||||
etickets: params.etickets.map((e) => ({
|
||||
id: String(e.id),
|
||||
code: e.code,
|
||||
num: Number(e.num) || 1,
|
||||
goodsValue: e.goodsValue,
|
||||
})),
|
||||
status: params.status,
|
||||
consumeType: params.consumeType,
|
||||
consumeTime: params.consumeTime,
|
||||
storeName: params.storeName,
|
||||
storeAddress: params.storeAddress,
|
||||
expressNo: params.expressNo,
|
||||
expressCode: params.expressCode,
|
||||
appointmentTime: params.appointmentTime,
|
||||
eticketType: params.eticketType,
|
||||
ext: params.ext,
|
||||
token: params.token,
|
||||
seriallNum: params.seriallNum,
|
||||
consumePoiId: params.consumePoiId,
|
||||
})
|
||||
|
||||
return buildIndustrySuccessResponse({
|
||||
oid: normalizedOid,
|
||||
consumedCount,
|
||||
})
|
||||
}
|
||||
|
||||
function fireConsumeCallback(input: Parameters<typeof consumeCallback>[0]) {
|
||||
consumeCallback(input).catch((err) => {
|
||||
logWarn('[kuaishou-industry/consume-code]', '核销回调异步执行异常', {
|
||||
oid: input.oid,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const KUAISHOU_OPEN_API = 'https://openapi.kuaixiaodian.com'
|
||||
|
||||
type DestroyCallbackInput = {
|
||||
oid: string
|
||||
etickets?: Array<{
|
||||
id: string
|
||||
code?: string
|
||||
num: number
|
||||
goodsValue?: number
|
||||
}>
|
||||
reason: string
|
||||
eticketType?: string
|
||||
ext?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
export async function destroyCallback(input: DestroyCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.sendCallbackEnabled) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', '回调未启用,跳过')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
logWarn('[kuaishou-industry/destroy-callback]', 'accessToken 未配置,跳过')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
reason: input.reason,
|
||||
}
|
||||
|
||||
if (input.etickets && input.etickets.length > 0) {
|
||||
bizParams.etickets = input.etickets.map((e) => {
|
||||
const item: JsonObject = { id: e.id, num: e.num }
|
||||
if (e.code) item.code = e.code
|
||||
if (e.goodsValue != null) item.goodsValue = e.goodsValue
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
if (input.eticketType) bizParams.eticketType = input.eticketType
|
||||
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 = `${KUAISHOU_OPEN_API}/integration/callback/virtual/eticket/destroy`
|
||||
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
||||
url,
|
||||
oid: input.oid,
|
||||
reason: input.reason,
|
||||
})
|
||||
|
||||
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) {
|
||||
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}`, {
|
||||
error: message,
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-re
|
||||
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||
import { logWarn } from '../../../utils/logger.js'
|
||||
import {
|
||||
KUISHOU_INDUSTRY_PLATFORM,
|
||||
getKuaishouIndustryConfig,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
buildIndustrySuccessResponse,
|
||||
buildIndustryErrorResponse,
|
||||
} from './response.js'
|
||||
import { destroyCallback } from './destroy-callback-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
@@ -67,5 +69,25 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
fireDestroyCallback({
|
||||
oid: normalizedOid,
|
||||
etickets: params.etickets.map((e) => ({
|
||||
id: String(e.id),
|
||||
code: e.code,
|
||||
num: Number(e.num) || 1,
|
||||
goodsValue: e.goodsValue,
|
||||
})),
|
||||
reason: params.reason,
|
||||
})
|
||||
|
||||
return buildIndustrySuccessResponse({ oid: normalizedOid })
|
||||
}
|
||||
|
||||
function fireDestroyCallback(input: Parameters<typeof destroyCallback>[0]) {
|
||||
destroyCallback(input).catch((err) => {
|
||||
logWarn('[kuaishou-industry/destroy-code]', '销毁回调异步执行异常', {
|
||||
oid: input.oid,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const KUAISHOU_OPEN_API = 'https://openapi.kuaixiaodian.com'
|
||||
|
||||
type SendCallbackInput = {
|
||||
oid: string
|
||||
sendType: string
|
||||
etickets: Array<{
|
||||
id: string
|
||||
code?: string
|
||||
num: number
|
||||
validStartTime: number
|
||||
validEndTime: number
|
||||
goodsValue?: number
|
||||
}>
|
||||
sendNum: number
|
||||
totalGoodsValue?: number
|
||||
token: string
|
||||
eticketType?: string
|
||||
ext?: string
|
||||
expressCode?: string
|
||||
expressNo?: string
|
||||
}
|
||||
|
||||
export async function sendCallback(input: SendCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.sendCallbackEnabled) {
|
||||
logInfo('[kuaishou-industry/send-callback]', '发码回调未启用,跳过')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
logWarn('[kuaishou-industry/send-callback]', 'accessToken 未配置,跳过发码回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
sendType: input.sendType,
|
||||
etickets: input.etickets.map((e) => {
|
||||
const item: JsonObject = {
|
||||
id: e.id,
|
||||
num: e.num,
|
||||
validStartTime: e.validStartTime,
|
||||
validEndTime: e.validEndTime,
|
||||
}
|
||||
if (e.code) item.code = e.code
|
||||
if (e.goodsValue != null) item.goodsValue = e.goodsValue
|
||||
return item
|
||||
}),
|
||||
sendNum: input.sendNum,
|
||||
token: input.token,
|
||||
}
|
||||
|
||||
if (input.totalGoodsValue != null) bizParams.totalGoodsValue = input.totalGoodsValue
|
||||
if (input.eticketType) bizParams.eticketType = input.eticketType
|
||||
if (input.ext) bizParams.ext = input.ext
|
||||
if (input.expressCode) bizParams.expressCode = input.expressCode
|
||||
if (input.expressNo) bizParams.expressNo = input.expressNo
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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 = `${KUAISHOU_OPEN_API}/integration/callback/virtual/eticket/send`
|
||||
|
||||
logInfo('[kuaishou-industry/send-callback]', `发起发码回调 oid=${input.oid}`, {
|
||||
url,
|
||||
oid: input.oid,
|
||||
sendNum: input.sendNum,
|
||||
})
|
||||
|
||||
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) {
|
||||
logInfo('[kuaishou-industry/send-callback]', `发码回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/send-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/send-callback]', `发码回调异常 oid=${input.oid}`, {
|
||||
error: message,
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { replaceOrderItems, listOrderItemsByOrderId } from '../../../repositorie
|
||||
import { listTasksByOrderId, createTask } from '../../../repositories/task-repo.js'
|
||||
import { upsertFulfillmentProfile, getFulfillmentProfileByKey } from '../../../repositories/fulfillment-profile-repo.js'
|
||||
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||
import { logWarn } from '../../../utils/logger.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import {
|
||||
KUISHOU_INDUSTRY_PROVIDER,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
buildIndustryEticketItem,
|
||||
buildIndustrySendCodeData,
|
||||
} from './response.js'
|
||||
import { sendCallback } from './send-callback-service.js'
|
||||
|
||||
const INDUSTRY_PROFILE_KEY = 'kuaishou-industry'
|
||||
|
||||
@@ -141,7 +143,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
||||
buildEticketFromTask(task, params),
|
||||
)
|
||||
|
||||
return buildIndustrySuccessResponse(
|
||||
const response = buildIndustrySuccessResponse(
|
||||
buildIndustrySendCodeData({
|
||||
oid: normalizedOid,
|
||||
sendType: params.sendType,
|
||||
@@ -149,6 +151,17 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
||||
etickets,
|
||||
}),
|
||||
)
|
||||
|
||||
fireSendCallback({
|
||||
oid: normalizedOid,
|
||||
sendType: params.sendType,
|
||||
etickets,
|
||||
token: params.token,
|
||||
eticketType: params.eticketType,
|
||||
ext: params.ext,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async function ensureIndustryProfile(now: string) {
|
||||
@@ -216,3 +229,35 @@ function mapTaskStatusToEticketStatus(taskStatus: string) {
|
||||
return 'UNUSED'
|
||||
}
|
||||
}
|
||||
|
||||
function fireSendCallback(input: {
|
||||
oid: string
|
||||
sendType: string
|
||||
etickets: ReturnType<typeof buildIndustryEticketItem>[]
|
||||
token: string
|
||||
eticketType?: string
|
||||
ext?: string
|
||||
}) {
|
||||
const eticketItems = input.etickets.map((e) => ({
|
||||
id: String(e.id || ''),
|
||||
code: e.code,
|
||||
num: Number(e.num) || 1,
|
||||
validStartTime: Number(e.validStartTime) || 0,
|
||||
validEndTime: Number(e.validEndTime) || 0,
|
||||
}))
|
||||
|
||||
sendCallback({
|
||||
oid: input.oid,
|
||||
sendType: input.sendType,
|
||||
etickets: eticketItems,
|
||||
sendNum: eticketItems.reduce((sum, e) => sum + e.num, 0),
|
||||
token: input.token,
|
||||
...(input.eticketType ? { eticketType: input.eticketType } : {}),
|
||||
...(input.ext ? { ext: input.ext } : {}),
|
||||
}).catch((err) => {
|
||||
logWarn('[kuaishou-industry/send-code]', '发码回调异步执行异常', {
|
||||
oid: input.oid,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user