Files
order_site/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.ts
T
yml2213 792680bd19 重构 lewan 履约:拆分发货模块并统一 registry 入口
将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例,
claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
2026-07-10 22:23:26 +08:00

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
}
}