514 lines
15 KiB
JavaScript
514 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import process from 'node:process'
|
|
|
|
import { runtimeConfig } from '../src/config/runtime.js'
|
|
import { closeDb, query } from '../src/db/client.js'
|
|
import { runDatabaseMigrations } from '../src/db/migrate.js'
|
|
import { updateClaimToken } from '../src/repositories/claim-token-repo.js'
|
|
import { getFulfillmentProfileByKey, upsertSkuFulfillmentBinding } from '../src/repositories/fulfillment-profile-repo.js'
|
|
import { createInventoryItems, releaseReservedInventoryItem } from '../src/repositories/inventory-repo.js'
|
|
import { getTaskById, updateTask } from '../src/repositories/task-repo.js'
|
|
import { ensureFulfillmentCatalogBootstrapped } from '../src/services/bootstrap/fulfillment-bootstrap-service.js'
|
|
import { completeAdminTaskManualDispatch } from '../src/services/admin/admin-service.js'
|
|
import { upsertOrderFromWebhook } from '../src/services/order/order-service.js'
|
|
import { addHours, nowIso } from '../src/utils/time.js'
|
|
|
|
const RESET_TABLES = [
|
|
'message_deliveries',
|
|
'tencent_browser_contexts',
|
|
'task_events',
|
|
'claim_tokens',
|
|
'task_inventory_bindings',
|
|
'fulfillment_tasks',
|
|
'order_items',
|
|
'orders',
|
|
'webhook_events',
|
|
'product_match_rules',
|
|
'inventory_items',
|
|
]
|
|
|
|
const SEED_PROVIDER = 'seed'
|
|
const SEED_PLATFORM = 'sandbox'
|
|
const SEED_SHOP_ID = 'seed-shop'
|
|
const SEED_SHOP_NAME = 'Seed Sandbox Shop'
|
|
|
|
const options = parseArgs(process.argv.slice(2))
|
|
|
|
if (options.help) {
|
|
printHelp()
|
|
process.exit(0)
|
|
}
|
|
|
|
try {
|
|
printPreview()
|
|
|
|
if (!options.apply) {
|
|
console.log('\n当前为预览模式。确认后追加 `--apply`,需要清库时再加 `--reset`。')
|
|
process.exit(0)
|
|
}
|
|
|
|
await runDatabaseMigrations()
|
|
await ensureFulfillmentCatalogBootstrapped()
|
|
|
|
if (options.reset) {
|
|
await query(`TRUNCATE TABLE ${RESET_TABLES.join(', ')} RESTART IDENTITY CASCADE`)
|
|
console.log('\n已先清空测试业务数据。')
|
|
}
|
|
|
|
await ensureSeedBindings()
|
|
await createSeedInventory()
|
|
|
|
const manualPending = await seedScenario({
|
|
scenarioKey: 'manual_pending',
|
|
orderLabel: 'SEED-MANUAL-PENDING',
|
|
skuCode: 'seed-manual-pending',
|
|
skuName: '开发种子-人工履约待处理',
|
|
})
|
|
|
|
const manualCompleted = await seedScenario({
|
|
scenarioKey: 'manual_completed',
|
|
orderLabel: 'SEED-MANUAL-COMPLETED',
|
|
skuCode: 'seed-manual-completed',
|
|
skuName: '开发种子-人工履约已完成',
|
|
afterCreate: async (result) => {
|
|
const task = result.tasks[0]
|
|
if (!task) {
|
|
return null
|
|
}
|
|
|
|
await completeAdminTaskManualDispatch(task.id, {
|
|
outcome: 'delivered',
|
|
resultMessage: '开发种子数据:人工履约已完成',
|
|
deliveryReference: 'SEED-MANUAL-REF-001',
|
|
deliveredCredential: '开发种子数据:已通过人工渠道发放',
|
|
})
|
|
|
|
return getTaskById(task.id)
|
|
},
|
|
})
|
|
|
|
const claimReady = await seedScenario({
|
|
scenarioKey: 'claim_ready',
|
|
orderLabel: 'SEED-CLAIM-READY',
|
|
skuCode: 'seed-claim-ready',
|
|
skuName: '开发种子-领取兑换可用',
|
|
})
|
|
|
|
const claimClaimed = await seedScenario({
|
|
scenarioKey: 'claim_claimed',
|
|
orderLabel: 'SEED-CLAIM-CLAIMED',
|
|
skuCode: 'seed-claim-claimed',
|
|
skuName: '开发种子-已打开领取链接',
|
|
afterCreate: async (result) => applyClaimedState(result.tasks[0]),
|
|
})
|
|
|
|
const claimRoleConfirmed = await seedScenario({
|
|
scenarioKey: 'claim_role_confirmed',
|
|
orderLabel: 'SEED-CLAIM-ROLE',
|
|
skuCode: 'seed-claim-role-confirmed',
|
|
skuName: '开发种子-已确认角色',
|
|
afterCreate: async (result) => applyRoleConfirmedState(result.tasks[0]),
|
|
})
|
|
|
|
const claimRetryPending = await seedScenario({
|
|
scenarioKey: 'claim_retry_pending',
|
|
orderLabel: 'SEED-CLAIM-RETRY',
|
|
skuCode: 'seed-claim-retry',
|
|
skuName: '开发种子-等待重试',
|
|
afterCreate: async (result) => applyRetryPendingState(result.tasks[0]),
|
|
})
|
|
|
|
const claimExpired = await seedScenario({
|
|
scenarioKey: 'claim_expired',
|
|
orderLabel: 'SEED-CLAIM-EXPIRED',
|
|
skuCode: 'seed-claim-expired',
|
|
skuName: '开发种子-领取链接已过期',
|
|
afterCreate: async (result) => applyExpiredState(result.tasks[0]),
|
|
})
|
|
|
|
const claimWaitingInventory = await seedScenario({
|
|
scenarioKey: 'claim_waiting_inventory',
|
|
orderLabel: 'SEED-CLAIM-WAIT',
|
|
skuCode: 'seed-claim-wait',
|
|
skuName: '开发种子-等待库存',
|
|
})
|
|
|
|
printResultSummary([
|
|
manualPending,
|
|
manualCompleted,
|
|
claimReady,
|
|
claimClaimed,
|
|
claimRoleConfirmed,
|
|
claimRetryPending,
|
|
claimExpired,
|
|
claimWaitingInventory,
|
|
])
|
|
} catch (error) {
|
|
console.error('\n写入开发种子数据失败:', error instanceof Error ? error.message : String(error || '未知错误'))
|
|
process.exitCode = 1
|
|
} finally {
|
|
await closeDb()
|
|
}
|
|
|
|
async function ensureSeedBindings() {
|
|
const timestamp = nowIso()
|
|
const manualProfile = await getRequiredProfile('manual_review')
|
|
const claimProfile = await getRequiredProfile('tencent_claim_redeem')
|
|
|
|
const bindings = [
|
|
{ skuCode: 'seed-manual-pending', profileId: manualProfile.id, priority: 10 },
|
|
{ skuCode: 'seed-manual-completed', profileId: manualProfile.id, priority: 20 },
|
|
{ skuCode: 'seed-claim-ready', profileId: claimProfile.id, priority: 30 },
|
|
{ skuCode: 'seed-claim-claimed', profileId: claimProfile.id, priority: 40 },
|
|
{ skuCode: 'seed-claim-role-confirmed', profileId: claimProfile.id, priority: 50 },
|
|
{ skuCode: 'seed-claim-retry', profileId: claimProfile.id, priority: 60 },
|
|
{ skuCode: 'seed-claim-expired', profileId: claimProfile.id, priority: 70 },
|
|
{ skuCode: 'seed-claim-wait', profileId: claimProfile.id, priority: 80 },
|
|
]
|
|
|
|
for (const binding of bindings) {
|
|
await upsertSkuFulfillmentBinding({
|
|
skuCode: binding.skuCode,
|
|
provider: SEED_PROVIDER,
|
|
platform: SEED_PLATFORM,
|
|
shopId: SEED_SHOP_ID,
|
|
profileId: binding.profileId,
|
|
enabled: true,
|
|
priority: binding.priority,
|
|
configJson: JSON.stringify({
|
|
seededBy: 'scripts/seed-dev-data.js',
|
|
scenario: binding.skuCode,
|
|
}),
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function createSeedInventory() {
|
|
const timestamp = nowIso()
|
|
|
|
await createInventoryItems([
|
|
{
|
|
skuCode: 'seed-claim-ready',
|
|
displayValue: 'SEED-CLAIM-CODE-READY-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-CLAIM-CODE-READY-001',
|
|
scenario: 'claim_ready',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
{
|
|
skuCode: 'seed-claim-claimed',
|
|
displayValue: 'SEED-CLAIM-CODE-CLAIMED-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-CLAIM-CODE-CLAIMED-001',
|
|
scenario: 'claim_claimed',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
{
|
|
skuCode: 'seed-claim-role-confirmed',
|
|
displayValue: 'SEED-CLAIM-CODE-ROLE-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-CLAIM-CODE-ROLE-001',
|
|
scenario: 'claim_role_confirmed',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
{
|
|
skuCode: 'seed-claim-retry',
|
|
displayValue: 'SEED-CLAIM-CODE-RETRY-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-CLAIM-CODE-RETRY-001',
|
|
scenario: 'claim_retry_pending',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
{
|
|
skuCode: 'seed-claim-expired',
|
|
displayValue: 'SEED-CLAIM-CODE-EXPIRED-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-CLAIM-CODE-EXPIRED-001',
|
|
scenario: 'claim_expired',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
{
|
|
skuCode: 'seed-spare-available',
|
|
displayValue: 'SEED-SPARE-CODE-001',
|
|
batchNo: 'seed-20260409',
|
|
credentialType: 'tencent_code',
|
|
payload: {
|
|
code: 'SEED-SPARE-CODE-001',
|
|
scenario: 'spare_available',
|
|
},
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
},
|
|
])
|
|
}
|
|
|
|
async function seedScenario({ scenarioKey, orderLabel, skuCode, skuName, afterCreate = null }) {
|
|
const result = await upsertOrderFromWebhook(buildSeedOrderEvent({
|
|
scenarioKey,
|
|
orderLabel,
|
|
skuCode,
|
|
skuName,
|
|
}))
|
|
|
|
const task = typeof afterCreate === 'function'
|
|
? await afterCreate(result)
|
|
: result.tasks[0] || null
|
|
|
|
return {
|
|
scenarioKey,
|
|
orderId: result.order.id,
|
|
platformOrderId: result.order.platform_order_id,
|
|
taskId: task?.id || null,
|
|
taskNo: task?.task_no || '',
|
|
taskStatus: task?.task_status || '',
|
|
deliveryStatus: task?.delivery_status || '',
|
|
inventoryStatus: task?.inventory_status || '',
|
|
inventoryItemId: Number(task?.primary_inventory_item_id || 0) || null,
|
|
claimTokenId: Number(task?.primary_claim_token_id || 0) || null,
|
|
executorKey: String(task?.executor_key || '').trim(),
|
|
}
|
|
}
|
|
|
|
async function applyClaimedState(task) {
|
|
if (!task) {
|
|
return null
|
|
}
|
|
|
|
const now = nowIso()
|
|
await updateTask(task.id, {
|
|
task_status: 'claimed',
|
|
user_action_status: 'claimed',
|
|
browser_session_id: `seed-browser-${task.id}`,
|
|
login_type: 'qq',
|
|
claimed_at: now,
|
|
updated_at: now,
|
|
last_error: '',
|
|
})
|
|
|
|
return getTaskById(task.id)
|
|
}
|
|
|
|
async function applyRoleConfirmedState(task) {
|
|
if (!task) {
|
|
return null
|
|
}
|
|
|
|
const claimedAt = addHours(nowIso(), -1)
|
|
const roleConfirmedAt = nowIso()
|
|
await updateTask(task.id, {
|
|
task_status: 'role_confirmed',
|
|
user_action_status: 'role_confirmed',
|
|
browser_session_id: `seed-browser-${task.id}`,
|
|
login_type: 'qq',
|
|
nickname: 'SeedHero',
|
|
role_id: 'seed-role-1001',
|
|
role_name: '开发角色-已确认',
|
|
area: 'seed-area',
|
|
partition_name: 'seed-partition',
|
|
claimed_at: claimedAt,
|
|
role_confirmed_at: roleConfirmedAt,
|
|
updated_at: roleConfirmedAt,
|
|
last_error: '',
|
|
})
|
|
|
|
return getTaskById(task.id)
|
|
}
|
|
|
|
async function applyRetryPendingState(task) {
|
|
if (!task) {
|
|
return null
|
|
}
|
|
|
|
const claimedAt = addHours(nowIso(), -2)
|
|
const roleConfirmedAt = addHours(nowIso(), -1)
|
|
const updatedAt = nowIso()
|
|
await updateTask(task.id, {
|
|
task_status: 'retry_pending',
|
|
delivery_status: 'pending',
|
|
inventory_status: 'reserved',
|
|
user_action_status: 'role_confirmed',
|
|
browser_session_id: `seed-browser-${task.id}`,
|
|
login_type: 'qq',
|
|
nickname: 'SeedRetryUser',
|
|
role_id: 'seed-role-retry',
|
|
role_name: '开发角色-待重试',
|
|
area: 'seed-area',
|
|
partition_name: 'seed-partition',
|
|
claimed_at: claimedAt,
|
|
role_confirmed_at: roleConfirmedAt,
|
|
attempt_count: 2,
|
|
last_error: '开发种子数据:模拟兑换失败,等待重试',
|
|
updated_at: updatedAt,
|
|
})
|
|
|
|
return getTaskById(task.id)
|
|
}
|
|
|
|
async function applyExpiredState(task) {
|
|
if (!task) {
|
|
return null
|
|
}
|
|
|
|
const now = nowIso()
|
|
if (task.primary_claim_token_id) {
|
|
await updateClaimToken(task.primary_claim_token_id, {
|
|
status: 'expired',
|
|
expired_at: addHours(now, -2),
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
if (task.primary_inventory_item_id) {
|
|
await releaseReservedInventoryItem(task.primary_inventory_item_id, now)
|
|
}
|
|
|
|
await updateTask(task.id, {
|
|
task_status: 'expired',
|
|
inventory_status: 'pending',
|
|
user_action_status: 'expired',
|
|
last_error: '领取链接已过期,预占库存项已释放',
|
|
updated_at: now,
|
|
})
|
|
|
|
return getTaskById(task.id)
|
|
}
|
|
|
|
function buildSeedOrderEvent({ scenarioKey, orderLabel, skuCode, skuName }) {
|
|
const timestamp = nowIso()
|
|
|
|
return {
|
|
provider: SEED_PROVIDER,
|
|
platform: SEED_PLATFORM,
|
|
shopId: SEED_SHOP_ID,
|
|
shopName: SEED_SHOP_NAME,
|
|
platformOrderId: orderLabel,
|
|
orderStatus: 'paid',
|
|
payStatus: 'paid',
|
|
buyerId: `${scenarioKey}-buyer`,
|
|
buyerName: `开发种子-${scenarioKey}`,
|
|
receiverContact: `seed+${scenarioKey}@example.com`,
|
|
totalAmount: 100,
|
|
currency: 'CNY',
|
|
paidAt: timestamp,
|
|
rawPayload: {
|
|
source: 'seed-dev-data',
|
|
scenarioKey,
|
|
platformOrderId: orderLabel,
|
|
},
|
|
items: [
|
|
{
|
|
skuCode,
|
|
skuName,
|
|
quantity: 1,
|
|
spec: {
|
|
title: skuName,
|
|
scenarioKey,
|
|
},
|
|
snapshot: {
|
|
title: skuName,
|
|
scenarioKey,
|
|
seeded: true,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
async function getRequiredProfile(profileKey) {
|
|
const profile = await getFulfillmentProfileByKey(profileKey)
|
|
|
|
if (!profile) {
|
|
throw new Error(`缺少履约档案: ${profileKey}`)
|
|
}
|
|
|
|
return profile
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
return {
|
|
apply: argv.includes('--apply'),
|
|
reset: argv.includes('--reset'),
|
|
help: argv.includes('-h') || argv.includes('--help'),
|
|
}
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`
|
|
用法:
|
|
node scripts/seed-dev-data.js [--apply] [--reset]
|
|
|
|
说明:
|
|
基于当前 PostgreSQL fulfillment schema 生成一组可重复验证的开发种子数据。
|
|
|
|
场景:
|
|
- manual_pending: 人工履约待处理
|
|
- manual_completed: 人工履约已完成
|
|
- claim_ready: 已预占库存并生成领取链接
|
|
- claim_claimed: 用户已打开领取链接
|
|
- claim_role_confirmed: 用户已确认角色
|
|
- claim_retry_pending: 自动履约失败等待重试
|
|
- claim_expired: 领取链接已过期
|
|
- claim_waiting_inventory: 等待库存
|
|
|
|
参数:
|
|
--apply 实际写入数据
|
|
--reset 写入前先清空测试业务数据
|
|
|
|
数据库连接:
|
|
${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'}
|
|
`.trim())
|
|
}
|
|
|
|
function printPreview() {
|
|
console.log('准备生成开发种子数据:')
|
|
console.log('- manual_pending -> 人工履约待处理')
|
|
console.log('- manual_completed -> 人工履约已完成')
|
|
console.log('- claim_ready -> 领取兑换已就绪')
|
|
console.log('- claim_claimed -> 用户已打开领取链接')
|
|
console.log('- claim_role_confirmed -> 用户已确认角色')
|
|
console.log('- claim_retry_pending -> 自动履约失败等待重试')
|
|
console.log('- claim_expired -> 领取链接已过期')
|
|
console.log('- claim_waiting_inventory -> 等待库存')
|
|
console.log(`- database: ${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'}`)
|
|
}
|
|
|
|
function printResultSummary(rows) {
|
|
console.log('\n开发种子数据已写入:')
|
|
|
|
for (const row of rows) {
|
|
console.log(`- ${row.scenarioKey}`)
|
|
console.log(` orderId=${row.orderId} platformOrderId=${row.platformOrderId}`)
|
|
console.log(` taskId=${row.taskId || '-'} taskNo=${row.taskNo || '-'} executor=${row.executorKey || '-'}`)
|
|
console.log(` taskStatus=${row.taskStatus || '-'} deliveryStatus=${row.deliveryStatus || '-'} inventoryStatus=${row.inventoryStatus || '-'}`)
|
|
console.log(` inventoryItemId=${row.inventoryItemId || '-'} claimTokenId=${row.claimTokenId || '-'}`)
|
|
}
|
|
|
|
console.log('\n建议验证入口:')
|
|
console.log('- `/api/v1/admin/orders?platformOrderId=SEED-`')
|
|
console.log('- `/api/v1/admin/tasks?taskNo=DT`')
|
|
console.log('- `/api/v1/admin/inventory`')
|
|
}
|