dedupe-dms-regions.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /**
  2. * 行政区划栏目去重:按业务 code(remarks 中 code=)分组,保留最早一条,其余删除/销毁。
  3. *
  4. * 用法: node scripts/dedupe-dms-regions.js
  5. * 可选: node scripts/dedupe-dms-regions.js --dry-run
  6. */
  7. const http = require("http");
  8. const qs = require("querystring");
  9. const fs = require("fs");
  10. const path = require("path");
  11. const HOST = "121.43.55.7";
  12. const PORT = 2101;
  13. const DRY = process.argv.includes("--dry-run");
  14. const COL = { columnId: 1842, modelId: 1965 };
  15. function req(urlPath, fields, token, method) {
  16. return new Promise((resolve, reject) => {
  17. const body = qs.stringify(fields || {});
  18. const headers = {
  19. "Content-Type": "application/x-www-form-urlencoded",
  20. "Content-Length": Buffer.byteLength(body),
  21. };
  22. if (token) {
  23. headers.Token = token;
  24. headers.token = token;
  25. }
  26. const r = http.request(
  27. { hostname: HOST, port: PORT, path: urlPath, method: method || "POST", headers },
  28. (res) => {
  29. let data = "";
  30. res.on("data", (c) => (data += c));
  31. res.on("end", () => {
  32. try {
  33. resolve(JSON.parse(data));
  34. } catch (e) {
  35. resolve({ raw: data, status: res.statusCode });
  36. }
  37. });
  38. }
  39. );
  40. r.on("error", reject);
  41. r.write(body);
  42. r.end();
  43. });
  44. }
  45. async function login() {
  46. const res = await req("/proxy_oauth/user/login", {
  47. userName: "user_liu",
  48. password: "WE176852439@lmx",
  49. clientId: "1",
  50. });
  51. if (res.code != 200) throw new Error("login fail: " + JSON.stringify(res));
  52. return res.message && String(res.message).startsWith("eyJ")
  53. ? res.message
  54. : res.content;
  55. }
  56. function remarkTag(remarks, key) {
  57. if (!remarks) return null;
  58. const m = String(remarks).match(new RegExp("(?:^|[|])" + key + "=([^|]+)"));
  59. return m ? m[1] : null;
  60. }
  61. function bizKey(row) {
  62. const remarks = row.c_remarks || row.content || "";
  63. const code = remarkTag(remarks, "code");
  64. if (code) return "code:" + code;
  65. const level = remarkTag(remarks, "level") || "";
  66. const parent = row.c_parent_id || "";
  67. const name = row.c_name || row.title || "";
  68. return "name:" + level + "|" + parent + "|" + name;
  69. }
  70. async function fetchAll(token) {
  71. const all = [];
  72. let page = 0;
  73. const pageSize = 100;
  74. while (page < 50) {
  75. const res = await req(
  76. "/proxy_dms/content/selectContentList",
  77. {
  78. columnId: String(COL.columnId),
  79. modelId: String(COL.modelId),
  80. states: "0",
  81. page: String(page),
  82. pageSize: String(pageSize),
  83. },
  84. token
  85. );
  86. if (res.code == 202) break;
  87. if (res.code != 200) {
  88. throw new Error("select fail: " + JSON.stringify(res).slice(0, 300));
  89. }
  90. const content = res.content || {};
  91. const rows = content.data || content.list || content.records || [];
  92. if (!rows.length) break;
  93. all.push(...rows);
  94. const total = content.count || content.total || 0;
  95. if (all.length >= total || rows.length < pageSize) break;
  96. page++;
  97. }
  98. return all;
  99. }
  100. async function hardDelete(token, id) {
  101. // DMS 原生删除(部分环境 columnId 解析有 bug,失败则走销毁)
  102. return req(
  103. "/proxy_dms/content/delContentById",
  104. { contentId: id, columnId: String(COL.columnId) },
  105. token,
  106. "DELETE"
  107. );
  108. }
  109. async function destroy(token, id) {
  110. // state=4 销毁;estimateType 对 4 恒为 true
  111. return req(
  112. "/proxy_dms/content/updateAudit",
  113. {
  114. columnId: String(COL.columnId),
  115. id,
  116. state: "4",
  117. auditorName: "dedupe-script",
  118. auditorComment: "行政区划去重销毁重复项",
  119. },
  120. token
  121. );
  122. }
  123. async function removeOne(token, id) {
  124. const hard = await hardDelete(token, id);
  125. if (hard && hard.code == 200) {
  126. return { ok: true, mode: "delete", res: hard };
  127. }
  128. const soft = await destroy(token, id);
  129. if (soft && soft.code == 200) {
  130. return { ok: true, mode: "destroy", res: soft, hardFail: hard };
  131. }
  132. return { ok: false, mode: "fail", hard, soft };
  133. }
  134. (async () => {
  135. const token = await login();
  136. console.log("login ok", DRY ? "(dry-run)" : "(live)");
  137. const rows = await fetchAll(token);
  138. console.log("loaded regions:", rows.length);
  139. const groups = new Map();
  140. for (const row of rows) {
  141. const key = bizKey(row);
  142. if (!groups.has(key)) groups.set(key, []);
  143. groups.get(key).push(row);
  144. }
  145. const plan = [];
  146. for (const [key, list] of groups.entries()) {
  147. if (list.length <= 1) continue;
  148. list.sort((a, b) => {
  149. const ta = Number(a.create_time || a.c_operate_time || 0);
  150. const tb = Number(b.create_time || b.c_operate_time || 0);
  151. if (ta !== tb) return ta - tb;
  152. return String(a.id).localeCompare(String(b.id));
  153. });
  154. const keep = list[0];
  155. const drop = list.slice(1);
  156. plan.push({
  157. key,
  158. keepId: keep.id,
  159. keepName: keep.c_name || keep.title,
  160. dropIds: drop.map((d) => d.id),
  161. dropNames: drop.map((d) => d.c_name || d.title),
  162. });
  163. }
  164. console.log("duplicate groups:", plan.length);
  165. const result = { dryRun: DRY, total: rows.length, groups: plan.length, kept: [], removed: [], errors: [] };
  166. for (const g of plan) {
  167. console.log(
  168. ` ${g.key} keep=${g.keepId}(${g.keepName}) drop=${g.dropIds.length} [${g.dropNames.join(",")}]`
  169. );
  170. result.kept.push({ key: g.key, id: g.keepId, name: g.keepName });
  171. if (DRY) {
  172. for (const id of g.dropIds) {
  173. result.removed.push({ id, key: g.key, mode: "dry-run" });
  174. }
  175. continue;
  176. }
  177. for (const id of g.dropIds) {
  178. const r = await removeOne(token, id);
  179. if (r.ok) {
  180. console.log(" -", r.mode, id);
  181. result.removed.push({ id, key: g.key, mode: r.mode });
  182. } else {
  183. console.log(" x fail", id, JSON.stringify(r.hard || r.soft).slice(0, 200));
  184. result.errors.push({ id, key: g.key, hard: r.hard, soft: r.soft });
  185. }
  186. }
  187. }
  188. const outPath = path.join(__dirname, "dedupe-dms-regions-result.json");
  189. fs.writeFileSync(outPath, JSON.stringify(result, null, 2));
  190. console.log("done. removed=", result.removed.length, "errors=", result.errors.length);
  191. console.log("result ->", outPath);
  192. })().catch((e) => {
  193. console.error(e);
  194. process.exit(1);
  195. });