重构 lewan 履约:拆分发货模块并统一 registry 入口
将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例, claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user