Files
order_site/apps/backend/scripts/seed-dev-data.ts
T
2026-05-21 17:37:31 +08:00

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.ts',
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(`
用法:
npm run seed:dev-data -- [--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`')
}