行业电子凭证接入

- 新增 4 个回调接口:send-code/destroy-code/query-code/consume-code
- 支持快手开放平台 MD5/HMAC_SHA256 签名验签
- send-code 回调自动创建 order/order_items/fulfillment_tasks/profiles
- consume-code 回调与既有 flow.consume 结构兼容
- isKuaishouCloudTask 同时匹配 kuaishou-industry executor_key
- claim 页面自动验证行业凭证 task,跳过核销码输入
- 退回时行业 task 跳过快手核销 API
- 新增 KUASHOU_INDUSTRY_* 环境变量配置
This commit is contained in:
yml2213
2026-06-30 17:33:51 +08:00
parent 693f4de45a
commit f38efa2edf
16 changed files with 471 additions and 4 deletions
+3
View File
@@ -30,7 +30,10 @@ KAQUAN91_USER_ID=
KAQUAN91_SECRET=
# 快手行业电子凭证
KUASHOU_INDUSTRY_APP_KEY=
KUASHOU_INDUSTRY_APP_SECRET=
KUASHOU_INDUSTRY_SIGN_SECRET=
KUASHOU_INDUSTRY_MESSAGE_SECRET=
# Cloudtentacles
CLOUDTENTACLES_DEVICE_ID=08bc9d8c-fd15-48ea-bc00-8d754076cafc
+3
View File
@@ -29,7 +29,10 @@ KAQUAN91_USER_ID=
KAQUAN91_SECRET=
# 快手行业电子凭证
KUASHOU_INDUSTRY_APP_KEY=
KUASHOU_INDUSTRY_APP_SECRET=
KUASHOU_INDUSTRY_SIGN_SECRET=
KUASHOU_INDUSTRY_MESSAGE_SECRET=
# Cloudtentacles
CLOUDTENTACLES_DEVICE_ID=08bc9d8c-fd15-48ea-bc00-8d754076cafc
+2
View File
@@ -46,7 +46,9 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
},
kuaishouIndustry: {
appKey: "",
appSecret: "",
signSecret: "",
messageSecret: "",
provider: "kuaishou-industry",
platform: "kuaishou",
shopId: "kuaishou-industry",
+10
View File
@@ -229,11 +229,21 @@ export const ENV_OVERRIDES: readonly EnvOverride[] = [
"kuaishouIndustry",
"appKey",
]),
stringEnv("KUASHOU_INDUSTRY_APP_SECRET", [
"platforms",
"kuaishouIndustry",
"appSecret",
]),
stringEnv("KUASHOU_INDUSTRY_SIGN_SECRET", [
"platforms",
"kuaishouIndustry",
"signSecret",
]),
stringEnv("KUASHOU_INDUSTRY_MESSAGE_SECRET", [
"platforms",
"kuaishouIndustry",
"messageSecret",
]),
stringEnv("KUASHOU_INDUSTRY_PROVIDER", [
"platforms",
"kuaishouIndustry",
@@ -4,6 +4,7 @@ import { createRateLimitMiddleware } from '../middleware/rate-limit.js'
import { handleSendCode } from '../services/platforms/kuaishou-industry/send-code-service.js'
import { handleDestroyCode } from '../services/platforms/kuaishou-industry/destroy-code-service.js'
import { handleQueryCode } from '../services/platforms/kuaishou-industry/query-code-service.js'
import { handleConsumeCode } from '../services/platforms/kuaishou-industry/consume-code-service.js'
import { buildIndustryErrorResponse } from '../services/platforms/kuaishou-industry/response.js'
import { createRequestId, logIntegration } from '../utils/logger.js'
@@ -117,6 +118,39 @@ router.post('/query-code', industryRateLimit, async (req, res) => {
}
})
router.post('/consume-code', industryRateLimit, async (req, res) => {
const requestId = createRequestId('ksind')
const startedAt = Date.now()
logIntegration('[kuaishou-industry/consume-code]', '收到快手行业电子凭证核销回调', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
ip: req.ip,
query: req.query,
body: req.body,
})
try {
const params = mergeRequestParams(req)
const result = await handleConsumeCode(params)
logIntegration('[kuaishou-industry/consume-code]', '核销处理完成', {
requestId,
durationMs: Date.now() - startedAt,
result,
})
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/consume-code]', '核销处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
function mergeRequestParams(req: any): Record<string, any> {
const query = req.query || {}
const body = req.body || {}
@@ -47,7 +47,13 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
const executorKey = String(context.task.executor_key || '').trim()
if (executorKey === 'kuaishou-industry') {
return verifyIndustryeVoucherTicket(context, now)
}
if (executorKey !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
@@ -237,6 +243,96 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
return getKuaishouCloudClaimDetail(token)
}
async function verifyIndustryeVoucherTicket(
context: Awaited<ReturnType<typeof getClaimContext>>,
now: string,
) {
const taskContext = parseTaskContext(context.task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (flow.consume.status === 'success') {
return getKuaishouCloudClaimDetail(context.claimToken.token)
}
const industryContext = typeof taskContext === 'object' ? taskContext : {}
const token = String(industryContext.token || '').trim()
const certExpireType = Number(industryContext.certExpireType || 0)
const certActualStartTime = Number(industryContext.certActualStartTime || 0)
const certActualEndTime = Number(industryContext.certActualEndTime || 0)
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: '',
status: 'verified',
capturedAt: flow.ticket.capturedAt || now,
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
verifiedAt: now,
oid: context.order.platform_order_id,
formToken: token,
leftCount: 0,
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
},
consume: {
...flow.consume,
status: 'pending',
shopId: context.order.shop_id,
shopName: context.order.shop_name,
autoConsumeEnabled: true,
},
certInfo: {
certExpireType,
certActualStartTime,
certActualEndTime,
},
},
}
const taskWithTicket = (await updateTask(context.task.id, {
claim_token: context.claimToken.token,
claim_expires_at: context.claimToken.expired_at,
context_json: JSON.stringify(nextContext),
claimed_at: context.task.claimed_at || now,
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
last_error: '',
updated_at: now,
})) || {
...context.task,
context_json: JSON.stringify(nextContext),
claimed_at: context.task.claimed_at || now,
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
last_error: '',
updated_at: now,
}
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
await prepareKuaishouCloudFulfillmentTask(taskWithTicket, {
source: 'send_code_callback',
actor: { source: 'send_code_callback' },
})
}
await createTaskEvent(
context.task.id,
'kuaishou_cloud_ticket_verified',
{
ticketCodeMasked: '',
source: 'send_code_callback',
industryVoucher: true,
verifiedAt: now,
},
now,
)
return getKuaishouCloudClaimDetail(context.claimToken.token)
}
export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
const normalized = String(filename || '').trim()
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
@@ -261,8 +357,18 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
const context = await getClaimContext(token)
let task = context.task
const executorKey = String(task.executor_key || '').trim()
if (executorKey === 'kuaishou-industry') {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (flow.consume.status !== 'success') {
const now = nowIso()
await verifyIndustryeVoucherTicket(context, now).catch(() => null)
}
}
if (
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
executorKey === 'kuaishou_ct_assisted' &&
!isKuaishouCloudMockTask(task)
) {
task = (await syncKuaishouCloudRoleInfo(task)) || task
@@ -5,9 +5,16 @@ export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1'
export type JsonObject = Record<string, any>
const KUAISHOU_CLOUD_EXECUTOR_KEYS = new Set(['kuaishou_ct_assisted', 'kuaishou-industry'])
export function isKuaishouCloudTask(task: unknown) {
const source = task && typeof task === 'object' ? (task as JsonObject) : {}
return String(source.executor_key || '').trim() === 'kuaishou_ct_assisted'
return KUAISHOU_CLOUD_EXECUTOR_KEYS.has(String(source.executor_key || '').trim())
}
export function isIndustryEVoucherTask(task: unknown) {
const source = task && typeof task === 'object' ? (task as JsonObject) : {}
return String(source.executor_key || '').trim() === 'kuaishou-industry'
}
export function normalizeKuaishouCloudFlow(value: unknown) {
@@ -24,6 +24,7 @@ import {
} from "../../platforms/kuaishou-eticket/source-config-service.js";
import {
isKuaishouCloudTask,
isIndustryEVoucherTask,
maskCode,
maskPhone,
normalizeKuaishouCloudFlow,
@@ -793,10 +794,15 @@ export async function returnKuaishouCloudFulfillmentTask(
let nextResultCode = "kuaishou_cloud_completed";
let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销";
const consumeAlreadyCompleted = flow.consume.status === "success";
const isIndustryTask = isIndustryEVoucherTask(task)
if (consumeAlreadyCompleted) {
consumeStatus = "success";
consumedAt = flow.consume.consumedAt || now;
} else if (isIndustryTask) {
consumeStatus = "success";
consumedAt = flow.consume.consumedAt || now;
consumeErrorMessage = "行业电子凭证核销由平台回调处理,已跳过主动核销";
} else if (!order) {
consumeStatus = "failed";
consumeErrorMessage = "任务关联订单不存在,无法执行快手核销";
@@ -15,7 +15,9 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustryRun
return {
appKey: String(config.appKey || '').trim(),
appSecret: String(config.appSecret || '').trim(),
signSecret: String(config.signSecret || '').trim(),
messageSecret: String(config.messageSecret || '').trim(),
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,
@@ -27,6 +29,13 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustryRun
export function assertKuaishouIndustryConfig() {
const config = getKuaishouIndustryConfig()
if (!config.appKey) {
throw createHttpError('快手行业电子凭证 appKey 未配置', {
statusCode: 500,
errorCode: 'kuaishou_industry_missing_app_key',
})
}
if (!config.signSecret) {
throw createHttpError('快手行业电子凭证 signSecret 未配置', {
statusCode: 500,
@@ -36,3 +45,18 @@ export function assertKuaishouIndustryConfig() {
return config
}
export function assertMatchingAppKey(incomingAppKey: string) {
const config = getKuaishouIndustryConfig()
if (!config.appKey) {
return
}
if (incomingAppKey !== config.appKey) {
throw createHttpError('appkey 不匹配', {
statusCode: 400,
errorCode: 'kuaishou_industry_app_key_mismatch',
})
}
}
@@ -0,0 +1,123 @@
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js'
import { normalizeTimestampIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
KUISHOU_INDUSTRY_PLATFORM,
getKuaishouIndustryConfig,
assertMatchingAppKey,
} from './config.js'
import { normalizeConsumeCodePayload, assertConsumeCodePayload } from './payload.js'
import { assertKuaishouIndustrySignature } from './crypto.js'
import {
buildIndustrySuccessResponse,
buildIndustryErrorResponse,
} from './response.js'
type JsonObject = Record<string, any>
export async function handleConsumeCode(rawBody: JsonObject = {}) {
const config = getKuaishouIndustryConfig()
assertKuaishouIndustrySignature(rawBody, config)
const params = normalizeConsumeCodePayload(rawBody)
assertConsumeCodePayload(params, config)
assertMatchingAppKey(params.appKey)
const now = normalizeTimestampIso(new Date().toISOString())
const normalizedOid = params.oid
const order = await findLatestOrderByPlatformOrderId({
provider: config.provider,
platform: KUISHOU_INDUSTRY_PLATFORM,
platformOrderId: normalizedOid,
})
if (!order) {
return buildIndustryErrorResponse(4012002, `订单不存在: ${normalizedOid}`)
}
const tasks = await listTasksByOrderId(order.id)
const matchedIds = new Set(params.etickets.map((e) => String(e.id)))
let consumedCount = 0
for (const task of tasks) {
const taskId = String(task.id || '')
if (!matchedIds.has(taskId)) {
continue
}
let contextJson: JsonObject = {}
try {
contextJson = typeof task.context_json === 'string'
? JSON.parse(task.context_json)
: (task.context_json || {})
} catch {
contextJson = {}
}
const flow = contextJson.kuaishouCloudFulfillment || {}
const existingConsumes = Array.isArray(flow.consumeDetails)
? contextJson.consumeDetails as JsonObject[]
: Array.isArray(contextJson.consumeDetails)
? contextJson.consumeDetails as JsonObject[]
: []
const consumeDetail = {
serialNum: params.seriallNum,
consumeType: params.consumeType,
consumeTime: params.consumeTime,
appointmentTime: params.appointmentTime || undefined,
storeName: params.storeName || undefined,
storeAddress: params.storeAddress || undefined,
expressCode: params.expressCode || undefined,
expressNo: params.expressNo || undefined,
consumePoiId: params.consumePoiId || undefined,
eticketType: params.eticketType || undefined,
ext: params.ext || undefined,
}
const nextContext = {
...contextJson,
kuaishouCloudFulfillment: {
...flow,
consume: {
...(flow.consume || {}),
status: 'success',
consumedAt: now,
consumeDetail,
},
},
consumeDetails: [...existingConsumes, consumeDetail],
}
const taskStatus = String(task.task_status || '')
const isDispatched = taskStatus === TASK_STATUS.DISPATCHED_PENDING_RETURN
const updatePayload: Record<string, unknown> = {
task_status: isDispatched ? TASK_STATUS.COMPLETED : taskStatus,
result_code: params.status,
result_message: `核销方式: ${params.consumeType}`,
context_json: JSON.stringify(nextContext),
updated_at: now,
}
if (isDispatched) {
updatePayload.delivery_status = 'delivered'
}
await updateTask(taskId, updatePayload as any)
consumedCount++
}
if (consumedCount === 0) {
return buildIndustryErrorResponse(4012005, `未找到匹配的卡券: ${params.etickets.map((e) => e.id).join(',')}`)
}
return buildIndustrySuccessResponse({
oid: normalizedOid,
consumedCount,
})
}
@@ -1,10 +1,11 @@
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
import { updateTask, getTaskById } from '../../../repositories/task-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { normalizeTimestampIso } from '../../../utils/time.js'
import {
KUISHOU_INDUSTRY_PLATFORM,
getKuaishouIndustryConfig,
assertMatchingAppKey,
} from './config.js'
import { normalizeDestroyCodePayload, assertDestroyCodePayload } from './payload.js'
import { assertKuaishouIndustrySignature } from './crypto.js'
@@ -22,6 +23,7 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
const params = normalizeDestroyCodePayload(rawBody)
assertDestroyCodePayload(params, config)
assertMatchingAppKey(params.appKey)
const now = normalizeTimestampIso(new Date().toISOString())
const normalizedOid = params.oid
@@ -125,6 +125,75 @@ export function assertQueryCodePayload(payload: ReturnType<typeof normalizeQuery
assertCommonPayload(payload, config)
}
export function normalizeConsumeCodePayload(raw: JsonObject = {}) {
const param = parseParamField(raw)
return {
appKey: normalizeIndustryString(raw.appkey),
version: normalizeIndustryString(raw.version),
timestamp: normalizeIndustryTimestamp(raw.timestamp),
signMethod: normalizeIndustryString(raw.signMethod || 'MD5'),
sign: normalizeIndustryString(raw.sign),
accessToken: normalizeIndustryString(raw.access_token),
method: normalizeIndustryString(raw.method),
paramRaw: normalizeIndustryString(raw.param),
oid: normalizeIndustryString(param.oid),
etickets: normalizeConsumeEtickets(param.etickets),
status: normalizeIndustryString(param.status),
consumeType: normalizeIndustryString(param.consumeType),
consumeTime: normalizeIndustryLong(param.consumeTime),
storeName: normalizeIndustryString(param.storeName),
storeAddress: normalizeIndustryString(param.storeAddress),
expressNo: normalizeIndustryString(param.expressNo),
expressCode: normalizeIndustryString(param.expressCode),
appointmentTime: normalizeIndustryString(param.appointmentTime),
eticketType: normalizeIndustryString(param.eticketType),
ext: normalizeIndustryString(param.ext),
token: normalizeIndustryString(param.token),
seriallNum: normalizeIndustryString(param.seriallNum),
consumePoiId: normalizeIndustryLong(param.consumePoiId),
}
}
export function assertConsumeCodePayload(payload: ReturnType<typeof normalizeConsumeCodePayload>, config = assertKuaishouIndustryConfig()) {
if (!payload.oid) {
throw createHttpError('缺少 oid', {
statusCode: 400,
errorCode: 'kuaishou_industry_missing_oid',
})
}
if (!Array.isArray(payload.etickets) || payload.etickets.length === 0) {
throw createHttpError('缺少 etickets', {
statusCode: 400,
errorCode: 'kuaishou_industry_missing_etickets',
})
}
if (!payload.status) {
throw createHttpError('缺少 status', {
statusCode: 400,
errorCode: 'kuaishou_industry_missing_status',
})
}
if (!payload.consumeType) {
throw createHttpError('缺少 consumeType', {
statusCode: 400,
errorCode: 'kuaishou_industry_missing_consume_type',
})
}
if (!payload.seriallNum) {
throw createHttpError('缺少 seriallNum', {
statusCode: 400,
errorCode: 'kuaishou_industry_missing_serial_num',
})
}
assertCommonPayload(payload, config)
}
function assertCommonPayload(
payload: { appKey: string, version: string, timestamp: number, signMethod: string, sign: string, method: string, accessToken: string, paramRaw: string },
config = assertKuaishouIndustryConfig(),
@@ -192,3 +261,16 @@ function normalizeDestroyEtickets(value: unknown) {
goodsValue: normalizeIndustryLong(item?.goodsValue),
}))
}
function normalizeConsumeEtickets(value: unknown) {
if (!Array.isArray(value)) {
return []
}
return value.map((item) => ({
id: normalizeIndustryString(item?.id),
code: normalizeIndustryString(item?.code),
num: normalizeIndustryInteger(item?.num),
goodsValue: normalizeIndustryLong(item?.goodsValue),
}))
}
@@ -4,6 +4,7 @@ import type { TaskRow } from '../../../types/repository/rows.js'
import {
KUISHOU_INDUSTRY_PLATFORM,
getKuaishouIndustryConfig,
assertMatchingAppKey,
} from './config.js'
import { normalizeQueryCodePayload, assertQueryCodePayload } from './payload.js'
import { assertKuaishouIndustrySignature } from './crypto.js'
@@ -23,6 +24,7 @@ export async function handleQueryCode(rawBody: JsonObject = {}) {
const params = normalizeQueryCodePayload(rawBody)
assertQueryCodePayload(params, config)
assertMatchingAppKey(params.appKey)
const normalizedOid = params.oid
@@ -8,6 +8,7 @@ import {
KUISHOU_INDUSTRY_PROVIDER,
KUISHOU_INDUSTRY_PLATFORM,
getKuaishouIndustryConfig,
assertMatchingAppKey,
} from './config.js'
import { normalizeSendCodePayload, assertSendCodePayload } from './payload.js'
import { assertKuaishouIndustrySignature } from './crypto.js'
@@ -29,6 +30,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
const params = normalizeSendCodePayload(rawBody)
assertSendCodePayload(params, config)
assertMatchingAppKey(params.appKey)
const now = normalizeTimestampIso(new Date().toISOString())
const normalizedOid = params.oid
+2
View File
@@ -79,7 +79,9 @@ export type RuntimeConfig = {
};
kuaishouIndustry: {
appKey: string;
appSecret: string;
signSecret: string;
messageSecret: string;
provider: string;
platform: string;
shopId: string;
@@ -0,0 +1,59 @@
integration.callback.virtual.eticket.consume
需用户授权
POST
电子凭证核销回调接口
更新时间: 2022-05-11 16:24:16
电子凭证核销回调接口
系统环境
环境 域名
线上环境(推荐) https://openapi.kwaixiaodian.com
线上环境(备用) https://open.kwaixiaodian.com
系统参数
参数名 类型 必须 详情描述
appkey String 是 平台分配的appId
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
version Number 是 请求的API版本号,目前版本为1
param JSON 是 业务参数,详见下方的请求入参
method String 是 请求的API英文名,详见各API定义
sign String 是 API入参的签名结果参数
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
请求参数
参数名 类型 必须 示例值 详情描述
oid String 是 231412412 快手订单号
etickets List<ETicket> 是 [{}] 电子凭证
id String 是 12313 电子凭证ID
code String 否 2131412 电子凭证Code(实体卡的Code
num Integer 是 1 核销数量(订单数量)
goodsValue Long 否 10 销毁的总货值
status String 否 "SUCCESS","FAILED" "SUCCESS","FAILED"
销毁成功/失败
consumeTime Long 否 1543843735000 核销时间(消费者核销的时间)
status String 是 核销状态: 核销成功-CONSUMED 预约成功-APPOINTED 核销状态:
核销成功-CONSUMED
预约成功-APPOINTED
consumeType String 是 门店消费-consume 门店自提-pickup 物流发货-delivery 预约发货-appointment_delivery 核销方式:
默认请使用
consume,其他场景请与产研沟通
storeName String 否 XXX线下店 核销门店名称(门店消费、门店自提时必传)
storeAddress String 否 XXX路CCC号 核销门店地址(门店消费、门店自提时必传)
expressNo String 否 EMS 仅在consumeType为预约发货场景下生效。
物流方式:圆通/韵达/顺丰/EMS(物流发货、预约发货方式必传)
expressCode String 否 12412412 仅在consumeType为预约发货场景下生效。
快递单号(预发发货方式必传)物流发货、预约发货方式必传)
appointmentTime String 否 123124124234523 仅在consumeType为门店自提场景下生效。
预约时间(预约发货方式必传)
eticketType String 否 DINING_OPEN_TICKET/DEFAULT_MEDICAL_TICKET 电子凭证类型,跟商家想要入驻的类目相关联,非必传,默认
DINING_OPEN_TICKET
ext String 否 {"key":"value"} 扩展信息(JSON格式)
token String 是 sdjafljasdlflnlxjlfjasldjfljkaw 鉴权Token,订单操作维度,在发码通知接口中下发
seriallNum String 否 D4F2D111FGG2341GH 核销序列号,对应某一次核销行为,必填!冲正时必须带上该值!
consumePoiId Long 否 657765522 消费门店的poi信息,必须是快手的POI,如果是百度、腾讯等图商的POI,请先用POI转换接口,将图商POI转换成快手POI
返回参数
参数名 类型 示例值 详情描述
code String 1 主返回码
msg String success 主返回信息
sub_code String 1 子返回码
sub_msg String success 子返回信息
result Integer 1 1-成功;其他-失败
error_msg String success 错误信息