接入多发货平台与电子凭证

This commit is contained in:
yml2213
2026-07-07 12:06:19 +08:00
parent c101b5f359
commit ddfa6c278f
35 changed files with 2627 additions and 365 deletions
@@ -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}`
}