#!/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') const PRESETS = { 'codex-debug': { platformOrderIds: ['DEBUG-ORDER-001', '2067719225654999', '2067719225655000', '2067719225655001'], shopIds: ['debug-shop', 'shop-10001', 'shop-10002', 'shop-10003'], }, } const options = parseArgs(process.argv.slice(2)) if (options.help) { printHelp() 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 容器,确保管理后台视图立即刷新。') } catch (error) { db.exec('ROLLBACK') console.error('\n清理失败:', error instanceof Error ? error.message : String(error || '未知错误')) process.exit(1) } 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) 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 ` 手动指定。') 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) } 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 } 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] 常用参数: --platform-order-id 按平台订单号删除,可重复传入 --shop-id 按店铺 ID 删除,可重复传入 --shop-name 按店铺名删除,可重复传入 --order-id 按内部订单 ID 删除,可重复传入 --provider 限定 provider,例如 agiso --platform 限定业务平台,例如 xianyu --db-path 手动指定数据库文件 --preset codex-debug 清理当前对话里生成的调试样例 --apply 真正执行删除;不传时只预览 示例: 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 `) }