| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * 清理批次统计脏数据(含软删行),避免建唯一索引时仍撞到历史重复键。
- *
- * 策略:
- * 1) 拉取 states 含 -1 的全部行
- * 2) state=-1 或 c_batch_id 以 REMOVED| 开头:尝试物理删 / 销毁
- * 3) 同一 (c_batch_id, c_pay_bank) 多条:保留分最高的一条,其余删除
- * 4) 删除失败则把 c_batch_id 改成 REMOVED|{id},不再占用业务键
- *
- * 用法:node scripts/cleanup-pctj-dirty.js
- */
- const http = require("http");
- const qs = require("querystring");
- const HOST = "121.43.55.7";
- const PORT = 2101;
- const COL = { columnId: 1846, modelId: 1969 };
- 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: String(data).slice(0, 300), status: res.statusCode });
- }
- });
- }
- );
- req.on("error", reject);
- if (body) 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 listAllStates(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,1,2,3,-1,4",
- },
- token
- );
- if (res.code == 202) break;
- if (res.code != 200) throw new Error("list fail: " + JSON.stringify(res).slice(0, 300));
- const data = (res.content && res.content.data) || [];
- rows.push.apply(rows, data);
- if (data.length < 100) break;
- }
- return rows;
- }
- async function hardDelete(token, id) {
- return request(
- "DELETE",
- "/proxy_dms/content/delContentById",
- { contentId: id, columnId: String(COL.columnId) },
- token
- );
- }
- async function destroy(token, id) {
- return request(
- "POST",
- "/proxy_dms/content/updateAudit",
- {
- columnId: String(COL.columnId),
- id,
- state: "4",
- auditorName: "pctj-cleanup",
- auditorComment: "批次统计脏数据清理",
- },
- token
- );
- }
- async function neutralize(token, id) {
- return request(
- "POST",
- "/proxy_dms/content/updateContent",
- {
- columnId: String(COL.columnId),
- modelId: String(COL.modelId),
- content: JSON.stringify({
- id,
- state: -1,
- c_batch_id: "REMOVED|" + id,
- c_remarks: "PCTJ_CLEANED|" + Date.now(),
- }),
- },
- token
- );
- }
- async function removeOne(token, id) {
- const hard = await hardDelete(token, id);
- if (hard && hard.code == 200) return { ok: true, mode: "delete" };
- const soft = await destroy(token, id);
- if (soft && soft.code == 200) return { ok: true, mode: "destroy", hard };
- const neu = await neutralize(token, id);
- if (neu && neu.code == 200) return { ok: true, mode: "neutralize", hard, soft };
- return { ok: false, hard, soft, neu };
- }
- function score(row) {
- 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 live = String(row.state) === "0" ? 100000 : 0;
- return live + should * 1000 + amt + t / 1e12;
- }
- (async () => {
- const token = await login();
- const rows = await listAllStates(token);
- console.log("全状态统计行=" + rows.length);
- const targets = new Map(); // id -> reason
- // A. 软删/销毁态但仍占用真实 batch_id
- for (const r of rows) {
- const st = String(r.state);
- const bid = String(r.c_batch_id || "");
- if ((st === "-1" || st === "4") && bid && !bid.startsWith("REMOVED|")) {
- targets.set(r.id, "soft_deleted_live_key state=" + st + " batch=" + bid + " bank=" + (r.c_pay_bank || ""));
- }
- }
- // B. 同一 (batch_id, pay_bank) 重复(含各 state)
- const groups = new Map();
- for (const r of rows) {
- const bid = String(r.c_batch_id || "").trim();
- if (!bid || bid.startsWith("REMOVED|")) continue;
- if (targets.has(r.id)) continue; // 已计划清理
- const bank = String(r.c_pay_bank || "").trim() || "(空)";
- const key = bid + "||" + bank;
- if (!groups.has(key)) groups.set(key, []);
- groups.get(key).push(r);
- }
- for (const [key, list] of groups) {
- if (list.length <= 1) continue;
- list.sort((a, b) => score(b) - score(a));
- console.log("重复组 " + key + " x" + list.length + " 保留 " + list[0].id);
- for (let i = 1; i < list.length; i++) {
- targets.set(list[i].id, "dup_of " + key + " keep=" + list[0].id);
- }
- }
- console.log("待清理条数=" + targets.size);
- let ok = 0;
- let fail = 0;
- for (const [id, reason] of targets) {
- const r = await removeOne(token, id);
- if (r.ok) {
- ok++;
- console.log("OK " + r.mode + " " + id + " :: " + reason);
- } else {
- fail++;
- console.log("FAIL " + id + " :: " + reason + " " + JSON.stringify(r).slice(0, 220));
- }
- }
- const after = await listAllStates(token);
- const liveKeys = after.filter(
- (r) => String(r.c_batch_id || "") && !String(r.c_batch_id).startsWith("REMOVED|")
- );
- const byBatch = new Map();
- for (const r of liveKeys) {
- const b = r.c_batch_id;
- byBatch.set(b, (byBatch.get(b) || 0) + 1);
- }
- const multi = [...byBatch.entries()].filter(([, n]) => n > 1);
- console.log("清理结果 ok=" + ok + " fail=" + fail);
- console.log("剩余占用业务键行=" + liveKeys.length + ";同 batch_id 多行组数=" + multi.length);
- for (const [b, n] of multi.slice(0, 15)) {
- const detail = liveKeys
- .filter((r) => r.c_batch_id === b)
- .map((r) => "bank=" + (r.c_pay_bank || "") + "/state=" + r.state)
- .join("; ");
- console.log(" " + b + " x" + n + " [" + detail + "]");
- }
- console.log(
- "说明:同 batch 多银行(合计/农商/农行)是正常数据;唯一索引请用 (c_batch_id, c_pay_bank),不要单独 UNIQUE(c_batch_id)。"
- );
- })().catch((e) => {
- console.error(e);
- process.exit(1);
- });
|