#!/usr/bin/env node 'use strict'; // 把 db-export.js 导出的 JSON 导入当前配置的存储(用于 SQLite → PostgreSQL 搬迁) // 用法:node scripts/db-import.js ops-export.json // // 约束:目标库应为空(上传 ID 是主键,重复导入会冲突)。要重来请先清空目标库。 const fs = require('fs'); const path = require('path'); const { createContext } = require('../server/context.js'); const { createOpsStore } = require('../ops-store.js'); (async () => { const input = process.argv[2]; if (!input) { console.error('用法:node scripts/db-import.js '); process.exit(1); } const file = path.resolve(input); if (!fs.existsSync(file)) { console.error('找不到文件:' + file); process.exit(1); } const payload = JSON.parse(fs.readFileSync(file, 'utf8')); if (!payload || !Array.isArray(payload.uploads) || !Array.isArray(payload.rows)) { console.error('文件格式不对:需要 uploads 与 rows 数组'); process.exit(1); } const missingContent = payload.uploads.filter((u) => !u.contentBase64); if (missingContent.length) { console.error(`有 ${missingContent.length} 次上传缺少原件(contentBase64)。请用 db-export.js --with-files 重新导出。`); process.exit(1); } const ctx = createContext(); const store = createOpsStore(ctx.config); const health = await store.health(); if (!health.ok) { console.error(`目标存储不可用:${health.reason || 'storage-unreachable'} ${health.detail || ''}`); process.exit(1); } console.log(`目标驱动:${store.kind}(${health.detail || ''})`); try { await store.withTransaction(async (tx) => { for (const upload of payload.uploads) { await tx.insertUpload({ id: upload.id, originalName: upload.originalName, sha256: upload.sha256, sizeBytes: upload.sizeBytes, uploadedAt: upload.uploadedAt, year: upload.year, parserKind: upload.parserKind, parserVersion: upload.parserVersion, report: upload.report || {}, content: Buffer.from(upload.contentBase64, 'base64'), }); } if (Array.isArray(payload.platformRows) && payload.platformRows.length) { await tx.upsertPlatformRows(payload.platformRows.map((row) => ({ date: row.date, shopId: row.shopId, uploadId: row.uploadId, school: row.school, shopName: row.shopName, data: row.data || {}, }))); } await tx.upsertRows(payload.rows.map((row) => ({ date: row.date, shopKey: row.shopKey, uploadId: row.uploadId, shopId: row.shopId, shopName: row.shopName, school: row.school, status: row.status, order_cnt: row.order_cnt, revenue: row.revenue, exposure: row.exposure, click_rate: row.click_rate, raw: row.raw || {}, quality: row.quality || [], }))); const latestUploadId = payload.uploads.length ? payload.uploads[0].id : null; await tx.upsertShops((payload.shops || []).map((shop) => ({ ...shop, lastUploadId: latestUploadId }))); }); } catch (e) { console.error('导入失败:' + (e.message || e)); await store.close(); process.exit(1); } if (Array.isArray(payload.reports) && payload.reports.length) { for (const item of payload.reports) { await store.insertReport({ id: item.id, createdAt: item.createdAt, periodFrom: item.from, periodTo: item.to, schools: item.schools || [], shops: item.shops || [], source: item.source || 'rules', model: item.model || '', title: item.title || '经营分析报告', report: item.report, }).catch((err) => console.error('报告导入失败:' + item.id + ' ' + (err.message || err))); } } const summary = await store.summary(); console.log(`导入完成:上传 ${summary.uploadCount} 次 · ${summary.rowCount} 行 · ${summary.schools.length} 个学校`); await store.close(); })();