237 lines
6.7 KiB
JavaScript
237 lines
6.7 KiB
JavaScript
|
|
"use strict";
|
|||
|
|
|
|||
|
|
const axios = require("axios");
|
|||
|
|
const { MongoClient } = require("mongodb");
|
|||
|
|
const { EJSON } = require("bson");
|
|||
|
|
|
|||
|
|
const CONFIG = {
|
|||
|
|
// 生产源库(请按真实生产配置核对)
|
|||
|
|
sourceMongoUri: "mongodb://root:PRO%26youcan2025233@101.35.91.238:27017/admin",
|
|||
|
|
// 通过 nginx 暴露的接收端地址(/jcpt/ 反代到 8081)
|
|||
|
|
receiverUrl: "https://crm.gykqyy.com/jcpt",
|
|||
|
|
corpId: "wpLgjyawAAeRkCPQMp9-z5q-xEzK64nA",
|
|||
|
|
token: "ca764511bdace06c64f22bb97dd19911bcc4f0c9a07b2211cbbc1d43e9271657",
|
|||
|
|
chunkSize: 300,
|
|||
|
|
includePublicDocs: true,
|
|||
|
|
// 可选:指定库,空数组表示扫描全部业务库
|
|||
|
|
dbAllowlist: ["admin", "corp", "public"],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const args = parseArgs(process.argv.slice(2));
|
|||
|
|
|
|||
|
|
const SOURCE_MONGO_URI =
|
|||
|
|
args.sourceMongoUri ||
|
|||
|
|
CONFIG.sourceMongoUri;
|
|||
|
|
const RECEIVER_BASE_URL = args.receiverUrl || CONFIG.receiverUrl;
|
|||
|
|
const MIGRATION_TOKEN = args.token || CONFIG.token;
|
|||
|
|
const CORP_ID = args.corpId || CONFIG.corpId;
|
|||
|
|
const CHUNK_SIZE = Number(args.chunkSize || CONFIG.chunkSize || 300);
|
|||
|
|
const DB_ALLOWLIST = parseCsv(args.dbs, CONFIG.dbAllowlist);
|
|||
|
|
const INCLUDE_PUBLIC_DOCS = parseBool(args.includePublicDocs, CONFIG.includePublicDocs);
|
|||
|
|
|
|||
|
|
if (!SOURCE_MONGO_URI) {
|
|||
|
|
console.error("[sender] Missing source mongo uri");
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
if (!RECEIVER_BASE_URL) {
|
|||
|
|
console.error("[sender] Missing receiver url");
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
if (!MIGRATION_TOKEN) {
|
|||
|
|
console.error("[sender] Missing migration token");
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
if (!CORP_ID) {
|
|||
|
|
console.error("[sender] Missing corpId");
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const filter = {
|
|||
|
|
$or: [
|
|||
|
|
{ corpId: CORP_ID },
|
|||
|
|
{ corpid: CORP_ID },
|
|||
|
|
{ corp_id: CORP_ID },
|
|||
|
|
{ wxCorpid: CORP_ID },
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const PUBLIC_CORP_FIELDS = ["corpId", "corpid", "corp_id", "wxCorpid"];
|
|||
|
|
|
|||
|
|
function buildPublicFilter() {
|
|||
|
|
return {
|
|||
|
|
$and: PUBLIC_CORP_FIELDS.map((field) => ({
|
|||
|
|
$or: [
|
|||
|
|
{ [field]: { $exists: false } },
|
|||
|
|
{ [field]: null },
|
|||
|
|
{ [field]: "" },
|
|||
|
|
],
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildMigrationFilter() {
|
|||
|
|
if (!INCLUDE_PUBLIC_DOCS) return filter;
|
|||
|
|
return { $or: [filter, buildPublicFilter()] };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const MIGRATION_FILTER = buildMigrationFilter();
|
|||
|
|
|
|||
|
|
async function requestReceiver(path, payload) {
|
|||
|
|
const url = `${RECEIVER_BASE_URL.replace(/\/$/, "")}${path}`;
|
|||
|
|
const res = await axios.post(url, payload, {
|
|||
|
|
timeout: 60000,
|
|||
|
|
headers: {
|
|||
|
|
"x-migration-token": MIGRATION_TOKEN,
|
|||
|
|
"content-type": "application/json",
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
if (!res.data || !res.data.success) {
|
|||
|
|
throw new Error(`[sender] Receiver error on ${path}: ${JSON.stringify(res.data)}`);
|
|||
|
|
}
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseCsv(input, defaultList = []) {
|
|||
|
|
if (!input || typeof input !== "string") return defaultList;
|
|||
|
|
return input
|
|||
|
|
.split(",")
|
|||
|
|
.map((s) => s.trim())
|
|||
|
|
.filter(Boolean);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseBool(input, defaultValue = false) {
|
|||
|
|
if (input === undefined || input === null || input === "") return Boolean(defaultValue);
|
|||
|
|
if (typeof input === "boolean") return input;
|
|||
|
|
const normalized = String(input).trim().toLowerCase();
|
|||
|
|
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|||
|
|
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|||
|
|
return Boolean(defaultValue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseArgs(argv) {
|
|||
|
|
const out = {};
|
|||
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|||
|
|
const a = argv[i];
|
|||
|
|
if (a === "--corpId") out.corpId = argv[++i];
|
|||
|
|
else if (a === "--sourceMongoUri") out.sourceMongoUri = argv[++i];
|
|||
|
|
else if (a === "--receiverUrl") out.receiverUrl = argv[++i];
|
|||
|
|
else if (a === "--token") out.token = argv[++i];
|
|||
|
|
else if (a === "--chunkSize") out.chunkSize = argv[++i];
|
|||
|
|
else if (a === "--dbs") out.dbs = argv[++i];
|
|||
|
|
else if (a === "--includePublicDocs") out.includePublicDocs = true;
|
|||
|
|
else if (a === "--noPublicDocs") out.includePublicDocs = false;
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function shouldSkipDb(name) {
|
|||
|
|
if (["admin", "config", "local"].includes(name)) return name !== "admin";
|
|||
|
|
if (!DB_ALLOWLIST.length) return false;
|
|||
|
|
return !DB_ALLOWLIST.includes(name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function migrateCollection(db, dbName, collectionName) {
|
|||
|
|
const col = db.collection(collectionName);
|
|||
|
|
|
|||
|
|
const count = await col.countDocuments(MIGRATION_FILTER);
|
|||
|
|
if (!count) return { migrated: 0, skipped: true };
|
|||
|
|
|
|||
|
|
const indexes = await col.indexes();
|
|||
|
|
await requestReceiver("/migrate/indexes", {
|
|||
|
|
dbName,
|
|||
|
|
collectionName,
|
|||
|
|
indexes: indexes.map((idx) => EJSON.serialize(idx, { relaxed: false })),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let migrated = 0;
|
|||
|
|
let page = 0;
|
|||
|
|
const cursor = col.find(MIGRATION_FILTER, { noCursorTimeout: true });
|
|||
|
|
|
|||
|
|
const buffer = [];
|
|||
|
|
while (await cursor.hasNext()) {
|
|||
|
|
const doc = await cursor.next();
|
|||
|
|
buffer.push(EJSON.serialize(doc, { relaxed: false }));
|
|||
|
|
|
|||
|
|
if (buffer.length >= CHUNK_SIZE) {
|
|||
|
|
page += 1;
|
|||
|
|
await requestReceiver("/migrate/chunk", {
|
|||
|
|
dbName,
|
|||
|
|
collectionName,
|
|||
|
|
docs: buffer.splice(0, buffer.length),
|
|||
|
|
});
|
|||
|
|
migrated += CHUNK_SIZE;
|
|||
|
|
console.log(`[sender] ${dbName}.${collectionName} page=${page} migrated=${migrated}/${count}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (buffer.length) {
|
|||
|
|
page += 1;
|
|||
|
|
const lastSize = buffer.length;
|
|||
|
|
await requestReceiver("/migrate/chunk", {
|
|||
|
|
dbName,
|
|||
|
|
collectionName,
|
|||
|
|
docs: buffer.splice(0, buffer.length),
|
|||
|
|
});
|
|||
|
|
migrated += lastSize;
|
|||
|
|
console.log(`[sender] ${dbName}.${collectionName} page=${page} migrated=${migrated}/${count}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await cursor.close();
|
|||
|
|
return { migrated, skipped: false };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
const client = new MongoClient(SOURCE_MONGO_URI, {
|
|||
|
|
maxPoolSize: 30,
|
|||
|
|
minPoolSize: 2,
|
|||
|
|
connectTimeoutMS: 30000,
|
|||
|
|
socketTimeoutMS: 60000,
|
|||
|
|
serverSelectionTimeoutMS: 30000,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const summary = {
|
|||
|
|
corpId: CORP_ID,
|
|||
|
|
dbs: 0,
|
|||
|
|
collections: 0,
|
|||
|
|
migratedDocs: 0,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await client.connect();
|
|||
|
|
await requestReceiver("/migrate/init", { corpId: CORP_ID });
|
|||
|
|
|
|||
|
|
const dbList = await client.db().admin().listDatabases();
|
|||
|
|
|
|||
|
|
for (const dbInfo of dbList.databases || []) {
|
|||
|
|
const dbName = dbInfo.name;
|
|||
|
|
if (shouldSkipDb(dbName)) continue;
|
|||
|
|
|
|||
|
|
const db = client.db(dbName);
|
|||
|
|
const collections = await db.listCollections({}, { nameOnly: true }).toArray();
|
|||
|
|
|
|||
|
|
for (const item of collections) {
|
|||
|
|
const collectionName = item.name;
|
|||
|
|
if (collectionName.startsWith("system.")) continue;
|
|||
|
|
|
|||
|
|
summary.collections += 1;
|
|||
|
|
const result = await migrateCollection(db, dbName, collectionName);
|
|||
|
|
if (!result.skipped) {
|
|||
|
|
summary.migratedDocs += result.migrated;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
summary.dbs += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await requestReceiver("/migrate/finish", { corpId: CORP_ID, summary });
|
|||
|
|
console.log("[sender] Migration completed", summary);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error("[sender] Migration failed", err.message);
|
|||
|
|
process.exitCode = 1;
|
|||
|
|
} finally {
|
|||
|
|
await client.close();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main();
|