Files
memind/scripts/sync-local-db-to-rds.mjs
john 229805a070 Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 23:06:43 +08:00

129 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Sync local MySQL (source) → RDS (target).
* Keeps tables listed in KEEP_TARGET where RDS has richer production data.
*
* Usage:
* node scripts/sync-local-db-to-rds.mjs
* node scripts/sync-local-db-to-rds.mjs --dry-run
*/
import mysql from 'mysql2/promise';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const dryRun = process.argv.includes('--dry-run');
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
const sourceUrl =
process.env.SYNC_SOURCE_DATABASE_URL ?? 'mysql://boot:password@127.0.0.1:3306/tkmind';
const targetUrl =
process.env.SYNC_TARGET_DATABASE_URL ??
process.env.PLAZA_PROD_DATABASE_URL;
if (!targetUrl) {
console.error('缺少 SYNC_TARGET_DATABASE_URL 或 PLAZA_PROD_DATABASE_URL');
process.exit(1);
}
/** RDS wins — do not overwrite from local. */
const KEEP_TARGET = new Set(['h5_publication_views']);
function serializeRow(row) {
const out = {};
for (const [key, value] of Object.entries(row)) {
if (value instanceof Date) out[key] = value;
else if (value !== null && typeof value === 'object') out[key] = JSON.stringify(value);
else out[key] = value;
}
return out;
}
async function copyTable(source, target, table) {
const [rows] = await source.query(`SELECT * FROM \`${table}\``);
if (rows.length === 0) {
if (!dryRun) await target.query(`DELETE FROM \`${table}\``).catch(() => {});
return 0;
}
if (dryRun) return rows.length;
await target.query('SET FOREIGN_KEY_CHECKS=0');
await target.query(`DELETE FROM \`${table}\``);
const batchSize = 200;
let copied = 0;
for (let i = 0; i < rows.length; i += batchSize) {
const chunk = rows.slice(i, i + batchSize).map(serializeRow);
const cols = Object.keys(chunk[0]);
const placeholders = chunk.map(() => `(${cols.map(() => '?').join(', ')})`).join(', ');
const values = chunk.flatMap((row) => cols.map((col) => row[col]));
await target.query(
`INSERT INTO \`${table}\` (${cols.map((c) => `\`${c}\``).join(', ')}) VALUES ${placeholders}`,
values,
);
copied += chunk.length;
}
await target.query('SET FOREIGN_KEY_CHECKS=1');
return copied;
}
async function main() {
const source = await mysql.createConnection(sourceUrl);
const target = await mysql.createConnection(targetUrl);
const [localTables] = await source.query('SHOW TABLES');
const [remoteTables] = await target.query('SHOW TABLES');
const remoteSet = new Set(remoteTables.map((r) => Object.values(r)[0]));
const tables = localTables
.map((r) => Object.values(r)[0])
.filter((t) => remoteSet.has(t))
.sort();
console.log(`源库: ${sourceUrl.replace(/:[^:@/]+@/, ':***@')}`);
console.log(`目标: ${targetUrl.replace(/:[^:@/]+@/, ':***@')}`);
console.log(`保留 RDS 数据(不覆盖): ${[...KEEP_TARGET].join(', ') || '(无)'}`);
console.log('');
const summary = [];
for (const table of tables) {
if (KEEP_TARGET.has(table)) {
const [[r]] = await target.query(`SELECT COUNT(*) AS c FROM \`${table}\``);
summary.push({ table, action: 'keep-rds', rows: Number(r.c) });
continue;
}
const [[l]] = await source.query(`SELECT COUNT(*) AS c FROM \`${table}\``);
const localCount = Number(l.c);
const copied = await copyTable(source, target, table);
summary.push({ table, action: dryRun ? 'would-sync' : 'synced', rows: copied, localCount });
}
console.log('TABLE\tACTION\tROWS');
for (const row of summary) {
console.log(`${row.table}\t${row.action}\t${row.rows}`);
}
await source.end();
await target.end();
console.log(dryRun ? '\n(dry-run,未写入)' : '\n✅ 同步完成');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});