将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例, claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
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
|
|
}
|
|
}
|