From f38efa2edfa173d6014d30a06e2bfd9a2b33c469 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 30 Jun 2026 17:33:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=8C=E4=B8=9A=E7=94=B5=E5=AD=90=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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_* 环境变量配置 --- .env.mac-docker.example | 3 + .env.server.example | 3 + apps/backend/src/config/defaults.ts | 2 + apps/backend/src/config/env-overrides.ts | 10 ++ apps/backend/src/routes/kuaishou-industry.ts | 34 +++++ .../claim/kuaishou-cloud-claim-service.ts | 110 +++++++++++++++- .../fulfillment/kuaishou-cloud/domain.ts | 9 +- .../kuaishou-cloud/task-finalization.ts | 6 + .../platforms/kuaishou-industry/config.ts | 24 ++++ .../kuaishou-industry/consume-code-service.ts | 123 ++++++++++++++++++ .../kuaishou-industry/destroy-code-service.ts | 4 +- .../platforms/kuaishou-industry/payload.ts | 82 ++++++++++++ .../kuaishou-industry/query-code-service.ts | 2 + .../kuaishou-industry/send-code-service.ts | 2 + apps/backend/src/types/runtime-config.ts | 2 + docs/行业电子凭证/4.电子凭证核销回调接口.md | 59 +++++++++ 16 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts create mode 100644 docs/行业电子凭证/4.电子凭证核销回调接口.md diff --git a/.env.mac-docker.example b/.env.mac-docker.example index afce6b92..b6fd3cc1 100644 --- a/.env.mac-docker.example +++ b/.env.mac-docker.example @@ -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 diff --git a/.env.server.example b/.env.server.example index 57b3e7b6..935bef6d 100644 --- a/.env.server.example +++ b/.env.server.example @@ -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 diff --git a/apps/backend/src/config/defaults.ts b/apps/backend/src/config/defaults.ts index 0bfdacaf..e716e680 100644 --- a/apps/backend/src/config/defaults.ts +++ b/apps/backend/src/config/defaults.ts @@ -46,7 +46,9 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig { }, kuaishouIndustry: { appKey: "", + appSecret: "", signSecret: "", + messageSecret: "", provider: "kuaishou-industry", platform: "kuaishou", shopId: "kuaishou-industry", diff --git a/apps/backend/src/config/env-overrides.ts b/apps/backend/src/config/env-overrides.ts index 81dc10bc..4c220f9d 100644 --- a/apps/backend/src/config/env-overrides.ts +++ b/apps/backend/src/config/env-overrides.ts @@ -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", diff --git a/apps/backend/src/routes/kuaishou-industry.ts b/apps/backend/src/routes/kuaishou-industry.ts index b8645ac9..ae9a5348 100644 --- a/apps/backend/src/routes/kuaishou-industry.ts +++ b/apps/backend/src/routes/kuaishou-industry.ts @@ -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 { const query = req.query || {} const body = req.body || {} diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index 487751ef..563564cd 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -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>, + 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 diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts index e1702757..d8318367 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts @@ -5,9 +5,16 @@ export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1' export type JsonObject = Record +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) { diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts index e78be0d1..fbf8ec67 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts @@ -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 = "任务关联订单不存在,无法执行快手核销"; diff --git a/apps/backend/src/services/platforms/kuaishou-industry/config.ts b/apps/backend/src/services/platforms/kuaishou-industry/config.ts index c3b94dfe..ac07e862 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/config.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/config.ts @@ -15,7 +15,9 @@ export function getKuaishouIndustryConfig(overrides: Partial + +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 = { + 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, + }) +} diff --git a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts index 7a1a5780..e385a9f4 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts @@ -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 diff --git a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts index 670cb4c8..651d5efe 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts @@ -125,6 +125,75 @@ export function assertQueryCodePayload(payload: ReturnType, 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), + })) +} diff --git a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts index 9ba9fac4..67c3cf69 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts @@ -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 diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 80e38a8d..6b21a757 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -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 diff --git a/apps/backend/src/types/runtime-config.ts b/apps/backend/src/types/runtime-config.ts index ed1f5eef..28abe92f 100644 --- a/apps/backend/src/types/runtime-config.ts +++ b/apps/backend/src/types/runtime-config.ts @@ -79,7 +79,9 @@ export type RuntimeConfig = { }; kuaishouIndustry: { appKey: string; + appSecret: string; signSecret: string; + messageSecret: string; provider: string; platform: string; shopId: string; diff --git a/docs/行业电子凭证/4.电子凭证核销回调接口.md b/docs/行业电子凭证/4.电子凭证核销回调接口.md new file mode 100644 index 00000000..310ff869 --- /dev/null +++ b/docs/行业电子凭证/4.电子凭证核销回调接口.md @@ -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 是 [{}] 电子凭证 +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 错误信息 \ No newline at end of file