/** * 数据库 SQL 约束检查。 * 该脚本检查关键防线是否被后续重构移除,不尝试替代 EXPLAIN 和生产监控。 */ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)) const BACKEND_ROOT = path.resolve(SCRIPT_DIR, '..') const checks: Array<{ name: string; file: string; patterns: string[] }> = [ { name: '慢查询阈值配置', file: path.join(BACKEND_ROOT, 'src/config/env-overrides.ts'), patterns: ['DATABASE_SLOW_QUERY_THRESHOLD_MS', 'slowQueryThresholdMs'], }, { name: '慢查询日志与参数脱敏', file: path.join(BACKEND_ROOT, 'src/db/client.ts'), patterns: ['logSlowQuery', 'fingerprintSql', '参数值始终不写入日志'], }, { name: '高频 pending 查询索引', file: path.join(BACKEND_ROOT, 'src/db/migrations/058_database_query_guardrails.sql'), patterns: [ 'idx_worker_cancel_requests_pending_worker_order', 'idx_worker_feedbacks_pending_worker_order', 'idx_work_order_events_order_type_id', ], }, { name: '打手订单拼单查询限定当前页', file: path.join(BACKEND_ROOT, 'src/repositories/worker-platform/work-order-share-repo.ts'), patterns: ['workOrderIds: number[]', 'wos.work_order_id = ANY($2::bigint[])'], }, { name: '数据保留清理扫描索引', file: path.join(BACKEND_ROOT, 'src/db/migrations/067_data_retention_indexes.sql'), patterns: [ 'idx_orders_updated_at', 'idx_kuaishou_industry_vouchers_updated_at', 'idx_task_events_created_at', 'idx_webhook_events_created_at', 'DROP INDEX IF EXISTS idx_work_product_match_logs_created_at', ], }, { name: '打手超时事件统计索引', file: path.join(BACKEND_ROOT, 'src/db/migrations/068_timeout_event_worker_index.sql'), patterns: [ 'idx_work_order_events_timeout_worker_id', "payload_json->>'workerId'", "WHERE event_type LIKE 'timeout_%'", ], }, ] const failures: string[] = [] for (const check of checks) { if (!fs.existsSync(check.file)) { failures.push(`${check.name}: 文件不存在 ${check.file}`) continue } const source = fs.readFileSync(check.file, 'utf8') for (const pattern of check.patterns) { if (!source.includes(pattern)) { failures.push(`${check.name}: 缺少约束 ${pattern} (${check.file})`) } } } if (failures.length > 0) { console.error('[sql-guard] SQL 约束检查失败') for (const failure of failures) console.error(`[sql-guard] ${failure}`) process.exitCode = 1 } else { console.info('[sql-guard] SQL 约束检查通过') }