2026-04-08 17:46:49 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* replace-team-qrcode-url
|
|
|
|
|
|
*
|
|
|
|
|
|
* 目的:
|
|
|
|
|
|
* - 扫描 corp.team collection
|
|
|
|
|
|
* - 将 team.qrcodes[].qrcode 中旧前缀替换为新前缀
|
2026-04-23 18:13:16 +08:00
|
|
|
|
* - 将 team.qrcodes[].qrcode 中 corpId 参数从旧值替换为新值
|
|
|
|
|
|
* - 将 team.groupWelcomeConfig 中的建档链接/附件链接同步替换
|
2026-04-15 11:39:12 +08:00
|
|
|
|
* - 扫描 corp.staff-qrcode collection
|
|
|
|
|
|
* - 将 staff-qrcode.archiveLink.url 中旧前缀替换为新前缀
|
2026-04-23 18:13:16 +08:00
|
|
|
|
* - 将 staff-qrcode.archiveLink.url 中 corpId 参数从旧值替换为新值
|
|
|
|
|
|
* - 将 staff-qrcode.fileList 中的附件链接同步替换
|
2026-04-08 17:46:49 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 安全:
|
|
|
|
|
|
* - 默认 dry-run(不落库)
|
|
|
|
|
|
* - `--apply --yes` 才会写入
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const { MongoClient } = require("mongodb");
|
|
|
|
|
|
const dotenv = require("dotenv");
|
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
const path = require("path");
|
|
|
|
|
|
|
|
|
|
|
|
function isNonEmptyString(v) {
|
|
|
|
|
|
return typeof v === "string" && v.trim().length > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function safeNumber(n, fallback) {
|
|
|
|
|
|
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadDotEnv() {
|
|
|
|
|
|
const nodeEnv = process.env.NODE_ENV || "development";
|
|
|
|
|
|
const envFileName = `.env.${nodeEnv}`;
|
|
|
|
|
|
const plainEnvFileName = `env.${nodeEnv}`;
|
|
|
|
|
|
const explicitEnvFile = process.env.ENV_FILE || process.env.DOTENV_CONFIG_PATH || "";
|
|
|
|
|
|
const candidates = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (isNonEmptyString(explicitEnvFile)) {
|
|
|
|
|
|
candidates.push(
|
|
|
|
|
|
path.isAbsolute(explicitEnvFile) ? explicitEnvFile : path.resolve(process.cwd(), explicitEnvFile),
|
|
|
|
|
|
path.isAbsolute(explicitEnvFile) ? explicitEnvFile : path.resolve(__dirname, explicitEnvFile)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
candidates.push(
|
|
|
|
|
|
path.resolve(process.cwd(), envFileName),
|
|
|
|
|
|
path.resolve(__dirname, envFileName),
|
|
|
|
|
|
path.resolve(process.cwd(), plainEnvFileName),
|
|
|
|
|
|
path.resolve(__dirname, plainEnvFileName),
|
|
|
|
|
|
path.resolve(process.cwd(), ".env"),
|
|
|
|
|
|
path.resolve(__dirname, ".env")
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const picked = candidates.find((p) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return fs.existsSync(p);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (picked) {
|
|
|
|
|
|
dotenv.config({ path: picked });
|
|
|
|
|
|
console.log(`[env] loaded: ${picked}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
dotenv.config();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadDotEnv();
|
|
|
|
|
|
|
|
|
|
|
|
function createDefaultOptions() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
dbName: "corp",
|
|
|
|
|
|
collection: "team",
|
2026-04-15 11:39:12 +08:00
|
|
|
|
staffCollection: "staff-qrcode",
|
2026-04-08 17:46:49 +08:00
|
|
|
|
batchDocs: 200,
|
|
|
|
|
|
skipDocs: 0,
|
|
|
|
|
|
limitDocs: 0,
|
|
|
|
|
|
sortById: true,
|
|
|
|
|
|
dryRun: true,
|
|
|
|
|
|
yes: false,
|
|
|
|
|
|
help: false,
|
|
|
|
|
|
reportZh: false,
|
|
|
|
|
|
logFile: "",
|
|
|
|
|
|
mongoUri: "",
|
|
|
|
|
|
mongoUriEnv: "",
|
|
|
|
|
|
mongoHost: "",
|
|
|
|
|
|
mongoPort: "",
|
|
|
|
|
|
mongoUser: "",
|
|
|
|
|
|
mongoPass: "",
|
|
|
|
|
|
mongoAuthDb: "",
|
|
|
|
|
|
corpId: "",
|
2026-04-23 18:13:16 +08:00
|
|
|
|
oldCorpId: "",
|
|
|
|
|
|
newCorpId: "",
|
2026-04-08 17:46:49 +08:00
|
|
|
|
oldPrefix: "https://www.youcan365.com/h5/#/",
|
|
|
|
|
|
newPrefix: "https://crm.gykqyy.com/gkPatientWeChat/#/",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseArgs(argv) {
|
|
|
|
|
|
const args = argv.slice(2);
|
|
|
|
|
|
const opts = createDefaultOptions();
|
|
|
|
|
|
|
|
|
|
|
|
function takeValue(i) {
|
|
|
|
|
|
if (i + 1 >= args.length) return "";
|
|
|
|
|
|
return args[i + 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setKeyValue(keyRaw, valueRaw) {
|
|
|
|
|
|
const key = String(keyRaw || "").replace(/^--/, "").trim().toLowerCase();
|
|
|
|
|
|
const value = valueRaw;
|
|
|
|
|
|
if (!key) return;
|
|
|
|
|
|
if (key === "dbname" || key === "db") opts.dbName = value;
|
|
|
|
|
|
else if (key === "collection") opts.collection = value;
|
2026-04-15 11:39:12 +08:00
|
|
|
|
else if (key === "staffcollection") opts.staffCollection = value;
|
2026-04-08 17:46:49 +08:00
|
|
|
|
else if (key === "mongouri" || key === "mongourl") opts.mongoUri = value;
|
|
|
|
|
|
else if (key === "mongourienv") opts.mongoUriEnv = value;
|
|
|
|
|
|
else if (key === "mongohost") opts.mongoHost = value;
|
|
|
|
|
|
else if (key === "mongoport") opts.mongoPort = value;
|
|
|
|
|
|
else if (key === "mongouser") opts.mongoUser = value;
|
|
|
|
|
|
else if (key === "mongopass") opts.mongoPass = value;
|
|
|
|
|
|
else if (key === "mongoauthdb") opts.mongoAuthDb = value;
|
|
|
|
|
|
else if (key === "corpid") opts.corpId = value;
|
2026-04-23 18:13:16 +08:00
|
|
|
|
else if (key === "oldcorpid" || key === "sourcecorpid" || key === "targetcorpid") opts.oldCorpId = value;
|
|
|
|
|
|
else if (key === "newcorpid" || key === "tokencorpid") opts.newCorpId = value;
|
2026-04-08 17:46:49 +08:00
|
|
|
|
else if (key === "oldprefix") opts.oldPrefix = value;
|
|
|
|
|
|
else if (key === "newprefix") opts.newPrefix = value;
|
|
|
|
|
|
else if (key === "batchdocs") opts.batchDocs = Number(value);
|
|
|
|
|
|
else if (key === "skipdocs" || key === "skip-docs") opts.skipDocs = Number(value);
|
|
|
|
|
|
else if (key === "limitdocs" || key === "limit-docs") opts.limitDocs = Number(value);
|
|
|
|
|
|
else if (key === "logfile" || key === "log_file") opts.logFile = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
|
|
|
const a = args[i];
|
|
|
|
|
|
if (!a) continue;
|
|
|
|
|
|
if (a === "--") continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof a === "string" && a.includes("=")) {
|
|
|
|
|
|
const [k, ...rest] = a.split("=");
|
|
|
|
|
|
setKeyValue(k, rest.join("="));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (a === "--help" || a === "-h") opts.help = true;
|
|
|
|
|
|
else if (a === "--apply") opts.dryRun = false;
|
|
|
|
|
|
else if (a === "--yes") opts.yes = true;
|
|
|
|
|
|
else if (a === "--reportZh" || a === "--zh") opts.reportZh = true;
|
|
|
|
|
|
else if (a === "--dbName" || a === "--db") {
|
|
|
|
|
|
opts.dbName = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--collection") {
|
|
|
|
|
|
opts.collection = takeValue(i);
|
|
|
|
|
|
i++;
|
2026-04-15 11:39:12 +08:00
|
|
|
|
} else if (a === "--staffCollection") {
|
|
|
|
|
|
opts.staffCollection = takeValue(i);
|
|
|
|
|
|
i++;
|
2026-04-08 17:46:49 +08:00
|
|
|
|
} else if (a === "--mongoUri" || a === "--mongoUrl" || a === "--mongoURL") {
|
|
|
|
|
|
opts.mongoUri = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoUriEnv") {
|
|
|
|
|
|
opts.mongoUriEnv = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoHost") {
|
|
|
|
|
|
opts.mongoHost = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoPort") {
|
|
|
|
|
|
opts.mongoPort = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoUser") {
|
|
|
|
|
|
opts.mongoUser = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoPass") {
|
|
|
|
|
|
opts.mongoPass = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--mongoAuthDb") {
|
|
|
|
|
|
opts.mongoAuthDb = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--corpId") {
|
|
|
|
|
|
opts.corpId = takeValue(i);
|
|
|
|
|
|
i++;
|
2026-04-23 18:13:16 +08:00
|
|
|
|
} else if (a === "--oldCorpId" || a === "--sourceCorpId" || a === "--targetCorpId") {
|
|
|
|
|
|
opts.oldCorpId = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--newCorpId" || a === "--tokenCorpId") {
|
|
|
|
|
|
opts.newCorpId = takeValue(i);
|
|
|
|
|
|
i++;
|
2026-04-08 17:46:49 +08:00
|
|
|
|
} else if (a === "--oldPrefix") {
|
|
|
|
|
|
opts.oldPrefix = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--newPrefix") {
|
|
|
|
|
|
opts.newPrefix = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--batchDocs") {
|
|
|
|
|
|
opts.batchDocs = Number(takeValue(i));
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--skipDocs" || a === "--skip-docs") {
|
|
|
|
|
|
opts.skipDocs = Number(takeValue(i));
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--limitDocs" || a === "--limit-docs") {
|
|
|
|
|
|
opts.limitDocs = Number(takeValue(i));
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (a === "--logFile" || a === "--log-file") {
|
|
|
|
|
|
opts.logFile = takeValue(i);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return opts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyEnvFallbacks(opts) {
|
|
|
|
|
|
if (!opts.mongoUri) opts.mongoUri = process.env.MONGO_URI || process.env.GK_MONGO_URI || "";
|
2026-04-09 10:34:03 +08:00
|
|
|
|
if (!opts.corpId) {
|
2026-04-23 18:13:16 +08:00
|
|
|
|
opts.corpId = process.env.TOKEN_CORP_ID || process.env.TARGET_CORP_ID || "";
|
2026-04-09 10:34:03 +08:00
|
|
|
|
}
|
2026-04-23 18:13:16 +08:00
|
|
|
|
if (!opts.oldCorpId) opts.oldCorpId = process.env.OLD_CORP_ID || process.env.SOURCE_CORP_ID || process.env.TARGET_CORP_ID || "";
|
|
|
|
|
|
if (!opts.newCorpId) opts.newCorpId = process.env.NEW_CORP_ID || process.env.TOKEN_CORP_ID || "";
|
2026-04-08 17:46:49 +08:00
|
|
|
|
if (!opts.oldPrefix && process.env.OLD_QRCODE_PREFIX) opts.oldPrefix = process.env.OLD_QRCODE_PREFIX;
|
|
|
|
|
|
if (!opts.newPrefix && process.env.NEW_QRCODE_PREFIX) opts.newPrefix = process.env.NEW_QRCODE_PREFIX;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function die(msg) {
|
|
|
|
|
|
console.error(msg);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTimestampForFile(d = new Date()) {
|
|
|
|
|
|
const pad = (n) => String(n).padStart(2, "0");
|
|
|
|
|
|
return [
|
|
|
|
|
|
d.getFullYear(),
|
|
|
|
|
|
pad(d.getMonth() + 1),
|
|
|
|
|
|
pad(d.getDate()),
|
|
|
|
|
|
"-",
|
|
|
|
|
|
pad(d.getHours()),
|
|
|
|
|
|
pad(d.getMinutes()),
|
|
|
|
|
|
pad(d.getSeconds()),
|
|
|
|
|
|
].join("");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initFileLogger(opts) {
|
|
|
|
|
|
const logsDir = path.resolve(process.cwd(), "logs");
|
|
|
|
|
|
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
|
|
|
|
const logFile = opts.logFile || path.join(logsDir, `replace-team-qrcode-url-${formatTimestampForFile()}.log`);
|
|
|
|
|
|
const stream = fs.createWriteStream(logFile, { flags: "a" });
|
|
|
|
|
|
const origLog = console.log;
|
|
|
|
|
|
const origErr = console.error;
|
|
|
|
|
|
const write = (level, args) => {
|
|
|
|
|
|
const line = args
|
|
|
|
|
|
.map((arg) => {
|
|
|
|
|
|
if (typeof arg === "string") return arg;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.stringify(arg);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return String(arg);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.join(" ");
|
|
|
|
|
|
stream.write(`[${new Date().toISOString()}] [${level}] ${line}\n`);
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log = (...args) => {
|
|
|
|
|
|
write("INFO", args);
|
|
|
|
|
|
origLog(...args);
|
|
|
|
|
|
};
|
|
|
|
|
|
console.error = (...args) => {
|
|
|
|
|
|
write("ERROR", args);
|
|
|
|
|
|
origErr(...args);
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log(`[log] writing to ${logFile}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMongoUrl(opts) {
|
|
|
|
|
|
if (isNonEmptyString(opts.mongoUriEnv) && isNonEmptyString(process.env[opts.mongoUriEnv])) {
|
|
|
|
|
|
return process.env[opts.mongoUriEnv];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isNonEmptyString(opts.mongoUri)) {
|
|
|
|
|
|
return opts.mongoUri;
|
|
|
|
|
|
}
|
|
|
|
|
|
const host = opts.mongoHost || process.env.MONGO_HOST || process.env.CONFIG_DB_HOST;
|
|
|
|
|
|
const port = opts.mongoPort || process.env.MONGO_PORT || process.env.CONFIG_DB_PORT || "27017";
|
|
|
|
|
|
const username = opts.mongoUser || process.env.MONGO_USER || process.env.CONFIG_DB_USERNAME;
|
|
|
|
|
|
const password = opts.mongoPass || process.env.MONGO_PASS || process.env.CONFIG_DB_PASSWORD;
|
|
|
|
|
|
const authDb = opts.mongoAuthDb || process.env.MONGO_AUTH_DB || "admin";
|
|
|
|
|
|
if (!host || !username || !password) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return `mongodb://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}/${authDb}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function replacePrefix(value, oldPrefix, newPrefix) {
|
|
|
|
|
|
if (!isNonEmptyString(value) || !isNonEmptyString(oldPrefix) || !isNonEmptyString(newPrefix)) return value;
|
|
|
|
|
|
if (!value.startsWith(oldPrefix)) return value;
|
|
|
|
|
|
return `${newPrefix}${value.slice(oldPrefix.length)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 18:13:16 +08:00
|
|
|
|
function replaceUrlCorpId(value, oldCorpId, newCorpId) {
|
|
|
|
|
|
if (!isNonEmptyString(value) || !isNonEmptyString(oldCorpId) || !isNonEmptyString(newCorpId)) return value;
|
|
|
|
|
|
return value.replace(
|
|
|
|
|
|
/([?&]corpId=)([^&#]*)/g,
|
|
|
|
|
|
(match, prefix, currentCorpId) => (currentCorpId === oldCorpId ? `${prefix}${newCorpId}` : match)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeQrcodeUrl(value, { oldPrefix, newPrefix, oldCorpId, newCorpId }) {
|
|
|
|
|
|
return replaceUrlCorpId(replacePrefix(value, oldPrefix, newPrefix), oldCorpId, newCorpId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeArchiveLink(archiveLink, options) {
|
|
|
|
|
|
if (!archiveLink || typeof archiveLink !== "object") {
|
|
|
|
|
|
return { changed: 0, nextArchiveLink: archiveLink };
|
|
|
|
|
|
}
|
|
|
|
|
|
const current = archiveLink.url;
|
|
|
|
|
|
const next = normalizeQrcodeUrl(current, options);
|
|
|
|
|
|
if (next === current) {
|
|
|
|
|
|
return { changed: 0, nextArchiveLink: archiveLink };
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
changed: 1,
|
|
|
|
|
|
nextArchiveLink: { ...archiveLink, url: next },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeFileList(fileList, options) {
|
|
|
|
|
|
if (!Array.isArray(fileList)) {
|
|
|
|
|
|
return { changed: 0, nextFileList: fileList };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let changed = 0;
|
|
|
|
|
|
const nextFileList = fileList.map((item) => {
|
|
|
|
|
|
if (!item || typeof item !== "object") return item;
|
|
|
|
|
|
|
|
|
|
|
|
let nextItem = item;
|
|
|
|
|
|
|
|
|
|
|
|
function setItemUrl(key) {
|
|
|
|
|
|
const current = item[key];
|
|
|
|
|
|
const next = normalizeQrcodeUrl(current, options);
|
|
|
|
|
|
if (next !== current) {
|
|
|
|
|
|
changed++;
|
|
|
|
|
|
nextItem = nextItem === item ? { ...item } : nextItem;
|
|
|
|
|
|
nextItem[key] = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setItemUrl("URL");
|
|
|
|
|
|
setItemUrl("url");
|
|
|
|
|
|
setItemUrl("href");
|
|
|
|
|
|
|
|
|
|
|
|
if (item.file && typeof item.file === "object") {
|
|
|
|
|
|
let nextFile = item.file;
|
|
|
|
|
|
for (const key of ["url", "URL", "href", "picurl", "imgUrl", "download_url"]) {
|
|
|
|
|
|
const current = item.file[key];
|
|
|
|
|
|
const next = normalizeQrcodeUrl(current, options);
|
|
|
|
|
|
if (next !== current) {
|
|
|
|
|
|
changed++;
|
|
|
|
|
|
nextFile = nextFile === item.file ? { ...item.file } : nextFile;
|
|
|
|
|
|
nextFile[key] = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextFile !== item.file) {
|
|
|
|
|
|
nextItem = nextItem === item ? { ...item } : nextItem;
|
|
|
|
|
|
nextItem.file = nextFile;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nextItem;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return { changed, nextFileList };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateTeamDoc(doc, { oldPrefix, newPrefix, oldCorpId, newCorpId }) {
|
|
|
|
|
|
const options = { oldPrefix, newPrefix, oldCorpId, newCorpId };
|
2026-04-08 17:46:49 +08:00
|
|
|
|
const qrcodes = Array.isArray(doc.qrcodes) ? doc.qrcodes : [];
|
|
|
|
|
|
let changed = 0;
|
|
|
|
|
|
const nextQrcodes = qrcodes.map((item) => {
|
|
|
|
|
|
if (!item || typeof item !== "object") return item;
|
|
|
|
|
|
const current = item.qrcode;
|
2026-04-23 18:13:16 +08:00
|
|
|
|
const next = normalizeQrcodeUrl(current, options);
|
2026-04-08 17:46:49 +08:00
|
|
|
|
if (next !== current) {
|
|
|
|
|
|
changed++;
|
|
|
|
|
|
return { ...item, qrcode: next };
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-23 18:13:16 +08:00
|
|
|
|
const set = {};
|
|
|
|
|
|
if (changed > 0) {
|
|
|
|
|
|
set.qrcodes = nextQrcodes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const groupWelcomeConfig =
|
|
|
|
|
|
doc && doc.groupWelcomeConfig && typeof doc.groupWelcomeConfig === "object"
|
|
|
|
|
|
? doc.groupWelcomeConfig
|
2026-04-15 11:39:12 +08:00
|
|
|
|
: null;
|
2026-04-23 18:13:16 +08:00
|
|
|
|
if (groupWelcomeConfig) {
|
|
|
|
|
|
let nextGroupWelcomeConfig = groupWelcomeConfig;
|
|
|
|
|
|
const archiveResult = normalizeArchiveLink(groupWelcomeConfig.archiveLink, options);
|
|
|
|
|
|
if (archiveResult.changed > 0) {
|
|
|
|
|
|
changed += archiveResult.changed;
|
|
|
|
|
|
nextGroupWelcomeConfig = { ...nextGroupWelcomeConfig, archiveLink: archiveResult.nextArchiveLink };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fileListResult = normalizeFileList(groupWelcomeConfig.fileList, options);
|
|
|
|
|
|
if (fileListResult.changed > 0) {
|
|
|
|
|
|
changed += fileListResult.changed;
|
|
|
|
|
|
nextGroupWelcomeConfig = { ...nextGroupWelcomeConfig, fileList: fileListResult.nextFileList };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nextGroupWelcomeConfig !== groupWelcomeConfig) {
|
|
|
|
|
|
set.groupWelcomeConfig = nextGroupWelcomeConfig;
|
|
|
|
|
|
}
|
2026-04-15 11:39:12 +08:00
|
|
|
|
}
|
2026-04-23 18:13:16 +08:00
|
|
|
|
|
|
|
|
|
|
return { changed, set };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateStaffQrcodeDoc(doc, { oldPrefix, newPrefix, oldCorpId, newCorpId }) {
|
|
|
|
|
|
const options = { oldPrefix, newPrefix, oldCorpId, newCorpId };
|
|
|
|
|
|
let changed = 0;
|
|
|
|
|
|
const set = {};
|
|
|
|
|
|
|
|
|
|
|
|
const archiveResult = normalizeArchiveLink(doc && doc.archiveLink, options);
|
|
|
|
|
|
if (archiveResult.changed > 0) {
|
|
|
|
|
|
changed += archiveResult.changed;
|
|
|
|
|
|
set.archiveLink = archiveResult.nextArchiveLink;
|
2026-04-15 11:39:12 +08:00
|
|
|
|
}
|
2026-04-23 18:13:16 +08:00
|
|
|
|
|
|
|
|
|
|
const fileListResult = normalizeFileList(doc && doc.fileList, options);
|
|
|
|
|
|
if (fileListResult.changed > 0) {
|
|
|
|
|
|
changed += fileListResult.changed;
|
|
|
|
|
|
set.fileList = fileListResult.nextFileList;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { changed, set };
|
2026-04-15 11:39:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function processCollectionDocs({
|
|
|
|
|
|
collection,
|
|
|
|
|
|
filter,
|
|
|
|
|
|
batchDocs,
|
|
|
|
|
|
sortById,
|
|
|
|
|
|
skipDocs,
|
|
|
|
|
|
limitDocs,
|
|
|
|
|
|
dryRun,
|
|
|
|
|
|
updateDoc,
|
|
|
|
|
|
buildUpdate,
|
|
|
|
|
|
describeDoc,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
let cursor = collection.find(filter, { batchSize: batchDocs });
|
|
|
|
|
|
if (sortById) cursor = cursor.sort({ _id: 1 });
|
|
|
|
|
|
if (skipDocs) cursor = cursor.skip(skipDocs);
|
|
|
|
|
|
if (limitDocs) cursor = cursor.limit(limitDocs);
|
|
|
|
|
|
|
|
|
|
|
|
let scanned = 0;
|
|
|
|
|
|
let matchedDocs = 0;
|
|
|
|
|
|
let replacedUrls = 0;
|
|
|
|
|
|
let batchIndex = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (await cursor.hasNext()) {
|
|
|
|
|
|
const batch = [];
|
|
|
|
|
|
while (batch.length < batchDocs && (await cursor.hasNext())) {
|
|
|
|
|
|
batch.push(await cursor.next());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (batch.length === 0) break;
|
|
|
|
|
|
batchIndex++;
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[step] ${collection.collectionName} batch#${batchIndex} 读取文档 ${batch.length} 条(已扫描 ${scanned} 条)`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const doc of batch) {
|
|
|
|
|
|
scanned++;
|
|
|
|
|
|
const { changed, ...payload } = updateDoc(doc);
|
|
|
|
|
|
if (changed === 0) continue;
|
|
|
|
|
|
matchedDocs++;
|
|
|
|
|
|
replacedUrls += changed;
|
|
|
|
|
|
console.log(`[match] ${collection.collectionName} ${describeDoc(doc)} replaced=${changed}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (!dryRun) {
|
|
|
|
|
|
await collection.updateOne({ _id: doc._id }, buildUpdate(payload));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { scanned, matchedDocs, replacedUrls };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:46:49 +08:00
|
|
|
|
async function main() {
|
|
|
|
|
|
const opts = parseArgs(process.argv);
|
|
|
|
|
|
applyEnvFallbacks(opts);
|
|
|
|
|
|
initFileLogger(opts);
|
|
|
|
|
|
|
|
|
|
|
|
if (opts.help) {
|
|
|
|
|
|
console.log(`replace-team-qrcode-url
|
|
|
|
|
|
|
|
|
|
|
|
用法:
|
|
|
|
|
|
node replace-team-qrcode-url.js --apply --yes
|
|
|
|
|
|
|
|
|
|
|
|
可选参数:
|
|
|
|
|
|
--dbName <db> 数据库名,默认 corp
|
|
|
|
|
|
--collection <name> collection 名,默认 team
|
|
|
|
|
|
--corpId <corpId> 仅处理指定机构
|
2026-04-23 18:13:16 +08:00
|
|
|
|
--oldCorpId <corpId> URL 参数中的旧 corpId,默认取 OLD_CORP_ID / SOURCE_CORP_ID / TARGET_CORP_ID
|
|
|
|
|
|
--newCorpId <corpId> URL 参数中的新 corpId,默认取 NEW_CORP_ID / TOKEN_CORP_ID
|
2026-04-08 17:46:49 +08:00
|
|
|
|
--oldPrefix <url> 旧前缀,默认 https://www.youcan365.com/h5/#/
|
|
|
|
|
|
--newPrefix <url> 新前缀,默认 https://crm.gykqyy.com/gkPatientWeChat/#/
|
|
|
|
|
|
--apply --yes 真正写库;默认 dry-run
|
|
|
|
|
|
`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
opts.batchDocs = safeNumber(opts.batchDocs, 200);
|
|
|
|
|
|
opts.skipDocs = Number.isFinite(opts.skipDocs) && opts.skipDocs > 0 ? Math.floor(opts.skipDocs) : 0;
|
|
|
|
|
|
opts.limitDocs = Number.isFinite(opts.limitDocs) && opts.limitDocs > 0 ? Math.floor(opts.limitDocs) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (!opts.dryRun && !opts.yes) {
|
|
|
|
|
|
die("该脚本会直接回写原 collection。请确认后加上 --yes 再执行。");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const mongoUrl = buildMongoUrl(opts);
|
|
|
|
|
|
if (!isNonEmptyString(mongoUrl)) {
|
|
|
|
|
|
die("缺少 MongoDB 连接信息:请传 --mongoUri,或配置 GK_MONGO_URI / MONGO_URI");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const client = new MongoClient(mongoUrl, {
|
|
|
|
|
|
serverSelectionTimeoutMS: 30000,
|
|
|
|
|
|
connectTimeoutMS: 30000,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("[step] 正在连接 MongoDB...");
|
|
|
|
|
|
await client.connect();
|
|
|
|
|
|
console.log("[step] MongoDB 已连接");
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const db = client.db(opts.dbName);
|
2026-04-15 11:39:12 +08:00
|
|
|
|
const teamCol = db.collection(opts.collection);
|
|
|
|
|
|
const staffCol = db.collection(opts.staffCollection);
|
2026-04-08 17:46:49 +08:00
|
|
|
|
const filter = {};
|
|
|
|
|
|
if (isNonEmptyString(opts.corpId)) filter.corpId = opts.corpId;
|
|
|
|
|
|
|
2026-04-15 11:39:12 +08:00
|
|
|
|
console.log(
|
|
|
|
|
|
`[step] collection=${opts.dbName}.${opts.collection} corpId=${opts.corpId || "全部"} mode=${opts.dryRun ? "dry-run" : "apply"}`
|
|
|
|
|
|
);
|
|
|
|
|
|
const teamStats = await processCollectionDocs({
|
|
|
|
|
|
collection: teamCol,
|
|
|
|
|
|
filter,
|
|
|
|
|
|
batchDocs: opts.batchDocs,
|
|
|
|
|
|
sortById: opts.sortById,
|
|
|
|
|
|
skipDocs: opts.skipDocs,
|
|
|
|
|
|
limitDocs: opts.limitDocs,
|
|
|
|
|
|
dryRun: opts.dryRun,
|
|
|
|
|
|
updateDoc: (doc) =>
|
|
|
|
|
|
updateTeamDoc(doc, {
|
|
|
|
|
|
oldPrefix: opts.oldPrefix,
|
|
|
|
|
|
newPrefix: opts.newPrefix,
|
2026-04-23 18:13:16 +08:00
|
|
|
|
oldCorpId: opts.oldCorpId,
|
|
|
|
|
|
newCorpId: opts.newCorpId,
|
2026-04-15 11:39:12 +08:00
|
|
|
|
}),
|
2026-04-23 18:13:16 +08:00
|
|
|
|
buildUpdate: ({ set }) => ({ $set: set }),
|
2026-04-15 11:39:12 +08:00
|
|
|
|
describeDoc: (doc) =>
|
|
|
|
|
|
`_id=${doc._id} teamId=${doc.teamId || ""} corpId=${doc.corpId || ""}`,
|
|
|
|
|
|
});
|
2026-04-08 17:46:49 +08:00
|
|
|
|
|
2026-04-15 11:39:12 +08:00
|
|
|
|
console.log(
|
|
|
|
|
|
`[step] collection=${opts.dbName}.${opts.staffCollection} corpId=${opts.corpId || "全部"} mode=${opts.dryRun ? "dry-run" : "apply"}`
|
|
|
|
|
|
);
|
|
|
|
|
|
const staffStats = await processCollectionDocs({
|
|
|
|
|
|
collection: staffCol,
|
|
|
|
|
|
filter,
|
|
|
|
|
|
batchDocs: opts.batchDocs,
|
|
|
|
|
|
sortById: opts.sortById,
|
|
|
|
|
|
skipDocs: opts.skipDocs,
|
|
|
|
|
|
limitDocs: opts.limitDocs,
|
|
|
|
|
|
dryRun: opts.dryRun,
|
|
|
|
|
|
updateDoc: (doc) =>
|
|
|
|
|
|
updateStaffQrcodeDoc(doc, {
|
2026-04-08 17:46:49 +08:00
|
|
|
|
oldPrefix: opts.oldPrefix,
|
|
|
|
|
|
newPrefix: opts.newPrefix,
|
2026-04-23 18:13:16 +08:00
|
|
|
|
oldCorpId: opts.oldCorpId,
|
|
|
|
|
|
newCorpId: opts.newCorpId,
|
2026-04-15 11:39:12 +08:00
|
|
|
|
}),
|
2026-04-23 18:13:16 +08:00
|
|
|
|
buildUpdate: ({ set }) => ({ $set: set }),
|
2026-04-15 11:39:12 +08:00
|
|
|
|
describeDoc: (doc) =>
|
|
|
|
|
|
`_id=${doc._id} qrcodeId=${doc.qrcodeId || ""} corpId=${doc.corpId || ""}`,
|
|
|
|
|
|
});
|
2026-04-08 17:46:49 +08:00
|
|
|
|
|
|
|
|
|
|
console.log(
|
2026-04-15 11:39:12 +08:00
|
|
|
|
`[summary] team: scanned=${teamStats.scanned} matchedDocs=${teamStats.matchedDocs} replacedUrls=${teamStats.replacedUrls} | staff-qrcode: scanned=${staffStats.scanned} matchedDocs=${staffStats.matchedDocs} replacedUrls=${staffStats.replacedUrls} mode=${opts.dryRun ? "dry-run" : "apply"}`
|
2026-04-08 17:46:49 +08:00
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await client.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
|
console.error(error && error.stack ? error.stack : error);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|