重构 lewan 履约:拆分发货模块并统一 registry 入口

将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例,
claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
This commit is contained in:
yml2213
2026-07-10 22:23:26 +08:00
parent c665ef5851
commit 792680bd19
19 changed files with 2111 additions and 1488 deletions
@@ -3,9 +3,20 @@ import {
shouldEnsureKuaishouCloudClaimLink,
TASK_STATUS,
} from '../../../domain/task-status.js'
import { ensureTaskClaimLink } from '../kuaishou-cloud/index.js'
import {
confirmKuaishouCloudTaskRole,
dispatchKuaishouCloudFulfillmentTask,
ensureTaskClaimLink,
prepareKuaishouCloudFulfillmentTask,
rebindKuaishouCloudTaskRole,
redeemKuaishouCloudTask,
refreshKuaishouCloudTaskRoleInfo,
returnKuaishouCloudFulfillmentTask,
} from '../kuaishou-cloud/index.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
type FulfillmentActionOptions,
type FulfillmentActionResult,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
@@ -16,6 +27,13 @@ export const kuaishouCloudExecutor: FulfillmentExecutor = {
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
preparePaidTask,
resolveDeliveryLink,
prepareBinding,
rebindRole,
refreshRole,
confirmRole,
redeemTask,
dispatchTask,
returnNumber,
}
async function preparePaidTask(
@@ -71,3 +89,115 @@ async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLi
return null
}
}
async function prepareBinding(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await prepareKuaishouCloudFulfillmentTask(task, {
source: options.source || 'executor_prepare_binding',
actor: options.actor,
force: options.force === true,
})
return {
task: result.task || task,
claimUrl: result.claimUrl,
token: result.token,
flow: result.flow,
}
}
async function rebindRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await rebindKuaishouCloudTaskRole(task, {
source: options.source || 'executor_rebind_role',
actor: options.actor,
})
return {
task: result.task || task,
claimUrl: result.claimUrl,
token: result.token,
}
}
async function refreshRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await refreshKuaishouCloudTaskRoleInfo(task, {
source: options.source || 'executor_refresh_role',
actor: options.actor,
recordEvent: options.recordEvent !== false,
forceProbe: options.forceProbe === true,
})
return {
task: result.task || task,
roleInfo: result.roleInfo,
}
}
async function confirmRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await confirmKuaishouCloudTaskRole(task, {
source: options.source || 'executor_confirm_role',
actor: options.actor,
errorCodePrefix: options.errorCodePrefix,
forceProbe: options.forceProbe !== false,
})
return {
task: result.task,
alreadySettled: result.alreadySettled,
}
}
async function redeemTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await redeemKuaishouCloudTask(task, {
source: options.source || 'executor_redeem',
actor: options.actor,
autoFinalize: options.autoFinalize !== false,
errorCodePrefix: options.errorCodePrefix,
})
return {
task: result.task,
alreadySettled: result.alreadySettled,
concurrentSkipped: result.concurrentSkipped,
}
}
async function dispatchTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await dispatchKuaishouCloudFulfillmentTask(task, {
source: options.source || 'executor_dispatch',
actor: options.actor,
autoFinalize: options.autoFinalize === true,
})
return {
task: result.task || task,
flow: result.flow,
}
}
async function returnNumber(
task: TaskRow,
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
const result = await returnKuaishouCloudFulfillmentTask(task, {
source: options.source || 'executor_return_number',
actor: options.actor,
// 未显式传入时保持默认 true(发货收尾);admin 清理会传 false
consumeIndustryVoucher: options.consumeIndustryVoucher,
})
return {
task: result.task || task,
flow: result.flow,
}
}
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
getFulfillmentExecutor,
prepareFulfillmentBinding,
redeemFulfillmentTask,
dispatchFulfillmentTask,
confirmFulfillmentRole,
rebindFulfillmentRole,
refreshFulfillmentRole,
returnFulfillmentNumber,
} from './registry.js'
import { FULFILLMENT_EXECUTOR_KEYS } from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
function makeTask(executorKey: string): TaskRow {
return {
id: 1,
executor_key: executorKey,
} as TaskRow
}
test('getFulfillmentExecutor 映射 lewan / industry / feifei / manual', () => {
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)?.key,
FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
)
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY)?.key,
FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
)
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)?.key,
FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
)
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)?.key,
FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH,
)
assert.equal(getFulfillmentExecutor('unknown'), null)
})
test('lewan executor 暴露完整履约动作', () => {
const executor = getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)
assert.ok(executor)
assert.equal(typeof executor.prepareBinding, 'function')
assert.equal(typeof executor.rebindRole, 'function')
assert.equal(typeof executor.refreshRole, 'function')
assert.equal(typeof executor.confirmRole, 'function')
assert.equal(typeof executor.redeemTask, 'function')
assert.equal(typeof executor.dispatchTask, 'function')
assert.equal(typeof executor.returnNumber, 'function')
})
test('feifei / manual 不支持 lewan 专属动作时 registry 返回 null', async () => {
const feifei = makeTask(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)
const manual = makeTask(FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
assert.equal(await prepareFulfillmentBinding(feifei), null)
assert.equal(await rebindFulfillmentRole(feifei), null)
assert.equal(await refreshFulfillmentRole(feifei), null)
assert.equal(await confirmFulfillmentRole(feifei), null)
assert.equal(await redeemFulfillmentTask(feifei), null)
assert.equal(await dispatchFulfillmentTask(feifei), null)
assert.equal(await returnFulfillmentNumber(feifei), null)
assert.equal(await redeemFulfillmentTask(manual), null)
assert.equal(await dispatchFulfillmentTask(manual), null)
})
@@ -6,6 +6,7 @@ import {
FULFILLMENT_EXECUTOR_KEYS,
isManualDispatchExecutor,
normalizeExecutorKey,
type FulfillmentActionOptions,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
@@ -53,3 +54,73 @@ export async function resolveFulfillmentDeliveryLink(
return executor.resolveDeliveryLink(task)
}
async function runExecutorAction(
task: TaskRow,
method:
| 'prepareBinding'
| 'rebindRole'
| 'refreshRole'
| 'confirmRole'
| 'redeemTask'
| 'dispatchTask'
| 'returnNumber',
options: FulfillmentActionOptions = {},
) {
const executor = getFulfillmentExecutor(task.executor_key)
const handler = executor?.[method]
if (!handler) {
return null
}
return handler(task, options)
}
export function prepareFulfillmentBinding(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'prepareBinding', options)
}
export function rebindFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'rebindRole', options)
}
export function refreshFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'refreshRole', options)
}
export function confirmFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'confirmRole', options)
}
export function redeemFulfillmentTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'redeemTask', options)
}
export function dispatchFulfillmentTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'dispatchTask', options)
}
export function returnFulfillmentNumber(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
return runExecutorAction(task, 'returnNumber', options)
}
@@ -31,6 +31,25 @@ export type FulfillmentPrepareDeps = {
nowIso: () => string
}
export type FulfillmentActionOptions = {
source?: string
actor?: unknown
autoFinalize?: boolean
errorCodePrefix?: string
/**
* 退号时是否顺带核销行业电子凭证。
* - true/默认:发货收尾(claim autoFinalize
* - false:仅清理虚拟号(admin 退号),与核销无关
*/
consumeIndustryVoucher?: boolean
[key: string]: unknown
}
export type FulfillmentActionResult = {
task: TaskRow
[key: string]: unknown
}
export type FulfillmentExecutor = {
key: FulfillmentExecutorKey
preparePaidTask?: (
@@ -38,6 +57,44 @@ export type FulfillmentExecutor = {
deps: FulfillmentPrepareDeps,
) => Promise<TaskRow | null>
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
/** lewan:准备绑定资源(虚拟号 / bindUrl) */
prepareBinding?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/** lewan:换绑角色 */
rebindRole?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/** lewan:刷新角色信息 */
refreshRole?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/** lewan:确认角色(UID 匹配后落 ROLE_CONFIRMED */
confirmRole?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/** lewan:一键兑换(状态机 + dispatch */
redeemTask?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/** lewan:后台/系统发货(不经 claim 一键兑换) */
dispatchTask?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
/**
* lewan:退还虚拟号。
* 默认可顺带核销;admin 清理请传 consumeIndustryVoucher: false。
*/
returnNumber?: (
task: TaskRow,
options?: FulfillmentActionOptions,
) => Promise<FulfillmentActionResult>
}
export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {