接入多发货平台与电子凭证
This commit is contained in:
@@ -18,6 +18,7 @@ import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsa
|
||||
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js";
|
||||
import { isKuaishouEticketMockTicketCode } from "../../platforms/kuaishou-eticket/mock-ticket-service.js";
|
||||
import { consumeKuaishouIndustryVouchersForTask } from "../../platforms/kuaishou-industry/voucher-service.js";
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
@@ -631,6 +632,15 @@ function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean {
|
||||
const voucher = isPlainObject(value.kuaishouIndustryVoucher)
|
||||
? value.kuaishouIndustryVoucher
|
||||
: {};
|
||||
const voucherCode = String(voucher.voucherCode || voucher.eticketId || "").trim();
|
||||
const status = String(voucher.status || "UNUSED").trim().toUpperCase();
|
||||
return Boolean(voucherCode && status !== "DESTROYED");
|
||||
}
|
||||
|
||||
export function buildDispatchStockItems(
|
||||
deliveryItems: DispatchDeliveryItem[],
|
||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: JsonObject[]; knapsackItems?: JsonObject[] } = {}
|
||||
@@ -794,15 +804,46 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
let nextResultCode = "kuaishou_cloud_completed";
|
||||
let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销";
|
||||
const consumeAlreadyCompleted = flow.consume.status === "success";
|
||||
const isIndustryTask = isIndustryEVoucherTask(task)
|
||||
const hasIndustryVoucher = hasKuaishouIndustryVoucherContext(taskContext);
|
||||
const isIndustryTask = isIndustryEVoucherTask(task);
|
||||
let industryVoucherContextPatch: JsonObject | null = null;
|
||||
|
||||
if (consumeAlreadyCompleted) {
|
||||
consumeStatus = "success";
|
||||
consumedAt = flow.consume.consumedAt || now;
|
||||
} else if (isIndustryTask) {
|
||||
consumeStatus = "success";
|
||||
consumedAt = flow.consume.consumedAt || now;
|
||||
consumeErrorMessage = "行业电子凭证核销由平台回调处理,已跳过主动核销";
|
||||
} else if (hasIndustryVoucher || isIndustryTask) {
|
||||
const industryResult = hasIndustryVoucher
|
||||
? await consumeKuaishouIndustryVouchersForTask(task, {
|
||||
source: String(options.source || "system_auto_finalize").trim() || "system_auto_finalize",
|
||||
token: String(taskContext.kuaishouIndustryVoucher?.token || "").trim(),
|
||||
consumeType: "delivery",
|
||||
consumeTime: Date.now(),
|
||||
})
|
||||
: { ok: true, consumed: [], failed: [] };
|
||||
|
||||
if (industryResult.ok) {
|
||||
consumeStatus = "success";
|
||||
consumedAt = now;
|
||||
const consumedVoucher = industryResult.consumed[0] || null;
|
||||
if (consumedVoucher) {
|
||||
industryVoucherContextPatch = {
|
||||
oid: consumedVoucher.oid,
|
||||
token: consumedVoucher.token,
|
||||
eticketId: consumedVoucher.voucher_code,
|
||||
voucherCode: consumedVoucher.voucher_code,
|
||||
unitIndex: Number(consumedVoucher.unit_index || 0) || 0,
|
||||
status: "CONSUMED",
|
||||
validStartTime: Number(consumedVoucher.valid_start_time || 0) || 0,
|
||||
validEndTime: Number(consumedVoucher.valid_end_time || 0) || 0,
|
||||
consumedAt: now,
|
||||
consumeSerialNum: consumedVoucher.consume_serial_num || `CONSUME-${consumedVoucher.voucher_code}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
consumeStatus = "failed";
|
||||
consumeErrorMessage =
|
||||
industryResult.failed[0]?.errorMessage || "电子凭证核销回调失败,请人工处理";
|
||||
}
|
||||
} else if (!order) {
|
||||
consumeStatus = "failed";
|
||||
consumeErrorMessage = "任务关联订单不存在,无法执行快手核销";
|
||||
@@ -860,6 +901,16 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
...(industryVoucherContextPatch
|
||||
? {
|
||||
kuaishouIndustryVoucher: {
|
||||
...(isPlainObject(taskContext.kuaishouIndustryVoucher)
|
||||
? taskContext.kuaishouIndustryVoucher
|
||||
: {}),
|
||||
...industryVoucherContextPatch,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
returnNumber: {
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { parseTaskContext } from '../../../utils/task-json.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js'
|
||||
import {
|
||||
createKuaishouFeifeiOrder,
|
||||
queryKuaishouFeifeiOrder,
|
||||
} from '../../platforms/kuaishou-feifei/order-service.js'
|
||||
import { consumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-service.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export function isKuaishouFeifeiTask(task: Partial<TaskRow> | null | undefined) {
|
||||
return String(task?.executor_key || '').trim() === 'kuaishou_feifei'
|
||||
}
|
||||
|
||||
export async function prepareKuaishouFeifeiTask(task: TaskRow) {
|
||||
if (!isKuaishouFeifeiTask(task)) {
|
||||
throw createHttpError('当前任务不是 kuaishou-feifei 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_feifei_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouFeifeiFlow(taskContext.kuaishouFeifei)
|
||||
const claimLinkState = await ensureTaskClaimLink(task)
|
||||
|
||||
if (flow.orderNo && flow.h5.rechargeUrl) {
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouFeifei: {
|
||||
...flow,
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
},
|
||||
}
|
||||
return updateTask(task.id, {
|
||||
task_status: TASK_STATUS.LINK_GENERATED,
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || task.claim_expires_at || null,
|
||||
user_action_status: 'pending_claim',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (!flow.productCode) {
|
||||
throw createHttpError('kuaishou-feifei 商品编码缺失,请检查商品规则配置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_feifei_product_code_missing',
|
||||
})
|
||||
}
|
||||
|
||||
const platformOrderNo = flow.platformOrderNo || buildFeifeiPlatformOrderNo(task)
|
||||
const order = await createKuaishouFeifeiOrder({
|
||||
platformOrderNo,
|
||||
productCode: flow.productCode,
|
||||
platformBuyNum: 1,
|
||||
})
|
||||
const nextFlow = mergeKuaishouFeifeiOrder(flow, order, {
|
||||
platformOrderNo,
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
syncedAt: now,
|
||||
})
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.LINK_GENERATED,
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || task.claim_expires_at || null,
|
||||
user_action_status: 'pending_claim',
|
||||
result_code: 'kuaishou_feifei_order_created',
|
||||
result_message: order.rechargeStatusLabel || 'kuaishou-feifei 订单已创建',
|
||||
context_json: JSON.stringify({
|
||||
...taskContext,
|
||||
kuaishouFeifei: nextFlow,
|
||||
}),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_feifei_order_created',
|
||||
{
|
||||
platformOrderNo,
|
||||
orderNo: order.orderNo,
|
||||
productCode: flow.productCode,
|
||||
rechargeStatus: order.rechargeStatus,
|
||||
rechargeStatusLabel: order.rechargeStatusLabel,
|
||||
rechargeUrl: order.h5.rechargeUrl,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
|
||||
if (!isKuaishouFeifeiTask(task)) {
|
||||
return task
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouFeifeiFlow(taskContext.kuaishouFeifei)
|
||||
if (!flow.platformOrderNo && !flow.orderNo) {
|
||||
return task
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const order = await queryKuaishouFeifeiOrder({
|
||||
platformOrderNo: flow.platformOrderNo,
|
||||
orderNo: flow.orderNo,
|
||||
})
|
||||
let nextTaskStatus = task.task_status
|
||||
let deliveryStatus = task.delivery_status
|
||||
let redeemedAt = task.redeemed_at
|
||||
let resultCode = task.result_code
|
||||
let resultMessage = task.result_message
|
||||
let lastError = task.last_error
|
||||
const nextFlow = mergeKuaishouFeifeiOrder(flow, order, {
|
||||
syncedAt: now,
|
||||
claimUrl: flow.claimUrl,
|
||||
})
|
||||
|
||||
if (order.rechargeStatus === 30) {
|
||||
const consumeResult = await consumeKuaishouIndustryVouchersForTask(task, {
|
||||
source: 'kuaishou_feifei_completed',
|
||||
consumeType: 'delivery',
|
||||
consumeTime: Date.now(),
|
||||
})
|
||||
|
||||
if (consumeResult.ok || consumeResult.vouchers.length === 0) {
|
||||
nextTaskStatus = TASK_STATUS.COMPLETED
|
||||
deliveryStatus = 'delivered'
|
||||
redeemedAt = redeemedAt || now
|
||||
resultCode = 'kuaishou_feifei_completed'
|
||||
resultMessage = order.rechargeStatusLabel || 'kuaishou-feifei 履约成功'
|
||||
lastError = ''
|
||||
nextFlow.consumeStatus = consumeResult.vouchers.length > 0 ? 'success' : 'not_required'
|
||||
} else {
|
||||
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW
|
||||
resultCode = 'kuaishou_feifei_industry_consume_failed'
|
||||
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
||||
lastError = resultMessage
|
||||
nextFlow.consumeStatus = 'failed'
|
||||
}
|
||||
} else if ([40, 50, 60].includes(order.rechargeStatus)) {
|
||||
nextTaskStatus = order.rechargeStatus === 60 ? TASK_STATUS.CLOSED : TASK_STATUS.MANUAL_REVIEW
|
||||
resultCode = `kuaishou_feifei_status_${order.rechargeStatus}`
|
||||
resultMessage = order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常'
|
||||
lastError = resultMessage
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: nextTaskStatus,
|
||||
delivery_status: deliveryStatus,
|
||||
redeemed_at: redeemedAt,
|
||||
result_code: resultCode,
|
||||
result_message: resultMessage,
|
||||
last_error: lastError,
|
||||
context_json: JSON.stringify({
|
||||
...taskContext,
|
||||
kuaishouFeifei: nextFlow,
|
||||
}),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
return updatedTask || task
|
||||
}
|
||||
|
||||
export function normalizeKuaishouFeifeiFlow(value: unknown) {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const h5 = source.h5 && typeof source.h5 === 'object' ? source.h5 as JsonObject : {}
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_feifei',
|
||||
productCode: String(source.productCode || '').trim(),
|
||||
productName: String(source.productName || '').trim(),
|
||||
platformOrderNo: String(source.platformOrderNo || '').trim(),
|
||||
orderNo: String(source.orderNo || '').trim(),
|
||||
rechargeStatus: Number(source.rechargeStatus || 0) || 0,
|
||||
rechargeStatusLabel: String(source.rechargeStatusLabel || '').trim(),
|
||||
rechargeResultMessage: String(source.rechargeResultMessage || '').trim(),
|
||||
claimUrl: String(source.claimUrl || '').trim(),
|
||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||
h5: {
|
||||
entryUrl: String(h5.entryUrl || '').trim(),
|
||||
rechargeUrl: String(h5.rechargeUrl || '').trim(),
|
||||
},
|
||||
lastSyncedAt: source.lastSyncedAt || null,
|
||||
raw: source.raw && typeof source.raw === 'object' ? source.raw : null,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeKuaishouFeifeiOrder(
|
||||
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>,
|
||||
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
|
||||
patch: {
|
||||
platformOrderNo?: string
|
||||
claimUrl?: string
|
||||
syncedAt: string
|
||||
},
|
||||
) {
|
||||
return {
|
||||
...flow,
|
||||
productCode: order.productCode || flow.productCode,
|
||||
productName: order.productName || flow.productName,
|
||||
platformOrderNo: order.platformOrderNo || patch.platformOrderNo || flow.platformOrderNo,
|
||||
orderNo: order.orderNo || flow.orderNo,
|
||||
rechargeStatus: order.rechargeStatus,
|
||||
rechargeStatusLabel: order.rechargeStatusLabel,
|
||||
rechargeResultMessage: order.rechargeResultMessage,
|
||||
claimUrl: patch.claimUrl || flow.claimUrl,
|
||||
h5: {
|
||||
entryUrl: order.h5.entryUrl || flow.h5.entryUrl,
|
||||
rechargeUrl: order.h5.rechargeUrl || flow.h5.rechargeUrl,
|
||||
},
|
||||
lastSyncedAt: patch.syncedAt,
|
||||
raw: order.raw,
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTaskClaimLink(task: TaskRow) {
|
||||
const tokenStatus = String(task?.primary_claim_token_status || '').trim()
|
||||
const token = String(task?.primary_claim_token || task?.claim_token || '').trim()
|
||||
const expiredAt = task?.primary_claim_expires_at || task?.claim_expires_at || null
|
||||
|
||||
if (tokenStatus === 'active' && token && expiredAt && new Date(expiredAt).getTime() > Date.now()) {
|
||||
return {
|
||||
token,
|
||||
expiredAt,
|
||||
claimUrl: buildClaimUrl(token),
|
||||
}
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
return {
|
||||
token: claimToken.token,
|
||||
expiredAt: claimToken.expired_at,
|
||||
claimUrl: claimToken.claimUrl,
|
||||
}
|
||||
}
|
||||
|
||||
function buildFeifeiPlatformOrderNo(task: TaskRow) {
|
||||
const taskNo = String(task.task_no || '').trim()
|
||||
if (taskNo && taskNo.length <= 64) {
|
||||
return taskNo
|
||||
}
|
||||
|
||||
return `OS-FEIFEI-${task.id}`
|
||||
}
|
||||
Reference in New Issue
Block a user