/** * 行政区划栏目去重:按业务 code(remarks 中 code=)分组,保留最早一条,其余删除/销毁。 * * 用法: node scripts/dedupe-dms-regions.js * 可选: node scripts/dedupe-dms-regions.js --dry-run */ const http = require("http"); const qs = require("querystring"); const fs = require("fs"); const path = require("path"); const HOST = "121.43.55.7"; const PORT = 2101; const DRY = process.argv.includes("--dry-run"); const COL = { columnId: 1842, modelId: 1965 }; function req(urlPath, fields, token, method) { return new Promise((resolve, reject) => { const body = qs.stringify(fields || {}); const headers = { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(body), }; if (token) { headers.Token = token; headers.token = token; } const r = http.request( { hostname: HOST, port: PORT, path: urlPath, method: method || "POST", headers }, (res) => { let data = ""; res.on("data", (c) => (data += c)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { resolve({ raw: data, status: res.statusCode }); } }); } ); r.on("error", reject); r.write(body); r.end(); }); } async function login() { const res = await req("/proxy_oauth/user/login", { userName: "user_liu", password: "WE176852439@lmx", clientId: "1", }); if (res.code != 200) throw new Error("login fail: " + JSON.stringify(res)); return res.message && String(res.message).startsWith("eyJ") ? res.message : res.content; } function remarkTag(remarks, key) { if (!remarks) return null; const m = String(remarks).match(new RegExp("(?:^|[|])" + key + "=([^|]+)")); return m ? m[1] : null; } function bizKey(row) { const remarks = row.c_remarks || row.content || ""; const code = remarkTag(remarks, "code"); if (code) return "code:" + code; const level = remarkTag(remarks, "level") || ""; const parent = row.c_parent_id || ""; const name = row.c_name || row.title || ""; return "name:" + level + "|" + parent + "|" + name; } async function fetchAll(token) { const all = []; let page = 0; const pageSize = 100; while (page < 50) { const res = await req( "/proxy_dms/content/selectContentList", { columnId: String(COL.columnId), modelId: String(COL.modelId), states: "0", page: String(page), pageSize: String(pageSize), }, token ); if (res.code == 202) break; if (res.code != 200) { throw new Error("select fail: " + JSON.stringify(res).slice(0, 300)); } const content = res.content || {}; const rows = content.data || content.list || content.records || []; if (!rows.length) break; all.push(...rows); const total = content.count || content.total || 0; if (all.length >= total || rows.length < pageSize) break; page++; } return all; } async function hardDelete(token, id) { // DMS 原生删除(部分环境 columnId 解析有 bug,失败则走销毁) return req( "/proxy_dms/content/delContentById", { contentId: id, columnId: String(COL.columnId) }, token, "DELETE" ); } async function destroy(token, id) { // state=4 销毁;estimateType 对 4 恒为 true return req( "/proxy_dms/content/updateAudit", { columnId: String(COL.columnId), id, state: "4", auditorName: "dedupe-script", auditorComment: "行政区划去重销毁重复项", }, token ); } async function removeOne(token, id) { const hard = await hardDelete(token, id); if (hard && hard.code == 200) { return { ok: true, mode: "delete", res: hard }; } const soft = await destroy(token, id); if (soft && soft.code == 200) { return { ok: true, mode: "destroy", res: soft, hardFail: hard }; } return { ok: false, mode: "fail", hard, soft }; } (async () => { const token = await login(); console.log("login ok", DRY ? "(dry-run)" : "(live)"); const rows = await fetchAll(token); console.log("loaded regions:", rows.length); const groups = new Map(); for (const row of rows) { const key = bizKey(row); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(row); } const plan = []; for (const [key, list] of groups.entries()) { if (list.length <= 1) continue; list.sort((a, b) => { const ta = Number(a.create_time || a.c_operate_time || 0); const tb = Number(b.create_time || b.c_operate_time || 0); if (ta !== tb) return ta - tb; return String(a.id).localeCompare(String(b.id)); }); const keep = list[0]; const drop = list.slice(1); plan.push({ key, keepId: keep.id, keepName: keep.c_name || keep.title, dropIds: drop.map((d) => d.id), dropNames: drop.map((d) => d.c_name || d.title), }); } console.log("duplicate groups:", plan.length); const result = { dryRun: DRY, total: rows.length, groups: plan.length, kept: [], removed: [], errors: [] }; for (const g of plan) { console.log( ` ${g.key} keep=${g.keepId}(${g.keepName}) drop=${g.dropIds.length} [${g.dropNames.join(",")}]` ); result.kept.push({ key: g.key, id: g.keepId, name: g.keepName }); if (DRY) { for (const id of g.dropIds) { result.removed.push({ id, key: g.key, mode: "dry-run" }); } continue; } for (const id of g.dropIds) { const r = await removeOne(token, id); if (r.ok) { console.log(" -", r.mode, id); result.removed.push({ id, key: g.key, mode: r.mode }); } else { console.log(" x fail", id, JSON.stringify(r.hard || r.soft).slice(0, 200)); result.errors.push({ id, key: g.key, hard: r.hard, soft: r.soft }); } } } const outPath = path.join(__dirname, "dedupe-dms-regions-result.json"); fs.writeFileSync(outPath, JSON.stringify(result, null, 2)); console.log("done. removed=", result.removed.length, "errors=", result.errors.length); console.log("result ->", outPath); })().catch((e) => { console.error(e); process.exit(1); });