From 9c981991a0d5a1b5f15cd3c60a6d0ea21271d04e Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 28 May 2026 09:05:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95mock=E5=92=8C=E9=A2=86?= =?UTF-8?q?=E5=8F=96=E9=A1=B5=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/package.json | 1 + .../scripts/mock-kuaishou-cloud-claim.ts | 445 ++++++++++++++++++ .../claim/kuaishou-cloud-claim-context.ts | 2 +- .../claim/kuaishou-cloud-claim-service.ts | 193 +++++++- .../kuaishou-cloud/KuaishouCloudClaimView.vue | 9 +- .../components/ClaimHeaderCard.vue | 102 +++- .../components/ClaimTicketStep.vue | 3 +- docs/履约配置/快手Cloud领取mock.md | 159 +++++++ 8 files changed, 855 insertions(+), 59 deletions(-) create mode 100644 apps/backend/scripts/mock-kuaishou-cloud-claim.ts create mode 100644 docs/履约配置/快手Cloud领取mock.md diff --git a/apps/backend/package.json b/apps/backend/package.json index 515af1cf..a01d9187 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -9,6 +9,7 @@ "db:migrate": "tsx src/db/migrate.ts", "dev": "tsx watch --clear-screen=false src/index.ts", "format": "prettier --write .", + "mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts", "mock:open91": "tsx scripts/mock-open91-order.ts", "test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)", "typecheck": "tsc -p tsconfig.json --noEmit", diff --git a/apps/backend/scripts/mock-kuaishou-cloud-claim.ts b/apps/backend/scripts/mock-kuaishou-cloud-claim.ts new file mode 100644 index 00000000..c0d870a5 --- /dev/null +++ b/apps/backend/scripts/mock-kuaishou-cloud-claim.ts @@ -0,0 +1,445 @@ +import process from 'node:process' + +import { closeDb } from '../src/db/client.js' +import { createOrder, findOrderByPlatformOrderId } from '../src/repositories/order-repo.js' +import { replaceOrderItems } from '../src/repositories/order-item-repo.js' +import { createTask, updateTask } from '../src/repositories/task-repo.js' +import { + getFulfillmentProfileByKey, + upsertFulfillmentProfile, +} from '../src/repositories/fulfillment-profile-repo.js' +import { buildClaimUrl, createTaskClaimToken } from '../src/services/claim/claim-service.js' +import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../src/services/open-91/config.js' +import { parseOpen91ProductNo } from '../src/services/platforms/ninetyone/order-service.js' +import { isProductionLike } from '../src/config/runtime-validation.js' +import { addHours, nowIso } from '../src/utils/time.js' +import { randomId } from '../src/utils/random.js' + +type JsonObject = Record +type DeliveryItem = { + cloudSkuId: number + cloudSkuName: string + quantity: number +} + +const args = parseArgs(process.argv.slice(2)) + +if (args.help) { + printHelp() + process.exit(0) +} + +if (isProductionLike() && args.force !== true) { + console.error('当前是生产环境,已拒绝生成 mock 数据。如确实要执行,请追加 --force。') + process.exit(1) +} + +const step = normalizeStep(args.step) +const orderNo = String(args.orderNo || `MOCK91${Date.now()}`).trim() +const productNo = String(args.productNo || '套餐_1----3676797936').trim() +const productInfo = parseOpen91ProductNo(productNo) +const productName = productInfo.productName || productInfo.rawProductNo || '套餐_1' +const consumeShopId = productInfo.shopId || String(args.kuaishouShopId || '3676797936').trim() +const deliveryItems = parseDeliveryItems(args.items) +const now = nowIso() +const existingOrder = await findOrderByPlatformOrderId({ + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + shopId: OPEN_91_PROVIDER, + platformOrderId: orderNo, +}) + +if (existingOrder) { + console.error(`订单号已存在:${orderNo}`) + console.error('请换一个 --orderNo,或不传 orderNo 让脚本自动生成。') + await closeDb() + process.exit(1) +} + +try { + const profile = await ensureKuaishouCloudProfile(now) + const order = await createOrder({ + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + shopId: OPEN_91_PROVIDER, + shopName: '91卡券', + platformOrderId: orderNo, + orderStatus: 'paid', + payStatus: 'paid', + buyerId: 'mock-buyer', + buyerName: 'mock 领取客户', + receiverContact: '', + totalAmount: 1, + currency: 'CNY', + rawPayloadJson: JSON.stringify({ + source: OPEN_91_PROVIDER, + mock: true, + receivedAt: now, + body: { + orderNo, + productNo, + productName, + buyNum: 1, + maxAmount: '0.01', + }, + }), + paidAt: now, + createdAt: now, + updatedAt: now, + }) + + if (!order) { + throw new Error('订单创建失败') + } + + const [orderItem] = await replaceOrderItems(order.id, [{ + skuCode: productName, + skuName: productName, + quantity: 1, + specJson: JSON.stringify({ + source: OPEN_91_PROVIDER, + orderNo, + productNo, + productName, + shopId: consumeShopId, + }), + itemSnapshotJson: JSON.stringify({ + source: OPEN_91_PROVIDER, + orderNo, + productNo, + productName, + shopId: consumeShopId, + cloudtentacles: { + matchMode: 'mock', + normalizedProductName: productName, + cloudSourceKeys: ['mock-cloudtentacles'], + resolvedSourceKey: 'mock-cloudtentacles', + deliveryItems, + }, + }), + createdAt: now, + updatedAt: now, + }]) + + if (!orderItem) { + throw new Error('订单商品创建失败') + } + + const task = await createTask({ + orderId: order.id, + orderItemId: orderItem.id, + unitIndex: 1, + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + shopId: OPEN_91_PROVIDER, + shopName: '91卡券', + platformOrderId: orderNo, + taskNo: randomId('DT'), + profileId: profile.id, + executorKey: 'kuaishou_ct_assisted', + taskStatus: resolveInitialTaskStatus(step), + deliveryStatus: step === 'result' ? 'success' : 'pending', + resultCode: step === 'result' ? 'mock_success' : '', + resultMessage: step === 'result' ? '开发 mock 已模拟兑换成功' : '', + claimToken: '', + claimExpiresAt: null, + automationMode: 'manual', + requiresClaim: false, + userActionStatus: step === 'result' ? 'not_required' : 'pending_claim', + attemptCount: 0, + lastError: '', + contextJson: JSON.stringify(buildMockTaskContext({ + step, + orderNo, + productName, + productNo, + consumeShopId, + deliveryItems, + timestamp: now, + })), + createdAt: now, + updatedAt: now, + }) + + if (!task) { + throw new Error('履约任务创建失败') + } + + const claimToken = await createTaskClaimToken(task.id) + const claimUrl = buildClaimUrl(claimToken.token) + await updateTask(task.id, { + claim_token: claimToken.token, + claim_expires_at: claimToken.expired_at, + ...(step !== 'ticket' ? { claimed_at: now } : {}), + ...(step === 'confirm' || step === 'result' ? { role_confirmed_at: now } : {}), + ...(step === 'result' ? { redeemed_at: now } : {}), + updated_at: now, + }) + + console.log('已生成快手 Cloud 领取 mock 数据:') + console.log(` 91订单号:${orderNo}`) + console.log(` 商品:${productName}`) + console.log(` 当前步骤:${step}`) + console.log(` 领取链接:${claimUrl}`) + console.log(` 前端本地链接:${buildFrontendClaimUrl(claimToken.token, args.frontendBaseUrl)}`) + console.log('') + console.log('页面核销码可填:MOCK') + console.log(`91 查询接口可用这个订单号测试:npm run mock:open91 -- --mode=query --orderNo=${orderNo}`) +} finally { + await closeDb() +} + +async function ensureKuaishouCloudProfile(timestamp: string) { + const existing = await getFulfillmentProfileByKey('kuaishou_ct_assisted') + if (existing) { + return existing + } + + const profile = await upsertFulfillmentProfile({ + profileKey: 'kuaishou_ct_assisted', + name: '快手 cloud 履约', + executorKey: 'kuaishou_ct_assisted', + requiresClaim: false, + autoDispatch: false, + inventoryStrategy: 'external_platform', + configJson: '{}', + createdAt: timestamp, + updatedAt: timestamp, + }) + + if (!profile) { + throw new Error('履约配置创建失败') + } + + return profile +} + +function buildMockTaskContext({ + step, + orderNo, + productName, + productNo, + consumeShopId, + deliveryItems, + timestamp, +}: { + step: string + orderNo: string + productName: string + productNo: string + consumeShopId: string + deliveryItems: DeliveryItem[] + timestamp: string +}) { + const ticketVerified = ['binding', 'confirm', 'result'].includes(step) + const roleReady = ['binding', 'confirm', 'result'].includes(step) + const roleConfirmed = ['confirm', 'result'].includes(step) + const completed = step === 'result' + const primaryItem = deliveryItems[0] || { + cloudSkuId: 910001, + cloudSkuName: 'Mock 商品', + quantity: 1, + } + + return { + profileKey: 'kuaishou_ct_assisted', + profileName: '快手 cloud 履约', + skuCode: productName, + skuName: productName, + kuaishouCloudFulfillment: { + flowType: 'kuaishou_cloud_fulfillment', + configId: `mock:${productName}`, + internalSkuCode: productName, + internalSkuName: productName, + deliveryItems, + mock: { + enabled: true, + orderNo, + productNo, + createdAt: timestamp, + }, + ticket: { + code: ticketVerified ? `MOCK-${orderNo}` : '', + status: ticketVerified ? 'verified' : 'pending', + capturedAt: ticketVerified ? timestamp : null, + capturedBy: ticketVerified ? { source: 'mock_script' } : null, + verifiedAt: ticketVerified ? timestamp : null, + oid: ticketVerified ? `MOCK-OID-${orderNo}` : '', + formToken: ticketVerified ? `MOCK-FORM-${orderNo}` : '', + leftCount: ticketVerified ? 1 : 0, + goodsTitle: ticketVerified ? productName : '', + }, + binding: { + prepareStatus: roleReady ? 'ready' : 'pending', + cloudSourceKeys: ['mock-cloudtentacles'], + resolvedSourceKey: 'mock-cloudtentacles', + skuId: primaryItem.cloudSkuId, + skuName: primaryItem.cloudSkuName, + vnKey: '1', + vnId: roleReady ? 900001 : 0, + vnPhone: roleReady ? '13800000000' : '', + bindUrl: roleReady ? `https://example.com/mock-kuaishou-cloud-bind/${orderNo}` : '', + bindPreparedAt: roleReady ? timestamp : null, + bindExpiresAt: roleReady ? addHours(timestamp, 24) : null, + bindProbeAt: roleReady ? timestamp : null, + bindProbeStatus: roleReady ? 'success' : '', + bindProbeMessage: '', + roleName: roleReady ? '测试角色' : '', + roleId: roleReady ? '10001' : '', + }, + role: { + status: roleReady ? 'ready' : 'pending', + name: roleReady ? '测试角色' : '', + rid: roleReady ? '10001' : '', + refreshedAt: roleReady ? timestamp : null, + errorMessage: '', + rawInfo: roleReady ? { mock: true } : null, + }, + purchase: { + autoBuyEnabled: true, + minAssetReserve: 0, + usedKnapsack: false, + purchaseTriggered: false, + assetBefore: 0, + assetAfter: 0, + purchaseAt: null, + items: [], + }, + dispatch: { + status: completed ? 'success' : 'pending', + dispatchAt: completed ? timestamp : null, + dispatchBy: completed ? { source: 'mock_script' } : null, + sendType: 0, + note: completed ? '开发 mock 已模拟发货成功' : '', + items: completed ? deliveryItems : [], + }, + returnNumber: { + status: completed ? 'success' : 'pending', + returnedAt: completed ? timestamp : null, + returnedBy: completed ? { source: 'mock_script' } : null, + autoReturnEnabled: true, + }, + consume: { + status: completed ? 'success' : 'pending', + shopId: consumeShopId, + shopName: 'Mock 快手小店', + autoConsumeEnabled: true, + consumedAt: completed ? timestamp : null, + errorMessage: '', + }, + notes: '开发 mock:不调用 cloudtentacles,只用于领取页流程验证', + }, + } +} + +function parseArgs(rawArgs: string[]) { + const parsed: JsonObject = {} + + for (const rawArg of rawArgs) { + const arg = String(rawArg || '').trim() + if (!arg) continue + if (arg === '--help' || arg === '-h') { + parsed.help = true + continue + } + + const normalized = arg.startsWith('--') ? arg.slice(2) : arg + const separatorIndex = normalized.indexOf('=') + if (separatorIndex < 0) { + parsed[normalized] = true + continue + } + + const key = normalized.slice(0, separatorIndex).trim() + const value = normalized.slice(separatorIndex + 1).trim() + if (key) { + parsed[key] = value + } + } + + return parsed +} + +function normalizeStep(value: unknown) { + const normalized = String(value || 'ticket').trim() + if (['ticket', 'binding', 'confirm', 'result'].includes(normalized)) { + return normalized + } + + throw new Error('--step 只能是 ticket、binding、confirm、result') +} + +function resolveInitialTaskStatus(step: string) { + if (step === 'ticket') return 'pending_binding_prepare' + if (step === 'binding') return 'waiting_binding' + if (step === 'confirm') return 'role_confirmed' + return 'completed' +} + +function parseDeliveryItems(value: unknown): DeliveryItem[] { + const text = String(value || '').trim() + const fallback = [ + { cloudSkuId: 910001, cloudSkuName: '套餐商品 A', quantity: 1 }, + { cloudSkuId: 910002, cloudSkuName: '套餐商品 B', quantity: 2 }, + { cloudSkuId: 910003, cloudSkuName: '套餐商品 C', quantity: 1 }, + ] + + if (!text) { + return fallback + } + + const items = text + .split(',') + .map((segment) => { + const [id, name, quantity] = segment.split(':') + const cloudSkuId = Number(id) + return { + cloudSkuId, + cloudSkuName: String(name || '').trim(), + quantity: Math.max(1, Number(quantity || 1) || 1), + } + }) + .filter((item) => Number.isInteger(item.cloudSkuId) && item.cloudSkuId > 0 && item.cloudSkuName) + + return items.length > 0 ? mergeDeliveryItems(items) : fallback +} + +function mergeDeliveryItems(items: DeliveryItem[]) { + const merged = new Map() + + for (const item of items) { + const existing = merged.get(item.cloudSkuId) + if (existing) { + existing.quantity += item.quantity + existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName + continue + } + merged.set(item.cloudSkuId, { ...item }) + } + + return Array.from(merged.values()) +} + +function buildFrontendClaimUrl(token: string, rawBaseUrl: unknown) { + const baseUrl = String(rawBaseUrl || process.env.MOCK_CLAIM_FRONTEND_BASE_URL || 'http://127.0.0.1:5173').replace(/\/+$/, '') + return `${baseUrl}/#/claim/${token}` +} + +function printHelp() { + console.log(` +生成快手 Cloud 领取 mock 数据: + + npm run mock:claim + npm run mock:claim -- --step=binding + npm run mock:claim -- --step=result --productNo=套餐_1----3676797936 + npm run mock:claim -- --items=910001:商品A:1,910002:商品B:2 + +参数: + --step ticket、binding、confirm、result,默认 ticket + --orderNo 91 订单号,默认自动生成 + --productNo 91 商品名,可带 ----快手小店ID,默认 套餐_1----3676797936 + --items 套餐明细,格式 skuId:名称:数量,skuId:名称:数量 + --frontendBaseUrl 前端地址,默认 http://127.0.0.1:5173 + --force 允许在 NODE_ENV=production 执行 +`) +} diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts index c87e21ca..52fe7465 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts @@ -335,7 +335,7 @@ function isOrderItemIdentifierName(value: string, skuCode: string) { } const normalizedSkuCode = String(skuCode || '').trim() - if (normalizedSkuCode && normalized === normalizedSkuCode) { + if (normalizedSkuCode && normalized === normalizedSkuCode && /^\d{8,}$/.test(normalizedSkuCode)) { return true } 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 3d1bda31..2bc4a3e1 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -5,7 +5,7 @@ import { PROJECT_ROOT } from '../../config/runtime.js' import { createTaskEvent } from '../../repositories/task-event-repo.js' import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js' import { createHttpError } from '../../utils/http.js' -import { nowIso } from '../../utils/time.js' +import { addHours, nowIso } from '../../utils/time.js' import { getKuaishouEticketSourceConfig, listKuaishouEticketShopConfigs, @@ -83,8 +83,9 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }) } + const taskContext = parseTaskContext(context.task) const nextContext = { - ...parseTaskContext(context.task), + ...taskContext, kuaishouCloudFulfillment: { ...flow, ticket: { @@ -108,30 +109,47 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js }, } - 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 { + if (isKuaishouCloudMockContext(nextContext)) { + const mockFlow = buildMockVerifiedKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment, now) await updateTask(context.task.id, { claim_token: context.claimToken.token, claim_expires_at: context.claimToken.expired_at, - context_json: JSON.stringify(nextContext), + context_json: JSON.stringify({ + ...nextContext, + kuaishouCloudFulfillment: mockFlow, + }), + claimed_at: context.task.claimed_at || now, + task_status: 'waiting_binding', + user_action_status: 'pending_claim', last_error: '', updated_at: now, }) - } + } 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, + }) + } - if (!context.task.claimed_at) { - await updateTask(context.task.id, { - claimed_at: now, - updated_at: now, - }) + if (!context.task.claimed_at) { + await updateTask(context.task.id, { + claimed_at: now, + updated_at: now, + }) + } } await createTaskEvent(context.task.id, 'kuaishou_cloud_ticket_verified', { @@ -169,7 +187,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown) { const context = await getClaimContext(token) let task = context.task - if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') { + if ( + String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' + && !isKuaishouCloudMockTask(task) + ) { task = await syncKuaishouCloudRoleInfo(task) || task } @@ -340,12 +361,16 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) { return getKuaishouCloudClaimDetail(token) } - const refreshed = await refreshKuaishouCloudTaskRoleInfo(context.task, { - source: 'claim_page_role_confirm', - actor: { source: 'claim_page' }, - recordEvent: false, - forceProbe: true, - }) + 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.ticket.status !== 'verified') { @@ -415,6 +440,11 @@ export async function redeemKuaishouCloudClaim(token: unknown) { return getKuaishouCloudClaimDetail(token) } + if (isKuaishouCloudMockTask(lockedTask)) { + await completeMockKuaishouCloudClaimTask(lockedTask, now) + return getKuaishouCloudClaimDetail(token) + } + try { await dispatchKuaishouCloudFulfillmentTask(lockedTask, { source: 'claim_page_redeem', @@ -447,3 +477,114 @@ export async function redeemKuaishouCloudClaim(token: unknown) { return getKuaishouCloudClaimDetail(token) } + +function isKuaishouCloudMockTask(task: Partial | null | undefined) { + return isKuaishouCloudMockContext(parseTaskContext(task)) +} + +function isKuaishouCloudMockContext(context: JsonObject = {}) { + const flow = context.kuaishouCloudFulfillment + && typeof context.kuaishouCloudFulfillment === 'object' + ? 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: { + ...(source.mock && typeof source.mock === 'object' ? 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: { + ...(source.mock && typeof source.mock === 'object' ? 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: '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) +} diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue b/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue index 4c59f16c..69e8dc94 100644 --- a/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue +++ b/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue @@ -5,7 +5,6 @@ import ClaimBindingStep from './components/ClaimBindingStep.vue' import ClaimConfirmStep from './components/ClaimConfirmStep.vue' import ClaimHeaderCard from './components/ClaimHeaderCard.vue' import ClaimResultStep from './components/ClaimResultStep.vue' -import ClaimSummaryCard from './components/ClaimSummaryCard.vue' import ClaimTicketStep from './components/ClaimTicketStep.vue' import { useKuaishouCloudClaim } from './composables/useKuaishouCloudClaim' @@ -22,6 +21,7 @@ const claim = useKuaishouCloudClaim(() => props.token) :order="claim.order.value" :product="claim.product.value" :current-step="claim.currentStep.value" + :progress-text="claim.progressText.value" />
@@ -35,13 +35,6 @@ const claim = useKuaishouCloudClaim(() => props.token)
@@ -41,43 +45,66 @@ defineProps<{ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08); backdrop-filter: blur(12px); padding: 24px 28px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 16px; + position: relative; } .header-copy h1 { margin: 0; color: #0f172a; line-height: 1.3; + max-width: calc(100% - 220px); + overflow-wrap: anywhere; } .header-copy p { margin: 0; color: #64748b; line-height: 1.6; + max-width: calc(100% - 220px); + overflow-wrap: anywhere; } .header-copy { min-width: 0; - flex: 1; } .eyebrow { display: inline-flex; + max-width: calc(100% - 220px); margin-bottom: 8px; color: #2563eb; font-size: 13px; font-weight: 700; letter-spacing: 0.08em; + overflow-wrap: anywhere; } .step-indicator { + position: absolute; + top: 28px; + right: 28px; + max-width: 220px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.step-dots { display: flex; gap: 10px; } +.step-progress-text { + max-width: 100%; + color: #64748b; + font-size: 13px; + font-weight: 700; + line-height: 1.4; + text-align: right; + overflow-wrap: anywhere; +} + .step-dot { width: 34px; height: 34px; @@ -99,36 +126,47 @@ defineProps<{ .header-card { max-width: 100%; padding: 16px 18px; - flex-direction: column; - align-items: flex-start; - gap: 12px; border-radius: 16px; } .header-copy h1 { font-size: 28px; line-height: 1.22; + max-width: calc(100% - 154px); overflow-wrap: anywhere; } .header-copy p { font-size: 14px; line-height: 1.4; + max-width: calc(100% - 154px); } .eyebrow { + max-width: calc(100% - 154px); margin-bottom: 6px; font-size: 12px; } .step-indicator { - gap: 8px; + top: 14px; + right: 16px; + max-width: 148px; + gap: 6px; + } + + .step-dots { + gap: 6px; } .step-dot { - width: 30px; - height: 30px; - font-size: 15px; + width: 28px; + height: 28px; + font-size: 14px; + } + + .step-progress-text { + font-size: 12px; } } @@ -138,13 +176,33 @@ defineProps<{ } .header-copy h1 { + max-width: calc(100% - 132px); font-size: 24px; } + .header-copy p { + max-width: 100%; + } + + .eyebrow { + max-width: calc(100% - 132px); + } + + .step-indicator { + top: 12px; + right: 14px; + max-width: 128px; + gap: 5px; + } + + .step-dots { + gap: 5px; + } + .step-dot { - width: 28px; - height: 28px; - font-size: 14px; + width: 26px; + height: 26px; + font-size: 13px; } } 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 f13fae5f..b5f5fd42 100644 --- a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue @@ -24,8 +24,7 @@ const activeGuideNames = ref(['guide'])

第 1 步:提交核销码

- 请先从快手小店复制核销码。验证通过后,系统会自动准备 Cloud - 绑定资源,不需要再让客服手工点"准备绑定资源"。 + 请您先从快手小店复制核销码。验证通过后,系统会自动准备绑定资源.