395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
import { getOrderItemById } from '../../../repositories/order-item-repo.js'
|
|
import { getOrderById } from '../../../repositories/order-repo.js'
|
|
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
|
import { updateTask } from '../../../repositories/task-repo.js'
|
|
import { createHttpError } from '../../../utils/http.js'
|
|
import { logIntegration } from '../../../utils/logger.js'
|
|
import { parseTaskContext } from '../../../utils/task-json.js'
|
|
import { nowIso } from '../../../utils/time.js'
|
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
|
import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js'
|
|
import type { JsonObject } from '../../../types/json.js'
|
|
import {
|
|
createKuaishouFeifeiOrder,
|
|
queryKuaishouFeifeiOrder,
|
|
} from '../../platforms/kuaishou-feifei/order-service.js'
|
|
import { consumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-service.js'
|
|
import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-industry/voucher-binding-service.js'
|
|
import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js'
|
|
import type { TaskRow } from '../../../types/repository/rows.js'
|
|
|
|
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 existingClaimUrl = resolveKuaishouFeifeiClaimUrl(flow)
|
|
if (flow.orderNo && existingClaimUrl) {
|
|
return updateTask(task.id, {
|
|
task_status: TASK_STATUS.LINK_GENERATED,
|
|
user_action_status: 'pending_claim',
|
|
context_json: JSON.stringify({
|
|
...taskContext,
|
|
kuaishouFeifei: flow,
|
|
}),
|
|
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 platformAmount = await resolveKuaishouFeifeiTaskPlatformAmount(task)
|
|
const orderInput: Parameters<typeof createKuaishouFeifeiOrder>[0] = {
|
|
platformOrderNo,
|
|
productCode: flow.productCode,
|
|
platformBuyNum: 1,
|
|
}
|
|
if (platformAmount > 0) {
|
|
orderInput.platformAmount = platformAmount
|
|
}
|
|
|
|
const order = await createKuaishouFeifeiOrder(orderInput)
|
|
const feifeiClaimUrl = resolveKuaishouFeifeiOrderClaimUrl(order)
|
|
if (!feifeiClaimUrl) {
|
|
throw createHttpError('kuaishou-feifei 未返回领取链接', {
|
|
statusCode: 502,
|
|
errorCode: 'kuaishou_feifei_missing_claim_url',
|
|
})
|
|
}
|
|
|
|
const nextFlow = mergeKuaishouFeifeiOrder(flow, order, {
|
|
platformOrderNo,
|
|
claimUrl: feifeiClaimUrl,
|
|
syncedAt: now,
|
|
})
|
|
const updatedTask = await updateTask(task.id, {
|
|
task_status: TASK_STATUS.LINK_GENERATED,
|
|
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,
|
|
platformAmount,
|
|
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: resolveKuaishouFeifeiOrderClaimUrl(order) || resolveKuaishouFeifeiClaimUrl(flow),
|
|
})
|
|
let nextIndustryVoucher = taskContext.kuaishouIndustryVoucher
|
|
|
|
if (order.rechargeStatus === 30) {
|
|
const consumeResult = await consumeKuaishouIndustryVouchersForTask(task, {
|
|
source: 'kuaishou_feifei_completed',
|
|
consumeTime: Date.now(),
|
|
})
|
|
const consumedVoucher = consumeResult.consumed[0] || null
|
|
|
|
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'
|
|
if (consumedVoucher) {
|
|
nextIndustryVoucher = buildKuaishouIndustryVoucherContext(
|
|
consumedVoucher,
|
|
taskContext.kuaishouIndustryVoucher,
|
|
now,
|
|
)
|
|
}
|
|
logIntegration('[kuaishou-feifei]', '飞飞履约完成,行业电子凭证核销处理完成', {
|
|
taskId: task.id,
|
|
platformOrderNo: nextFlow.platformOrderNo,
|
|
orderNo: nextFlow.orderNo,
|
|
voucherCount: consumeResult.vouchers.length,
|
|
consumedCount: consumeResult.consumed.length,
|
|
consumeStatus: nextFlow.consumeStatus,
|
|
})
|
|
} else {
|
|
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW
|
|
resultCode = 'kuaishou_feifei_industry_consume_failed'
|
|
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
|
lastError = resultMessage
|
|
nextFlow.consumeStatus = 'failed'
|
|
logIntegration(
|
|
'[kuaishou-feifei]',
|
|
'飞飞履约完成,但行业电子凭证核销失败',
|
|
{
|
|
taskId: task.id,
|
|
platformOrderNo: nextFlow.platformOrderNo,
|
|
orderNo: nextFlow.orderNo,
|
|
voucherCount: consumeResult.vouchers.length,
|
|
failedCount: consumeResult.failed.length,
|
|
errorMessage: resultMessage,
|
|
},
|
|
{ level: 'warn' },
|
|
)
|
|
}
|
|
} 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,
|
|
kuaishouIndustryVoucher: nextIndustryVoucher,
|
|
}),
|
|
updated_at: now,
|
|
})
|
|
|
|
return updatedTask || task
|
|
}
|
|
|
|
/** normalizeKuaishouFeifeiFlow 出口类型 */
|
|
export type KuaishouFeifeiFlow = {
|
|
flowType: 'kuaishou_feifei'
|
|
productCode: string
|
|
productName: string
|
|
platformOrderNo: string
|
|
orderNo: string
|
|
rechargeStatus: number
|
|
rechargeStatusLabel: string
|
|
rechargeResultMessage: string
|
|
claimUrl: string
|
|
consumeStatus: string
|
|
shortLink: ReturnType<typeof normalizeShortLinkPayload>
|
|
h5: {
|
|
entryUrl: string
|
|
rechargeUrl: string
|
|
}
|
|
lastSyncedAt: unknown
|
|
raw: unknown
|
|
}
|
|
|
|
export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow {
|
|
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(),
|
|
// 历史字段保留读取,新流程不再生成 shortLink
|
|
shortLink: normalizeShortLinkPayload(source.shortLink),
|
|
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,
|
|
}
|
|
}
|
|
|
|
/** 平台原始 H5 链接(不拼 uid、不用短链)。 */
|
|
export function resolveKuaishouFeifeiClaimUrl(value: unknown) {
|
|
const flow = normalizeKuaishouFeifeiFlow(value)
|
|
return resolveKuaishouFeifeiDirectClaimUrl(flow)
|
|
}
|
|
|
|
/** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */
|
|
export function resolveKuaishouFeifeiH5UrlWithUid(value: unknown, expectedUid?: unknown) {
|
|
const directUrl = resolveKuaishouFeifeiClaimUrl(value)
|
|
if (!directUrl) {
|
|
return ''
|
|
}
|
|
|
|
const uid = String(expectedUid || '').trim()
|
|
if (!uid) {
|
|
return directUrl
|
|
}
|
|
|
|
return appendUidToUrl(directUrl, uid)
|
|
}
|
|
|
|
export function resolveKuaishouFeifeiH5UrlWithTaskUid(
|
|
task: Partial<TaskRow> | null | undefined,
|
|
flowValue?: unknown,
|
|
) {
|
|
const context = parseTaskContext(task)
|
|
const identity = getClaimIdentityFromContext(context)
|
|
const flow = flowValue === undefined ? context.kuaishouFeifei : flowValue
|
|
return resolveKuaishouFeifeiH5UrlWithUid(flow, identity.expectedUid)
|
|
}
|
|
|
|
function mergeKuaishouFeifeiOrder(
|
|
flow: KuaishouFeifeiFlow,
|
|
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,
|
|
shortLink: flow.shortLink,
|
|
h5: {
|
|
entryUrl: order.h5.entryUrl || flow.h5.entryUrl,
|
|
rechargeUrl: order.h5.rechargeUrl || flow.h5.rechargeUrl,
|
|
},
|
|
lastSyncedAt: patch.syncedAt,
|
|
raw: order.raw,
|
|
}
|
|
}
|
|
|
|
function resolveKuaishouFeifeiDirectClaimUrl(flow: KuaishouFeifeiFlow) {
|
|
const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl
|
|
if (h5ClaimUrl) {
|
|
return h5ClaimUrl
|
|
}
|
|
|
|
// 兼容历史 shortLink 中的原始 target
|
|
if (flow.shortLink?.targetUrl) {
|
|
return flow.shortLink.targetUrl
|
|
}
|
|
|
|
return isLocalClaimUrl(flow.claimUrl) ? '' : flow.claimUrl
|
|
}
|
|
|
|
function resolveKuaishouFeifeiOrderClaimUrl(
|
|
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
|
|
) {
|
|
return String(order.h5.rechargeUrl || order.h5.entryUrl || '').trim()
|
|
}
|
|
|
|
function isLocalClaimUrl(value: unknown) {
|
|
const url = String(value || '').trim()
|
|
return !/^https?:\/\//i.test(url) || url.includes('/#/claim/')
|
|
}
|
|
|
|
export function buildFeifeiPlatformOrderNo(task: TaskRow) {
|
|
const platformOrderId = String(task.platform_order_id || '').trim()
|
|
if (platformOrderId && platformOrderId.length <= 64) {
|
|
return platformOrderId
|
|
}
|
|
|
|
const taskNo = String(task.task_no || '').trim()
|
|
if (taskNo && taskNo.length <= 64) {
|
|
return taskNo
|
|
}
|
|
|
|
return `OS-FEIFEI-${task.id}`
|
|
}
|
|
|
|
export async function resolveKuaishouFeifeiTaskPlatformAmount(task: TaskRow) {
|
|
const [order, orderItem] = await Promise.all([
|
|
getOrderById(task.order_id),
|
|
getOrderItemById(task.order_item_id),
|
|
])
|
|
|
|
return resolveKuaishouFeifeiPlatformAmount({
|
|
totalAmountFen: order?.total_amount,
|
|
quantity: orderItem?.quantity,
|
|
})
|
|
}
|
|
|
|
export function resolveKuaishouFeifeiPlatformAmount({
|
|
totalAmountFen,
|
|
quantity,
|
|
}: {
|
|
totalAmountFen?: unknown
|
|
quantity?: unknown
|
|
} = {}) {
|
|
const totalFen = normalizePositiveFen(totalAmountFen)
|
|
if (totalFen <= 0) {
|
|
return 0
|
|
}
|
|
|
|
const normalizedQuantity = Math.max(1, Math.trunc(Number(quantity || 1)) || 1)
|
|
return Number((totalFen / normalizedQuantity / 100).toFixed(2))
|
|
}
|
|
|
|
function normalizePositiveFen(value: unknown) {
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
|
|
}
|