重构 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
@@ -7,10 +7,6 @@ import {
listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByTaskId,
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
import {
backCloudtentaclesVirtualNumber,
getCloudtentaclesBindInfo,
} from '../../platforms/cloudtentacles/virtual-number-service.js'
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
import {
@@ -22,17 +18,16 @@ import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
import { maskCode, maskPhone } from '../write-helpers.js'
import {
dispatchKuaishouCloudFulfillmentTask,
prepareKuaishouCloudFulfillmentTask,
rebindKuaishouCloudTaskRole,
} from '../../fulfillment/kuaishou-cloud/index.js'
dispatchFulfillmentTask,
prepareFulfillmentBinding,
rebindFulfillmentRole,
refreshFulfillmentRole,
returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js'
import {
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
resolvePersistedCloudtentaclesContext,
} from './kuaishou-cloud-helpers.js'
import type {
@@ -66,15 +61,21 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
})
}
const result = await prepareKuaishouCloudFulfillmentTask(task, {
const result = await prepareFulfillmentBinding(task, {
source: 'admin_task_prepare',
actor: buildAdminActionActor(session),
})
if (!result?.task) {
throw createHttpError('当前任务不支持准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_prepare_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: result.claimUrl,
token: result.token,
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
@@ -99,11 +100,17 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
})
}
const result = await dispatchKuaishouCloudFulfillmentTask(task, {
const result = await dispatchFulfillmentTask(task, {
source: 'admin_task_dispatch',
actor: buildAdminActionActor(session),
autoFinalize: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持 executor 发货', {
statusCode: 409,
errorCode: 'admin_task_dispatch_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
@@ -115,7 +122,6 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateAssistedTask) {
@@ -132,74 +138,21 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
const result = await refreshFulfillmentRole(task, {
source: 'admin_task_refresh_role',
actor: buildAdminActionActor(session),
forceProbe: true,
recordEvent: true,
})
if (!result?.task) {
throw createHttpError('当前任务不支持刷新角色信息', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_bind_info_context',
errorCode: 'admin_task_refresh_not_supported',
})
}
const cloudContext = resolvePersistedCloudtentaclesContext([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
roleName: bindInfo.name,
roleId: bindInfo.rid,
},
role: {
status: bindInfo.name || bindInfo.rid ? 'ready' : 'pending',
name: bindInfo.name,
rid: bindInfo.rid,
refreshedAt: now,
errorMessage:
bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
rawInfo: bindInfo.rawInfo,
},
},
}
const updatedTask = await updateTask(task.id, {
role_id: bindInfo.rid || '',
role_name: bindInfo.name || '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_role_info_refreshed',
{
roleName: bindInfo.name,
roleId: bindInfo.rid,
vnId: flow.binding.vnId,
refreshedBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
},
now,
)
return {
task: mapTaskActionPayload(updatedTask),
task: mapTaskActionPayload(result.task),
}
}
@@ -218,21 +171,21 @@ export async function rebindAdminTaskKuaishouCloudRole(
})
}
const result = await rebindKuaishouCloudTaskRole(task, {
const result = await rebindFulfillmentRole(task, {
source: 'admin_task_rebind_role',
actor: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
actor: buildAdminActionActor(session),
})
if (!result?.task) {
throw createHttpError('当前任务不支持换绑角色', {
statusCode: 409,
errorCode: 'admin_task_rebind_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: result.claimUrl,
token: result.token,
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
@@ -241,7 +194,6 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
@@ -258,108 +210,21 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少可退还的虚拟号信息', {
// admin 退号 = 清理虚拟号占位,不核销电子凭证
const result = await returnFulfillmentNumber(task, {
source: 'admin_task_return_number',
actor: buildAdminActionActor(session),
consumeIndustryVoucher: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持退还号码', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_return_context',
errorCode: 'admin_task_return_not_supported',
})
}
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const ticketCode = String(flow.ticket.code || '').trim()
const consumeAlreadyCompleted = flow.consume.status === 'success'
const consumeStatus = consumeAlreadyCompleted ? 'success' : 'skipped'
let consumeErrorMessage = ''
const consumedAt = consumeAlreadyCompleted ? flow.consume.consumedAt || now : null
const nextTaskStatus = 'completed'
const nextResultCode = consumeAlreadyCompleted
? 'kuaishou_cloud_completed'
: 'kuaishou_cloud_completed_without_eticket_consume'
const nextResultMessage = consumeAlreadyCompleted
? 'cloudtentacles 发货、退号并完成电子凭证核销'
: 'cloudtentacles 发货、退号并收口'
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
returnNumber: {
...flow.returnNumber,
status: 'success',
returnedAt: now,
returnedBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
},
consume: {
...flow.consume,
status: consumeStatus,
shopId: flow.consume.shopId,
shopName: flow.consume.shopName,
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
consumedAt,
errorMessage: consumeErrorMessage,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: nextTaskStatus,
delivery_status: 'delivered',
result_code: nextResultCode,
result_message: nextResultMessage,
redeemed_at: now,
last_error: consumeErrorMessage,
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_number_returned',
{
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
},
now,
)
const consumeEventType = consumeAlreadyCompleted
? 'kuaishou_cloud_consume_already_completed'
: 'kuaishou_cloud_consume_skipped'
await createTaskEvent(
task.id,
consumeEventType,
{
ticketCodeMasked: maskCode(ticketCode),
shopId: flow.consume.shopId,
shopName: flow.consume.shopName,
consumeStatus,
consumeMode: 'legacy_writeoff_disabled',
errorMessage: consumeErrorMessage,
},
now,
)
return {
task: mapTaskActionPayload(updatedTask),
task: mapTaskActionPayload(result.task),
}
}