重构 lewan 履约:拆分发货模块并统一 registry 入口
将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例, claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import {
|
||||
getTaskById,
|
||||
updateTask,
|
||||
updateTaskStatusIfCurrent,
|
||||
} from '../../../repositories/task-repo.js'
|
||||
import {
|
||||
TASK_STATUS,
|
||||
canRedeemKuaishouCloudClaimStatus,
|
||||
isKuaishouCloudRedeemSettledStatus,
|
||||
normalizeTaskStatus,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import {
|
||||
assertBoundUidMatchesExpected,
|
||||
assertClaimExpectedUidReady,
|
||||
getClaimIdentityFromTask,
|
||||
} from '../../claim/claim-identity.js'
|
||||
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow, type JsonObject } from './domain.js'
|
||||
import { dispatchKuaishouCloudFulfillmentTask } from './dispatch-fulfillment.js'
|
||||
import {
|
||||
completeMockKuaishouCloudTask,
|
||||
isKuaishouCloudMockTask,
|
||||
} from './mock-helpers.js'
|
||||
import { normalizeActor, parseTaskContext } from './task-context.js'
|
||||
|
||||
export type RedeemKuaishouCloudTaskResult = {
|
||||
task: TaskRow
|
||||
/** 是否已处于终态/已兑换,调用方无需再 dispatch */
|
||||
alreadySettled: boolean
|
||||
/** 并发抢锁失败(他人已推进) */
|
||||
concurrentSkipped: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* lewan 一键兑换用例(履约引擎入口):
|
||||
* waiting_binding + UID 匹配 → role_confirmed → redeeming → dispatch(+autoFinalize)
|
||||
*
|
||||
* claim / admin 应只调此函数,不要直接拼状态机。
|
||||
*/
|
||||
export async function redeemKuaishouCloudTask(
|
||||
task: TaskRow,
|
||||
options: JsonObject = {},
|
||||
): Promise<RedeemKuaishouCloudTaskResult> {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = String(options.now || '').trim() || nowIso()
|
||||
const source = String(options.source || 'system_redeem').trim() || 'system_redeem'
|
||||
const actor = normalizeActor(options.actor) || { source }
|
||||
const autoFinalize = options.autoFinalize !== false
|
||||
const errorCodePrefix =
|
||||
String(options.errorCodePrefix || 'kuaishou_cloud_redeem').trim() || 'kuaishou_cloud_redeem'
|
||||
|
||||
let workingTask = task
|
||||
const currentStatus = normalizeTaskStatus(workingTask.task_status)
|
||||
|
||||
if (isKuaishouCloudRedeemSettledStatus(currentStatus)) {
|
||||
return { task: workingTask, alreadySettled: true, concurrentSkipped: false }
|
||||
}
|
||||
|
||||
if (!canRedeemKuaishouCloudClaimStatus(currentStatus)) {
|
||||
throw createHttpError('当前状态不可兑换,请先完成绑定并匹配 UID', {
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_not_ready`,
|
||||
})
|
||||
}
|
||||
|
||||
// WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换
|
||||
if (currentStatus === TASK_STATUS.WAITING_BINDING) {
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flow, {
|
||||
errorCodePrefix,
|
||||
})
|
||||
} 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: `${source}_one_click`,
|
||||
},
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
const flowBeforeRedeem = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flowBeforeRedeem, {
|
||||
errorCodePrefix,
|
||||
})
|
||||
} 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) {
|
||||
const latest = (await getTaskById(workingTask.id)) || workingTask
|
||||
return {
|
||||
task: latest,
|
||||
alreadySettled: isKuaishouCloudRedeemSettledStatus(latest.task_status),
|
||||
concurrentSkipped: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (isKuaishouCloudMockTask(lockedTask)) {
|
||||
await completeMockKuaishouCloudTask(lockedTask, now)
|
||||
const completed = (await getTaskById(lockedTask.id)) || lockedTask
|
||||
return { task: completed, alreadySettled: false, concurrentSkipped: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await dispatchKuaishouCloudFulfillmentTask(lockedTask, {
|
||||
source,
|
||||
actor,
|
||||
autoFinalize,
|
||||
})
|
||||
return {
|
||||
task: result.task || lockedTask,
|
||||
alreadySettled: false,
|
||||
concurrentSkipped: false,
|
||||
}
|
||||
} 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 { task: latestTask, alreadySettled: true, concurrentSkipped: false }
|
||||
}
|
||||
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,
|
||||
errorMessage: message,
|
||||
},
|
||||
nowIso(),
|
||||
)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user