445 lines
13 KiB
TypeScript
445 lines
13 KiB
TypeScript
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<string, unknown>
|
|
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 },
|
|
]
|
|
|
|
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<number, DeliveryItem>()
|
|
|
|
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 执行
|
|
`)
|
|
}
|