/** * 把业务流程从备注协议迁到正式字段(2026-08-13) * 用法:node scripts/migrate-remarks-to-fields.js */ const http = require("http"); const qs = require("querystring"); const fs = require("fs"); const path = require("path"); const HOST = "121.43.55.7"; const PORT = 2101; function post(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: "POST", headers }, (res) => { let data = ""; res.on("data", (c) => (data += c)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { resolve({ raw: data, status: res.statusCode }); } }); } ); req.on("error", reject); req.write(body); req.end(); }); } function text(shortName, alias, must, seq) { return { alias, customType: "", describe: "格式为富文本", frontType: "content", id: 2, must: !!must, name: "c_" + shortName, searchType: "2", sequence: seq || 1, showParam: "name,alias,desc,type,front_type,must,default_value", type: "text", }; } function num(shortName, alias, must, seq) { return { alias, customType: "", describe: "number", frontType: "float_num", id: 6, must: !!must, name: "c_" + shortName, searchType: "1,3", sequence: seq || 5, showParam: "name,alias,desc,type,front_type,must,default_value", sortType: "1,2", type: "double", }; } function ts(shortName, alias, must, seq) { return { alias, customType: "", describe: "时间戳", frontType: "date_time", id: 3, must: !!must, name: "c_" + shortName, searchType: "3", sequence: seq || 2, showParam: "name,alias,describe,type,front_type,must,date_type,date_picker", sortType: "1,2", type: "timestamp", }; } function intf(shortName, alias, must, seq) { return { alias, customType: "", describe: "number", frontType: "int_num", id: 5, must: !!must, name: "c_" + shortName, searchType: "1,3", sequence: seq || 4, showParam: "name,alias,desc,type,front_type,must,default_value", sortType: "1,2", type: "integer", }; } function normalizeFieldList(raw) { let fl = raw; if (typeof fl === "string") fl = JSON.parse(fl); const out = {}; Object.keys(fl || {}).forEach((k) => { let v = fl[k]; if (typeof v === "string") v = JSON.parse(v); out[k] = v; }); return out; } async function getModel(token, modelId) { const res = await post("/proxy_dms/model/getModelById", { modelId: String(modelId) }, token); if (res.code != 200) throw new Error("getModel " + modelId + " fail: " + JSON.stringify(res)); return res.content; } async function updateFieldList(token, modelId, addMap) { const model = await getModel(token, modelId); const fieldList = normalizeFieldList(model.fieldList); if (Object.keys(fieldList).length < 2) { throw new Error("model " + modelId + " fieldList 异常,拒绝覆盖"); } const addfieldList = {}; Object.keys(addMap || {}).forEach((shortName) => { const full = "c_" + shortName; if (fieldList[full]) return; const p = addMap[shortName]; p.name = full; fieldList[full] = p; addfieldList[shortName] = { alias: p.alias, type: p.type, frontType: p.frontType, must: p.must, name: shortName, describe: p.describe, searchType: p.searchType, sortType: p.sortType, sequence: p.sequence, showParam: p.showParam, customType: p.customType || "", id: p.id, }; }); if (Object.keys(addfieldList).length === 0) { return { modelId, modelName: model.modelName, code: 200, message: "无需新增", added: [], fields: Object.keys(fieldList).sort(), }; } const res = await post( "/proxy_dms/model/updateFieldList", { modelId: String(modelId), fieldList: JSON.stringify(fieldList), addfieldList: JSON.stringify(addfieldList), delfieldList: JSON.stringify({}), updatefieldList: JSON.stringify({}), }, token ); const verify = await getModel(token, modelId); return { modelId, modelName: model.modelName, code: res.code, message: res.message || res.content, added: Object.keys(addfieldList), fields: Object.keys(normalizeFieldList(verify.fieldList)).sort(), }; } function remarkTag(remarks, key) { if (!remarks || !key) return null; const parts = String(remarks).split("|"); for (const p of parts) { const i = p.indexOf("="); if (i > 0 && p.slice(0, i).trim() === key) return p.slice(i + 1).trim(); } return null; } function pipeVal(remarks, key) { if (!remarks || !key) return null; const token = "|" + key + "|"; const s = String(remarks); const i = s.indexOf(token); if (i < 0) return null; const rest = s.slice(i + token.length); const cut = rest.indexOf("|"); const v = cut >= 0 ? rest.slice(0, cut) : rest; return v ? v : null; } async function listAll(token, columnId, modelId) { const all = []; for (let page = 0; page < 50; page++) { const list = await post( "/proxy_dms/content/selectContentList", { columnId: String(columnId), modelId: String(modelId), page: String(page), pageSize: "100", states: "0" }, token ); const rows = (list.content && list.content.data) || []; all.push(...rows); if (rows.length < 100) break; } return all; } async function updateRow(token, columnId, modelId, content) { return post( "/proxy_dms/content/updateContent", { columnId: String(columnId), modelId: String(modelId), content: JSON.stringify(content) }, token ); } function extractBizId(remarks) { if (!remarks) return null; const parts = String(remarks).split("|"); for (const p of parts) { if (/^P\d+$/.test(p.trim())) return p.trim(); } return null; } function cleanPersonnelRemarks(remarks) { if (!remarks) return ""; let s = String(remarks); s = s.replace(/^DELETED\|/, "").replace(/^PURGED\|/, ""); s = s.replace(/^MOCK_SEED_\d+\|/, ""); s = s.replace(/^P\d+\|/, ""); s = s.replace(/\|FUNERAL_PAID\|[^|]*/g, ""); s = s.replace(/\|heirRelation=[^|]*/g, ""); s = s.replace(/\|ALLOW_DIRECT/g, ""); s = s.replace(/\|+/g, "|").replace(/^\||\|$/g, "").trim(); if (s === "管理员修改" || s === "村级录入" || s === "Excel导入") return s; return s; } async function backfillPersonnel(token) { const rows = await listAll(token, 1847, 1970); let updated = 0; for (const row of rows) { const remarks = row.c_remarks || ""; const content = { id: row.id }; let dirty = false; const biz = row.c_biz_id || extractBizId(remarks); if (biz && biz !== row.c_biz_id) { content.c_biz_id = biz; dirty = true; } const heir = row.c_heir_relation || remarkTag(remarks, "heirRelation"); if (heir && heir !== row.c_heir_relation) { content.c_heir_relation = heir; dirty = true; } if (/FUNERAL_PAID/.test(remarks) && row.c_funeral_paid !== "true") { content.c_funeral_paid = "true"; dirty = true; } const cleaned = cleanPersonnelRemarks(remarks); if (cleaned !== remarks) { content.c_remarks = cleaned; dirty = true; } if (!dirty) continue; const res = await updateRow(token, 1847, 1970, content); if (res.code == 200) updated++; } return { total: rows.length, updated }; } async function backfillRegion(token) { const rows = await listAll(token, 1842, 1965); let updated = 0; for (const row of rows) { const remarks = row.c_remarks || ""; const content = { id: row.id }; let dirty = false; const bank = row.c_default_bank || remarkTag(remarks, "bank"); if (bank && bank !== row.c_default_bank) { content.c_default_bank = bank; dirty = true; } const enabledTag = remarkTag(remarks, "enabled"); const enabled = row.c_enabled || (enabledTag === "false" ? "false" : "true"); if (enabled && enabled !== row.c_enabled) { content.c_enabled = enabled; dirty = true; } const d = row.c_district_share_percent || remarkTag(remarks, "DISTRICT_SHARE") || remarkTag(remarks, "districtShare"); if (d != null && d !== "" && String(d) !== String(row.c_district_share_percent)) { content.c_district_share_percent = Number(String(d).replace("%", "")); dirty = true; } const t = row.c_town_share_percent || remarkTag(remarks, "TOWN_SHARE") || remarkTag(remarks, "townShare"); if (t != null && t !== "" && String(t) !== String(row.c_town_share_percent)) { content.c_town_share_percent = Number(String(t).replace("%", "")); dirty = true; } let cleaned = String(remarks) .replace(/(^|\|)(level|code|parent|plan|entrust|bank|enabled|DISTRICT_SHARE|TOWN_SHARE|districtShare|townShare)=[^|]*/g, "") .replace(/\|+/g, "|") .replace(/^\||\|$/g, "") .trim(); if (cleaned !== remarks) { content.c_remarks = cleaned; dirty = true; } if (!dirty) continue; const res = await updateRow(token, 1842, 1965, content); if (res.code == 200) updated++; } return { total: rows.length, updated }; } async function backfillBatch(token) { const rows = await listAll(token, 1845, 1968); let updated = 0; for (const row of rows) { const remarks = String(row.c_remarks || ""); const content = { id: row.id }; let dirty = false; const m = remarks.match(/SUBMIT_TIME\|(\d+)/); if (m && !row.c_submit_time) { content.c_submit_time = Number(m[1]); dirty = true; } if (/CALC_LOCKED\|true/.test(remarks) && row.c_calc_locked !== "true") { content.c_calc_locked = "true"; dirty = true; } if ((/YEARLY_ADJUST/.test(remarks) || /调标/.test(remarks)) && row.c_yearly_adjust !== "true") { content.c_yearly_adjust = "true"; dirty = true; } if (/DELETED\|/.test(remarks) && row.c_deleted !== "true") { content.c_deleted = "true"; dirty = true; } const rf = remarks.match(/RETURN_FILE\|([^|]+)/); if (rf && !row.c_return_file_name) { content.c_return_file_name = rf[1]; dirty = true; } let cleaned = remarks .replace(/SUBMIT_TIME\|\d+/g, "") .replace(/\|?CALC_LOCKED\|(true|false)/g, "") .replace(/^MOCK_SEED_\d+\|/, "") .replace(/^API创建\|/, "") .replace(/RETURN_FILE\|[^|]+\|/g, "") .replace(/^DELETED\|/, "") .replace(/^PURGED\|/, "") .replace(/\|+/g, "|") .replace(/^\||\|$/g, "") .trim(); if (cleaned !== remarks) { content.c_remarks = cleaned; dirty = true; } if (!dirty) continue; const res = await updateRow(token, 1845, 1968, content); if (res.code == 200) updated++; } return { total: rows.length, updated }; } async function backfillPayment(token) { const rows = await listAll(token, 1849, 1974); let updated = 0; for (const row of rows) { const remarks = String(row.c_remarks || ""); const content = { id: row.id }; let dirty = false; const dist = remarkTag(remarks, "DIST_AMT") || pipeVal(remarks, "DIST_AMT"); const town = remarkTag(remarks, "TOWN_AMT") || pipeVal(remarks, "TOWN_AMT"); if (dist && row.c_district_amount == null) { content.c_district_amount = Number(dist); dirty = true; } if (town && row.c_town_amount == null) { content.c_town_amount = Number(town); dirty = true; } const oldM = remarks.match(/旧月标\s*([0-9.]+)/); const newM = remarks.match(/新月标\s*([0-9.]+)/); if (oldM && row.c_old_monthly_amount == null) { content.c_old_monthly_amount = Number(oldM[1]); dirty = true; } if (newM && row.c_new_monthly_amount == null) { content.c_new_monthly_amount = Number(newM[1]); dirty = true; } const km = remarks.match(/应发\s*(\d+)\s*月/); if (km && row.c_adjust_months == null) { content.c_adjust_months = Number(km[1]); dirty = true; } if (!dirty) continue; const res = await updateRow(token, 1849, 1974, content); if (res.code == 200) updated++; } return { total: rows.length, updated }; } async function backfillStandard(token) { const rows = await listAll(token, 1843, 1966); let updated = 0; for (const row of rows) { const remarks = String(row.c_remarks || ""); if (!/DELETED\|/.test(remarks) || row.c_deleted === "true") continue; const res = await updateRow(token, 1843, 1966, { id: row.id, c_deleted: "true" }); if (res.code == 200) updated++; } return { total: rows.length, updated }; } async function backfillProcess(token) { const rows = await listAll(token, 1844, 1967); let updated = 0; for (const row of rows) { const remarks = String(row.c_remarks || ""); const content = { id: row.id }; let dirty = false; if (remarks.includes("|SPECIAL|")) { const amt = pipeVal(remarks, "AMT"); const town = pipeVal(remarks, "TOWN"); const batches = pipeVal(remarks, "BATCHES"); const death = pipeVal(remarks, "DEATH_DATE"); if (amt && row.c_amount == null) { content.c_amount = Number(amt); dirty = true; } if (town && !row.c_town_id) { content.c_town_id = town; dirty = true; } if (batches && !row.c_related_batch_ids) { content.c_related_batch_ids = batches; dirty = true; } if (death && !row.c_death_date) { content.c_death_date = death; dirty = true; } const namePart = remarks.split("|SPECIAL|")[1] || ""; const nm = namePart.split("|AMT|")[0]; if (nm && !row.c_personnel_name) { content.c_personnel_name = nm.trim(); dirty = true; } } const rp = pipeVal(remarks, "REFUND_PROGRESS"); if (rp && !row.c_refund_progress) { content.c_refund_progress = rp; dirty = true; } if (remarks.includes("|EXPORTED|") && row.c_exported !== "true") { content.c_exported = "true"; dirty = true; } if (remarks.includes("STANDARD_PAYLOAD|") && !row.c_payload) { const part = remarks.split("STANDARD_PAYLOAD|")[1] || ""; const cut = part.indexOf("|SPECIAL|"); content.c_payload = cut >= 0 ? part.slice(0, cut) : part; dirty = true; } const events = pipeVal(remarks, "EVENTS"); if (events && !row.c_events) { content.c_events = events; dirty = true; } if (row.c_biz_type === "CALC_ROUND" || remarks.match(/^\d+\|\d+\|/)) { const segs = remarks.split("|"); if (segs.length >= 3 && row.c_calc_version == null) { content.c_calc_version = Number(segs[0]); content.c_people_count = Number(segs[1]); if (row.c_amount == null) content.c_amount = Number(segs[2]); dirty = true; } } if (!dirty) continue; const res = await updateRow(token, 1844, 1967, content); if (res.code == 200) updated++; } return { total: rows.length, updated }; } (async () => { const login = await post("/proxy_oauth/user/login", { userName: "user_liu", password: "WE176852439@lmx", clientId: "1", }); if (login.code != 200) throw new Error("login fail " + JSON.stringify(login)); const token = login.message; console.log("login ok"); const results = []; results.push( await updateFieldList(token, 1970, { biz_id: text("biz_id", "人员业务号", false, 50), heir_relation: text("heir_relation", "继承人与逝者关系", false, 51), funeral_paid: text("funeral_paid", "丧葬是否已发放", false, 52), }) ); console.log("personnel", results[results.length - 1]); results.push( await updateFieldList(token, 1965, { enabled: text("enabled", "是否启用", false, 20), default_bank: text("default_bank", "默认发放银行", false, 21), district_share_percent: intf("district_share_percent", "区级资金承担比例", false, 22), town_share_percent: intf("town_share_percent", "镇级资金承担比例", false, 23), sort_no: intf("sort_no", "排序号", false, 24), }) ); console.log("region", results[results.length - 1]); results.push( await updateFieldList(token, 1974, { district_amount: num("district_amount", "区级承担金额", false, 40), town_amount: num("town_amount", "镇级承担金额", false, 41), old_monthly_amount: num("old_monthly_amount", "调标前月补", false, 42), new_monthly_amount: num("new_monthly_amount", "调标后月补", false, 43), adjust_months: intf("adjust_months", "调标应发月数", false, 44), }) ); console.log("payment", results[results.length - 1]); results.push( await updateFieldList(token, 1968, { submit_time: ts("submit_time", "提交初审时间", false, 50), calc_locked: text("calc_locked", "测算是否锁定", false, 51), yearly_adjust: text("yearly_adjust", "是否年调标批", false, 52), deleted: text("deleted", "是否已删除", false, 53), return_file_name: text("return_file_name", "回盘文件名", false, 54), }) ); console.log("batch", results[results.length - 1]); results.push( await updateFieldList(token, 1966, { deleted: text("deleted", "是否已删除", false, 20), }) ); console.log("amount", results[results.length - 1]); results.push( await updateFieldList(token, 1967, { events: text("events", "待补发事件流水", false, 60), amount: num("amount", "业务金额", false, 61), related_batch_ids: text("related_batch_ids", "关联批次ID列表", false, 62), death_date: text("death_date", "死亡日期", false, 63), refund_method: text("refund_method", "退款方式", false, 64), refund_progress: text("refund_progress", "退款进度", false, 65), payload: text("payload", "审批载荷", false, 66), heir_name: text("heir_name", "继承人姓名", false, 67), heir_id_number: text("heir_id_number", "继承人身份证", false, 68), heir_bank_card: text("heir_bank_card", "继承人银行卡", false, 69), heir_phone: text("heir_phone", "继承人电话", false, 70), calc_version: intf("calc_version", "测算轮次", false, 71), people_count: intf("people_count", "测算人数", false, 72), exported: text("exported", "特殊业务是否已出盘", false, 73), funeral_paid: text("funeral_paid", "丧葬是否已发放", false, 74), }) ); console.log("process", results[results.length - 1]); const bad = results.filter((r) => r.code != 200); if (bad.length) { console.error("add fields fail", JSON.stringify(bad, null, 2)); process.exit(1); } const backfill = { personnel: await backfillPersonnel(token), region: await backfillRegion(token), batch: await backfillBatch(token), payment: await backfillPayment(token), amount: await backfillStandard(token), process: await backfillProcess(token), }; console.log("backfill", backfill); const out = { results, backfill }; const outPath = path.join(__dirname, "migrate-remarks-to-fields-result.json"); fs.writeFileSync(outPath, JSON.stringify(out, null, 2), "utf8"); console.log("DONE ->", outPath); })().catch((e) => { console.error(e); process.exit(1); });