重构 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
@@ -1,27 +1,22 @@
import { createTaskEvent } from '../../repositories/task-event-repo.js'
import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js'
import { updateTask } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js'
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
import { addHours, nowIso } from '../../utils/time.js'
import { asJsonObject, type JsonObject } from '../../types/json.js'
import { nowIso } from '../../utils/time.js'
import type { JsonObject } from '../../types/json.js'
import { TASK_STATUS } from '../../domain/task-status.js'
import {
TASK_STATUS,
canRedeemKuaishouCloudClaimStatus,
isKuaishouCloudRedeemSettledStatus,
isKuaishouCloudRoleConfirmSettledStatus,
normalizeTaskStatus,
} from '../../domain/task-status.js'
confirmFulfillmentRole,
prepareFulfillmentBinding,
rebindFulfillmentRole,
redeemFulfillmentTask,
} from '../fulfillment/executors/registry.js'
import {
dispatchKuaishouCloudFulfillmentTask,
isKuaishouCloudMockTask,
normalizeKuaishouCloudFlow,
prepareKuaishouCloudFulfillmentTask,
rebindKuaishouCloudTaskRole,
refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud/index.js'
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.js'
import {
assertBoundUidMatchesExpected,
assertClaimExpectedUidReady,
assertValidClaimUid,
canUpdateClaimUid,
getClaimIdentityFromTask,
@@ -32,6 +27,30 @@ import type { TaskRow } from '../../types/repository/rows.js'
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
function assertLewanClaimTask(task: TaskRow) {
if (String(task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
}
async function requireExecutorAction<T>(
result: Promise<T | null> | T | null,
errorMessage: string,
errorCode: string,
): Promise<NonNullable<T>> {
const value = await result
if (!value) {
throw createHttpError(errorMessage, {
statusCode: 409,
errorCode,
})
}
return value
}
async function verifyIndustryVoucherTicket(
context: Awaited<ReturnType<typeof getClaimContext>>,
now: string,
@@ -126,7 +145,7 @@ async function verifyIndustryVoucherTicket(
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
await prepareKuaishouCloudFulfillmentTask(taskWithTicket, {
await prepareFulfillmentBinding(taskWithTicket, {
source: 'send_code_callback',
actor: { source: 'send_code_callback' },
})
@@ -262,7 +281,7 @@ export async function submitClaimUid(
const executorKey = String(updatedTask.executor_key || '').trim()
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
try {
const prepared = await prepareKuaishouCloudFulfillmentTask(updatedTask, {
const prepared = await prepareFulfillmentBinding(updatedTask, {
source: 'claim_page_submit_uid',
actor: { source: 'claim_page' },
})
@@ -279,18 +298,55 @@ export async function submitClaimUid(
export async function rebindKuaishouCloudClaimRole(token: unknown) {
const context = await getClaimContext(token)
assertLewanClaimTask(context.task)
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
await requireExecutorAction(
rebindFulfillmentRole(context.task, {
source: 'claim_page_rebind_role',
actor: { source: 'claim_page' },
}),
'当前领取链接不支持换绑角色',
'claim_rebind_not_supported',
)
await rebindKuaishouCloudTaskRole(context.task, {
source: 'claim_page_rebind_role',
actor: { source: 'claim_page' },
})
return getKuaishouCloudClaimDetail(token)
}
export async function confirmKuaishouCloudClaimRole(token: unknown) {
const context = await getClaimContext(token)
assertLewanClaimTask(context.task)
await requireExecutorAction(
confirmFulfillmentRole(context.task, {
source: 'claim_page_role_confirm',
actor: { source: 'claim_page' },
errorCodePrefix: 'claim_kuaishou_cloud',
forceProbe: true,
}),
'当前领取链接不支持确认角色',
'claim_confirm_not_supported',
)
return getKuaishouCloudClaimDetail(token)
}
/**
* 领取页一键兑换:鉴权后交给 lewan 履约引擎 redeem 用例。
*/
export async function redeemKuaishouCloudClaim(token: unknown) {
const context = await getClaimContext(token)
assertLewanClaimTask(context.task)
await requireExecutorAction(
redeemFulfillmentTask(context.task, {
source: 'claim_page_redeem',
actor: { source: 'claim_page' },
autoFinalize: true,
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
}),
'当前领取链接不支持一键兑换',
'claim_redeem_not_supported',
)
return getKuaishouCloudClaimDetail(token)
}
@@ -327,321 +383,3 @@ function normalizeIndustryVoucherContext(value: unknown): JsonObject {
consumedAt: source.consumedAt || null,
}
}
export async function confirmKuaishouCloudClaimRole(token: unknown) {
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
if (isKuaishouCloudRoleConfirmSettledStatus(context.task.task_status)) {
return getKuaishouCloudClaimDetail(token)
}
const expectedUid = assertClaimExpectedUidReady(context.task)
const contextSource = parseTaskContext(context.task)
const mockMode = isKuaishouCloudMockContext(contextSource)
const refreshed = mockMode
? { task: context.task }
: await refreshKuaishouCloudTaskRoleInfo(context.task, {
source: 'claim_page_role_confirm',
actor: { source: 'claim_page' },
recordEvent: false,
forceProbe: true,
})
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
statusCode: 409,
errorCode: 'claim_kuaishou_cloud_role_not_ready',
})
}
if (!mockMode) {
assertBoundUidMatchesExpected(refreshed.task, flow, {
errorCodePrefix: 'claim_kuaishou_cloud',
})
}
await updateTask(context.task.id, {
task_status: TASK_STATUS.ROLE_CONFIRMED,
role_id: flow.binding.roleId,
role_name: flow.binding.roleName,
user_action_status: TASK_STATUS.ROLE_CONFIRMED,
role_confirmed_at: now,
last_error: '',
updated_at: now,
})
await createTaskEvent(
context.task.id,
'kuaishou_cloud_role_confirmed',
{
expectedUid,
vnPhone: flow.binding.vnPhone,
roleName: flow.binding.roleName,
roleId: flow.binding.roleId,
},
now,
)
return getKuaishouCloudClaimDetail(token)
}
export async function redeemKuaishouCloudClaim(token: unknown) {
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
let workingTask = context.task
const currentStatus = normalizeTaskStatus(workingTask.task_status)
if (isKuaishouCloudRedeemSettledStatus(currentStatus)) {
return getKuaishouCloudClaimDetail(token)
}
if (!canRedeemKuaishouCloudClaimStatus(currentStatus)) {
throw createHttpError('当前状态不可兑换,请先完成绑定并匹配 UID', {
statusCode: 409,
errorCode: 'claim_kuaishou_cloud_not_ready_for_redeem',
})
}
// WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换
if (currentStatus === TASK_STATUS.WAITING_BINDING) {
const flow = normalizeKuaishouCloudFlow(
parseTaskContext(workingTask).kuaishouCloudFulfillment,
)
if (!isKuaishouCloudMockTask(workingTask)) {
assertBoundUidMatchesExpected(workingTask, flow, {
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
})
} else {
assertClaimExpectedUidReady(workingTask)
}
const confirmed = await updateTask(workingTask.id, {
task_status: TASK_STATUS.ROLE_CONFIRMED,
role_id: flow.binding.roleId || workingTask.role_id || '',
role_name: flow.binding.roleName || workingTask.role_name || '',
user_action_status: TASK_STATUS.ROLE_CONFIRMED,
role_confirmed_at: now,
last_error: '',
updated_at: now,
})
if (confirmed) {
workingTask = confirmed
}
await createTaskEvent(
workingTask.id,
'kuaishou_cloud_role_confirmed',
{
expectedUid: getClaimIdentityFromTask(workingTask).expectedUid,
vnPhone: flow.binding.vnPhone,
roleName: flow.binding.roleName,
roleId: flow.binding.roleId,
source: 'claim_page_one_click_redeem',
},
now,
)
}
const flowBeforeRedeem = normalizeKuaishouCloudFlow(
parseTaskContext(workingTask).kuaishouCloudFulfillment,
)
if (!isKuaishouCloudMockTask(workingTask)) {
assertBoundUidMatchesExpected(workingTask, flowBeforeRedeem, {
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
})
} else {
assertClaimExpectedUidReady(workingTask)
}
const lockedTask = await updateTaskStatusIfCurrent(workingTask.id, TASK_STATUS.ROLE_CONFIRMED, {
task_status: TASK_STATUS.REDEEMING,
user_action_status: 'not_required',
last_error: '',
updated_at: now,
})
if (!lockedTask) {
return getKuaishouCloudClaimDetail(token)
}
if (isKuaishouCloudMockTask(lockedTask)) {
await completeMockKuaishouCloudClaimTask(lockedTask, now)
return getKuaishouCloudClaimDetail(token)
}
try {
await dispatchKuaishouCloudFulfillmentTask(lockedTask, {
source: 'claim_page_redeem',
actor: { source: 'claim_page' },
autoFinalize: true,
})
} catch (error) {
const latestTask = await getTaskById(lockedTask.id)
const latestStatus = latestTask ? normalizeTaskStatus(latestTask.task_status) : ''
if (latestTask && latestStatus !== TASK_STATUS.REDEEMING) {
if (
latestStatus === TASK_STATUS.DISPATCHED_PENDING_RETURN ||
latestStatus === TASK_STATUS.COMPLETED ||
latestStatus === TASK_STATUS.REDEEMED
) {
return getKuaishouCloudClaimDetail(token)
}
throw error
}
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
await updateTask(lockedTask.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
user_action_status: 'not_required',
last_error: message,
result_code: 'kuaishou_cloud_redeem_failed',
result_message: message,
updated_at: nowIso(),
})
await createTaskEvent(
lockedTask.id,
'kuaishou_cloud_redeem_failed',
{
source: 'claim_page_redeem',
errorMessage: message,
},
nowIso(),
)
throw error
}
return getKuaishouCloudClaimDetail(token)
}
function isKuaishouCloudMockTask(task: Partial<TaskRow> | null | undefined) {
return isKuaishouCloudMockContext(parseTaskContext(task))
}
function isKuaishouCloudMockContext(context: JsonObject = {}) {
const flow =
asJsonObject(context.kuaishouCloudFulfillment)
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
}
function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
const flow = normalizeKuaishouCloudFlow(value)
const source = flow as JsonObject
const bindUrl =
flow.binding.bindUrl ||
`https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
const vnPhone = flow.binding.vnPhone || '13800000000'
const roleName = flow.binding.roleName || flow.role.name || '测试角色'
const roleId = flow.binding.roleId || flow.role.rid || '10001'
return {
...flow,
mock: {
...(asJsonObject(source.mock)),
enabled: true,
},
binding: {
...flow.binding,
prepareStatus: 'ready',
vnId: flow.binding.vnId || 900001,
vnPhone,
bindUrl,
bindPreparedAt: flow.binding.bindPreparedAt || timestamp,
bindExpiresAt: flow.binding.bindExpiresAt || addHours(timestamp, 24),
roleName,
roleId,
},
role: {
...flow.role,
status: 'ready',
name: roleName,
rid: roleId,
refreshedAt: flow.role.refreshedAt || timestamp,
errorMessage: '',
rawInfo: flow.role.rawInfo || {
mock: true,
},
},
}
}
async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: string) {
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const source = flow as JsonObject
const nextFlow = {
...flow,
mock: {
...(asJsonObject(source.mock)),
enabled: true,
},
dispatch: {
...flow.dispatch,
status: 'success',
dispatchAt: timestamp,
dispatchBy: {
source: 'claim_page_mock',
},
note: '开发 mock 已模拟发货成功',
items: flow.deliveryItems,
},
returnNumber: {
...flow.returnNumber,
status: 'success',
returnedAt: timestamp,
returnedBy: {
source: 'claim_page_mock',
},
},
consume: {
...flow.consume,
status: 'success',
consumedAt: timestamp,
errorMessage: '',
},
}
await updateTask(task.id, {
task_status: TASK_STATUS.COMPLETED,
delivery_status: 'success',
result_code: 'mock_success',
result_message: '开发 mock 已模拟兑换成功',
user_action_status: 'not_required',
last_error: '',
context_json: JSON.stringify({
...taskContext,
kuaishouCloudFulfillment: nextFlow,
}),
redeemed_at: timestamp,
updated_at: timestamp,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_mock_redeemed',
{
source: 'claim_page_mock',
deliveryItems: flow.deliveryItems,
},
timestamp,
)
}
@@ -1,8 +1,8 @@
import {
normalizeKuaishouCloudFlow,
prepareKuaishouCloudFulfillmentTask,
refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud/index.js'
prepareFulfillmentBinding,
refreshFulfillmentRole,
} from '../fulfillment/executors/registry.js'
import { normalizeKuaishouCloudFlow } from '../fulfillment/kuaishou-cloud/index.js'
import type { TaskRow } from '../../types/repository/rows.js'
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
@@ -12,11 +12,11 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
if (flow.ticket.status === 'verified' && flow.binding.prepareStatus !== 'ready') {
try {
const prepared = await prepareKuaishouCloudFulfillmentTask(task, {
const prepared = await prepareFulfillmentBinding(task, {
source: 'claim_page_retry_binding_prepare',
actor: { source: 'system' },
})
return prepared.task
return prepared?.task || task
} catch {
return task
}
@@ -28,13 +28,13 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
try {
// 只走 refresh:内部会按间隔 probe 绑链,必要时再查 bind_info
// 不再 forceProbe + 前置 probe,避免同轮重复打上游
const result = await refreshKuaishouCloudTaskRoleInfo(task, {
const result = await refreshFulfillmentRole(task, {
source: 'claim_page_polling',
actor: { source: 'system' },
recordEvent: false,
forceProbe: false,
})
return result.task || task
return result?.task || task
} catch {
return task
}