cleanup-pctj-dirty.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * 清理批次统计脏数据(含软删行),避免建唯一索引时仍撞到历史重复键。
  3. *
  4. * 策略:
  5. * 1) 拉取 states 含 -1 的全部行
  6. * 2) state=-1 或 c_batch_id 以 REMOVED| 开头:尝试物理删 / 销毁
  7. * 3) 同一 (c_batch_id, c_pay_bank) 多条:保留分最高的一条,其余删除
  8. * 4) 删除失败则把 c_batch_id 改成 REMOVED|{id},不再占用业务键
  9. *
  10. * 用法:node scripts/cleanup-pctj-dirty.js
  11. */
  12. const http = require("http");
  13. const qs = require("querystring");
  14. const HOST = "121.43.55.7";
  15. const PORT = 2101;
  16. const COL = { columnId: 1846, modelId: 1969 };
  17. function request(method, urlPath, fields, token) {
  18. return new Promise((resolve, reject) => {
  19. const body = qs.stringify(fields || {});
  20. const headers = {
  21. "Content-Type": "application/x-www-form-urlencoded",
  22. "Content-Length": Buffer.byteLength(body),
  23. };
  24. if (token) {
  25. headers.Token = token;
  26. headers.token = token;
  27. }
  28. const req = http.request(
  29. { hostname: HOST, port: PORT, path: urlPath, method: method || "POST", headers },
  30. (res) => {
  31. let data = "";
  32. res.on("data", (c) => (data += c));
  33. res.on("end", () => {
  34. try {
  35. resolve(JSON.parse(data));
  36. } catch (e) {
  37. resolve({ raw: String(data).slice(0, 300), status: res.statusCode });
  38. }
  39. });
  40. }
  41. );
  42. req.on("error", reject);
  43. if (body) req.write(body);
  44. req.end();
  45. });
  46. }
  47. async function login() {
  48. const res = await request("POST", "/proxy_oauth/user/login", {
  49. userName: "user_liu",
  50. password: "WE176852439@lmx",
  51. clientId: "1",
  52. });
  53. if (res.code != 200) throw new Error("login fail: " + JSON.stringify(res));
  54. return res.message;
  55. }
  56. async function listAllStates(token) {
  57. const rows = [];
  58. for (let page = 0; page < 80; page++) {
  59. const res = await request(
  60. "POST",
  61. "/proxy_dms/content/selectContentList",
  62. {
  63. columnId: String(COL.columnId),
  64. modelId: String(COL.modelId),
  65. page: String(page),
  66. pageSize: "100",
  67. states: "0,1,2,3,-1,4",
  68. },
  69. token
  70. );
  71. if (res.code == 202) break;
  72. if (res.code != 200) throw new Error("list fail: " + JSON.stringify(res).slice(0, 300));
  73. const data = (res.content && res.content.data) || [];
  74. rows.push.apply(rows, data);
  75. if (data.length < 100) break;
  76. }
  77. return rows;
  78. }
  79. async function hardDelete(token, id) {
  80. return request(
  81. "DELETE",
  82. "/proxy_dms/content/delContentById",
  83. { contentId: id, columnId: String(COL.columnId) },
  84. token
  85. );
  86. }
  87. async function destroy(token, id) {
  88. return request(
  89. "POST",
  90. "/proxy_dms/content/updateAudit",
  91. {
  92. columnId: String(COL.columnId),
  93. id,
  94. state: "4",
  95. auditorName: "pctj-cleanup",
  96. auditorComment: "批次统计脏数据清理",
  97. },
  98. token
  99. );
  100. }
  101. async function neutralize(token, id) {
  102. return request(
  103. "POST",
  104. "/proxy_dms/content/updateContent",
  105. {
  106. columnId: String(COL.columnId),
  107. modelId: String(COL.modelId),
  108. content: JSON.stringify({
  109. id,
  110. state: -1,
  111. c_batch_id: "REMOVED|" + id,
  112. c_remarks: "PCTJ_CLEANED|" + Date.now(),
  113. }),
  114. },
  115. token
  116. );
  117. }
  118. async function removeOne(token, id) {
  119. const hard = await hardDelete(token, id);
  120. if (hard && hard.code == 200) return { ok: true, mode: "delete" };
  121. const soft = await destroy(token, id);
  122. if (soft && soft.code == 200) return { ok: true, mode: "destroy", hard };
  123. const neu = await neutralize(token, id);
  124. if (neu && neu.code == 200) return { ok: true, mode: "neutralize", hard, soft };
  125. return { ok: false, hard, soft, neu };
  126. }
  127. function score(row) {
  128. const should = Number(row.c_should_pay_count || 0) + Number(row.c_actual_pay_count || 0);
  129. const amt = Number(row.c_should_pay_amount || 0) + Number(row.c_actual_pay_amount || 0);
  130. const t = Date.parse(row.update_time || row.create_time || 0) || 0;
  131. const live = String(row.state) === "0" ? 100000 : 0;
  132. return live + should * 1000 + amt + t / 1e12;
  133. }
  134. (async () => {
  135. const token = await login();
  136. const rows = await listAllStates(token);
  137. console.log("全状态统计行=" + rows.length);
  138. const targets = new Map(); // id -> reason
  139. // A. 软删/销毁态但仍占用真实 batch_id
  140. for (const r of rows) {
  141. const st = String(r.state);
  142. const bid = String(r.c_batch_id || "");
  143. if ((st === "-1" || st === "4") && bid && !bid.startsWith("REMOVED|")) {
  144. targets.set(r.id, "soft_deleted_live_key state=" + st + " batch=" + bid + " bank=" + (r.c_pay_bank || ""));
  145. }
  146. }
  147. // B. 同一 (batch_id, pay_bank) 重复(含各 state)
  148. const groups = new Map();
  149. for (const r of rows) {
  150. const bid = String(r.c_batch_id || "").trim();
  151. if (!bid || bid.startsWith("REMOVED|")) continue;
  152. if (targets.has(r.id)) continue; // 已计划清理
  153. const bank = String(r.c_pay_bank || "").trim() || "(空)";
  154. const key = bid + "||" + bank;
  155. if (!groups.has(key)) groups.set(key, []);
  156. groups.get(key).push(r);
  157. }
  158. for (const [key, list] of groups) {
  159. if (list.length <= 1) continue;
  160. list.sort((a, b) => score(b) - score(a));
  161. console.log("重复组 " + key + " x" + list.length + " 保留 " + list[0].id);
  162. for (let i = 1; i < list.length; i++) {
  163. targets.set(list[i].id, "dup_of " + key + " keep=" + list[0].id);
  164. }
  165. }
  166. console.log("待清理条数=" + targets.size);
  167. let ok = 0;
  168. let fail = 0;
  169. for (const [id, reason] of targets) {
  170. const r = await removeOne(token, id);
  171. if (r.ok) {
  172. ok++;
  173. console.log("OK " + r.mode + " " + id + " :: " + reason);
  174. } else {
  175. fail++;
  176. console.log("FAIL " + id + " :: " + reason + " " + JSON.stringify(r).slice(0, 220));
  177. }
  178. }
  179. const after = await listAllStates(token);
  180. const liveKeys = after.filter(
  181. (r) => String(r.c_batch_id || "") && !String(r.c_batch_id).startsWith("REMOVED|")
  182. );
  183. const byBatch = new Map();
  184. for (const r of liveKeys) {
  185. const b = r.c_batch_id;
  186. byBatch.set(b, (byBatch.get(b) || 0) + 1);
  187. }
  188. const multi = [...byBatch.entries()].filter(([, n]) => n > 1);
  189. console.log("清理结果 ok=" + ok + " fail=" + fail);
  190. console.log("剩余占用业务键行=" + liveKeys.length + ";同 batch_id 多行组数=" + multi.length);
  191. for (const [b, n] of multi.slice(0, 15)) {
  192. const detail = liveKeys
  193. .filter((r) => r.c_batch_id === b)
  194. .map((r) => "bank=" + (r.c_pay_bank || "") + "/state=" + r.state)
  195. .join("; ");
  196. console.log(" " + b + " x" + n + " [" + detail + "]");
  197. }
  198. console.log(
  199. "说明:同 batch 多银行(合计/农商/农行)是正常数据;唯一索引请用 (c_batch_id, c_pay_bank),不要单独 UNIQUE(c_batch_id)。"
  200. );
  201. })().catch((e) => {
  202. console.error(e);
  203. process.exit(1);
  204. });