删除咸鱼旧链路
This commit is contained in:
@@ -1,513 +0,0 @@
|
||||
#!/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`')
|
||||
}
|
||||
@@ -1,97 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
is_true() {
|
||||
normalized=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$normalized" in
|
||||
1|true|yes|on)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
start_browser_debug_services() {
|
||||
display="${TENCENT_BROWSER_DISPLAY:-:99}"
|
||||
resolution="${TENCENT_BROWSER_VNC_RESOLUTION:-1600x900x24}"
|
||||
display_number="${display#:}"
|
||||
display_number="${display_number%%.*}"
|
||||
display_socket="/tmp/.X11-unix/X${display_number}"
|
||||
display_lock="/tmp/.X${display_number}-lock"
|
||||
|
||||
if [ -S "$display_socket" ] && [ ! -f "$display_lock" ]; then
|
||||
rm -f "$display_socket"
|
||||
fi
|
||||
|
||||
if [ -f "$display_lock" ]; then
|
||||
display_pid=$(cat "$display_lock" 2>/dev/null || true)
|
||||
if [ -z "$display_pid" ] || ! kill -0 "$display_pid" 2>/dev/null; then
|
||||
rm -f "$display_lock" "$display_socket"
|
||||
fi
|
||||
fi
|
||||
|
||||
export DISPLAY="$display"
|
||||
Xvfb "$display" -screen 0 "$resolution" -nolisten tcp >/tmp/xvfb.log 2>&1 &
|
||||
xvfb_pid=$!
|
||||
|
||||
ready=0
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 50 ]; do
|
||||
if [ -S "$display_socket" ]; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
|
||||
if ! kill -0 "$xvfb_pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
echo "[backend-dev] Xvfb display ${DISPLAY} not ready for x11vnc; skip noVNC startup" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
if ! is_true "${TENCENT_BROWSER_NOVNC_ENABLED:-true}"; then
|
||||
echo "[backend-dev] headed browser enabled on ${DISPLAY}; noVNC disabled"
|
||||
return
|
||||
fi
|
||||
|
||||
vnc_port="${TENCENT_BROWSER_VNC_PORT:-5900}"
|
||||
novnc_port="${TENCENT_BROWSER_NOVNC_PORT:-6080}"
|
||||
|
||||
x11vnc \
|
||||
-display "$display" \
|
||||
-rfbport "$vnc_port" \
|
||||
-forever \
|
||||
-shared \
|
||||
-nopw \
|
||||
-listen 0.0.0.0 \
|
||||
-xkb >/tmp/x11vnc.log 2>&1 &
|
||||
|
||||
if command -v novnc_proxy >/dev/null 2>&1; then
|
||||
novnc_proxy --listen "$novnc_port" --vnc "127.0.0.1:${vnc_port}" >/tmp/novnc.log 2>&1 &
|
||||
elif [ -x /usr/share/novnc/utils/novnc_proxy ]; then
|
||||
/usr/share/novnc/utils/novnc_proxy --listen "$novnc_port" --vnc "127.0.0.1:${vnc_port}" >/tmp/novnc.log 2>&1 &
|
||||
elif command -v websockify >/dev/null 2>&1 && [ -d /usr/share/novnc ]; then
|
||||
websockify --web=/usr/share/novnc "$novnc_port" "127.0.0.1:${vnc_port}" >/tmp/novnc.log 2>&1 &
|
||||
else
|
||||
echo "[backend-dev] noVNC requested but novnc/websockify not found"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[backend-dev] headed browser enabled on ${DISPLAY}; noVNC: http://127.0.0.1:${novnc_port}/vnc.html"
|
||||
}
|
||||
|
||||
npm install --no-fund --no-audit
|
||||
|
||||
if ! is_true "${TENCENT_BROWSER_HEADLESS:-true}"; then
|
||||
start_browser_debug_services
|
||||
fi
|
||||
|
||||
exec npm run dev
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
|
||||
import { query, closeDb } from '../src/db/client.js'
|
||||
import { parseJsonObject } from '../src/utils/json.js'
|
||||
import { getAgisoShopConfig } from '../src/services/platforms/agiso/shop-config-service.js'
|
||||
import { queryAgisoXianyuOrderDetail } from '../src/services/platforms/agiso/xianyu/order-detail-service.js'
|
||||
|
||||
const DEFAULT_DUMMY_ENDPOINT = 'https://gw-api.agiso.com/aldsIdle/Order/DummySend'
|
||||
const DEFAULT_POLL_SECONDS = [0, 3, 10]
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
if (options.help || !options.orderId) {
|
||||
printHelp()
|
||||
process.exit(options.help ? 0 : 1)
|
||||
}
|
||||
|
||||
const orderRecord = await resolveOrderRecord(options)
|
||||
const shopId = orderRecord?.shop_id || options.shopId
|
||||
const shopConfig = getAgisoShopConfig(shopId) || {}
|
||||
const accessToken = String(shopConfig.accessToken || '').trim()
|
||||
const appSecret = String(shopConfig.appSecret || process.env.AGISO_APP_SECRET || '').trim()
|
||||
|
||||
if (!shopId) {
|
||||
throw new Error('未提供 shopId,且数据库中也没有查到该订单对应的店铺')
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error(`店铺 ${shopId} 缺少 accessToken 配置,请检查 apps/backend/data/agiso-shops.json`)
|
||||
}
|
||||
|
||||
if (!appSecret) {
|
||||
throw new Error('缺少 AGISO_APP_SECRET 或店铺级 appSecret 配置')
|
||||
}
|
||||
|
||||
printJson('测试输入', {
|
||||
orderId: options.orderId,
|
||||
shopId,
|
||||
pollSeconds: options.pollSeconds,
|
||||
orderRecord: orderRecord
|
||||
? {
|
||||
id: Number(orderRecord.id || 0) || null,
|
||||
platformOrderId: String(orderRecord.platform_order_id || '').trim(),
|
||||
orderStatus: String(orderRecord.order_status || '').trim(),
|
||||
payStatus: String(orderRecord.pay_status || '').trim(),
|
||||
provider: String(orderRecord.provider || '').trim(),
|
||||
platform: String(orderRecord.platform || '').trim(),
|
||||
}
|
||||
: null,
|
||||
})
|
||||
|
||||
const beforeDetail = await queryAgisoXianyuOrderDetail({
|
||||
shopId,
|
||||
platformOrderId: options.orderId,
|
||||
requestId: `diag-before-${Date.now()}`,
|
||||
})
|
||||
printJson('调用前订单详情', summarizeDetail(beforeDetail))
|
||||
|
||||
const result = await executeRequest({
|
||||
platformOrderId: options.orderId,
|
||||
accessToken,
|
||||
appSecret,
|
||||
})
|
||||
printJson('接口响应 更新发货状态 DummySend', result)
|
||||
|
||||
for (const seconds of options.pollSeconds) {
|
||||
if (seconds > 0) {
|
||||
await sleep(seconds * 1000)
|
||||
}
|
||||
|
||||
const detail = await queryAgisoXianyuOrderDetail({
|
||||
shopId,
|
||||
platformOrderId: options.orderId,
|
||||
requestId: `diag-after-${seconds}s-${Date.now()}`,
|
||||
})
|
||||
printJson(`调用后订单详情 +${seconds}s`, summarizeDetail(detail))
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const output = {
|
||||
orderId: '',
|
||||
shopId: '',
|
||||
pollSeconds: [...DEFAULT_POLL_SECONDS],
|
||||
help: false,
|
||||
}
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const current = String(argv[index] || '').trim()
|
||||
if (!current) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!current.startsWith('--')) {
|
||||
if (!output.orderId) {
|
||||
output.orderId = current
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error(`无法识别的参数:${current}`)
|
||||
}
|
||||
|
||||
if (current === '--help') {
|
||||
output.help = true
|
||||
continue
|
||||
}
|
||||
|
||||
const [rawKey, inlineValue = ''] = current.split('=', 2)
|
||||
const key = rawKey.slice(2)
|
||||
const nextValue = inlineValue || argv[index + 1] || ''
|
||||
const shouldConsumeNext = !inlineValue && argv[index + 1] && !String(argv[index + 1]).startsWith('--')
|
||||
|
||||
switch (key) {
|
||||
case 'shop-id':
|
||||
output.shopId = String(nextValue || '').trim()
|
||||
break
|
||||
case 'poll':
|
||||
output.pollSeconds = parsePollSeconds(nextValue)
|
||||
break
|
||||
default:
|
||||
throw new Error(`无法识别的参数:${current}`)
|
||||
}
|
||||
|
||||
if (shouldConsumeNext) {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function parsePollSeconds(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return [...DEFAULT_POLL_SECONDS]
|
||||
}
|
||||
|
||||
const parsed = normalized
|
||||
.split(',')
|
||||
.map((item) => Number(String(item || '').trim()))
|
||||
.filter((item) => Number.isFinite(item) && item >= 0)
|
||||
.map((item) => Math.floor(item))
|
||||
|
||||
if (parsed.length === 0) {
|
||||
throw new Error(`poll 参数格式无效:${value}`)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
async function resolveOrderRecord(options) {
|
||||
const explicitShopId = String(options.shopId || '').trim()
|
||||
if (explicitShopId) {
|
||||
const result = await query(
|
||||
`
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE provider = 'agiso'
|
||||
AND platform = 'xianyu'
|
||||
AND shop_id = $1
|
||||
AND platform_order_id = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
[explicitShopId, options.orderId],
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE provider = 'agiso'
|
||||
AND platform = 'xianyu'
|
||||
AND platform_order_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT 2
|
||||
`,
|
||||
[options.orderId],
|
||||
)
|
||||
|
||||
if (result.rows.length > 1) {
|
||||
throw new Error(`数据库里命中多条同订单号记录,请显式传入 --shop-id。订单号:${options.orderId}`)
|
||||
}
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
async function executeRequest(context) {
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const requestBody = buildDummySendBody(context, timestamp)
|
||||
const response = await fetch(DEFAULT_DUMMY_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.accessToken}`,
|
||||
ApiVersion: '1',
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
},
|
||||
body: new URLSearchParams(requestBody).toString(),
|
||||
})
|
||||
const rawText = await response.text()
|
||||
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
|
||||
|
||||
return {
|
||||
endpoint: DEFAULT_DUMMY_ENDPOINT,
|
||||
status: response.status,
|
||||
requestBody,
|
||||
response: payload,
|
||||
rawText,
|
||||
}
|
||||
}
|
||||
|
||||
function buildDummySendBody(context, timestamp) {
|
||||
const payload = {
|
||||
tid: context.platformOrderId,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
payload.sign = generateSign(payload, context.appSecret)
|
||||
return payload
|
||||
}
|
||||
|
||||
function generateSign(params, appSecret) {
|
||||
let raw = String(appSecret || '').trim()
|
||||
for (const [key, value] of Object.entries(params).sort(([left], [right]) => left.localeCompare(right))) {
|
||||
raw += `${key}${value}`
|
||||
}
|
||||
raw += String(appSecret || '').trim()
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function summarizeDetail(detailResult) {
|
||||
return {
|
||||
success: Boolean(detailResult?.success),
|
||||
reason: String(detailResult?.reason || '').trim(),
|
||||
errorMessage: String(detailResult?.errorMessage || '').trim(),
|
||||
responseStatus: Number(detailResult?.responseStatus || 0),
|
||||
shipped: Boolean(detailResult?.shipped),
|
||||
shipTime: Number(detailResult?.shipTime || 0),
|
||||
orderStatus: Number(detailResult?.orderStatus || 0),
|
||||
bizOrderId: String(detailResult?.detailPayload?.biz_order_id || '').trim(),
|
||||
sellerNick: String(detailResult?.detailPayload?.seller_nick || '').trim(),
|
||||
buyerNick: String(detailResult?.detailPayload?.buyer_nick || '').trim(),
|
||||
sku: String(detailResult?.detailPayload?.sku || '').trim(),
|
||||
itemTitle: String(detailResult?.detailPayload?.item?.title || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function printJson(title, value) {
|
||||
process.stdout.write(`\n=== ${title} ===\n`)
|
||||
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
process.stdout.write(`用法:
|
||||
npm run agiso:auto-delivery:test -- <订单号> [--shop-id <店铺ID>] [--poll 0,3,10]
|
||||
|
||||
说明:
|
||||
1. 该脚本只测试正确的发货接口 DummySend。
|
||||
2. 会先查 Order/Detail,再调用 DummySend,再按 poll 秒数轮询发货状态。
|
||||
|
||||
示例:
|
||||
npm run agiso:auto-delivery:test -- 4502285714045005830
|
||||
npm run agiso:auto-delivery:test -- 4502285714045005830 --poll 0,5,15
|
||||
npm run agiso:auto-delivery:test -- 4502285714045005830 --shop-id 2209880145223
|
||||
`)
|
||||
}
|
||||
|
||||
const isNodeTestRunner = Array.isArray(process.execArgv) && process.execArgv.includes('--test')
|
||||
const isDirectRun = !isNodeTestRunner && process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]
|
||||
|
||||
if (isDirectRun) {
|
||||
main()
|
||||
.catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await closeDb()
|
||||
})
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="http://127.0.0.1:3000"
|
||||
APP_SECRET=$(cd /Users/yml/codes/order-site-workspace/apps/backend && node --input-type=module -e "import { runtimeConfig } from './src/config/runtime.js'; process.stdout.write(String(runtimeConfig.platforms.agiso.appSecret || ''))")
|
||||
|
||||
ORDER_ID="${1:-SIM-ORDER-20260408-0001}"
|
||||
SKU_ID="32768"
|
||||
SKU_NAME="海底捞套餐"
|
||||
|
||||
sign_agiso() {
|
||||
local raw_json="$1"
|
||||
local ts="$2"
|
||||
|
||||
node -e '
|
||||
const crypto = require("crypto")
|
||||
const appSecret = process.argv[1]
|
||||
const rawJson = process.argv[2]
|
||||
const timestamp = process.argv[3]
|
||||
const sign = crypto
|
||||
.createHash("md5")
|
||||
.update(`${appSecret}json${rawJson}timestamp${timestamp}${appSecret}`, "utf8")
|
||||
.digest("hex")
|
||||
.toLowerCase()
|
||||
process.stdout.write(sign)
|
||||
' "$APP_SECRET" "$raw_json" "$ts"
|
||||
}
|
||||
|
||||
push_agiso_event() {
|
||||
local aopic="$1"
|
||||
local raw_json="$2"
|
||||
local ts
|
||||
ts=$(date +%s)
|
||||
local sign
|
||||
sign=$(sign_agiso "$raw_json" "$ts")
|
||||
|
||||
echo
|
||||
echo "========== PUSH aopic=$aopic =========="
|
||||
echo "ORDER_ID=$ORDER_ID"
|
||||
echo
|
||||
|
||||
curl -sS -X POST "${BASE_URL}/api/v1/webhooks/agiso/trade?aopic=${aopic}×tamp=${ts}&sign=${sign}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "json=${raw_json}"
|
||||
|
||||
echo
|
||||
echo
|
||||
}
|
||||
|
||||
create_payload() {
|
||||
cat <<EOF
|
||||
{"biz_order_id":"${ORDER_ID}","buyer_id":"buyer-local-001","buyer_name":"本地测试用户","receiver_contact":"13800000000","total_fee":"1","currency":"CNY","items":[{"goods_id":"${SKU_ID}","goods_name":"${SKU_NAME}","quantity":1}]}
|
||||
EOF
|
||||
}
|
||||
|
||||
paid_payload() {
|
||||
cat <<EOF
|
||||
{"biz_order_id":"${ORDER_ID}","buyer_id":"buyer-local-001","buyer_name":"本地测试用户","receiver_contact":"13800000000","total_fee":"1","currency":"CNY","order_status":"3","items":[{"goods_id":"${SKU_ID}","goods_name":"${SKU_NAME}","quantity":1}]}
|
||||
EOF
|
||||
}
|
||||
|
||||
confirm_payload() {
|
||||
cat <<EOF
|
||||
{"biz_order_id":"${ORDER_ID}","buyer_id":"buyer-local-001","buyer_name":"本地测试用户","receiver_contact":"13800000000","total_fee":"1","currency":"CNY","order_status":"4","items":[{"goods_id":"${SKU_ID}","goods_name":"${SKU_NAME}","quantity":1}]}
|
||||
EOF
|
||||
}
|
||||
|
||||
push_create() {
|
||||
push_agiso_event "32" "$(create_payload)"
|
||||
}
|
||||
|
||||
push_paid() {
|
||||
push_agiso_event "1" "$(paid_payload)"
|
||||
}
|
||||
|
||||
push_confirm() {
|
||||
push_agiso_event "256" "$(confirm_payload)"
|
||||
}
|
||||
|
||||
case "${2:-all}" in
|
||||
create)
|
||||
push_create
|
||||
;;
|
||||
paid)
|
||||
push_paid
|
||||
;;
|
||||
confirm)
|
||||
push_confirm
|
||||
;;
|
||||
all)
|
||||
push_create
|
||||
push_paid
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 [ORDER_ID] [create|paid|confirm|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user