feat: 添加 replace-qrcode-url 脚本并更新构建配置

This commit is contained in:
Jafeng 2026-04-08 17:46:49 +08:00
parent 9f102eb514
commit c95657dbb1
4 changed files with 402 additions and 2 deletions

View File

@ -41,7 +41,7 @@ function getEmbeddedEnvBanner() {
cleanDist();
build({
entryPoints: ["./transfer.js", "./corpid.js"],
entryPoints: ["./transfer.js", "./corpid.js", "./replace-qrcode-url.js"],
outdir: "./dist",
entryNames: "[name]",
platform: "node",

View File

@ -6,7 +6,8 @@
"build": "node esbuild.config.js",
"package": "node esbuild.config.js",
"start": "node dist/transfer.js",
"corpid": "node corpid.js"
"corpid": "node corpid.js",
"replace-qrcode-url": "node dist/replace-qrcode-url.js"
},
"dependencies": {
"axios": "^1.7.0",

7
replace-qrcode-url.js Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env node
if (!process.env.NODE_ENV && !process.env.ENV_FILE && !process.env.DOTENV_CONFIG_PATH) {
process.env.NODE_ENV = "gk";
}
require("./scripts/replace-team-qrcode-url");

View File

@ -0,0 +1,392 @@
#!/usr/bin/env node
/**
* replace-team-qrcode-url
*
* 目的
* - 扫描 corp.team collection
* - team.qrcodes[].qrcode 中旧前缀替换为新前缀
*
* 安全
* - 默认 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",
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: "",
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;
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;
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++;
} 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++;
} 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 || "";
if (!opts.corpId && process.env.TARGET_CORP_ID) opts.corpId = process.env.TARGET_CORP_ID;
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)}`;
}
function updateTeamDoc(doc, { oldPrefix, newPrefix }) {
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;
const next = replacePrefix(current, oldPrefix, newPrefix);
if (next !== current) {
changed++;
return { ...item, qrcode: next };
}
return item;
});
return { changed, nextQrcodes };
}
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> 仅处理指定机构
--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);
const col = db.collection(opts.collection);
const filter = {};
if (isNonEmptyString(opts.corpId)) filter.corpId = opts.corpId;
console.log(`[step] collection=${opts.dbName}.${opts.collection} corpId=${opts.corpId || "全部"} mode=${opts.dryRun ? "dry-run" : "apply"}`);
let cursor = col.find(filter, { batchSize: opts.batchDocs });
if (opts.sortById) cursor = cursor.sort({ _id: 1 });
if (opts.skipDocs) cursor = cursor.skip(opts.skipDocs);
if (opts.limitDocs) cursor = cursor.limit(opts.limitDocs);
let scanned = 0;
let matchedDocs = 0;
let replacedUrls = 0;
let batchIndex = 0;
while (await cursor.hasNext()) {
const batch = [];
while (batch.length < opts.batchDocs && (await cursor.hasNext())) {
batch.push(await cursor.next());
}
if (batch.length === 0) break;
batchIndex++;
console.log(`[step] batch#${batchIndex} 读取文档 ${batch.length} 条(已扫描 ${scanned} 条)`);
for (const doc of batch) {
scanned++;
const { changed, nextQrcodes } = updateTeamDoc(doc, {
oldPrefix: opts.oldPrefix,
newPrefix: opts.newPrefix,
});
if (changed === 0) continue;
matchedDocs++;
replacedUrls += changed;
console.log(`[match] _id=${doc._id} teamId=${doc.teamId || ""} corpId=${doc.corpId || ""} replaced=${changed}`);
if (!opts.dryRun) {
await col.updateOne(
{ _id: doc._id },
{ $set: { qrcodes: nextQrcodes } }
);
}
}
}
console.log(
`[summary] scanned=${scanned} matchedDocs=${matchedDocs} replacedUrls=${replacedUrls} mode=${opts.dryRun ? "dry-run" : "apply"}`
);
} finally {
await client.close();
}
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});