migrate-remarks-to-fields.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. /**
  2. * 把业务流程从备注协议迁到正式字段(2026-08-13)
  3. * 用法:node scripts/migrate-remarks-to-fields.js
  4. */
  5. const http = require("http");
  6. const qs = require("querystring");
  7. const fs = require("fs");
  8. const path = require("path");
  9. const HOST = "121.43.55.7";
  10. const PORT = 2101;
  11. function post(urlPath, fields, token) {
  12. return new Promise((resolve, reject) => {
  13. const body = qs.stringify(fields || {});
  14. const headers = {
  15. "Content-Type": "application/x-www-form-urlencoded",
  16. "Content-Length": Buffer.byteLength(body),
  17. };
  18. if (token) {
  19. headers.Token = token;
  20. headers.token = token;
  21. }
  22. const req = http.request(
  23. { hostname: HOST, port: PORT, path: urlPath, method: "POST", headers },
  24. (res) => {
  25. let data = "";
  26. res.on("data", (c) => (data += c));
  27. res.on("end", () => {
  28. try {
  29. resolve(JSON.parse(data));
  30. } catch (e) {
  31. resolve({ raw: data, status: res.statusCode });
  32. }
  33. });
  34. }
  35. );
  36. req.on("error", reject);
  37. req.write(body);
  38. req.end();
  39. });
  40. }
  41. function text(shortName, alias, must, seq) {
  42. return {
  43. alias,
  44. customType: "",
  45. describe: "格式为富文本",
  46. frontType: "content",
  47. id: 2,
  48. must: !!must,
  49. name: "c_" + shortName,
  50. searchType: "2",
  51. sequence: seq || 1,
  52. showParam: "name,alias,desc,type,front_type,must,default_value",
  53. type: "text",
  54. };
  55. }
  56. function num(shortName, alias, must, seq) {
  57. return {
  58. alias,
  59. customType: "",
  60. describe: "number",
  61. frontType: "float_num",
  62. id: 6,
  63. must: !!must,
  64. name: "c_" + shortName,
  65. searchType: "1,3",
  66. sequence: seq || 5,
  67. showParam: "name,alias,desc,type,front_type,must,default_value",
  68. sortType: "1,2",
  69. type: "double",
  70. };
  71. }
  72. function ts(shortName, alias, must, seq) {
  73. return {
  74. alias,
  75. customType: "",
  76. describe: "时间戳",
  77. frontType: "date_time",
  78. id: 3,
  79. must: !!must,
  80. name: "c_" + shortName,
  81. searchType: "3",
  82. sequence: seq || 2,
  83. showParam: "name,alias,describe,type,front_type,must,date_type,date_picker",
  84. sortType: "1,2",
  85. type: "timestamp",
  86. };
  87. }
  88. function intf(shortName, alias, must, seq) {
  89. return {
  90. alias,
  91. customType: "",
  92. describe: "number",
  93. frontType: "int_num",
  94. id: 5,
  95. must: !!must,
  96. name: "c_" + shortName,
  97. searchType: "1,3",
  98. sequence: seq || 4,
  99. showParam: "name,alias,desc,type,front_type,must,default_value",
  100. sortType: "1,2",
  101. type: "integer",
  102. };
  103. }
  104. function normalizeFieldList(raw) {
  105. let fl = raw;
  106. if (typeof fl === "string") fl = JSON.parse(fl);
  107. const out = {};
  108. Object.keys(fl || {}).forEach((k) => {
  109. let v = fl[k];
  110. if (typeof v === "string") v = JSON.parse(v);
  111. out[k] = v;
  112. });
  113. return out;
  114. }
  115. async function getModel(token, modelId) {
  116. const res = await post("/proxy_dms/model/getModelById", { modelId: String(modelId) }, token);
  117. if (res.code != 200) throw new Error("getModel " + modelId + " fail: " + JSON.stringify(res));
  118. return res.content;
  119. }
  120. async function updateFieldList(token, modelId, addMap) {
  121. const model = await getModel(token, modelId);
  122. const fieldList = normalizeFieldList(model.fieldList);
  123. if (Object.keys(fieldList).length < 2) {
  124. throw new Error("model " + modelId + " fieldList 异常,拒绝覆盖");
  125. }
  126. const addfieldList = {};
  127. Object.keys(addMap || {}).forEach((shortName) => {
  128. const full = "c_" + shortName;
  129. if (fieldList[full]) return;
  130. const p = addMap[shortName];
  131. p.name = full;
  132. fieldList[full] = p;
  133. addfieldList[shortName] = {
  134. alias: p.alias,
  135. type: p.type,
  136. frontType: p.frontType,
  137. must: p.must,
  138. name: shortName,
  139. describe: p.describe,
  140. searchType: p.searchType,
  141. sortType: p.sortType,
  142. sequence: p.sequence,
  143. showParam: p.showParam,
  144. customType: p.customType || "",
  145. id: p.id,
  146. };
  147. });
  148. if (Object.keys(addfieldList).length === 0) {
  149. return {
  150. modelId,
  151. modelName: model.modelName,
  152. code: 200,
  153. message: "无需新增",
  154. added: [],
  155. fields: Object.keys(fieldList).sort(),
  156. };
  157. }
  158. const res = await post(
  159. "/proxy_dms/model/updateFieldList",
  160. {
  161. modelId: String(modelId),
  162. fieldList: JSON.stringify(fieldList),
  163. addfieldList: JSON.stringify(addfieldList),
  164. delfieldList: JSON.stringify({}),
  165. updatefieldList: JSON.stringify({}),
  166. },
  167. token
  168. );
  169. const verify = await getModel(token, modelId);
  170. return {
  171. modelId,
  172. modelName: model.modelName,
  173. code: res.code,
  174. message: res.message || res.content,
  175. added: Object.keys(addfieldList),
  176. fields: Object.keys(normalizeFieldList(verify.fieldList)).sort(),
  177. };
  178. }
  179. function remarkTag(remarks, key) {
  180. if (!remarks || !key) return null;
  181. const parts = String(remarks).split("|");
  182. for (const p of parts) {
  183. const i = p.indexOf("=");
  184. if (i > 0 && p.slice(0, i).trim() === key) return p.slice(i + 1).trim();
  185. }
  186. return null;
  187. }
  188. function pipeVal(remarks, key) {
  189. if (!remarks || !key) return null;
  190. const token = "|" + key + "|";
  191. const s = String(remarks);
  192. const i = s.indexOf(token);
  193. if (i < 0) return null;
  194. const rest = s.slice(i + token.length);
  195. const cut = rest.indexOf("|");
  196. const v = cut >= 0 ? rest.slice(0, cut) : rest;
  197. return v ? v : null;
  198. }
  199. async function listAll(token, columnId, modelId) {
  200. const all = [];
  201. for (let page = 0; page < 50; page++) {
  202. const list = await post(
  203. "/proxy_dms/content/selectContentList",
  204. { columnId: String(columnId), modelId: String(modelId), page: String(page), pageSize: "100", states: "0" },
  205. token
  206. );
  207. const rows = (list.content && list.content.data) || [];
  208. all.push(...rows);
  209. if (rows.length < 100) break;
  210. }
  211. return all;
  212. }
  213. async function updateRow(token, columnId, modelId, content) {
  214. return post(
  215. "/proxy_dms/content/updateContent",
  216. { columnId: String(columnId), modelId: String(modelId), content: JSON.stringify(content) },
  217. token
  218. );
  219. }
  220. function extractBizId(remarks) {
  221. if (!remarks) return null;
  222. const parts = String(remarks).split("|");
  223. for (const p of parts) {
  224. if (/^P\d+$/.test(p.trim())) return p.trim();
  225. }
  226. return null;
  227. }
  228. function cleanPersonnelRemarks(remarks) {
  229. if (!remarks) return "";
  230. let s = String(remarks);
  231. s = s.replace(/^DELETED\|/, "").replace(/^PURGED\|/, "");
  232. s = s.replace(/^MOCK_SEED_\d+\|/, "");
  233. s = s.replace(/^P\d+\|/, "");
  234. s = s.replace(/\|FUNERAL_PAID\|[^|]*/g, "");
  235. s = s.replace(/\|heirRelation=[^|]*/g, "");
  236. s = s.replace(/\|ALLOW_DIRECT/g, "");
  237. s = s.replace(/\|+/g, "|").replace(/^\||\|$/g, "").trim();
  238. if (s === "管理员修改" || s === "村级录入" || s === "Excel导入") return s;
  239. return s;
  240. }
  241. async function backfillPersonnel(token) {
  242. const rows = await listAll(token, 1847, 1970);
  243. let updated = 0;
  244. for (const row of rows) {
  245. const remarks = row.c_remarks || "";
  246. const content = { id: row.id };
  247. let dirty = false;
  248. const biz = row.c_biz_id || extractBizId(remarks);
  249. if (biz && biz !== row.c_biz_id) {
  250. content.c_biz_id = biz;
  251. dirty = true;
  252. }
  253. const heir = row.c_heir_relation || remarkTag(remarks, "heirRelation");
  254. if (heir && heir !== row.c_heir_relation) {
  255. content.c_heir_relation = heir;
  256. dirty = true;
  257. }
  258. if (/FUNERAL_PAID/.test(remarks) && row.c_funeral_paid !== "true") {
  259. content.c_funeral_paid = "true";
  260. dirty = true;
  261. }
  262. const cleaned = cleanPersonnelRemarks(remarks);
  263. if (cleaned !== remarks) {
  264. content.c_remarks = cleaned;
  265. dirty = true;
  266. }
  267. if (!dirty) continue;
  268. const res = await updateRow(token, 1847, 1970, content);
  269. if (res.code == 200) updated++;
  270. }
  271. return { total: rows.length, updated };
  272. }
  273. async function backfillRegion(token) {
  274. const rows = await listAll(token, 1842, 1965);
  275. let updated = 0;
  276. for (const row of rows) {
  277. const remarks = row.c_remarks || "";
  278. const content = { id: row.id };
  279. let dirty = false;
  280. const bank = row.c_default_bank || remarkTag(remarks, "bank");
  281. if (bank && bank !== row.c_default_bank) {
  282. content.c_default_bank = bank;
  283. dirty = true;
  284. }
  285. const enabledTag = remarkTag(remarks, "enabled");
  286. const enabled = row.c_enabled || (enabledTag === "false" ? "false" : "true");
  287. if (enabled && enabled !== row.c_enabled) {
  288. content.c_enabled = enabled;
  289. dirty = true;
  290. }
  291. const d = row.c_district_share_percent || remarkTag(remarks, "DISTRICT_SHARE") || remarkTag(remarks, "districtShare");
  292. if (d != null && d !== "" && String(d) !== String(row.c_district_share_percent)) {
  293. content.c_district_share_percent = Number(String(d).replace("%", ""));
  294. dirty = true;
  295. }
  296. const t = row.c_town_share_percent || remarkTag(remarks, "TOWN_SHARE") || remarkTag(remarks, "townShare");
  297. if (t != null && t !== "" && String(t) !== String(row.c_town_share_percent)) {
  298. content.c_town_share_percent = Number(String(t).replace("%", ""));
  299. dirty = true;
  300. }
  301. let cleaned = String(remarks)
  302. .replace(/(^|\|)(level|code|parent|plan|entrust|bank|enabled|DISTRICT_SHARE|TOWN_SHARE|districtShare|townShare)=[^|]*/g, "")
  303. .replace(/\|+/g, "|")
  304. .replace(/^\||\|$/g, "")
  305. .trim();
  306. if (cleaned !== remarks) {
  307. content.c_remarks = cleaned;
  308. dirty = true;
  309. }
  310. if (!dirty) continue;
  311. const res = await updateRow(token, 1842, 1965, content);
  312. if (res.code == 200) updated++;
  313. }
  314. return { total: rows.length, updated };
  315. }
  316. async function backfillBatch(token) {
  317. const rows = await listAll(token, 1845, 1968);
  318. let updated = 0;
  319. for (const row of rows) {
  320. const remarks = String(row.c_remarks || "");
  321. const content = { id: row.id };
  322. let dirty = false;
  323. const m = remarks.match(/SUBMIT_TIME\|(\d+)/);
  324. if (m && !row.c_submit_time) {
  325. content.c_submit_time = Number(m[1]);
  326. dirty = true;
  327. }
  328. if (/CALC_LOCKED\|true/.test(remarks) && row.c_calc_locked !== "true") {
  329. content.c_calc_locked = "true";
  330. dirty = true;
  331. }
  332. if ((/YEARLY_ADJUST/.test(remarks) || /调标/.test(remarks)) && row.c_yearly_adjust !== "true") {
  333. content.c_yearly_adjust = "true";
  334. dirty = true;
  335. }
  336. if (/DELETED\|/.test(remarks) && row.c_deleted !== "true") {
  337. content.c_deleted = "true";
  338. dirty = true;
  339. }
  340. const rf = remarks.match(/RETURN_FILE\|([^|]+)/);
  341. if (rf && !row.c_return_file_name) {
  342. content.c_return_file_name = rf[1];
  343. dirty = true;
  344. }
  345. let cleaned = remarks
  346. .replace(/SUBMIT_TIME\|\d+/g, "")
  347. .replace(/\|?CALC_LOCKED\|(true|false)/g, "")
  348. .replace(/^MOCK_SEED_\d+\|/, "")
  349. .replace(/^API创建\|/, "")
  350. .replace(/RETURN_FILE\|[^|]+\|/g, "")
  351. .replace(/^DELETED\|/, "")
  352. .replace(/^PURGED\|/, "")
  353. .replace(/\|+/g, "|")
  354. .replace(/^\||\|$/g, "")
  355. .trim();
  356. if (cleaned !== remarks) {
  357. content.c_remarks = cleaned;
  358. dirty = true;
  359. }
  360. if (!dirty) continue;
  361. const res = await updateRow(token, 1845, 1968, content);
  362. if (res.code == 200) updated++;
  363. }
  364. return { total: rows.length, updated };
  365. }
  366. async function backfillPayment(token) {
  367. const rows = await listAll(token, 1849, 1974);
  368. let updated = 0;
  369. for (const row of rows) {
  370. const remarks = String(row.c_remarks || "");
  371. const content = { id: row.id };
  372. let dirty = false;
  373. const dist = remarkTag(remarks, "DIST_AMT") || pipeVal(remarks, "DIST_AMT");
  374. const town = remarkTag(remarks, "TOWN_AMT") || pipeVal(remarks, "TOWN_AMT");
  375. if (dist && row.c_district_amount == null) {
  376. content.c_district_amount = Number(dist);
  377. dirty = true;
  378. }
  379. if (town && row.c_town_amount == null) {
  380. content.c_town_amount = Number(town);
  381. dirty = true;
  382. }
  383. const oldM = remarks.match(/旧月标\s*([0-9.]+)/);
  384. const newM = remarks.match(/新月标\s*([0-9.]+)/);
  385. if (oldM && row.c_old_monthly_amount == null) {
  386. content.c_old_monthly_amount = Number(oldM[1]);
  387. dirty = true;
  388. }
  389. if (newM && row.c_new_monthly_amount == null) {
  390. content.c_new_monthly_amount = Number(newM[1]);
  391. dirty = true;
  392. }
  393. const km = remarks.match(/应发\s*(\d+)\s*月/);
  394. if (km && row.c_adjust_months == null) {
  395. content.c_adjust_months = Number(km[1]);
  396. dirty = true;
  397. }
  398. if (!dirty) continue;
  399. const res = await updateRow(token, 1849, 1974, content);
  400. if (res.code == 200) updated++;
  401. }
  402. return { total: rows.length, updated };
  403. }
  404. async function backfillStandard(token) {
  405. const rows = await listAll(token, 1843, 1966);
  406. let updated = 0;
  407. for (const row of rows) {
  408. const remarks = String(row.c_remarks || "");
  409. if (!/DELETED\|/.test(remarks) || row.c_deleted === "true") continue;
  410. const res = await updateRow(token, 1843, 1966, { id: row.id, c_deleted: "true" });
  411. if (res.code == 200) updated++;
  412. }
  413. return { total: rows.length, updated };
  414. }
  415. async function backfillProcess(token) {
  416. const rows = await listAll(token, 1844, 1967);
  417. let updated = 0;
  418. for (const row of rows) {
  419. const remarks = String(row.c_remarks || "");
  420. const content = { id: row.id };
  421. let dirty = false;
  422. if (remarks.includes("|SPECIAL|")) {
  423. const amt = pipeVal(remarks, "AMT");
  424. const town = pipeVal(remarks, "TOWN");
  425. const batches = pipeVal(remarks, "BATCHES");
  426. const death = pipeVal(remarks, "DEATH_DATE");
  427. if (amt && row.c_amount == null) {
  428. content.c_amount = Number(amt);
  429. dirty = true;
  430. }
  431. if (town && !row.c_town_id) {
  432. content.c_town_id = town;
  433. dirty = true;
  434. }
  435. if (batches && !row.c_related_batch_ids) {
  436. content.c_related_batch_ids = batches;
  437. dirty = true;
  438. }
  439. if (death && !row.c_death_date) {
  440. content.c_death_date = death;
  441. dirty = true;
  442. }
  443. const namePart = remarks.split("|SPECIAL|")[1] || "";
  444. const nm = namePart.split("|AMT|")[0];
  445. if (nm && !row.c_personnel_name) {
  446. content.c_personnel_name = nm.trim();
  447. dirty = true;
  448. }
  449. }
  450. const rp = pipeVal(remarks, "REFUND_PROGRESS");
  451. if (rp && !row.c_refund_progress) {
  452. content.c_refund_progress = rp;
  453. dirty = true;
  454. }
  455. if (remarks.includes("|EXPORTED|") && row.c_exported !== "true") {
  456. content.c_exported = "true";
  457. dirty = true;
  458. }
  459. if (remarks.includes("STANDARD_PAYLOAD|") && !row.c_payload) {
  460. const part = remarks.split("STANDARD_PAYLOAD|")[1] || "";
  461. const cut = part.indexOf("|SPECIAL|");
  462. content.c_payload = cut >= 0 ? part.slice(0, cut) : part;
  463. dirty = true;
  464. }
  465. const events = pipeVal(remarks, "EVENTS");
  466. if (events && !row.c_events) {
  467. content.c_events = events;
  468. dirty = true;
  469. }
  470. if (row.c_biz_type === "CALC_ROUND" || remarks.match(/^\d+\|\d+\|/)) {
  471. const segs = remarks.split("|");
  472. if (segs.length >= 3 && row.c_calc_version == null) {
  473. content.c_calc_version = Number(segs[0]);
  474. content.c_people_count = Number(segs[1]);
  475. if (row.c_amount == null) content.c_amount = Number(segs[2]);
  476. dirty = true;
  477. }
  478. }
  479. if (!dirty) continue;
  480. const res = await updateRow(token, 1844, 1967, content);
  481. if (res.code == 200) updated++;
  482. }
  483. return { total: rows.length, updated };
  484. }
  485. (async () => {
  486. const login = await post("/proxy_oauth/user/login", {
  487. userName: "user_liu",
  488. password: "WE176852439@lmx",
  489. clientId: "1",
  490. });
  491. if (login.code != 200) throw new Error("login fail " + JSON.stringify(login));
  492. const token = login.message;
  493. console.log("login ok");
  494. const results = [];
  495. results.push(
  496. await updateFieldList(token, 1970, {
  497. biz_id: text("biz_id", "人员业务号", false, 50),
  498. heir_relation: text("heir_relation", "继承人与逝者关系", false, 51),
  499. funeral_paid: text("funeral_paid", "丧葬是否已发放", false, 52),
  500. })
  501. );
  502. console.log("personnel", results[results.length - 1]);
  503. results.push(
  504. await updateFieldList(token, 1965, {
  505. enabled: text("enabled", "是否启用", false, 20),
  506. default_bank: text("default_bank", "默认发放银行", false, 21),
  507. district_share_percent: intf("district_share_percent", "区级资金承担比例", false, 22),
  508. town_share_percent: intf("town_share_percent", "镇级资金承担比例", false, 23),
  509. sort_no: intf("sort_no", "排序号", false, 24),
  510. })
  511. );
  512. console.log("region", results[results.length - 1]);
  513. results.push(
  514. await updateFieldList(token, 1974, {
  515. district_amount: num("district_amount", "区级承担金额", false, 40),
  516. town_amount: num("town_amount", "镇级承担金额", false, 41),
  517. old_monthly_amount: num("old_monthly_amount", "调标前月补", false, 42),
  518. new_monthly_amount: num("new_monthly_amount", "调标后月补", false, 43),
  519. adjust_months: intf("adjust_months", "调标应发月数", false, 44),
  520. })
  521. );
  522. console.log("payment", results[results.length - 1]);
  523. results.push(
  524. await updateFieldList(token, 1968, {
  525. submit_time: ts("submit_time", "提交初审时间", false, 50),
  526. calc_locked: text("calc_locked", "测算是否锁定", false, 51),
  527. yearly_adjust: text("yearly_adjust", "是否年调标批", false, 52),
  528. deleted: text("deleted", "是否已删除", false, 53),
  529. return_file_name: text("return_file_name", "回盘文件名", false, 54),
  530. })
  531. );
  532. console.log("batch", results[results.length - 1]);
  533. results.push(
  534. await updateFieldList(token, 1966, {
  535. deleted: text("deleted", "是否已删除", false, 20),
  536. })
  537. );
  538. console.log("amount", results[results.length - 1]);
  539. results.push(
  540. await updateFieldList(token, 1967, {
  541. events: text("events", "待补发事件流水", false, 60),
  542. amount: num("amount", "业务金额", false, 61),
  543. related_batch_ids: text("related_batch_ids", "关联批次ID列表", false, 62),
  544. death_date: text("death_date", "死亡日期", false, 63),
  545. refund_method: text("refund_method", "退款方式", false, 64),
  546. refund_progress: text("refund_progress", "退款进度", false, 65),
  547. payload: text("payload", "审批载荷", false, 66),
  548. heir_name: text("heir_name", "继承人姓名", false, 67),
  549. heir_id_number: text("heir_id_number", "继承人身份证", false, 68),
  550. heir_bank_card: text("heir_bank_card", "继承人银行卡", false, 69),
  551. heir_phone: text("heir_phone", "继承人电话", false, 70),
  552. calc_version: intf("calc_version", "测算轮次", false, 71),
  553. people_count: intf("people_count", "测算人数", false, 72),
  554. exported: text("exported", "特殊业务是否已出盘", false, 73),
  555. funeral_paid: text("funeral_paid", "丧葬是否已发放", false, 74),
  556. })
  557. );
  558. console.log("process", results[results.length - 1]);
  559. const bad = results.filter((r) => r.code != 200);
  560. if (bad.length) {
  561. console.error("add fields fail", JSON.stringify(bad, null, 2));
  562. process.exit(1);
  563. }
  564. const backfill = {
  565. personnel: await backfillPersonnel(token),
  566. region: await backfillRegion(token),
  567. batch: await backfillBatch(token),
  568. payment: await backfillPayment(token),
  569. amount: await backfillStandard(token),
  570. process: await backfillProcess(token),
  571. };
  572. console.log("backfill", backfill);
  573. const out = { results, backfill };
  574. const outPath = path.join(__dirname, "migrate-remarks-to-fields-result.json");
  575. fs.writeFileSync(outPath, JSON.stringify(out, null, 2), "utf8");
  576. console.log("DONE ->", outPath);
  577. })().catch((e) => {
  578. console.error(e);
  579. process.exit(1);
  580. });