彻底重构-3

This commit is contained in:
yml
2026-04-09 12:24:14 +08:00
parent 606ef875e2
commit 60054adcc4
37 changed files with 1324 additions and 3630 deletions
+65 -427
View File
@@ -1,21 +1,22 @@
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { DatabaseSync } from 'node:sqlite'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const DEFAULT_DB_PATH = path.join(PROJECT_ROOT, 'data', 'order-site.db')
import { closeDb, query } from '../src/db/client.js'
import { runtimeConfig } from '../src/config/runtime.js'
const PRESETS = {
'codex-debug': {
platformOrderIds: ['DEBUG-ORDER-001', '2067719225654999', '2067719225655000', '2067719225655001'],
shopIds: ['debug-shop', 'shop-10001', 'shop-10002', 'shop-10003'],
},
}
const RESET_TABLES = [
'message_deliveries',
'tencent_browser_contexts',
'task_events',
'claim_tokens',
'task_inventory_bindings',
'fulfillment_tasks',
'order_items',
'orders',
'webhook_events',
'inventory_items',
]
const options = parseArgs(process.argv.slice(2))
@@ -24,437 +25,74 @@ if (options.help) {
process.exit(0)
}
const preset = options.preset ? PRESETS[options.preset] : null
if (options.preset && !preset) {
console.error(`未知 preset: ${options.preset}`)
printHelp()
process.exit(1)
}
const selectors = {
provider: options.provider,
platform: options.platform,
orderIds: normalizeNumberList(options.orderIds),
platformOrderIds: uniqueList([...(preset?.platformOrderIds || []), ...options.platformOrderIds]),
shopIds: uniqueList([...(preset?.shopIds || []), ...options.shopIds]),
shopNames: uniqueList(options.shopNames),
}
if (!hasAnySelector(selectors)) {
console.error('至少需要一个筛选条件,或使用 --preset codex-debug')
printHelp()
process.exit(1)
}
const dbPath = resolveDbPath(options.dbPath)
const db = new DatabaseSync(dbPath)
db.exec('PRAGMA foreign_keys = ON;')
db.exec('PRAGMA busy_timeout = 3000;')
const matched = collectMatches(db, selectors)
printSummary(dbPath, selectors, matched, options.apply)
if (!options.apply) {
console.log('\n当前为预览模式。确认后追加 `--apply` 执行删除。')
process.exit(0)
}
db.exec('BEGIN')
try {
deleteByIds(db, 'message_deliveries', matched.messageDeliveryIds)
deleteByIds(db, 'webhook_events', matched.webhookEventIds)
deleteByIds(db, 'orders', matched.orderIds)
db.exec('COMMIT')
console.log('\n清理完成。若后台服务正在运行,建议重启一次 backend 容器,确保管理后台视图立即刷新。')
const counts = await collectCounts()
printSummary(counts)
if (!options.apply) {
console.log('\n当前为预览模式。确认后追加 `--apply` 执行清库。')
process.exit(0)
}
await query(`TRUNCATE TABLE ${RESET_TABLES.join(', ')} RESTART IDENTITY CASCADE`)
console.log('\n已清空测试业务数据。')
console.log('保留内容:fulfillment profiles/bindings、admin users、配置文件。')
} catch (error) {
db.exec('ROLLBACK')
console.error('\n清理失败:', error instanceof Error ? error.message : String(error || '未知错误'))
process.exit(1)
process.exitCode = 1
} finally {
await closeDb()
}
function collectMatches(db, selectors) {
const orderWhere = buildOrderWhere(selectors)
const orders = db.prepare(`
SELECT id, provider, platform, shop_id, shop_name, platform_order_id
FROM orders
${orderWhere.sql}
ORDER BY id DESC
`).all(...orderWhere.params)
async function collectCounts() {
const output = []
const orderIds = orders.map((item) => Number(item.id))
const taskIds = orderIds.length > 0
? db.prepare(`
SELECT id
FROM delivery_tasks
WHERE order_id IN (${placeholders(orderIds.length)})
ORDER BY id DESC
`).all(...orderIds).map((item) => Number(item.id))
: []
const webhookWhere = buildWebhookWhere(selectors, orderIds)
const webhookEvents = db.prepare(`
SELECT id, provider, platform, shop_id, shop_name, event_type, event_key, related_order_id
FROM webhook_events
${webhookWhere.sql}
ORDER BY id DESC
`).all(...webhookWhere.params)
const messageWhere = buildMessageWhere(selectors, orderIds, taskIds)
const messageDeliveries = db.prepare(`
SELECT id, provider, platform, shop_id, shop_name, platform_order_id, status
FROM message_deliveries
${messageWhere.sql}
ORDER BY id DESC
`).all(...messageWhere.params)
return {
orders,
orderIds,
taskIds,
webhookEvents,
webhookEventIds: webhookEvents.map((item) => Number(item.id)),
messageDeliveries,
messageDeliveryIds: messageDeliveries.map((item) => Number(item.id)),
}
}
function buildOrderWhere(selectors) {
const baseFilters = []
const matchFilters = []
const params = []
const matchParams = []
if (selectors.provider) {
baseFilters.push('provider = ?')
params.push(selectors.provider)
}
if (selectors.platform) {
baseFilters.push('platform = ?')
params.push(selectors.platform)
}
if (selectors.orderIds.length > 0) {
matchFilters.push(`id IN (${placeholders(selectors.orderIds.length)})`)
matchParams.push(...selectors.orderIds)
}
if (selectors.platformOrderIds.length > 0) {
matchFilters.push(`platform_order_id IN (${placeholders(selectors.platformOrderIds.length)})`)
matchParams.push(...selectors.platformOrderIds)
}
if (selectors.shopIds.length > 0) {
matchFilters.push(`shop_id IN (${placeholders(selectors.shopIds.length)})`)
matchParams.push(...selectors.shopIds)
}
if (selectors.shopNames.length > 0) {
matchFilters.push(`shop_name IN (${placeholders(selectors.shopNames.length)})`)
matchParams.push(...selectors.shopNames)
}
const filters = [...baseFilters]
if (matchFilters.length > 0) {
filters.push(`(${matchFilters.join(' OR ')})`)
params.push(...matchParams)
}
return {
sql: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
function buildWebhookWhere(selectors, orderIds) {
const baseFilters = []
const matchFilters = []
const params = []
const matchParams = []
if (selectors.provider) {
baseFilters.push('provider = ?')
params.push(selectors.provider)
}
if (selectors.platform) {
baseFilters.push('platform = ?')
params.push(selectors.platform)
}
if (orderIds.length > 0) {
matchFilters.push(`related_order_id IN (${placeholders(orderIds.length)})`)
matchParams.push(...orderIds)
}
if (selectors.shopIds.length > 0) {
matchFilters.push(`shop_id IN (${placeholders(selectors.shopIds.length)})`)
matchParams.push(...selectors.shopIds)
}
if (selectors.shopNames.length > 0) {
matchFilters.push(`shop_name IN (${placeholders(selectors.shopNames.length)})`)
matchParams.push(...selectors.shopNames)
}
if (selectors.platformOrderIds.length > 0) {
const eventKeyFilters = selectors.platformOrderIds.map(() => 'event_key LIKE ?').join(' OR ')
const bodyFilters = selectors.platformOrderIds.map(() => 'body_json LIKE ?').join(' OR ')
matchFilters.push(`(${eventKeyFilters})`)
matchParams.push(...selectors.platformOrderIds.map((item) => `%${item}%`))
matchFilters.push(`(${bodyFilters})`)
matchParams.push(...selectors.platformOrderIds.map((item) => `%${item}%`))
}
const filters = [...baseFilters]
if (matchFilters.length > 0) {
filters.push(`(${matchFilters.join(' OR ')})`)
params.push(...matchParams)
}
return {
sql: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
function buildMessageWhere(selectors, orderIds, taskIds) {
const baseFilters = []
const matchFilters = []
const params = []
const matchParams = []
if (selectors.provider) {
baseFilters.push('provider = ?')
params.push(selectors.provider)
}
if (selectors.platform) {
baseFilters.push('platform = ?')
params.push(selectors.platform)
}
if (orderIds.length > 0) {
matchFilters.push(`order_id IN (${placeholders(orderIds.length)})`)
matchParams.push(...orderIds)
}
if (taskIds.length > 0) {
matchFilters.push(`task_id IN (${placeholders(taskIds.length)})`)
matchParams.push(...taskIds)
}
if (selectors.shopIds.length > 0) {
matchFilters.push(`shop_id IN (${placeholders(selectors.shopIds.length)})`)
matchParams.push(...selectors.shopIds)
}
if (selectors.shopNames.length > 0) {
matchFilters.push(`shop_name IN (${placeholders(selectors.shopNames.length)})`)
matchParams.push(...selectors.shopNames)
}
if (selectors.platformOrderIds.length > 0) {
matchFilters.push(`platform_order_id IN (${placeholders(selectors.platformOrderIds.length)})`)
matchParams.push(...selectors.platformOrderIds)
}
const filters = [...baseFilters]
if (matchFilters.length > 0) {
filters.push(`(${matchFilters.join(' OR ')})`)
params.push(...matchParams)
}
return {
sql: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
function deleteByIds(db, tableName, ids) {
if (!Array.isArray(ids) || ids.length === 0) {
return
}
db.prepare(`DELETE FROM ${tableName} WHERE id IN (${placeholders(ids.length)})`).run(...ids)
}
function resolveDbPath(overridePath) {
const candidates = [
String(overridePath || '').trim(),
DEFAULT_DB_PATH,
].filter(Boolean)
for (const candidate of candidates) {
const resolved = path.resolve(candidate)
if (fs.existsSync(resolved)) {
return resolved
}
}
console.error(`未找到数据库文件。默认查找路径: ${DEFAULT_DB_PATH}`)
console.error('可使用 `--db-path <path>` 手动指定。')
process.exit(1)
}
function parseArgs(argv) {
const output = {
help: false,
apply: false,
dbPath: '',
preset: '',
provider: '',
platform: '',
orderIds: [],
platformOrderIds: [],
shopIds: [],
shopNames: [],
}
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim()
if (!current) {
continue
}
if (current === '--help' || current === '-h') {
output.help = true
continue
}
if (current === '--apply') {
output.apply = true
continue
}
if (current === '--db-path') {
output.dbPath = String(argv[index + 1] || '').trim()
index += 1
continue
}
if (current === '--preset') {
output.preset = String(argv[index + 1] || '').trim()
index += 1
continue
}
if (current === '--provider') {
output.provider = String(argv[index + 1] || '').trim()
index += 1
continue
}
if (current === '--platform') {
output.platform = String(argv[index + 1] || '').trim()
index += 1
continue
}
if (current === '--order-id') {
output.orderIds.push(String(argv[index + 1] || '').trim())
index += 1
continue
}
if (current === '--platform-order-id') {
output.platformOrderIds.push(String(argv[index + 1] || '').trim())
index += 1
continue
}
if (current === '--shop-id') {
output.shopIds.push(String(argv[index + 1] || '').trim())
index += 1
continue
}
if (current === '--shop-name') {
output.shopNames.push(String(argv[index + 1] || '').trim())
index += 1
continue
}
console.error(`未知参数: ${current}`)
printHelp()
process.exit(1)
for (const tableName of RESET_TABLES) {
const result = await query(`SELECT COUNT(*)::int AS total FROM ${tableName}`)
output.push({
tableName,
total: Number(result.rows[0]?.total || 0),
})
}
return output
}
function printSummary(dbPath, selectors, matched, apply) {
console.log(`数据库: ${dbPath}`)
console.log(`模式: ${apply ? '执行删除' : '预览'}`)
console.log('筛选条件:')
console.log(JSON.stringify(selectors, null, 2))
console.log('\n匹配结果:')
console.log(`orders: ${matched.orders.length}`)
printRows(matched.orders, ['id', 'provider', 'platform', 'shop_id', 'shop_name', 'platform_order_id'])
console.log(`delivery_tasks: ${matched.taskIds.length}`)
console.log(`webhook_events: ${matched.webhookEvents.length}`)
printRows(matched.webhookEvents, ['id', 'provider', 'platform', 'shop_id', 'shop_name', 'event_type', 'related_order_id'])
console.log(`message_deliveries: ${matched.messageDeliveries.length}`)
printRows(matched.messageDeliveries, ['id', 'provider', 'platform', 'shop_id', 'shop_name', 'platform_order_id', 'status'])
}
function printRows(rows, keys) {
if (!Array.isArray(rows) || rows.length === 0) {
return
function parseArgs(argv) {
return {
apply: argv.includes('--apply'),
help: argv.includes('-h') || argv.includes('--help'),
}
const preview = rows.slice(0, 10).map((row) => Object.fromEntries(keys.map((key) => [key, row[key]])))
console.log(JSON.stringify(preview, null, 2))
}
function placeholders(count) {
return new Array(count).fill('?').join(', ')
}
function uniqueList(values) {
return [...new Set(values.map((item) => String(item || '').trim()).filter(Boolean))]
}
function normalizeNumberList(values) {
return uniqueList(values)
.map((item) => Number(item))
.filter((item) => Number.isInteger(item) && item > 0)
}
function hasAnySelector(selectors) {
return Boolean(
selectors.provider ||
selectors.platform ||
selectors.orderIds.length > 0 ||
selectors.platformOrderIds.length > 0 ||
selectors.shopIds.length > 0 ||
selectors.shopNames.length > 0,
)
}
function printHelp() {
console.log(`
用法:
node scripts/cleanup-dev-data.js [筛选条件] [--apply]
node scripts/cleanup-dev-data.js [--apply]
常用参数:
--platform-order-id <id> 按平台订单号删除,可重复传入
--shop-id <id> 按店铺 ID 删除,可重复传入
--shop-name <name> 按店铺名删除,可重复传入
--order-id <id> 按内部订单 ID 删除,可重复传入
--provider <name> 限定 provider,例如 agiso
--platform <name> 限定业务平台,例如 xianyu
--db-path <path> 手动指定数据库文件
--preset codex-debug 清理当前对话里生成的调试样例
--apply 真正执行删除;不传时只预览
说明:
基于当前 PostgreSQL fulfillment schema 清空测试业务数据。
默认只预览各表记录数,追加 --apply 后执行 TRUNCATE。
示例:
npm run cleanup:dev-data -- --platform-order-id DEBUG-ORDER-001
npm run cleanup:dev-data -- --shop-id shop-10003 --apply
npm run cleanup:dev-data -- --preset codex-debug --apply
`)
会清空:
${RESET_TABLES.join('\n ')}
会保留:
fulfillment_profiles / fulfillment_profile_requirements / sku_fulfillment_bindings
admin_users / admin_audit_logs
本地配置文件
数据库连接: ${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'}
`.trim())
}
function printSummary(counts) {
const totalRows = counts.reduce((sum, item) => sum + item.total, 0)
console.log('准备清理当前测试业务数据:')
for (const item of counts) {
console.log(`- ${item.tableName}: ${item.total}`)
}
console.log(`- total: ${totalRows}`)
}