Files
delivery-desk/scripts/migrate-sqlite-to-postgres.ts
yuzhe 6091d61612 feat(review): 支持多候选稿验收轮次
- 支持每轮提交 1–5 个候选稿并按指定稿验收
- 保留历史轮次只读并兼容单候选稿版本接口
- 同步 SQLite/PostgreSQL schema、迁移验证、测试与项目文档
2026-07-21 20:25:52 +08:00

64 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';
import path from 'node:path';
import Database from 'better-sqlite3';
import pg from 'pg';
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('缺少 DATABASE_URL迁移未执行');
const replace = process.argv.includes('--replace');
const sqlitePath = path.resolve(process.env.SQLITE_PATH || 'data/app.db');
const schemaPath = path.resolve('db/postgres/schema.sql');
if (!fs.existsSync(sqlitePath)) throw new Error(`SQLite 数据库不存在:${sqlitePath}`);
const tables = [
'operation_groups', 'users', 'projects', 'collections', 'notes', 'images', 'annotations',
'work_comments', 'work_versions', 'review_rounds', 'review_events', 'sessions', 'customer_sessions',
'audit_logs', 'api_keys', 'storage_configs',
] as const;
const booleanColumns: Record<string, Set<string>> = {
users: new Set(['must_change_password']),
projects: new Set(['customer_access_enabled']),
};
const sqlite = new Database(sqlitePath, { readonly: true });
const client = new pg.Client({ connectionString: databaseUrl, ssl: process.env.PGSSL === 'disable' ? false : undefined });
try {
await client.connect();
const schema = fs.readFileSync(schemaPath, 'utf8');
await client.query(schema);
const existing = Number((await client.query('SELECT COUNT(*)::int AS count FROM operation_groups')).rows[0].count);
if (existing && !replace) throw new Error('PostgreSQL 已有数据。确认覆盖时请显式添加 --replace');
await client.query('BEGIN');
if (replace) await client.query(`TRUNCATE ${[...tables].reverse().map((table) => `"${table}"`).join(', ')} RESTART IDENTITY CASCADE`);
for (const table of tables) {
const exists = sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
if (!exists) continue;
const rows = sqlite.prepare(`SELECT * FROM "${table}" ORDER BY id`).all() as Record<string, unknown>[];
for (const row of rows) {
const columns = Object.keys(row);
const values = columns.map((column) => booleanColumns[table]?.has(column) ? Boolean(row[column]) : row[column]);
const names = columns.map((column) => `"${column}"`).join(', ');
const placeholders = columns.map((_, index) => `$${index + 1}`).join(', ');
await client.query(`INSERT INTO "${table}" (${names}) VALUES (${placeholders})`, values);
}
if (rows.length) await client.query(`SELECT setval(pg_get_serial_sequence('${table}', 'id'), (SELECT MAX(id) FROM "${table}"), true)`);
}
const collectionStatusRepair = schema.match(/-- COLLECTION_STATUS_REPAIR_START([\s\S]+?)-- COLLECTION_STATUS_REPAIR_END/)?.[1];
const reviewRoundRepair = schema.match(/-- REVIEW_ROUND_REPAIR_START([\s\S]+?)-- REVIEW_ROUND_REPAIR_END/)?.[1];
if (!collectionStatusRepair) throw new Error('PostgreSQL schema 缺少作品交付集状态修复脚本');
if (!reviewRoundRepair) throw new Error('PostgreSQL schema 缺少验收轮次修复脚本');
await client.query(reviewRoundRepair);
await client.query(collectionStatusRepair);
await client.query('COMMIT');
process.stdout.write(`迁移完成:${tables.length} 张表已从 ${sqlitePath} 导入 PostgreSQL\n`);
} catch (error) {
await client.query('ROLLBACK').catch(() => undefined);
throw error;
} finally {
sqlite.close();
await client.end().catch(() => undefined);
}