diff --git a/apps/backend/src/routes/claims.ts b/apps/backend/src/routes/claims.ts index f1f0f0f2..9e866c55 100644 --- a/apps/backend/src/routes/claims.ts +++ b/apps/backend/src/routes/claims.ts @@ -38,8 +38,8 @@ router.post( '/:token/kuaishou-cloud/verify-ticket', claimWriteRateLimit, createRouteHandler((req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body), { - successMessage: '核销码校验成功', - errorMessage: '校验核销码失败', + successMessage: '核销码提交并核销成功', + errorMessage: '提交核销码失败', scope: '[claims/:token/kuaishou-cloud/verify-ticket]', }), ) diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index ca5f0c24..854f690a 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -549,8 +549,12 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment( let nextTaskStatus = 'completed' let nextResultCode = 'kuaishou_cloud_completed' let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销' + const consumeAlreadyCompleted = flow.consume.status === 'success' - if (!order) { + if (consumeAlreadyCompleted) { + consumeStatus = 'success' + consumedAt = flow.consume.consumedAt || now + } else if (!order) { consumeStatus = 'failed' consumeErrorMessage = '任务关联订单不存在,无法执行快手核销' } else if (!ticketCode) { @@ -652,7 +656,11 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment( await createTaskEvent( task.id, - consumeStatus === 'success' ? 'kuaishou_cloud_consumed' : 'kuaishou_cloud_consume_failed', + consumeAlreadyCompleted + ? 'kuaishou_cloud_consume_already_completed' + : consumeStatus === 'success' + ? 'kuaishou_cloud_consumed' + : 'kuaishou_cloud_consume_failed', { ticketCodeMasked: maskCode(ticketCode), shopId, diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index 0e95cb8f..55607e6f 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -19,7 +19,10 @@ import { resolveKuaishouEticketShopConfig, type KuaishouEticketShopConfig, } from '../platforms/kuaishou-eticket/source-config-service.js' -import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js' +import { + consumeKuaishouEticket, + queryKuaishouEticketConsumeDetail, +} from '../platforms/kuaishou-eticket/consume-service.js' import { isKuaishouEticketMockTicketCode } from '../platforms/kuaishou-eticket/mock-ticket-service.js' import { dispatchKuaishouCloudFulfillmentTask, @@ -59,12 +62,26 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }) } + const persistedTicketCode = String(flow.ticket.code || '').trim() + if (flow.consume.status === 'success') { + if (!persistedTicketCode || persistedTicketCode === ticketCode) { + return getKuaishouCloudClaimDetail(token) + } + + throw createHttpError('当前领取链接已完成核销,不能更换核销码', { + statusCode: 409, + errorCode: 'claim_kuaishou_cloud_ticket_already_consumed', + }) + } + const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode) let shopId = String( flow.consume.shopId || context.order.shop_id || (mockTicketCode ? 'mock' : ''), ).trim() let shopName = String(flow.consume.shopName || context.order.shop_name || '').trim() let detailResult + let shopCookie = '' + let consumeBaseUrl = '' if (mockTicketCode) { detailResult = await queryKuaishouEticketConsumeDetail({ @@ -82,6 +99,8 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js detailResult = matched.detailResult shopId = matched.shopConfig.shopId || shopId shopName = matched.shopConfig.kshopName || shopName + shopCookie = matched.shopConfig.cookie + consumeBaseUrl = getKuaishouEticketSourceConfig().baseUrl } if (!detailResult.ok || detailResult.alreadyConsumed || !detailResult.detail) { @@ -93,6 +112,22 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }) } + const consumePayload = { + baseUrl: consumeBaseUrl, + cookie: shopCookie, + eTicketId: detailResult.eTicketId || ticketCode, + oid: String(detailResult.detail.oid || '').trim(), + formToken: String(detailResult.detail.formToken || '').trim(), + num: Number(detailResult.detail.leftCount || 0) || 1, + } + const consumeResult = await consumeKuaishouEticket(consumePayload) + if (!consumeResult.consumed) { + throw createHttpError(consumeResult.errorMessage || '核销失败,请确认核销码状态后重试', { + statusCode: 409, + errorCode: 'claim_kuaishou_cloud_consume_failed', + }) + } + const taskContext = parseTaskContext(context.task) const nextContext = { ...taskContext, @@ -114,7 +149,11 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }, consume: { ...flow.consume, + status: 'success', shopId, + shopName, + consumedAt: now, + errorMessage: '', }, }, } @@ -136,31 +175,29 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }) } else { const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment) - if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) { - await prepareKuaishouCloudFulfillmentTask( - { - ...context.task, - context_json: JSON.stringify(nextContext), - }, - { - source: 'claim_page_ticket_verified', - actor: { source: 'claim_page' }, - }, - ) - } else { - await updateTask(context.task.id, { - claim_token: context.claimToken.token, - claim_expires_at: context.claimToken.expired_at, - context_json: JSON.stringify(nextContext), - last_error: '', - updated_at: now, - }) + const taskWithConsumedTicket = (await updateTask(context.task.id, { + claim_token: context.claimToken.token, + claim_expires_at: context.claimToken.expired_at, + context_json: JSON.stringify(nextContext), + claimed_at: context.task.claimed_at || now, + task_status: TASK_STATUS.WAITING_BINDING, + user_action_status: 'pending_claim', + last_error: '', + updated_at: now, + })) || { + ...context.task, + context_json: JSON.stringify(nextContext), + claimed_at: context.task.claimed_at || now, + task_status: TASK_STATUS.WAITING_BINDING, + user_action_status: 'pending_claim', + last_error: '', + updated_at: now, } - if (!context.task.claimed_at) { - await updateTask(context.task.id, { - claimed_at: now, - updated_at: now, + if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) { + await prepareKuaishouCloudFulfillmentTask(taskWithConsumedTicket, { + source: 'claim_page_ticket_verified', + actor: { source: 'claim_page' }, }) } } @@ -174,6 +211,23 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js shopName, goodsTitle: String(detailResult.goods?.itemTitle || '').trim(), mock: mockTicketCode, + consumedAt: now, + consumeAlreadyCompleted: consumeResult.alreadyConsumed === true, + }, + now, + ) + + await createTaskEvent( + context.task.id, + 'kuaishou_cloud_consumed', + { + source: 'claim_page_ticket_submit', + ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode), + shopId, + shopName, + consumeStatus: 'success', + consumedAt: now, + mock: mockTicketCode, }, now, ) diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts index aeb2fb7e..a3387f6b 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts @@ -523,8 +523,12 @@ export async function returnKuaishouCloudFulfillmentTask( let nextTaskStatus: TaskStatus = TASK_STATUS.COMPLETED; let nextResultCode = "kuaishou_cloud_completed"; let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销"; + const consumeAlreadyCompleted = flow.consume.status === "success"; - if (!order) { + if (consumeAlreadyCompleted) { + consumeStatus = "success"; + consumedAt = flow.consume.consumedAt || now; + } else if (!order) { consumeStatus = "failed"; consumeErrorMessage = "任务关联订单不存在,无法执行快手核销"; } else if (!ticketCode) { @@ -626,9 +630,11 @@ export async function returnKuaishouCloudFulfillmentTask( await createTaskEvent( task.id, - consumeStatus === "success" - ? "kuaishou_cloud_consumed" - : "kuaishou_cloud_consume_failed", + consumeAlreadyCompleted + ? "kuaishou_cloud_consume_already_completed" + : consumeStatus === "success" + ? "kuaishou_cloud_consumed" + : "kuaishou_cloud_consume_failed", { source: String(options.source || "system").trim() || "system", ticketCodeMasked: maskCode(ticketCode), diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue index b5f5fd42..e9507005 100644 --- a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue @@ -24,7 +24,7 @@ const activeGuideNames = ref(['guide'])

第 1 步:提交核销码

- 请您先从快手小店复制核销码。验证通过后,系统会自动准备绑定资源. + 请您先从快手小店复制核销码。提交后会立即核销,核销成功后系统会自动准备绑定资源。

- 验证核销码并继续 + 提交核销码并继续
{{ - isBindingPreparing - ? '绑定资源会在核销通过后自动准备。' - : '核销完成后会自动进入扫码绑定步骤。' + isBindingPreparing ? '核销完成后会自动准备绑定资源。' : '核销完成后会自动进入扫码绑定步骤。' }}
diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts index 681a8038..307c31e9 100644 --- a/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts +++ b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts @@ -116,7 +116,7 @@ export function useKuaishouCloudClaim(token: () => string) { if (isTicketVerified.value) { return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候' } - return '等待提交核销码' + return '等待提交并核销' }) const resultTitle = computed(() => { @@ -198,7 +198,7 @@ export function useKuaishouCloudClaim(token: () => string) { ticketCode: normalizedTicketCode, }) await applyDetail(response.data) - showSuccess('核销码验证通过,系统已开始准备绑定资源') + showSuccess('核销已完成,系统已开始准备绑定资源') syncPolling() } catch (error) { showError(error instanceof Error ? error.message : '核销码验证失败')