重构订单履约发货流程

This commit is contained in:
yml2213
2026-07-08 21:57:34 +08:00
parent 7b215a66a5
commit 9a24268b5a
17 changed files with 1415 additions and 810 deletions
@@ -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
}