重构订单履约发货流程
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 { createHttpError } from "../../../utils/http.js";
|
||||||
import { normalizeProductName } from "../../order/product-match-service.js";
|
import { normalizeProductName } from "../product-resolution-service.js";
|
||||||
import {
|
import {
|
||||||
appointCloudtentaclesVirtualNumber,
|
appointCloudtentaclesVirtualNumber,
|
||||||
backCloudtentaclesVirtualNumber,
|
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 { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
||||||
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
|
||||||
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js'
|
|
||||||
import {
|
|
||||||
ensureKuaishouFeifeiClaimShortLink,
|
|
||||||
resolveKuaishouFeifeiClaimUrl,
|
|
||||||
} from '../fulfillment/kuaishou-feifei/index.js'
|
|
||||||
import { logIntegration } from '../../utils/logger.js'
|
import { logIntegration } from '../../utils/logger.js'
|
||||||
import {
|
import {
|
||||||
OPEN_91_MANUAL_FAILED_STATUS,
|
OPEN_91_MANUAL_FAILED_STATUS,
|
||||||
@@ -107,28 +102,8 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
|
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
let claimUrl = ''
|
const deliveryLink = await resolveTaskDeliveryLink(task)
|
||||||
let expireTime: unknown = task.primary_claim_expires_at || task.claim_expires_at || ''
|
const claimUrl = String(deliveryLink?.claimUrl || '').trim()
|
||||||
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 = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!claimUrl) {
|
if (!claimUrl) {
|
||||||
continue
|
continue
|
||||||
@@ -141,7 +116,7 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
readyTaskIds.push(Number(task.id))
|
readyTaskIds.push(Number(task.id))
|
||||||
cardItems.push(buildOpen91CardItem({
|
cardItems.push(buildOpen91CardItem({
|
||||||
claimUrl,
|
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 { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.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 { nowIso } from '../../utils/time.js'
|
||||||
import { randomId } from '../../utils/random.js'
|
import { randomId } from '../../utils/random.js'
|
||||||
import {
|
import {
|
||||||
TASK_STATUS,
|
TASK_STATUS,
|
||||||
isPaidPreparationStableStatus,
|
|
||||||
resolveInitialPaidTaskStatus,
|
resolveInitialPaidTaskStatus,
|
||||||
shouldEnsureKuaishouCloudClaimLink,
|
|
||||||
} from '../../domain/task-status.js'
|
} from '../../domain/task-status.js'
|
||||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, unknown>
|
|
||||||
|
|
||||||
type ClaimTokenLike = {
|
type ClaimTokenLike = {
|
||||||
token: string
|
token: string
|
||||||
expired_at: string
|
expired_at: string
|
||||||
[key: string]: unknown
|
[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 & {
|
type DeliveryTaskRow = TaskRow & {
|
||||||
skuCode?: string
|
skuCode?: string
|
||||||
skuName?: string
|
skuName?: string
|
||||||
@@ -54,17 +42,6 @@ type DeliveryTaskDeps = {
|
|||||||
randomId?: (prefix?: string) => string
|
randomId?: (prefix?: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDeliveryTaskDeps = Required<
|
|
||||||
Pick<
|
|
||||||
DeliveryTaskDeps,
|
|
||||||
'updateTask' | 'createTaskClaimToken' | 'notifyTaskAutoManualReview' | 'nowIso'
|
|
||||||
>
|
|
||||||
>
|
|
||||||
|
|
||||||
type TaskContext = {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncDeliveryTasksForOrder(
|
export async function syncDeliveryTasksForOrder(
|
||||||
order: OrderRow,
|
order: OrderRow,
|
||||||
orderItems: OrderItemRow[],
|
orderItems: OrderItemRow[],
|
||||||
@@ -88,7 +65,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
|||||||
randomId: createRandomId = randomId,
|
randomId: createRandomId = randomId,
|
||||||
} = deps
|
} = deps
|
||||||
|
|
||||||
const runtimeDeps: RuntimeDeliveryTaskDeps = {
|
const runtimeDeps: FulfillmentPrepareDeps = {
|
||||||
updateTask: updateDeliveryTask,
|
updateTask: updateDeliveryTask,
|
||||||
createTaskClaimToken: createClaimToken,
|
createTaskClaimToken: createClaimToken,
|
||||||
notifyTaskAutoManualReview: notifyManualReview,
|
notifyTaskAutoManualReview: notifyManualReview,
|
||||||
@@ -102,53 +79,30 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
|||||||
return existingTasks
|
return existingTasks
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemMap = new Map(orderItems.map((item) => [item.id, item]))
|
return preparePaidTasks(existingTasks, runtimeDeps)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks: DeliveryTaskRow[] = []
|
const tasks: DeliveryTaskRow[] = []
|
||||||
|
|
||||||
for (const item of orderItems) {
|
for (const item of orderItems) {
|
||||||
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
|
const plan = await planFulfillmentTaskForOrderItem({
|
||||||
|
order,
|
||||||
|
item,
|
||||||
|
getProfileByKey,
|
||||||
|
})
|
||||||
|
|
||||||
if (!profile) {
|
if (!plan) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const itemSnapshot = parseJsonObject(item.item_snapshot_json)
|
|
||||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
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) {
|
for (let index = 0; index < quantity; index += 1) {
|
||||||
const createdAt = getNowIso()
|
const createdAt = getNowIso()
|
||||||
const initialStatus =
|
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({
|
const task = await createDeliveryTask({
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
@@ -160,131 +114,20 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
|||||||
shopName: order.shop_name,
|
shopName: order.shop_name,
|
||||||
platformOrderId: order.platform_order_id,
|
platformOrderId: order.platform_order_id,
|
||||||
taskNo: createRandomId('DT'),
|
taskNo: createRandomId('DT'),
|
||||||
profileId: Number(profile.profile_id || profile.id),
|
profileId: plan.profileId,
|
||||||
executorKey: String(profile.executor_key || 'manual_dispatch'),
|
executorKey: plan.executorKey,
|
||||||
taskStatus: initialStatus,
|
taskStatus: initialStatus,
|
||||||
deliveryStatus: 'pending',
|
deliveryStatus: 'pending',
|
||||||
resultCode: '',
|
resultCode: '',
|
||||||
resultMessage: '',
|
resultMessage: '',
|
||||||
claimToken: '',
|
claimToken: '',
|
||||||
claimExpiresAt: null,
|
claimExpiresAt: null,
|
||||||
automationMode: profile.auto_dispatch ? 'automatic' : 'manual',
|
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
|
||||||
requiresClaim: Boolean(profile.requires_claim),
|
requiresClaim: plan.requiresClaim,
|
||||||
userActionStatus: profile.requires_claim ? 'pending_claim' : 'not_required',
|
userActionStatus: plan.requiresClaim ? 'pending_claim' : 'not_required',
|
||||||
attemptCount: 0,
|
attemptCount: 0,
|
||||||
lastError: '',
|
lastError: '',
|
||||||
contextJson: JSON.stringify({
|
contextJson: JSON.stringify(plan.context),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt: createdAt,
|
updatedAt: createdAt,
|
||||||
})
|
})
|
||||||
@@ -303,336 +146,26 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
|||||||
return tasks
|
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)
|
return preparedTasks.filter(isTaskRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preparePaidTask(
|
async function preparePaidTasks(
|
||||||
task: DeliveryTaskRow,
|
tasks: TaskRow[],
|
||||||
deps: Partial<RuntimeDeliveryTaskDeps> = {},
|
deps: FulfillmentPrepareDeps,
|
||||||
): Promise<TaskRow | DeliveryTaskRow | null> {
|
) {
|
||||||
const {
|
const preparedTasks = await Promise.all(
|
||||||
updateTask: updateDeliveryTask = updateTask,
|
tasks.map((task) => preparePaidFulfillmentTask(task, deps)),
|
||||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
)
|
||||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
return preparedTasks.filter(isTaskRow)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
||||||
return resolveInitialPaidTaskStatus(profile)
|
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 {
|
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
|
||||||
return Boolean(task && Number(task.id || 0) > 0)
|
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,
|
updateOrder,
|
||||||
} from '../../repositories/order-repo.js'
|
} from '../../repositories/order-repo.js'
|
||||||
import { replaceOrderItems } from '../../repositories/order-item-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 { syncDeliveryTasksForOrder } from './delivery-task-service.js'
|
||||||
import { resolveOrderItemForFulfillment } from './product-match-service.js'
|
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
|
||||||
import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
|
||||||
import {
|
import {
|
||||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
resolveOrderFulfillmentReadiness,
|
||||||
resolveKuaishouIndustryVoucherSendCallbackMessage,
|
syncOrderFulfillmentAttachments,
|
||||||
} from '../platforms/kuaishou-industry/voucher-service.js'
|
} from '../fulfillment/order-fulfillment-readiness-service.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { logIntegration } from '../../utils/logger.js'
|
import { logIntegration } from '../../utils/logger.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
@@ -189,19 +187,19 @@ export async function upsertOrderFromSource(
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
const industryVoucherGate = await resolveKuaishouIndustryVoucherGate(order.platform_order_id)
|
const readiness = await resolveOrderFulfillmentReadiness(order)
|
||||||
if (!industryVoucherGate.allowed) {
|
if (!readiness.allowed) {
|
||||||
logIntegration('[order-service]', `${sourceLabel} 订单暂停履约:电子凭证发码回调未确认`, {
|
logIntegration('[order-service]', `${sourceLabel} 订单暂停履约:电子凭证发码回调未确认`, {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
provider: order.provider,
|
provider: order.provider,
|
||||||
platform: order.platform,
|
platform: order.platform,
|
||||||
platformOrderId: order.platform_order_id,
|
platformOrderId: order.platform_order_id,
|
||||||
voucherCount: industryVoucherGate.voucherCount,
|
voucherCount: readiness.voucherCount,
|
||||||
reason: industryVoucherGate.reason,
|
reason: readiness.reason,
|
||||||
}, { level: 'warn' })
|
}, { level: 'warn' })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ignoreReason: 'kuaishou_industry_send_callback_unconfirmed',
|
ignoreReason: readiness.blockReason || 'fulfillment_not_ready',
|
||||||
order,
|
order,
|
||||||
orderItems,
|
orderItems,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
@@ -209,7 +207,7 @@ export async function upsertOrderFromSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
||||||
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
|
await syncOrderFulfillmentAttachments(order, tasks, {
|
||||||
source: `${sourceLabel}_order_upsert`,
|
source: `${sourceLabel}_order_upsert`,
|
||||||
now,
|
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 = {
|
const ORDER_STATUS_PRIORITY = {
|
||||||
created: 0,
|
created: 0,
|
||||||
paid: 1,
|
paid: 1,
|
||||||
|
|||||||
@@ -1,236 +1,9 @@
|
|||||||
import {
|
export {
|
||||||
normalizeCloudtentaclesMatchName,
|
hasConfiguredOrderItems,
|
||||||
resolveCloudtentaclesSkuByProductName,
|
normalizeProductName,
|
||||||
type CloudtentaclesNameMatchResult,
|
resolveOrderItemForFulfillment,
|
||||||
} from './cloudtentacles-name-match-service.js'
|
} from '../fulfillment/product-resolution-service.js'
|
||||||
import {
|
export type {
|
||||||
resolveKuaishouFeifeiProductByName,
|
FulfillmentItem,
|
||||||
type KuaishouFeifeiProductMatch,
|
ResolvedFulfillmentItem,
|
||||||
} from '../platforms/kuaishou-feifei/product-rule-service.js'
|
} from '../fulfillment/product-resolution-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]'
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user