/** * 清理批次统计脏数据:同一 (c_batch_id, c_pay_bank) 只保留一条。 * 远端无法可靠物理删,将多余行 state 置为 -1(业务查询 states=0 不可见)。 * * 用法:node scripts/dedupe-pctj-stats.js * node scripts/dedupe-pctj-stats.js --dry-run */ const http = require("http"); const qs = require("querystring"); const HOST = "121.43.55.7"; const PORT = 2101; const DRY = process.argv.includes("--dry-run"); const COL = { columnId: 1846, modelId: 1969, name: "批次统计" }; function request(method, urlPath, fields, token) { 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 req = 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 }); } }); } ); req.on("error", reject); req.write(body); req.end(); }); } async function login() { const res = await request("POST", "/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; } async function listAll(token) { const rows = []; for (let page = 0; page < 80; page++) { const res = await request( "POST", "/proxy_dms/content/selectContentList", { columnId: String(COL.columnId), modelId: String(COL.modelId), page: String(page), pageSize: "100", states: "0", }, token ); if (res.code == 202) break; if (res.code != 200) { throw new Error("list fail: " + JSON.stringify(res).slice(0, 400)); } const data = (res.content && res.content.data) || []; rows.push.apply(rows, data); const count = Number((res.content && res.content.count) || 0); if (rows.length >= count || data.length === 0) break; } return rows; } async function softDelete(token, id, reason) { return request( "POST", "/proxy_dms/content/updateContent", { columnId: String(COL.columnId), modelId: String(COL.modelId), content: JSON.stringify({ id: id, state: -1, c_remarks: "PCTJ_DEDUPE|" + reason + "|" + Date.now(), }), }, token ); } function score(row) { // 优先保留:有金额/人数、更新时间较新、有 grain const should = Number(row.c_should_pay_count || 0) + Number(row.c_actual_pay_count || 0); const amt = Number(row.c_should_pay_amount || 0) + Number(row.c_actual_pay_amount || 0); const t = Date.parse(row.update_time || row.create_time || 0) || 0; const grain = row.c_grain ? 1 : 0; return should * 1000 + amt + t / 1e12 + grain * 10; } (async () => { const token = await login(); const rows = await listAll(token); console.log("可见统计行=" + rows.length + (DRY ? " [dry-run]" : "")); const groups = new Map(); const orphan = []; for (const r of rows) { const bid = (r.c_batch_id || "").trim(); const bank = (r.c_pay_bank || "").trim() || "(空)"; if (!bid) { orphan.push(r); continue; } const key = bid + "||" + bank; if (!groups.has(key)) groups.set(key, []); groups.get(key).push(r); } const toDrop = []; for (const [key, list] of groups) { if (list.length <= 1) continue; list.sort((a, b) => score(b) - score(a)); const keep = list[0]; console.log( "重复 " + key + " x" + list.length + " 保留 id=" + keep.id + " 丢弃=" + list .slice(1) .map((x) => x.id) .join(",") ); for (let i = 1; i < list.length; i++) { toDrop.push({ row: list[i], reason: "dup:" + key }); } } for (const r of orphan) { console.log("无 batch_id 孤儿 id=" + r.id); toDrop.push({ row: r, reason: "orphan_no_batch" }); } console.log("待清理=" + toDrop.length); let ok = 0; let fail = 0; for (const item of toDrop) { if (DRY) { ok++; continue; } const res = await softDelete(token, item.row.id, item.reason); if (res.code == 200) ok++; else { fail++; console.log("FAIL id=" + item.row.id + " " + JSON.stringify(res).slice(0, 200)); } } const left = DRY ? rows.length - toDrop.length : (await listAll(token)).length; console.log("完成 purged=" + ok + " failed=" + fail + " 剩余可见=" + left); // 复检 (batch_id, pay_bank) const after = DRY ? rows.filter((r) => !toDrop.some((d) => d.row.id === r.id)) : await listAll(token); const chk = new Map(); for (const r of after) { const key = (r.c_batch_id || "").trim() + "||" + ((r.c_pay_bank || "").trim() || "(空)"); chk.set(key, (chk.get(key) || 0) + 1); } const still = [...chk.entries()].filter(([, n]) => n > 1); console.log("仍重复组数=" + still.length); for (const [k, n] of still.slice(0, 10)) console.log(" " + k + " x" + n); })().catch((e) => { console.error(e); process.exit(1); });