重构订单履约发货流程
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||
import { resolveTaskDeliveryLink } from './delivery-link-service.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
primary_claim_token: 'cloud-token',
|
||||
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
|
||||
}))
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('cloud-token'),
|
||||
expireTime: '2026-07-09T00:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou-industry',
|
||||
claim_token: 'industry-token',
|
||||
claim_expires_at: '2026-07-09T08:00:00.000Z',
|
||||
}))
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('industry-token'),
|
||||
expireTime: '2026-07-09T08:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 为 feifei 任务返回已保存短链', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
shortLink: {
|
||||
code: 'AbCd1234',
|
||||
url: 'https://ks.khhao.com/s/AbCd1234',
|
||||
targetUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: 'https://ks.khhao.com/s/AbCd1234',
|
||||
expireTime: '',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'manual_dispatch',
|
||||
}))
|
||||
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
function createTask(patch: Partial<TaskRow> = {}): TaskRow {
|
||||
return {
|
||||
id: 123,
|
||||
order_id: 456,
|
||||
order_item_id: 789,
|
||||
unit_index: 1,
|
||||
platform_order_id: '2614900602069169',
|
||||
profile_id: 0,
|
||||
task_no: 'DT-test',
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
task_status: 'pending_binding_prepare',
|
||||
delivery_status: 'pending',
|
||||
result_code: '',
|
||||
result_message: '',
|
||||
automation_mode: 'manual',
|
||||
requires_claim: true,
|
||||
user_action_status: 'pending_claim',
|
||||
attempt_count: 0,
|
||||
runtime_session_id: '',
|
||||
login_type: '',
|
||||
nickname: '',
|
||||
role_name: '',
|
||||
role_id: '',
|
||||
area: '',
|
||||
partition_name: '',
|
||||
claim_token: '',
|
||||
primary_claim_token: '',
|
||||
primary_claim_token_id: null,
|
||||
primary_claim_token_status: '',
|
||||
artifacts_json: '{}',
|
||||
context_json: '{}',
|
||||
screenshot_path: '',
|
||||
last_error: '',
|
||||
retry_count: 0,
|
||||
created_at: '2026-07-08T00:00:00.000Z',
|
||||
updated_at: '2026-07-08T00:00:00.000Z',
|
||||
claimed_at: null,
|
||||
role_confirmed_at: null,
|
||||
redeemed_at: null,
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { resolveFulfillmentDeliveryLink } from './executors/registry.js'
|
||||
import type { FulfillmentDeliveryLink } from './executors/types.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export type TaskDeliveryLink = FulfillmentDeliveryLink
|
||||
|
||||
export async function resolveTaskDeliveryLink(
|
||||
task: TaskRow,
|
||||
): Promise<TaskDeliveryLink | null> {
|
||||
return resolveFulfillmentDeliveryLink(task)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { buildClaimUrl } from '../../claim/claim-service.js'
|
||||
import {
|
||||
shouldEnsureKuaishouCloudClaimLink,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { ensureTaskClaimLink } from '../kuaishou-cloud/index.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
type FulfillmentDeliveryLink,
|
||||
type FulfillmentExecutor,
|
||||
type FulfillmentPrepareDeps,
|
||||
} from './types.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
export const kuaishouCloudExecutor: FulfillmentExecutor = {
|
||||
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
||||
preparePaidTask,
|
||||
resolveDeliveryLink,
|
||||
}
|
||||
|
||||
async function preparePaidTask(
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
): Promise<TaskRow | null> {
|
||||
const now = deps.nowIso()
|
||||
|
||||
if (shouldEnsureKuaishouCloudClaimLink(task.task_status)) {
|
||||
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
|
||||
const claimToken = await deps.createTaskClaimToken(task.id)
|
||||
return deps.updateTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
const claimToken = await deps.createTaskClaimToken(task.id)
|
||||
|
||||
return deps.updateTask(task.id, {
|
||||
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: task.last_error || '领取链接已生成,等待客户提交核销码',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
|
||||
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
|
||||
const expireTime = task.primary_claim_expires_at || task.claim_expires_at || ''
|
||||
|
||||
if (primaryToken) {
|
||||
return {
|
||||
claimUrl: buildClaimUrl(primaryToken),
|
||||
expireTime,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const ensured = await ensureTaskClaimLink(task)
|
||||
return {
|
||||
claimUrl: String(ensured.claimUrl || '').trim(),
|
||||
expireTime: ensured.expiredAt || expireTime,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
ensureKuaishouFeifeiClaimShortLink,
|
||||
prepareKuaishouFeifeiTask,
|
||||
resolveKuaishouFeifeiClaimUrl,
|
||||
} from '../kuaishou-feifei/index.js'
|
||||
import { parseTaskContext } from '../../../utils/task-json.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
type FulfillmentDeliveryLink,
|
||||
type FulfillmentExecutor,
|
||||
type FulfillmentPrepareDeps,
|
||||
} from './types.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
export const kuaishouFeifeiExecutor: FulfillmentExecutor = {
|
||||
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
||||
preparePaidTask,
|
||||
resolveDeliveryLink,
|
||||
}
|
||||
|
||||
async function preparePaidTask(
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
): Promise<TaskRow | null> {
|
||||
try {
|
||||
return await prepareKuaishouFeifeiTask(task)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'kuaishou-feifei 订单创建失败'
|
||||
const updatedTask = await deps.updateTask(task.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
last_error: message,
|
||||
result_code: 'kuaishou_feifei_prepare_failed',
|
||||
result_message: message,
|
||||
updated_at: deps.nowIso(),
|
||||
})
|
||||
await deps.notifyTaskAutoManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: message,
|
||||
source: 'kuaishou_feifei_prepare_failed',
|
||||
})
|
||||
return updatedTask
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
|
||||
let claimUrl = await ensureKuaishouFeifeiClaimShortLink(task)
|
||||
if (!claimUrl) {
|
||||
const context = parseTaskContext(task)
|
||||
claimUrl = resolveKuaishouFeifeiClaimUrl(context.kuaishouFeifei)
|
||||
}
|
||||
|
||||
if (!claimUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
claimUrl,
|
||||
expireTime: '',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
type FulfillmentExecutor,
|
||||
type FulfillmentPrepareDeps,
|
||||
} from './types.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
export const manualDispatchExecutor: FulfillmentExecutor = {
|
||||
key: FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH,
|
||||
preparePaidTask,
|
||||
}
|
||||
|
||||
async function preparePaidTask(
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
): Promise<TaskRow | null> {
|
||||
const lastError = task.last_error || '当前任务需要人工履约处理'
|
||||
const updatedTask = await deps.updateTask(task.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
last_error: lastError,
|
||||
updated_at: deps.nowIso(),
|
||||
})
|
||||
|
||||
await deps.notifyTaskAutoManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'manual_dispatch_profile',
|
||||
})
|
||||
|
||||
return updatedTask
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { isPaidPreparationStableStatus } from '../../../domain/task-status.js'
|
||||
import { kuaishouCloudExecutor } from './kuaishou-cloud-executor.js'
|
||||
import { kuaishouFeifeiExecutor } from './kuaishou-feifei-executor.js'
|
||||
import { manualDispatchExecutor } from './manual-executor.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
isManualDispatchExecutor,
|
||||
normalizeExecutorKey,
|
||||
type FulfillmentDeliveryLink,
|
||||
type FulfillmentExecutor,
|
||||
type FulfillmentPrepareDeps,
|
||||
} from './types.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
const EXECUTORS = new Map<string, FulfillmentExecutor>([
|
||||
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD, kuaishouCloudExecutor],
|
||||
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY, kuaishouCloudExecutor],
|
||||
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI, kuaishouFeifeiExecutor],
|
||||
[FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH, manualDispatchExecutor],
|
||||
])
|
||||
|
||||
export function getFulfillmentExecutor(executorKey: unknown): FulfillmentExecutor | null {
|
||||
return EXECUTORS.get(normalizeExecutorKey(executorKey)) || null
|
||||
}
|
||||
|
||||
export async function preparePaidFulfillmentTask(
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
): Promise<TaskRow | null> {
|
||||
if (isPaidPreparationStableStatus(task.task_status)) {
|
||||
return task
|
||||
}
|
||||
|
||||
const executor = getFulfillmentExecutor(task.executor_key)
|
||||
if (executor?.preparePaidTask) {
|
||||
return executor.preparePaidTask(task, deps)
|
||||
}
|
||||
|
||||
if (!task.requires_claim || isManualDispatchExecutor(task.executor_key)) {
|
||||
return manualDispatchExecutor.preparePaidTask?.(task, deps) || task
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
export async function resolveFulfillmentDeliveryLink(
|
||||
task: TaskRow,
|
||||
): Promise<FulfillmentDeliveryLink | null> {
|
||||
const executor = getFulfillmentExecutor(task.executor_key)
|
||||
if (!executor?.resolveDeliveryLink) {
|
||||
return null
|
||||
}
|
||||
|
||||
return executor.resolveDeliveryLink(task)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { TaskUpdatePatch } from '../../../types/repository/inputs.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
export const FULFILLMENT_EXECUTOR_KEYS = {
|
||||
MANUAL_DISPATCH: 'manual_dispatch',
|
||||
KUAISHOU_CLOUD: 'kuaishou_ct_assisted',
|
||||
KUAISHOU_INDUSTRY: 'kuaishou-industry',
|
||||
KUAISHOU_FEIFEI: 'kuaishou_feifei',
|
||||
} as const
|
||||
|
||||
export type FulfillmentExecutorKey =
|
||||
(typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {})
|
||||
|
||||
export type FulfillmentDeliveryLink = {
|
||||
claimUrl: string
|
||||
expireTime?: unknown
|
||||
}
|
||||
|
||||
export type FulfillmentPrepareDeps = {
|
||||
updateTask: (taskId: number | string, patch: TaskUpdatePatch) => Promise<TaskRow | null>
|
||||
createTaskClaimToken: (taskId: number | string) => Promise<{
|
||||
token: string
|
||||
expired_at: string
|
||||
[key: string]: unknown
|
||||
}>
|
||||
notifyTaskAutoManualReview: (payload: {
|
||||
task: unknown
|
||||
reason: string
|
||||
source: string
|
||||
}) => Promise<unknown> | unknown
|
||||
nowIso: () => string
|
||||
}
|
||||
|
||||
export type FulfillmentExecutor = {
|
||||
key: FulfillmentExecutorKey
|
||||
preparePaidTask?: (
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
) => Promise<TaskRow | null>
|
||||
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
|
||||
}
|
||||
|
||||
export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {
|
||||
return String(value || '').trim() as FulfillmentExecutorKey
|
||||
}
|
||||
|
||||
export function isKuaishouCloudExecutor(value: unknown): boolean {
|
||||
const executorKey = normalizeExecutorKey(value)
|
||||
return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY
|
||||
}
|
||||
|
||||
export function isKuaishouFeifeiExecutor(value: unknown): boolean {
|
||||
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
|
||||
}
|
||||
|
||||
export function isManualDispatchExecutor(value: unknown): boolean {
|
||||
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { normalizeProductName } from "../../order/product-match-service.js";
|
||||
import { normalizeProductName } from "../product-resolution-service.js";
|
||||
import {
|
||||
appointCloudtentaclesVirtualNumber,
|
||||
backCloudtentaclesVirtualNumber,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
ORDER_FULFILLMENT_BLOCK_REASONS,
|
||||
resolveOrderFulfillmentReadiness,
|
||||
} from './order-fulfillment-readiness-service.js'
|
||||
import type { KuaishouIndustryVoucherRow } from '../../types/repository/rows.js'
|
||||
|
||||
test('resolveOrderFulfillmentReadiness 无电子凭证时允许继续履约', async () => {
|
||||
const readiness = await resolveOrderFulfillmentReadiness(
|
||||
{ platform_order_id: '2614900602069169' },
|
||||
{
|
||||
listKuaishouIndustryVouchersByOid: async () => [],
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(readiness, {
|
||||
allowed: true,
|
||||
voucherCount: 0,
|
||||
reason: '',
|
||||
blockReason: '',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveOrderFulfillmentReadiness 电子凭证发码成功时允许继续履约', async () => {
|
||||
const readiness = await resolveOrderFulfillmentReadiness(
|
||||
{ platform_order_id: '2614900602069169' },
|
||||
{
|
||||
listKuaishouIndustryVouchersByOid: async () => [
|
||||
createVoucher({ send_callback_status: 'success' }),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(readiness, {
|
||||
allowed: true,
|
||||
voucherCount: 1,
|
||||
reason: '',
|
||||
blockReason: '',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveOrderFulfillmentReadiness 电子凭证发码未确认时阻止履约', async () => {
|
||||
const readiness = await resolveOrderFulfillmentReadiness(
|
||||
{ platform_order_id: '2614900602069169' },
|
||||
{
|
||||
listKuaishouIndustryVouchersByOid: async () => [
|
||||
createVoucher({
|
||||
send_callback_status: 'pending',
|
||||
send_callback_last_error: '回调等待中',
|
||||
}),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(readiness, {
|
||||
allowed: false,
|
||||
voucherCount: 1,
|
||||
reason: '回调等待中',
|
||||
blockReason: ORDER_FULFILLMENT_BLOCK_REASONS.KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED,
|
||||
})
|
||||
})
|
||||
|
||||
function createVoucher(patch: Partial<KuaishouIndustryVoucherRow> = {}): KuaishouIndustryVoucherRow {
|
||||
return {
|
||||
id: 1,
|
||||
voucher_code: 'ETICKET-1',
|
||||
oid: '2614900602069169',
|
||||
order_id: null,
|
||||
task_id: null,
|
||||
unit_index: 1,
|
||||
seller_id: '',
|
||||
token: '',
|
||||
status: 'UNUSED',
|
||||
valid_start_time: 0,
|
||||
valid_end_time: 0,
|
||||
consume_serial_num: '',
|
||||
consume_details_json: [],
|
||||
consumed_at: null,
|
||||
destroyed_at: null,
|
||||
send_callback_status: 'success',
|
||||
send_callback_attempt_count: 0,
|
||||
send_callback_last_error: '',
|
||||
send_callback_response_json: {},
|
||||
send_callback_sent_at: null,
|
||||
raw_payload_json: {},
|
||||
created_at: '2026-07-08T00:00:00.000Z',
|
||||
updated_at: '2026-07-08T00:00:00.000Z',
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import {
|
||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||
resolveKuaishouIndustryVoucherSendCallbackMessage,
|
||||
} from '../platforms/kuaishou-industry/voucher-service.js'
|
||||
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export const ORDER_FULFILLMENT_BLOCK_REASONS = {
|
||||
KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED: 'kuaishou_industry_send_callback_unconfirmed',
|
||||
} as const
|
||||
|
||||
type FulfillmentReadinessDeps = {
|
||||
listKuaishouIndustryVouchersByOid?: typeof listKuaishouIndustryVouchersByOid
|
||||
}
|
||||
|
||||
export async function resolveOrderFulfillmentReadiness(
|
||||
order: Pick<OrderRow, 'platform_order_id'> | null | undefined,
|
||||
deps: FulfillmentReadinessDeps = {},
|
||||
) {
|
||||
const listVouchers = deps.listKuaishouIndustryVouchersByOid || listKuaishouIndustryVouchersByOid
|
||||
const oid = String(order?.platform_order_id || '').trim()
|
||||
if (!oid) {
|
||||
return {
|
||||
allowed: true,
|
||||
voucherCount: 0,
|
||||
reason: '',
|
||||
blockReason: '',
|
||||
}
|
||||
}
|
||||
|
||||
const vouchers = await listVouchers(oid)
|
||||
if (vouchers.length === 0 || vouchers.every(isKuaishouIndustryVoucherSendCallbackSuccess)) {
|
||||
return {
|
||||
allowed: true,
|
||||
voucherCount: vouchers.length,
|
||||
reason: '',
|
||||
blockReason: '',
|
||||
}
|
||||
}
|
||||
|
||||
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
voucherCount: vouchers.length,
|
||||
reason: blockedVoucher
|
||||
? resolveKuaishouIndustryVoucherSendCallbackMessage(blockedVoucher)
|
||||
: '电子凭证发码回调未确认',
|
||||
blockReason: ORDER_FULFILLMENT_BLOCK_REASONS.KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED,
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncOrderFulfillmentAttachments(
|
||||
order: OrderRow,
|
||||
tasks: TaskRow[] = [],
|
||||
options: {
|
||||
source?: string
|
||||
now?: string
|
||||
} = {},
|
||||
): Promise<KuaishouIndustryVoucherRow[]> {
|
||||
return bindKuaishouIndustryVouchersToOrderTasks(order, tasks, options)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { planFulfillmentTaskForOrderItem } from './planner.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
|
||||
test('planFulfillmentTaskForOrderItem 为 cloudtentacles 商品生成 cloud 履约计划', async () => {
|
||||
const plan = await planFulfillmentTaskForOrderItem({
|
||||
order: createOrder(),
|
||||
item: createOrderItem({
|
||||
sku_code: '荣耀勋章礼包(30个)',
|
||||
sku_name: '荣耀勋章礼包(30个)',
|
||||
item_snapshot_json: {
|
||||
cloudtentacles: {
|
||||
matchMode: 'cloudtentacles_name',
|
||||
normalizedProductName: '荣耀勋章礼包(30个)',
|
||||
cloudSourceKeys: ['account-a'],
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '荣耀勋章礼包(30个)',
|
||||
},
|
||||
},
|
||||
}),
|
||||
getProfileByKey: async (profileKey) => ({
|
||||
id: 9,
|
||||
profile_key: profileKey,
|
||||
name: '快手 cloud 履约',
|
||||
executor_key: profileKey,
|
||||
requires_claim: true,
|
||||
auto_dispatch: true,
|
||||
}),
|
||||
})
|
||||
|
||||
assert.ok(plan)
|
||||
assert.equal(plan.executorKey, 'kuaishou_ct_assisted')
|
||||
assert.equal(plan.requiresClaim, false)
|
||||
assert.equal(plan.autoDispatch, false)
|
||||
assert.equal(plan.context.profileKey, 'kuaishou_ct_assisted')
|
||||
assert.equal(
|
||||
(plan.context.kuaishouCloudFulfillment as any).binding.resolvedSourceKey,
|
||||
'account-a',
|
||||
)
|
||||
assert.equal((plan.context.kuaishouCloudFulfillment as any).binding.skuId, 74)
|
||||
assert.equal(plan.context.kuaishouFeifei, null)
|
||||
})
|
||||
|
||||
test('planFulfillmentTaskForOrderItem 为 feifei 商品生成 feifei 履约计划', async () => {
|
||||
const plan = await planFulfillmentTaskForOrderItem({
|
||||
order: createOrder(),
|
||||
item: createOrderItem({
|
||||
sku_code: 'FF-1001',
|
||||
sku_name: '测试皮肤',
|
||||
item_snapshot_json: {
|
||||
kuaishouFeifei: {
|
||||
productCode: 'FF-1001',
|
||||
skuName: '测试皮肤',
|
||||
matchMode: 'kuaishou_feifei_name',
|
||||
},
|
||||
},
|
||||
}),
|
||||
getProfileByKey: async (profileKey) => ({
|
||||
id: 10,
|
||||
profile_key: profileKey,
|
||||
name: 'kuaishou-feifei 履约',
|
||||
executor_key: profileKey,
|
||||
requires_claim: false,
|
||||
auto_dispatch: true,
|
||||
}),
|
||||
})
|
||||
|
||||
assert.ok(plan)
|
||||
assert.equal(plan.executorKey, 'kuaishou_feifei')
|
||||
assert.equal(plan.requiresClaim, true)
|
||||
assert.equal(plan.autoDispatch, false)
|
||||
assert.equal(plan.context.kuaishouCloudFulfillment, null)
|
||||
assert.equal((plan.context.kuaishouFeifei as any).productCode, 'FF-1001')
|
||||
assert.equal((plan.context.kuaishouFeifei as any).productName, '测试皮肤')
|
||||
})
|
||||
|
||||
test('planFulfillmentTaskForOrderItem 未命中平台配置时返回 null', async () => {
|
||||
const plan = await planFulfillmentTaskForOrderItem({
|
||||
order: createOrder(),
|
||||
item: createOrderItem({
|
||||
item_snapshot_json: {
|
||||
cloudtentacles: null,
|
||||
kuaishouFeifei: null,
|
||||
},
|
||||
}),
|
||||
getProfileByKey: async () => null,
|
||||
})
|
||||
|
||||
assert.equal(plan, null)
|
||||
})
|
||||
|
||||
function createOrder(patch: Partial<OrderRow> = {}): OrderRow {
|
||||
return {
|
||||
id: 1,
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shop_id: '91kaquan',
|
||||
shop_name: '91卡券',
|
||||
platform_order_id: '2614900602069169',
|
||||
order_status: 'paid',
|
||||
pay_status: 'paid',
|
||||
buyer_id: '',
|
||||
buyer_name: '',
|
||||
receiver_contact: '',
|
||||
total_amount: 0,
|
||||
currency: 'CNY',
|
||||
raw_payload_json: '{}',
|
||||
paid_at: '2026-07-08T00:00:00.000Z',
|
||||
created_at: '2026-07-08T00:00:00.000Z',
|
||||
updated_at: '2026-07-08T00:00:00.000Z',
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
|
||||
function createOrderItem(patch: Partial<OrderItemRow> = {}): OrderItemRow {
|
||||
return {
|
||||
id: 10,
|
||||
order_id: 1,
|
||||
sku_code: 'SKU-1',
|
||||
sku_name: '测试商品',
|
||||
quantity: 1,
|
||||
spec_json: '{}',
|
||||
item_snapshot_json: {},
|
||||
created_at: '2026-07-08T00:00:00.000Z',
|
||||
updated_at: '2026-07-08T00:00:00.000Z',
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
isKuaishouCloudExecutor,
|
||||
isKuaishouFeifeiExecutor,
|
||||
} from './executors/types.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
export type FulfillmentBindingLike = {
|
||||
id: number
|
||||
profile_id?: number
|
||||
profile_key?: string
|
||||
profile_name?: string
|
||||
name?: string
|
||||
executor_key?: string
|
||||
requires_claim?: boolean
|
||||
auto_dispatch?: boolean
|
||||
config_json?: string | JsonObject
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type FulfillmentProfileResolver = (
|
||||
profileKey: string,
|
||||
) => Promise<FulfillmentBindingLike | null>
|
||||
|
||||
export type FulfillmentTaskPlan = {
|
||||
profile: FulfillmentBindingLike
|
||||
profileId: number
|
||||
profileKey: string
|
||||
profileName: string
|
||||
executorKey: string
|
||||
requiresClaim: boolean
|
||||
autoDispatch: boolean
|
||||
context: JsonObject
|
||||
}
|
||||
|
||||
export async function planFulfillmentTaskForOrderItem({
|
||||
order,
|
||||
item,
|
||||
getProfileByKey = getFulfillmentProfileByKey,
|
||||
}: {
|
||||
order: OrderRow
|
||||
item: OrderItemRow
|
||||
getProfileByKey?: FulfillmentProfileResolver
|
||||
}): Promise<FulfillmentTaskPlan | null> {
|
||||
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
|
||||
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profileId = Number(profile.profile_id || profile.id || 0)
|
||||
const profileKey = String(profile.profile_key || '').trim()
|
||||
const profileName = String(profile.profile_name || profile.name || '').trim()
|
||||
const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
|
||||
.trim()
|
||||
|| FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const context = buildFulfillmentTaskContext({
|
||||
order,
|
||||
item,
|
||||
profile,
|
||||
profileKey,
|
||||
profileName,
|
||||
executorKey,
|
||||
fulfillmentConfig,
|
||||
})
|
||||
|
||||
return {
|
||||
profile,
|
||||
profileId,
|
||||
profileKey,
|
||||
profileName,
|
||||
executorKey,
|
||||
requiresClaim: Boolean(profile.requires_claim),
|
||||
autoDispatch: Boolean(profile.auto_dispatch),
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function buildFulfillmentTaskContext({
|
||||
order,
|
||||
item,
|
||||
profileKey,
|
||||
profileName,
|
||||
executorKey,
|
||||
fulfillmentConfig,
|
||||
}: {
|
||||
order: OrderRow
|
||||
item: OrderItemRow
|
||||
profile: FulfillmentBindingLike
|
||||
profileKey: string
|
||||
profileName: string
|
||||
executorKey: string
|
||||
fulfillmentConfig: JsonObject
|
||||
}) {
|
||||
const itemSnapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
||||
const kuaishouFeifeiConfig = parseJsonObject(fulfillmentConfig.kuaishouFeifei)
|
||||
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
|
||||
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
|
||||
const cloudSourceKeys = normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys)
|
||||
const resolvedCloudSourceKey =
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentaclesConfig.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: ''
|
||||
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
|
||||
const primaryDeliveryItem = deliveryItems[0] || {
|
||||
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
|
||||
cloudSkuName: String(cloudtentaclesConfig.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
return {
|
||||
profileKey,
|
||||
profileName,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
kuaishouCloudFulfillment: isKuaishouCloudExecutor(executorKey)
|
||||
? {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(fulfillmentConfig.configId || '').trim(),
|
||||
internalSkuCode: item.sku_code,
|
||||
internalSkuName: item.sku_name,
|
||||
deliveryItems,
|
||||
ticket: {
|
||||
code: '',
|
||||
status: 'pending',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
verifiedAt: null,
|
||||
oid: '',
|
||||
formToken: '',
|
||||
leftCount: 0,
|
||||
goodsTitle: '',
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
cloudSourceKeys,
|
||||
resolvedSourceKey: resolvedCloudSourceKey,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
vnKey: '1',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
bindExpiresAt: null,
|
||||
bindProbeAt: null,
|
||||
bindProbeStatus: '',
|
||||
bindProbeMessage: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: false,
|
||||
purchaseTriggered: false,
|
||||
assetBefore: 0,
|
||||
assetAfter: 0,
|
||||
purchaseAt: null,
|
||||
},
|
||||
dispatch: {
|
||||
status: 'pending',
|
||||
dispatchAt: null,
|
||||
dispatchBy: null,
|
||||
sendType: 0,
|
||||
note: '',
|
||||
},
|
||||
returnNumber: {
|
||||
status: 'pending',
|
||||
returnedAt: null,
|
||||
returnedBy: null,
|
||||
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(
|
||||
kuaishouConsumeConfig.shopId ||
|
||||
kuaishouShopConfig.shopId ||
|
||||
itemSnapshot.shopId ||
|
||||
order.shop_id ||
|
||||
'',
|
||||
).trim(),
|
||||
shopName: String(
|
||||
kuaishouConsumeConfig.shopName ||
|
||||
kuaishouShopConfig.kshopName ||
|
||||
itemSnapshot.shopName ||
|
||||
order.shop_name ||
|
||||
'',
|
||||
).trim(),
|
||||
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
: null,
|
||||
kuaishouFeifei: isKuaishouFeifeiExecutor(executorKey)
|
||||
? {
|
||||
flowType: 'kuaishou_feifei',
|
||||
productCode: String(kuaishouFeifeiConfig.productCode || '').trim(),
|
||||
productName: String(kuaishouFeifeiConfig.productName || item.sku_name || '').trim(),
|
||||
platformOrderNo: '',
|
||||
orderNo: '',
|
||||
rechargeStatus: 0,
|
||||
rechargeStatusLabel: '',
|
||||
rechargeResultMessage: '',
|
||||
claimUrl: '',
|
||||
consumeStatus: 'pending',
|
||||
h5: {
|
||||
entryUrl: '',
|
||||
rechargeUrl: '',
|
||||
},
|
||||
lastSyncedAt: null,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDynamicFulfillmentProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: FulfillmentProfileResolver,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
return (
|
||||
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveDynamicCloudtentaclesProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: FulfillmentProfileResolver,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
|
||||
const cloudSourceKeys = normalizeStringArray(cloudtentacles.cloudSourceKeys)
|
||||
const deliveryItems = normalizeCloudDeliveryItems({
|
||||
deliveryItems: cloudtentacles.deliveryItems,
|
||||
skuId: cloudtentacles.cloudSkuId,
|
||||
skuName: cloudtentacles.cloudSkuName || item.sku_name,
|
||||
})
|
||||
const primaryDeliveryItem = deliveryItems[0] || {
|
||||
cloudSkuId: 0,
|
||||
cloudSkuName: '',
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
if (
|
||||
!primaryDeliveryItem.cloudSkuId ||
|
||||
!primaryDeliveryItem.cloudSkuName ||
|
||||
cloudSourceKeys.length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = await getProfileByKey(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
profile_id: Number(profile.id || profile.profile_id || 0),
|
||||
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
||||
profile_name: String(profile.profile_name || profile.name || '快手 cloud 履约').trim(),
|
||||
executor_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
||||
requires_claim: false,
|
||||
auto_dispatch: false,
|
||||
config_json: {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: `${String(cloudtentacles.matchMode || 'cloudtentacles_name').trim()}:${String(cloudtentacles.normalizedProductName || primaryDeliveryItem.cloudSkuId).trim()}`,
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
resolvedSourceKey:
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentacles.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: '',
|
||||
deliveryItems,
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: true,
|
||||
},
|
||||
kuaishouConsume: {
|
||||
shopId: String(snapshot.shopId || '').trim(),
|
||||
shopName: String(snapshot.shopName || '').trim(),
|
||||
autoConsumeAfterDispatch: false,
|
||||
},
|
||||
notes:
|
||||
cloudtentacles.matchMode === 'cloudtentacles_override'
|
||||
? '91卡券商品名命中 cloudtentacles 覆盖规则'
|
||||
: '91卡券商品名自动匹配 cloudtentacles 商品',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDynamicKuaishouFeifeiProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: FulfillmentProfileResolver,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const feifei = parseJsonObject(snapshot.kuaishouFeifei)
|
||||
const productCode = String(feifei.productCode || '').trim()
|
||||
if (!productCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = await getProfileByKey(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
profile_id: Number(profile.id || profile.profile_id || 0),
|
||||
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
||||
profile_name: String(profile.profile_name || profile.name || 'kuaishou-feifei 履约').trim(),
|
||||
executor_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
||||
requires_claim: true,
|
||||
auto_dispatch: false,
|
||||
config_json: {
|
||||
flowType: 'kuaishou_feifei',
|
||||
configId: `kuaishou_feifei:${productCode}`,
|
||||
kuaishouFeifei: {
|
||||
productCode,
|
||||
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
|
||||
matchMode: String(feifei.matchMode || 'kuaishou_feifei_name').trim(),
|
||||
},
|
||||
notes: '91卡券商品名自动匹配 kuaishou-feifei 商品映射',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(value: unknown): JsonObject {
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as JsonObject)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}> {
|
||||
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
|
||||
const items = rawItems
|
||||
.map((item) => normalizeCloudDeliveryItem(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeCloudDeliveryItems(items)
|
||||
}
|
||||
|
||||
const cloudSkuId = Number(value.skuId || 0) || 0
|
||||
if (!cloudSkuId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(value.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItem(value: unknown) {
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const quantity = Number(source.quantity || 1) || 1
|
||||
return {
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
||||
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeCloudDeliveryItems(
|
||||
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
|
||||
) {
|
||||
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = merged.get(item.cloudSkuId)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity
|
||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
|
||||
continue
|
||||
}
|
||||
|
||||
merged.set(item.cloudSkuId, { ...item })
|
||||
}
|
||||
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
normalizeCloudtentaclesMatchName,
|
||||
resolveCloudtentaclesSkuByProductName,
|
||||
type CloudtentaclesNameMatchResult,
|
||||
} from '../order/cloudtentacles-name-match-service.js'
|
||||
import {
|
||||
resolveKuaishouFeifeiProductByName,
|
||||
type KuaishouFeifeiProductMatch,
|
||||
} from '../platforms/kuaishou-feifei/product-rule-service.js'
|
||||
|
||||
export type FulfillmentItem = {
|
||||
itemId?: string
|
||||
externalItemId?: string
|
||||
skuCode?: string
|
||||
skuName?: string
|
||||
externalSkuCode?: string
|
||||
externalSkuName?: string
|
||||
quantity?: number
|
||||
spec?: Record<string, unknown>
|
||||
snapshot?: Record<string, unknown>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ResolveOrderItemForFulfillmentInput = {
|
||||
provider?: string
|
||||
platform?: string
|
||||
item?: FulfillmentItem
|
||||
}
|
||||
|
||||
type HasConfiguredOrderItemsInput = Omit<ResolveOrderItemForFulfillmentInput, 'item'> & {
|
||||
items?: FulfillmentItem[]
|
||||
}
|
||||
|
||||
type FulfillmentItemCandidate = {
|
||||
externalItemId: string
|
||||
externalSkuCode: string
|
||||
externalSkuName: string
|
||||
externalSkuNameNormalized: string
|
||||
resolvedSkuCode: string
|
||||
cloudtentaclesNameMatch: CloudtentaclesNameMatchResult | null
|
||||
kuaishouFeifeiMatch: KuaishouFeifeiProductMatch | null
|
||||
isConfigured: boolean
|
||||
}
|
||||
|
||||
export type ResolvedFulfillmentItem = FulfillmentItem & {
|
||||
itemId: string
|
||||
externalItemId: string
|
||||
externalSkuCode: string
|
||||
externalSkuName: string
|
||||
skuCode: string
|
||||
skuName: string
|
||||
snapshot: Record<string, unknown>
|
||||
isConfigured: boolean
|
||||
}
|
||||
|
||||
export async function resolveOrderItemForFulfillment({
|
||||
provider = '',
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<ResolvedFulfillmentItem> {
|
||||
const candidate = await resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})
|
||||
const {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
cloudtentaclesNameMatch,
|
||||
kuaishouFeifeiMatch,
|
||||
} = candidate
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.productCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuName = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.skuName,
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
])
|
||||
const snapshot = {
|
||||
...(isPlainObject(item.snapshot) ? item.snapshot : {}),
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
matchMode: cloudtentaclesNameMatch?.matchMode || '',
|
||||
cloudtentacles: cloudtentaclesNameMatch
|
||||
? {
|
||||
matchMode: cloudtentaclesNameMatch.matchMode,
|
||||
productName: cloudtentaclesNameMatch.productName,
|
||||
normalizedProductName: cloudtentaclesNameMatch.normalizedProductName,
|
||||
cloudSkuId: cloudtentaclesNameMatch.cloudSkuId,
|
||||
cloudSkuName: cloudtentaclesNameMatch.cloudSkuName,
|
||||
cloudSkuPrice: cloudtentaclesNameMatch.cloudSkuPrice,
|
||||
cloudSkuInventory: cloudtentaclesNameMatch.cloudSkuInventory,
|
||||
cloudSourceKeys: cloudtentaclesNameMatch.cloudSourceKeys,
|
||||
resolvedSourceKey: cloudtentaclesNameMatch.resolvedSourceKey,
|
||||
deliveryItems: cloudtentaclesNameMatch.deliveryItems,
|
||||
skuSnapshot: cloudtentaclesNameMatch.skuSnapshot,
|
||||
}
|
||||
: null,
|
||||
kuaishouFeifei: kuaishouFeifeiMatch
|
||||
? {
|
||||
matchMode: kuaishouFeifeiMatch.matchMode,
|
||||
productName: kuaishouFeifeiMatch.productName,
|
||||
normalizedProductName: kuaishouFeifeiMatch.normalizedProductName,
|
||||
productCode: kuaishouFeifeiMatch.productCode,
|
||||
skuName: kuaishouFeifeiMatch.skuName,
|
||||
}
|
||||
: null,
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
itemId: externalItemId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
skuCode: resolvedSkuCode,
|
||||
skuName: resolvedSkuName,
|
||||
snapshot,
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasConfiguredOrderItems({
|
||||
provider = '',
|
||||
platform = '',
|
||||
items = [],
|
||||
}: HasConfiguredOrderItemsInput): Promise<boolean> {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
|
||||
return candidates.some((item) => item.isConfigured)
|
||||
}
|
||||
|
||||
export function normalizeProductName(value: unknown): string {
|
||||
return normalizeCloudtentaclesMatchName(value)
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
async function resolveConfiguredItemCandidate({
|
||||
provider = '',
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
item.externalItemId,
|
||||
item.itemId,
|
||||
])
|
||||
const externalSkuCode = pickFirstNonEmpty([
|
||||
item.externalSkuCode,
|
||||
item.skuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const externalSkuName = pickFirstNonEmpty([
|
||||
item.externalSkuName,
|
||||
item.skuName,
|
||||
externalSkuCode,
|
||||
])
|
||||
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
|
||||
|
||||
if (isOpen91KuaishouOrder(provider, platform)) {
|
||||
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
|
||||
const kuaishouFeifeiMatch = cloudtentaclesNameMatch
|
||||
? null
|
||||
: resolveKuaishouFeifeiProductByName(externalSkuName)
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.productCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
cloudtentaclesNameMatch,
|
||||
kuaishouFeifeiMatch,
|
||||
isConfigured: Boolean(cloudtentaclesNameMatch || kuaishouFeifeiMatch),
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
cloudtentaclesNameMatch: null,
|
||||
kuaishouFeifeiMatch: null,
|
||||
isConfigured: false,
|
||||
}
|
||||
}
|
||||
|
||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||
return String(provider || '').trim() === '91kaquan' &&
|
||||
String(platform || '').trim() === 'kuaishou'
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
||||
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js'
|
||||
import {
|
||||
ensureKuaishouFeifeiClaimShortLink,
|
||||
resolveKuaishouFeifeiClaimUrl,
|
||||
} from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import {
|
||||
OPEN_91_MANUAL_FAILED_STATUS,
|
||||
@@ -107,28 +102,8 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
||||
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
|
||||
|
||||
for (const task of tasks) {
|
||||
let claimUrl = ''
|
||||
let expireTime: unknown = task.primary_claim_expires_at || task.claim_expires_at || ''
|
||||
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
|
||||
|
||||
if (String(task.executor_key || '').trim() === 'kuaishou_feifei') {
|
||||
const context = parseJsonObject(task.context_json)
|
||||
claimUrl = await ensureKuaishouFeifeiClaimShortLink(task)
|
||||
if (!claimUrl) {
|
||||
claimUrl = resolveKuaishouFeifeiClaimUrl(context.kuaishouFeifei)
|
||||
}
|
||||
expireTime = ''
|
||||
} else if (primaryToken) {
|
||||
claimUrl = buildClaimUrl(primaryToken)
|
||||
} else if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
|
||||
try {
|
||||
const ensured = await ensureTaskClaimLink(task)
|
||||
claimUrl = String(ensured.claimUrl || '').trim()
|
||||
expireTime = ensured.expiredAt || expireTime
|
||||
} catch {
|
||||
claimUrl = ''
|
||||
}
|
||||
}
|
||||
const deliveryLink = await resolveTaskDeliveryLink(task)
|
||||
const claimUrl = String(deliveryLink?.claimUrl || '').trim()
|
||||
|
||||
if (!claimUrl) {
|
||||
continue
|
||||
@@ -141,7 +116,7 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
||||
readyTaskIds.push(Number(task.id))
|
||||
cardItems.push(buildOpen91CardItem({
|
||||
claimUrl,
|
||||
expireTime,
|
||||
expireTime: deliveryLink?.expireTime || '',
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,38 +2,26 @@ import { createTask, listTasksByOrderId, updateTask } from '../../repositories/t
|
||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
||||
import { prepareKuaishouFeifeiTask } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import {
|
||||
planFulfillmentTaskForOrderItem,
|
||||
type FulfillmentBindingLike,
|
||||
} from '../fulfillment/planner.js'
|
||||
import { preparePaidFulfillmentTask } from '../fulfillment/executors/registry.js'
|
||||
import type { FulfillmentPrepareDeps } from '../fulfillment/executors/types.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import {
|
||||
TASK_STATUS,
|
||||
isPaidPreparationStableStatus,
|
||||
resolveInitialPaidTaskStatus,
|
||||
shouldEnsureKuaishouCloudClaimLink,
|
||||
} from '../../domain/task-status.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
type ClaimTokenLike = {
|
||||
token: string
|
||||
expired_at: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type FulfillmentBindingLike = {
|
||||
id: number
|
||||
profile_id?: number
|
||||
profile_key?: string
|
||||
profile_name?: string
|
||||
name?: string
|
||||
executor_key?: string
|
||||
requires_claim?: boolean
|
||||
auto_dispatch?: boolean
|
||||
config_json?: string | JsonObject
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type DeliveryTaskRow = TaskRow & {
|
||||
skuCode?: string
|
||||
skuName?: string
|
||||
@@ -54,17 +42,6 @@ type DeliveryTaskDeps = {
|
||||
randomId?: (prefix?: string) => string
|
||||
}
|
||||
|
||||
type RuntimeDeliveryTaskDeps = Required<
|
||||
Pick<
|
||||
DeliveryTaskDeps,
|
||||
'updateTask' | 'createTaskClaimToken' | 'notifyTaskAutoManualReview' | 'nowIso'
|
||||
>
|
||||
>
|
||||
|
||||
type TaskContext = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export async function syncDeliveryTasksForOrder(
|
||||
order: OrderRow,
|
||||
orderItems: OrderItemRow[],
|
||||
@@ -88,7 +65,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
randomId: createRandomId = randomId,
|
||||
} = deps
|
||||
|
||||
const runtimeDeps: RuntimeDeliveryTaskDeps = {
|
||||
const runtimeDeps: FulfillmentPrepareDeps = {
|
||||
updateTask: updateDeliveryTask,
|
||||
createTaskClaimToken: createClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview,
|
||||
@@ -102,53 +79,30 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
return existingTasks
|
||||
}
|
||||
|
||||
const itemMap = new Map(orderItems.map((item) => [item.id, item]))
|
||||
const preparedTasks = await Promise.all(
|
||||
existingTasks.map((task) =>
|
||||
preparePaidTask(
|
||||
{
|
||||
...task,
|
||||
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
|
||||
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
|
||||
},
|
||||
runtimeDeps,
|
||||
),
|
||||
),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
return preparePaidTasks(existingTasks, runtimeDeps)
|
||||
}
|
||||
|
||||
const tasks: DeliveryTaskRow[] = []
|
||||
|
||||
for (const item of orderItems) {
|
||||
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
|
||||
const plan = await planFulfillmentTaskForOrderItem({
|
||||
order,
|
||||
item,
|
||||
getProfileByKey,
|
||||
})
|
||||
|
||||
if (!profile) {
|
||||
if (!plan) {
|
||||
continue
|
||||
}
|
||||
const itemSnapshot = parseJsonObject(item.item_snapshot_json)
|
||||
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
||||
const kuaishouFeifeiConfig = parseJsonObject(fulfillmentConfig.kuaishouFeifei)
|
||||
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
|
||||
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
|
||||
const cloudSourceKeys = normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys)
|
||||
const resolvedCloudSourceKey =
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentaclesConfig.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: ''
|
||||
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
|
||||
const primaryDeliveryItem = deliveryItems[0] || {
|
||||
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
|
||||
cloudSkuName: String(cloudtentaclesConfig.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = getNowIso()
|
||||
const initialStatus =
|
||||
order.pay_status === 'paid' ? resolvePaidTaskStatus(profile) : TASK_STATUS.PENDING_PAYMENT
|
||||
order.pay_status === 'paid'
|
||||
? resolvePaidTaskStatus(plan.profile)
|
||||
: TASK_STATUS.PENDING_PAYMENT
|
||||
|
||||
const task = await createDeliveryTask({
|
||||
orderId: order.id,
|
||||
@@ -160,131 +114,20 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
shopName: order.shop_name,
|
||||
platformOrderId: order.platform_order_id,
|
||||
taskNo: createRandomId('DT'),
|
||||
profileId: Number(profile.profile_id || profile.id),
|
||||
executorKey: String(profile.executor_key || 'manual_dispatch'),
|
||||
profileId: plan.profileId,
|
||||
executorKey: plan.executorKey,
|
||||
taskStatus: initialStatus,
|
||||
deliveryStatus: 'pending',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
claimToken: '',
|
||||
claimExpiresAt: null,
|
||||
automationMode: profile.auto_dispatch ? 'automatic' : 'manual',
|
||||
requiresClaim: Boolean(profile.requires_claim),
|
||||
userActionStatus: profile.requires_claim ? 'pending_claim' : 'not_required',
|
||||
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
|
||||
requiresClaim: plan.requiresClaim,
|
||||
userActionStatus: plan.requiresClaim ? 'pending_claim' : 'not_required',
|
||||
attemptCount: 0,
|
||||
lastError: '',
|
||||
contextJson: JSON.stringify({
|
||||
profileKey: String(profile.profile_key || ''),
|
||||
profileName: String(profile.profile_name || profile.name || ''),
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
|
||||
? {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(fulfillmentConfig.configId || '').trim(),
|
||||
internalSkuCode: item.sku_code,
|
||||
internalSkuName: item.sku_name,
|
||||
deliveryItems,
|
||||
ticket: {
|
||||
code: '',
|
||||
status: 'pending',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
verifiedAt: null,
|
||||
oid: '',
|
||||
formToken: '',
|
||||
leftCount: 0,
|
||||
goodsTitle: '',
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
cloudSourceKeys,
|
||||
resolvedSourceKey: resolvedCloudSourceKey,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
vnKey: '1',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
bindExpiresAt: null,
|
||||
bindProbeAt: null,
|
||||
bindProbeStatus: '',
|
||||
bindProbeMessage: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: false,
|
||||
purchaseTriggered: false,
|
||||
assetBefore: 0,
|
||||
assetAfter: 0,
|
||||
purchaseAt: null,
|
||||
},
|
||||
dispatch: {
|
||||
status: 'pending',
|
||||
dispatchAt: null,
|
||||
dispatchBy: null,
|
||||
sendType: 0,
|
||||
note: '',
|
||||
},
|
||||
returnNumber: {
|
||||
status: 'pending',
|
||||
returnedAt: null,
|
||||
returnedBy: null,
|
||||
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(
|
||||
kuaishouConsumeConfig.shopId ||
|
||||
kuaishouShopConfig.shopId ||
|
||||
itemSnapshot.shopId ||
|
||||
order.shop_id ||
|
||||
'',
|
||||
).trim(),
|
||||
shopName: String(
|
||||
kuaishouConsumeConfig.shopName ||
|
||||
kuaishouShopConfig.kshopName ||
|
||||
itemSnapshot.shopName ||
|
||||
order.shop_name ||
|
||||
'',
|
||||
).trim(),
|
||||
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
: null,
|
||||
kuaishouFeifei: isKuaishouFeifeiExecutor(profile.executor_key)
|
||||
? {
|
||||
flowType: 'kuaishou_feifei',
|
||||
productCode: String(kuaishouFeifeiConfig.productCode || '').trim(),
|
||||
productName: String(kuaishouFeifeiConfig.productName || item.sku_name || '').trim(),
|
||||
platformOrderNo: '',
|
||||
orderNo: '',
|
||||
rechargeStatus: 0,
|
||||
rechargeStatusLabel: '',
|
||||
rechargeResultMessage: '',
|
||||
claimUrl: '',
|
||||
consumeStatus: 'pending',
|
||||
h5: {
|
||||
entryUrl: '',
|
||||
rechargeUrl: '',
|
||||
},
|
||||
lastSyncedAt: null,
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
contextJson: JSON.stringify(plan.context),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
@@ -303,336 +146,26 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
return tasks
|
||||
}
|
||||
|
||||
const preparedTasks = await Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
|
||||
const preparedTasks = await Promise.all(
|
||||
tasks.map((task) => preparePaidFulfillmentTask(task, runtimeDeps)),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
}
|
||||
|
||||
async function preparePaidTask(
|
||||
task: DeliveryTaskRow,
|
||||
deps: Partial<RuntimeDeliveryTaskDeps> = {},
|
||||
): Promise<TaskRow | DeliveryTaskRow | null> {
|
||||
const {
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
} = deps
|
||||
|
||||
const now = getNowIso()
|
||||
|
||||
if (isPaidPreparationStableStatus(task.task_status)) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (isKuaishouCloudExecutor(task.executor_key)) {
|
||||
if (shouldEnsureKuaishouCloudClaimLink(task.task_status)) {
|
||||
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
return updateDeliveryTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: task.last_error || '领取链接已生成,等待客户提交核销码',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (isKuaishouFeifeiExecutor(task.executor_key)) {
|
||||
try {
|
||||
return await prepareKuaishouFeifeiTask(task as TaskRow)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'kuaishou-feifei 订单创建失败'
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
last_error: message,
|
||||
result_code: 'kuaishou_feifei_prepare_failed',
|
||||
result_message: message,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: message,
|
||||
source: 'kuaishou_feifei_prepare_failed',
|
||||
})
|
||||
return updatedTask
|
||||
}
|
||||
}
|
||||
|
||||
if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
|
||||
const lastError = task.last_error || '当前任务需要人工履约处理'
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'manual_dispatch_profile',
|
||||
})
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
return task
|
||||
async function preparePaidTasks(
|
||||
tasks: TaskRow[],
|
||||
deps: FulfillmentPrepareDeps,
|
||||
) {
|
||||
const preparedTasks = await Promise.all(
|
||||
tasks.map((task) => preparePaidFulfillmentTask(task, deps)),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
}
|
||||
|
||||
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
||||
return resolveInitialPaidTaskStatus(profile)
|
||||
}
|
||||
|
||||
function parseTaskContext(task: { context_json?: unknown } | null | undefined): TaskContext {
|
||||
const value = task?.context_json
|
||||
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value as TaskContext
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as TaskContext)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(value: unknown): JsonObject {
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as JsonObject)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDynamicCloudtentaclesProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
|
||||
const cloudSourceKeys = normalizeStringArray(cloudtentacles.cloudSourceKeys)
|
||||
const deliveryItems = normalizeCloudDeliveryItems({
|
||||
deliveryItems: cloudtentacles.deliveryItems,
|
||||
skuId: cloudtentacles.cloudSkuId,
|
||||
skuName: cloudtentacles.cloudSkuName || item.sku_name,
|
||||
})
|
||||
const primaryDeliveryItem = deliveryItems[0] || {
|
||||
cloudSkuId: 0,
|
||||
cloudSkuName: '',
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
if (
|
||||
!primaryDeliveryItem.cloudSkuId ||
|
||||
!primaryDeliveryItem.cloudSkuName ||
|
||||
cloudSourceKeys.length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = await getProfileByKey('kuaishou_ct_assisted')
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
profile_id: Number(profile.id || profile.profile_id || 0),
|
||||
profile_key: 'kuaishou_ct_assisted',
|
||||
profile_name: String(profile.profile_name || profile.name || '快手 cloud 履约').trim(),
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
requires_claim: false,
|
||||
auto_dispatch: false,
|
||||
config_json: {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: `${String(cloudtentacles.matchMode || 'cloudtentacles_name').trim()}:${String(cloudtentacles.normalizedProductName || primaryDeliveryItem.cloudSkuId).trim()}`,
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
resolvedSourceKey:
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentacles.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: '',
|
||||
deliveryItems,
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: true,
|
||||
},
|
||||
kuaishouConsume: {
|
||||
shopId: String(snapshot.shopId || '').trim(),
|
||||
shopName: String(snapshot.shopName || '').trim(),
|
||||
autoConsumeAfterDispatch: false,
|
||||
},
|
||||
notes:
|
||||
cloudtentacles.matchMode === 'cloudtentacles_override'
|
||||
? '91卡券商品名命中 cloudtentacles 覆盖规则'
|
||||
: '91卡券商品名自动匹配 cloudtentacles 商品',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDynamicFulfillmentProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
return (
|
||||
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveDynamicKuaishouFeifeiProfile(
|
||||
item: OrderItemRow,
|
||||
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
|
||||
): Promise<FulfillmentBindingLike | null> {
|
||||
const snapshot = parseJsonObject(item.item_snapshot_json)
|
||||
const feifei = parseJsonObject(snapshot.kuaishouFeifei)
|
||||
const productCode = String(feifei.productCode || '').trim()
|
||||
if (!productCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = await getProfileByKey('kuaishou_feifei')
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
profile_id: Number(profile.id || profile.profile_id || 0),
|
||||
profile_key: 'kuaishou_feifei',
|
||||
profile_name: String(profile.profile_name || profile.name || 'kuaishou-feifei 履约').trim(),
|
||||
executor_key: 'kuaishou_feifei',
|
||||
requires_claim: true,
|
||||
auto_dispatch: false,
|
||||
config_json: {
|
||||
flowType: 'kuaishou_feifei',
|
||||
configId: `kuaishou_feifei:${productCode}`,
|
||||
kuaishouFeifei: {
|
||||
productCode,
|
||||
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
|
||||
matchMode: String(feifei.matchMode || 'kuaishou_feifei_name').trim(),
|
||||
},
|
||||
notes: '91卡券商品名自动匹配 kuaishou-feifei 商品映射',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}> {
|
||||
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
|
||||
const items = rawItems
|
||||
.map((item) => normalizeCloudDeliveryItem(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeCloudDeliveryItems(items)
|
||||
}
|
||||
|
||||
const cloudSkuId = Number(value.skuId || 0) || 0
|
||||
if (!cloudSkuId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(value.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItem(value: unknown) {
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const quantity = Number(source.quantity || 1) || 1
|
||||
return {
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
||||
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeCloudDeliveryItems(
|
||||
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
|
||||
) {
|
||||
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = merged.get(item.cloudSkuId)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity
|
||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
|
||||
continue
|
||||
}
|
||||
|
||||
merged.set(item.cloudSkuId, { ...item })
|
||||
}
|
||||
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
|
||||
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
|
||||
return Boolean(task && Number(task.id || 0) > 0)
|
||||
}
|
||||
|
||||
function isKuaishouCloudExecutor(value: unknown): boolean {
|
||||
return String(value || '').trim() === 'kuaishou_ct_assisted'
|
||||
}
|
||||
|
||||
function isKuaishouFeifeiExecutor(value: unknown): boolean {
|
||||
return String(value || '').trim() === 'kuaishou_feifei'
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import {
|
||||
updateOrder,
|
||||
} from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
|
||||
import { resolveOrderItemForFulfillment } from './product-match-service.js'
|
||||
import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
|
||||
import {
|
||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||
resolveKuaishouIndustryVoucherSendCallbackMessage,
|
||||
} from '../platforms/kuaishou-industry/voucher-service.js'
|
||||
resolveOrderFulfillmentReadiness,
|
||||
syncOrderFulfillmentAttachments,
|
||||
} from '../fulfillment/order-fulfillment-readiness-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
@@ -189,19 +187,19 @@ export async function upsertOrderFromSource(
|
||||
})),
|
||||
)
|
||||
|
||||
const industryVoucherGate = await resolveKuaishouIndustryVoucherGate(order.platform_order_id)
|
||||
if (!industryVoucherGate.allowed) {
|
||||
const readiness = await resolveOrderFulfillmentReadiness(order)
|
||||
if (!readiness.allowed) {
|
||||
logIntegration('[order-service]', `${sourceLabel} 订单暂停履约:电子凭证发码回调未确认`, {
|
||||
orderId: order.id,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
voucherCount: industryVoucherGate.voucherCount,
|
||||
reason: industryVoucherGate.reason,
|
||||
voucherCount: readiness.voucherCount,
|
||||
reason: readiness.reason,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return {
|
||||
ignoreReason: 'kuaishou_industry_send_callback_unconfirmed',
|
||||
ignoreReason: readiness.blockReason || 'fulfillment_not_ready',
|
||||
order,
|
||||
orderItems,
|
||||
tasks: [],
|
||||
@@ -209,7 +207,7 @@ export async function upsertOrderFromSource(
|
||||
}
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
||||
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
|
||||
await syncOrderFulfillmentAttachments(order, tasks, {
|
||||
source: `${sourceLabel}_order_upsert`,
|
||||
now,
|
||||
})
|
||||
@@ -233,36 +231,6 @@ export async function upsertOrderFromSource(
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveKuaishouIndustryVoucherGate(platformOrderId: unknown) {
|
||||
const oid = String(platformOrderId || '').trim()
|
||||
if (!oid) {
|
||||
return {
|
||||
allowed: true,
|
||||
voucherCount: 0,
|
||||
reason: '',
|
||||
}
|
||||
}
|
||||
|
||||
const vouchers = await listKuaishouIndustryVouchersByOid(oid)
|
||||
if (vouchers.length === 0 || vouchers.every(isKuaishouIndustryVoucherSendCallbackSuccess)) {
|
||||
return {
|
||||
allowed: true,
|
||||
voucherCount: vouchers.length,
|
||||
reason: '',
|
||||
}
|
||||
}
|
||||
|
||||
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
voucherCount: vouchers.length,
|
||||
reason: blockedVoucher
|
||||
? resolveKuaishouIndustryVoucherSendCallbackMessage(blockedVoucher)
|
||||
: '电子凭证发码回调未确认',
|
||||
}
|
||||
}
|
||||
|
||||
const ORDER_STATUS_PRIORITY = {
|
||||
created: 0,
|
||||
paid: 1,
|
||||
|
||||
@@ -1,236 +1,9 @@
|
||||
import {
|
||||
normalizeCloudtentaclesMatchName,
|
||||
resolveCloudtentaclesSkuByProductName,
|
||||
type CloudtentaclesNameMatchResult,
|
||||
} from './cloudtentacles-name-match-service.js'
|
||||
import {
|
||||
resolveKuaishouFeifeiProductByName,
|
||||
type KuaishouFeifeiProductMatch,
|
||||
} from '../platforms/kuaishou-feifei/product-rule-service.js'
|
||||
|
||||
export type FulfillmentItem = {
|
||||
itemId?: string
|
||||
externalItemId?: string
|
||||
skuCode?: string
|
||||
skuName?: string
|
||||
externalSkuCode?: string
|
||||
externalSkuName?: string
|
||||
quantity?: number
|
||||
spec?: Record<string, unknown>
|
||||
snapshot?: Record<string, unknown>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ResolveOrderItemForFulfillmentInput = {
|
||||
provider?: string
|
||||
platform?: string
|
||||
item?: FulfillmentItem
|
||||
}
|
||||
|
||||
type HasConfiguredOrderItemsInput = Omit<ResolveOrderItemForFulfillmentInput, 'item'> & {
|
||||
items?: FulfillmentItem[]
|
||||
}
|
||||
|
||||
type FulfillmentItemCandidate = {
|
||||
externalItemId: string
|
||||
externalSkuCode: string
|
||||
externalSkuName: string
|
||||
externalSkuNameNormalized: string
|
||||
resolvedSkuCode: string
|
||||
cloudtentaclesNameMatch: CloudtentaclesNameMatchResult | null
|
||||
kuaishouFeifeiMatch: KuaishouFeifeiProductMatch | null
|
||||
isConfigured: boolean
|
||||
}
|
||||
|
||||
export type ResolvedFulfillmentItem = FulfillmentItem & {
|
||||
itemId: string
|
||||
externalItemId: string
|
||||
externalSkuCode: string
|
||||
externalSkuName: string
|
||||
skuCode: string
|
||||
skuName: string
|
||||
snapshot: Record<string, unknown>
|
||||
isConfigured: boolean
|
||||
}
|
||||
|
||||
export async function resolveOrderItemForFulfillment({
|
||||
provider = '',
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<ResolvedFulfillmentItem> {
|
||||
const candidate = await resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})
|
||||
const {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
cloudtentaclesNameMatch,
|
||||
kuaishouFeifeiMatch,
|
||||
} = candidate
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.productCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuName = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.skuName,
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
])
|
||||
const snapshot = {
|
||||
...(isPlainObject(item.snapshot) ? item.snapshot : {}),
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
matchMode: cloudtentaclesNameMatch?.matchMode || '',
|
||||
cloudtentacles: cloudtentaclesNameMatch
|
||||
? {
|
||||
matchMode: cloudtentaclesNameMatch.matchMode,
|
||||
productName: cloudtentaclesNameMatch.productName,
|
||||
normalizedProductName: cloudtentaclesNameMatch.normalizedProductName,
|
||||
cloudSkuId: cloudtentaclesNameMatch.cloudSkuId,
|
||||
cloudSkuName: cloudtentaclesNameMatch.cloudSkuName,
|
||||
cloudSkuPrice: cloudtentaclesNameMatch.cloudSkuPrice,
|
||||
cloudSkuInventory: cloudtentaclesNameMatch.cloudSkuInventory,
|
||||
cloudSourceKeys: cloudtentaclesNameMatch.cloudSourceKeys,
|
||||
resolvedSourceKey: cloudtentaclesNameMatch.resolvedSourceKey,
|
||||
deliveryItems: cloudtentaclesNameMatch.deliveryItems,
|
||||
skuSnapshot: cloudtentaclesNameMatch.skuSnapshot,
|
||||
}
|
||||
: null,
|
||||
kuaishouFeifei: kuaishouFeifeiMatch
|
||||
? {
|
||||
matchMode: kuaishouFeifeiMatch.matchMode,
|
||||
productName: kuaishouFeifeiMatch.productName,
|
||||
normalizedProductName: kuaishouFeifeiMatch.normalizedProductName,
|
||||
productCode: kuaishouFeifeiMatch.productCode,
|
||||
skuName: kuaishouFeifeiMatch.skuName,
|
||||
}
|
||||
: null,
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
itemId: externalItemId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
skuCode: resolvedSkuCode,
|
||||
skuName: resolvedSkuName,
|
||||
snapshot,
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasConfiguredOrderItems({
|
||||
provider = '',
|
||||
platform = '',
|
||||
items = [],
|
||||
}: HasConfiguredOrderItemsInput): Promise<boolean> {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
|
||||
return candidates.some((item) => item.isConfigured)
|
||||
}
|
||||
|
||||
export function normalizeProductName(value: unknown): string {
|
||||
return normalizeCloudtentaclesMatchName(value)
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
async function resolveConfiguredItemCandidate({
|
||||
provider = '',
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
item.externalItemId,
|
||||
item.itemId,
|
||||
])
|
||||
const externalSkuCode = pickFirstNonEmpty([
|
||||
item.externalSkuCode,
|
||||
item.skuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const externalSkuName = pickFirstNonEmpty([
|
||||
item.externalSkuName,
|
||||
item.skuName,
|
||||
externalSkuCode,
|
||||
])
|
||||
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
|
||||
|
||||
if (isOpen91KuaishouOrder(provider, platform)) {
|
||||
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
|
||||
const kuaishouFeifeiMatch = cloudtentaclesNameMatch
|
||||
? null
|
||||
: resolveKuaishouFeifeiProductByName(externalSkuName)
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
kuaishouFeifeiMatch?.productCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
cloudtentaclesNameMatch,
|
||||
kuaishouFeifeiMatch,
|
||||
isConfigured: Boolean(cloudtentaclesNameMatch || kuaishouFeifeiMatch),
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
cloudtentaclesNameMatch: null,
|
||||
kuaishouFeifeiMatch: null,
|
||||
isConfigured: false,
|
||||
}
|
||||
}
|
||||
|
||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||
return String(provider || '').trim() === '91kaquan'
|
||||
&& String(platform || '').trim() === 'kuaishou'
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
export {
|
||||
hasConfiguredOrderItems,
|
||||
normalizeProductName,
|
||||
resolveOrderItemForFulfillment,
|
||||
} from '../fulfillment/product-resolution-service.js'
|
||||
export type {
|
||||
FulfillmentItem,
|
||||
ResolvedFulfillmentItem,
|
||||
} from '../fulfillment/product-resolution-service.js'
|
||||
|
||||
Reference in New Issue
Block a user