| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- #!/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 <ops-export.json>');
- 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();
- })();
|