| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- /**
- * 清理批次统计脏数据:同一 (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);
- });
|