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 执行 `) }