dedupe-pctj-stats.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * 清理批次统计脏数据:同一 (c_batch_id, c_pay_bank) 只保留一条。
  3. * 远端无法可靠物理删,将多余行 state 置为 -1(业务查询 states=0 不可见)。
  4. *
  5. * 用法:node scripts/dedupe-pctj-stats.js
  6. * node scripts/dedupe-pctj-stats.js --dry-run
  7. */
  8. const http = require("http");
  9. const qs = require("querystring");
  10. const HOST = "121.43.55.7";
  11. const PORT = 2101;
  12. const DRY = process.argv.includes("--dry-run");
  13. const COL = { columnId: 1846, modelId: 1969, name: "批次统计" };
  14. function request(method, urlPath, fields, token) {
  15. return new Promise((resolve, reject) => {
  16. const body = qs.stringify(fields || {});
  17. const headers = {
  18. "Content-Type": "application/x-www-form-urlencoded",
  19. "Content-Length": Buffer.byteLength(body),
  20. };
  21. if (token) {
  22. headers.Token = token;
  23. headers.token = token;
  24. }
  25. const req = http.request(
  26. { hostname: HOST, port: PORT, path: urlPath, method: method || "POST", headers },
  27. (res) => {
  28. let data = "";
  29. res.on("data", (c) => (data += c));
  30. res.on("end", () => {
  31. try {
  32. resolve(JSON.parse(data));
  33. } catch (e) {
  34. resolve({ raw: data, status: res.statusCode });
  35. }
  36. });
  37. }
  38. );
  39. req.on("error", reject);
  40. req.write(body);
  41. req.end();
  42. });
  43. }
  44. async function login() {
  45. const res = await request("POST", "/proxy_oauth/user/login", {
  46. userName: "user_liu",
  47. password: "WE176852439@lmx",
  48. clientId: "1",
  49. });
  50. if (res.code != 200) throw new Error("login fail: " + JSON.stringify(res));
  51. return res.message;
  52. }
  53. async function listAll(token) {
  54. const rows = [];
  55. for (let page = 0; page < 80; page++) {
  56. const res = await request(
  57. "POST",
  58. "/proxy_dms/content/selectContentList",
  59. {
  60. columnId: String(COL.columnId),
  61. modelId: String(COL.modelId),
  62. page: String(page),
  63. pageSize: "100",
  64. states: "0",
  65. },
  66. token
  67. );
  68. if (res.code == 202) break;
  69. if (res.code != 200) {
  70. throw new Error("list fail: " + JSON.stringify(res).slice(0, 400));
  71. }
  72. const data = (res.content && res.content.data) || [];
  73. rows.push.apply(rows, data);
  74. const count = Number((res.content && res.content.count) || 0);
  75. if (rows.length >= count || data.length === 0) break;
  76. }
  77. return rows;
  78. }
  79. async function softDelete(token, id, reason) {
  80. return request(
  81. "POST",
  82. "/proxy_dms/content/updateContent",
  83. {
  84. columnId: String(COL.columnId),
  85. modelId: String(COL.modelId),
  86. content: JSON.stringify({
  87. id: id,
  88. state: -1,
  89. c_remarks: "PCTJ_DEDUPE|" + reason + "|" + Date.now(),
  90. }),
  91. },
  92. token
  93. );
  94. }
  95. function score(row) {
  96. // 优先保留:有金额/人数、更新时间较新、有 grain
  97. const should = Number(row.c_should_pay_count || 0) + Number(row.c_actual_pay_count || 0);
  98. const amt = Number(row.c_should_pay_amount || 0) + Number(row.c_actual_pay_amount || 0);
  99. const t = Date.parse(row.update_time || row.create_time || 0) || 0;
  100. const grain = row.c_grain ? 1 : 0;
  101. return should * 1000 + amt + t / 1e12 + grain * 10;
  102. }
  103. (async () => {
  104. const token = await login();
  105. const rows = await listAll(token);
  106. console.log("可见统计行=" + rows.length + (DRY ? " [dry-run]" : ""));
  107. const groups = new Map();
  108. const orphan = [];
  109. for (const r of rows) {
  110. const bid = (r.c_batch_id || "").trim();
  111. const bank = (r.c_pay_bank || "").trim() || "(空)";
  112. if (!bid) {
  113. orphan.push(r);
  114. continue;
  115. }
  116. const key = bid + "||" + bank;
  117. if (!groups.has(key)) groups.set(key, []);
  118. groups.get(key).push(r);
  119. }
  120. const toDrop = [];
  121. for (const [key, list] of groups) {
  122. if (list.length <= 1) continue;
  123. list.sort((a, b) => score(b) - score(a));
  124. const keep = list[0];
  125. console.log(
  126. "重复 " +
  127. key +
  128. " x" +
  129. list.length +
  130. " 保留 id=" +
  131. keep.id +
  132. " 丢弃=" +
  133. list
  134. .slice(1)
  135. .map((x) => x.id)
  136. .join(",")
  137. );
  138. for (let i = 1; i < list.length; i++) {
  139. toDrop.push({ row: list[i], reason: "dup:" + key });
  140. }
  141. }
  142. for (const r of orphan) {
  143. console.log("无 batch_id 孤儿 id=" + r.id);
  144. toDrop.push({ row: r, reason: "orphan_no_batch" });
  145. }
  146. console.log("待清理=" + toDrop.length);
  147. let ok = 0;
  148. let fail = 0;
  149. for (const item of toDrop) {
  150. if (DRY) {
  151. ok++;
  152. continue;
  153. }
  154. const res = await softDelete(token, item.row.id, item.reason);
  155. if (res.code == 200) ok++;
  156. else {
  157. fail++;
  158. console.log("FAIL id=" + item.row.id + " " + JSON.stringify(res).slice(0, 200));
  159. }
  160. }
  161. const left = DRY ? rows.length - toDrop.length : (await listAll(token)).length;
  162. console.log("完成 purged=" + ok + " failed=" + fail + " 剩余可见=" + left);
  163. // 复检 (batch_id, pay_bank)
  164. const after = DRY ? rows.filter((r) => !toDrop.some((d) => d.row.id === r.id)) : await listAll(token);
  165. const chk = new Map();
  166. for (const r of after) {
  167. const key = (r.c_batch_id || "").trim() + "||" + ((r.c_pay_bank || "").trim() || "(空)");
  168. chk.set(key, (chk.get(key) || 0) + 1);
  169. }
  170. const still = [...chk.entries()].filter(([, n]) => n > 1);
  171. console.log("仍重复组数=" + still.length);
  172. for (const [k, n] of still.slice(0, 10)) console.log(" " + k + " x" + n);
  173. })().catch((e) => {
  174. console.error(e);
  175. process.exit(1);
  176. });