461 lines
13 KiB
JavaScript
461 lines
13 KiB
JavaScript
#!/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 <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 <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 真正执行删除;不传时只预览
|
|
|
|
示例:
|
|
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
|
|
`)
|
|
}
|