package com.xly.tool; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.AgentIdentity; import com.xly.service.ErpClient; import com.xly.service.FormResolverService; import com.xly.service.OpService; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import org.springframework.jdbc.core.JdbcTemplate; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * ProposeWrite 工具(写操作,人在环)。 * *

**只提议并暂存,绝不立即执行**:把「改某条记录的某字段」解析成具体的 表/记录id/列/新值, * 写一条 draft 到 ai_op_queue,返回一个提议。真正执行发生在用户点【确认】后的确定性端点里 * (见 OpController),不经过 LLM。 */ public class ProposeWriteTool { private final ErpClient erp; private final JdbcTemplate jdbc; private final OpService ops; private final ObjectMapper mapper; private final AgentIdentity identity; private final FormResolverService resolver; public ProposeWriteTool(ErpClient erp, JdbcTemplate jdbc, OpService ops, ObjectMapper mapper, AgentIdentity identity, FormResolverService resolver) { this.erp = erp; this.jdbc = jdbc; this.ops = ops; this.mapper = mapper; this.identity = identity; this.resolver = resolver; } @Tool("提议一次写操作(人在环:只生成待确认提议、绝不立即执行;用户点【确认】才写入,你绝不能声称已完成)。" + "用 action 指定动作:create=新增;update=改某字段;invalid=作废(业务单据要“删除/取消”一律用它,可复原);" + "cancelInvalid=复原/取消作废;examine=审核;cancelExamine=销审/反审核;delete=物理删除(明细行/极少用,业务单据别用)。" + "按 action 传参:create 用 fieldsJson;update 用 recordKeyword+fieldChinese+newValue;" + "invalid/cancelInvalid/examine/cancelExamine/delete 用 recordKeyword 定位单据。本工具自行定位主表,无需先 findForms。") public String proposeWrite( @P("动作:create|update|invalid|cancelInvalid|examine|cancelExamine|delete") String action, @P("实体/单据类型,如 报价 / 客户 / 销售订单") String entityKeyword, @P(value = "记录名称或单号关键词(create 不需要)", required = false) String recordKeyword, @P(value = "要改的字段中文名(仅 update)", required = false) String fieldChinese, @P(value = "新值(仅 update)", required = false) String newValue, @P(value = "字段 JSON 中文名->值(仅 create)", required = false) String fieldsJson) { if (isBlank(action)) { return err("缺少 action。"); } switch (action.trim().toLowerCase()) { case "create": return doCreate(entityKeyword, fieldsJson); case "update": return doUpdate(entityKeyword, recordKeyword, fieldChinese, newValue); case "invalid": return doInvalidate(entityKeyword, recordKeyword, false); case "cancelinvalid": return doInvalidate(entityKeyword, recordKeyword, true); case "examine": return doExamineOp(entityKeyword, recordKeyword, 1); case "cancelexamine": return doExamineOp(entityKeyword, recordKeyword, 0); case "delete": return doDelete(entityKeyword, recordKeyword); default: return err("未知 action:" + action + "(支持 create/update/invalid/cancelInvalid/examine/cancelExamine/delete)"); } } /** 作废(cancel=false)/复原·取消作废(cancel=true)一条记录 → ERP updatebInvalid(Sp_Invalidation)。业务单据的“删除”走这里。 */ private String doInvalidate(String entityKeyword, String recordKeyword, boolean cancel) { String verb = cancel ? "复原" : "作废"; if (isBlank(entityKeyword) || isBlank(recordKeyword)) { return err("缺少实体类型或记录名称。"); } Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可操作主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!identity.canAccessModule(moduleId)) { return err("你没有操作「" + entityKeyword + "」的权限。"); } String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(identity.token(), formId, moduleId, 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位记录失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位记录失败:" + root.path("msg").asText("未知错误")); } JsonNode rows = root.path("dataset").path("rows"); JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null; int n = (data != null && data.isArray()) ? data.size() : 0; if (n == 0) { return err("没有找到名称含「" + recordKeyword + "」的记录。"); } if (n > 1) { StringBuilder names = new StringBuilder(); for (int i = 0; i < data.size() && i < 5; i++) { if (i > 0) names.append("、"); names.append(nameField == null ? "" : data.get(i).path(nameField).asText("")); } return err("匹配到多条记录(" + names + "),请提供更精确的名称,只" + verb + "其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的记录缺少主键 sId,无法" + verb + "。"); } String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); String description = verb + "【" + recordName + "】(" + entityKeyword + ")"; // sNewValue 存 handleType:toVoid=作废 / cancel=复原;执行走 ERP updatebInvalid String opId = ops.createDraft(identity.userId(), "invalid", formId, moduleId, table, billId, null, null, null, cancel ? "cancel" : "toVoid", description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的" + verb + ",请在下方点【确认】执行、或【取消】。"); return toJson(out); } /** 修改某条记录的某个字段。见 {@link #proposeWrite}。 */ private String doUpdate(String entityKeyword, String recordKeyword, String fieldChinese, String newValue) { if (isBlank(entityKeyword) || isBlank(recordKeyword) || isBlank(fieldChinese)) { return err("缺少信息:需要实体类型、记录名称关键词、字段中文名、新值。"); } // 1) 定位可修改的主表(该实体名下、table 类型、最常用的一张) Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可修改主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!identity.canAccessModule(moduleId)) { return err("你没有修改「" + entityKeyword + "」的权限。"); } // 2) 字段中文名 -> 技术列名 String field = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sChinese=? ORDER BY iFormUses DESC LIMIT 1", table, fieldChinese.trim()); if (field == null) { field = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sChinese LIKE ? ORDER BY iFormUses DESC LIMIT 1", table, "%" + fieldChinese.trim() + "%"); } if (field == null) { return err("在该表单里找不到叫「" + fieldChinese + "」的字段,请换个字段名或先查看该表单有哪些字段。"); } // 3) 定位唯一记录 + 旧值(用名称字段过滤) String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(identity.token(), formId.trim(), moduleId.trim(), 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位记录时读取失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位记录失败:" + root.path("msg").asText("未知错误")); } JsonNode rows = root.path("dataset").path("rows"); JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null; int n = (data != null && data.isArray()) ? data.size() : 0; if (n == 0) { return err("没有找到名称含「" + recordKeyword + "」的记录,无法修改。"); } if (n > 1) { StringBuilder names = new StringBuilder(); for (int i = 0; i < data.size() && i < 5; i++) { if (i > 0) names.append("、"); names.append(data.get(i).path(nameField == null ? "" : nameField).asText("")); } return err("匹配到多条记录(" + names + "),请提供更精确的名称,只改其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的记录缺少主键 sId,无法安全修改。"); } String oldValue = rec.path(field).asText(""); String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); // 4) 暂存 draft(不执行) String description = "将【" + recordName + "】的【" + fieldChinese + "】" + (oldValue.isBlank() ? "" : ("由「" + oldValue + "」")) + "改为「" + newValue + "」"; String opId = ops.createDraft(identity.userId(), "update", formId.trim(), moduleId.trim(), table, billId, field, fieldChinese, oldValue, newValue, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的修改,请在下方点【确认】执行、或【取消】。"); return toJson(out); } /** 物理删除一条记录(罕用;业务单据请用 doInvalidate 作废)。见 {@link #proposeWrite}。 */ private String doDelete(String entityKeyword, String recordKeyword) { if (isBlank(entityKeyword) || isBlank(recordKeyword)) { return err("缺少实体类型或记录名称。"); } Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可操作主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!identity.canAccessModule(moduleId)) { return err("你没有操作「" + entityKeyword + "」的权限。"); } String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(identity.token(), formId, moduleId, 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位记录失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位记录失败:" + root.path("msg").asText("未知错误")); } JsonNode rows = root.path("dataset").path("rows"); JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null; int n = (data != null && data.isArray()) ? data.size() : 0; if (n == 0) { return err("没有找到名称含「" + recordKeyword + "」的记录。"); } if (n > 1) { StringBuilder names = new StringBuilder(); for (int i = 0; i < data.size() && i < 5; i++) { if (i > 0) names.append("、"); names.append(nameField == null ? "" : data.get(i).path(nameField).asText("")); } return err("匹配到多条记录(" + names + "),请提供更精确的名称,只删其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的记录缺少主键 sId,无法安全删除。"); } String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); String description = "删除【" + recordName + "】(" + entityKeyword + ")"; String opId = ops.createDraft(identity.userId(), "delete", formId, moduleId, table, billId, null, null, recordName, null, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的删除,请在下方点【确认】执行、或【取消】。删除不可恢复,请谨慎。"); return toJson(out); } /** 新增一条记录(报价走多表 proposeQuote)。见 {@link #proposeWrite}。 */ private String doCreate(String entityKeyword, String fieldsJson) { if (isBlank(entityKeyword)) { return err("缺少实体类型。"); } Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可新增主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!identity.canAccessModule(moduleId)) { return err("你没有新增「" + entityKeyword + "」的权限。"); } // 报价是跨表主-从单据 → 走多表创建(主表 + 印刷/部件从表 + 多数量) if ("quoquotationmaster".equalsIgnoreCase(table)) { return proposeQuote(fieldsJson, formId, moduleId, table); } Map types = resolver.columnTypes(table); // 权威 label -> {col, fk} 映射(与 collectForm 同源:策展/字段字典),保证前端标签能被正确回映射 Map> labelMap = new LinkedHashMap<>(); for (Map bfm : resolver.businessFields(table, 40)) { labelMap.put(normLabel(String.valueOf(bfm.get("label"))), bfm); } Map col = new LinkedHashMap<>(); List descParts = new ArrayList<>(); try { if (!isBlank(fieldsJson)) { JsonNode fj = mapper.readTree(fieldsJson.trim()); Iterator> it = fj.fields(); while (it.hasNext()) { Map.Entry e = it.next(); String zh = e.getKey(); String v = e.getValue().asText(""); if (isBlank(v)) { continue; } Map fm = labelMap.get(normLabel(zh)); if (fm == null) { fm = resolveColumn(table, zh); // 回退字段字典 } if (fm == null) { continue; // 该实体没有这个字段,忽略 } String colName = str(fm.get("col")); // 别让 LLM/用户直填系统列(制单人/单据日期/租户/单号…),交给 ERP 与下方系统列处理 if (resolver.isSystemColumn(colName)) { continue; } String fk = str(fm.get("fk")); if (fk != null && !fk.isBlank()) { String id = resolver.resolveFk(fk, v); // 外键:名称 -> id if (id == null) { String ent = zh.replace("名称", ""); return err("「" + v + "」不是系统里已有的" + ent + "。请让用户从下拉里选真实存在的" + ent + ";**不要**因此新建" + ent + "。"); } col.put(colName, id); } else { Object cv = resolver.coerce(types.get(colName), v); // 按列类型强转 if (cv != null) { col.put(colName, cv); } } descParts.add(zh + "=" + v); } } } catch (Exception ex) { return err("字段解析失败:" + ex.getMessage()); } if (descParts.isEmpty()) { return err("请至少提供一个有效字段(如客户名称)。"); } // 自动补齐 NOT-NULL 无默认列(系统列跳过;其余按类型给默认 0/'') for (String rc : requiredCols(table)) { if (col.containsKey(rc) || resolver.isSystemColumn(rc)) { continue; } col.put(rc, resolver.typeDefault(rc, types.get(rc))); } // 系统列:主键 + 表单id + 单号(租户/制单人/日期由 ERP 注入,不填) col.put("sId", erp.newUuid(identity.token())); col.put("sFormId", formId); String billNo = resolver.nextBillNo(table, identity.brandsId()); if (billNo != null) { col.put("sBillNo", billNo); } String payload; try { payload = mapper.writeValueAsString(col); } catch (Exception e) { return err("内部错误:" + e.getMessage()); } String description = "新增【" + entityKeyword + "】:" + String.join(",", descParts); String opId = ops.createDraftPayload(identity.userId(), "create", formId, moduleId, table, payload, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的新增,请在下方点【确认】执行、或【取消】。"); return toJson(out); } /** 审核(iFlag=1)/反审核·销审(iFlag=0)一条单据 → ERP doExamine。见 {@link #proposeWrite}。 */ private String doExamineOp(String entityKeyword, String recordKeyword, int iFlag) { if (isBlank(entityKeyword) || isBlank(recordKeyword)) { return err("缺少单据类型或单号/名称。"); } Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可审核单据主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!identity.canAccessModule(moduleId)) { return err("你没有审核「" + entityKeyword + "」的权限。"); } String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(identity.token(), formId, moduleId, 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位单据失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位单据失败:" + root.path("msg").asText("未知错误")); } JsonNode rows = root.path("dataset").path("rows"); JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null; int n = (data != null && data.isArray()) ? data.size() : 0; if (n == 0) { return err("没有找到含「" + recordKeyword + "」的单据。"); } if (n > 1) { StringBuilder names = new StringBuilder(); for (int i = 0; i < data.size() && i < 5; i++) { if (i > 0) names.append("、"); names.append(nameField == null ? "" : data.get(i).path(nameField).asText("")); } return err("匹配到多条单据(" + names + "),请提供更精确的单号,只审核其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的单据缺少主键 sId,无法审核。"); } String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); String verb = iFlag == 1 ? "审核" : "反审核"; String description = verb + "【" + recordName + "】(" + entityKeyword + ")"; // examine:sNewValue 存 iFlag(1=审核,0=反审核/销审);执行走 ERP doExamine(存储过程驱动) String opId = ops.createDraft(identity.userId(), "examine", formId, moduleId, table, billId, null, null, null, String.valueOf(iFlag), description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的" + verb + ",请在下方点【确认】执行、或【取消】。审核有业务后果,请谨慎。"); return toJson(out); } /** * 报价多表创建:主表(quoquotationmaster) + 印刷/部件从表(quoquotationslave) + 多数量(quoquotationmanyqtys)。 * 字段按策展的 target 表分流;印刷/颜色/单双面无独立列 → 合进从表 sMaterialsMemo;多数量按逗号拆多行。 * 价格由 ERP【核价】计算,本工具只落主-从明细。生成 __tables__ 结构化 payload,确认端点做多表写入。 */ private String proposeQuote(String fieldsJson, String formId, String moduleId, String table) { Map masterTypes = resolver.columnTypes(table); Map slaveTypes = resolver.columnTypes("quoquotationslave"); Map> labelMap = new LinkedHashMap<>(); for (Map bfm : resolver.businessFields(table, 40)) { labelMap.put(normLabel(String.valueOf(bfm.get("label"))), bfm); } Map masterCol = new LinkedHashMap<>(); Map slaveCol = new LinkedHashMap<>(); List notes = new ArrayList<>(); List manyQtys = new ArrayList<>(); List descParts = new ArrayList<>(); String custId = null; try { if (!isBlank(fieldsJson)) { JsonNode fj = mapper.readTree(fieldsJson.trim()); Iterator> it = fj.fields(); while (it.hasNext()) { Map.Entry e = it.next(); String zh = e.getKey(); String v = e.getValue().asText(""); if (isBlank(v)) { continue; } Map fm = labelMap.get(normLabel(zh)); if (fm == null) { continue; } String colName = str(fm.get("col")); String tgt = fm.get("table") == null ? "master" : str(fm.get("table")); String fk = str(fm.get("fk")); if ("note".equals(tgt)) { notes.add(zh + "=" + v); descParts.add(zh + "=" + v); continue; } if ("manyqtys".equals(tgt)) { for (String q : v.split("[,,、\\s]+")) { String n = q.replaceAll("[^0-9.]", ""); if (!n.isEmpty()) { manyQtys.add(n); } } descParts.add(zh + "=" + v); continue; } Object value; if (fk != null && !fk.isBlank()) { String id = resolver.resolveFk(fk, v); if (id == null) { String ent = zh.replace("名称", ""); return err("「" + v + "」不是系统里已有的" + ent + "。请用 collectForm 让用户从下拉里选真实存在的" + ent + ";**不要**因此新建" + ent + ",也不要把产品/规格当成客户。"); } value = id; if ("sCustomerId".equals(colName)) { custId = id; } } else { String dt = "slave".equals(tgt) ? slaveTypes.get(colName) : masterTypes.get(colName); value = resolver.coerce(dt, v); } if (value != null) { if ("slave".equals(tgt)) { slaveCol.put(colName, value); } else { masterCol.put(colName, value); } } descParts.add(zh + "=" + v); } } } catch (Exception ex) { return err("字段解析失败:" + ex.getMessage()); } if (descParts.isEmpty()) { return err("请至少填写一个字段(如客户名称/产品名称/数量)。"); } // 主表:补必填(非系统) + 主键 + 表单id + 单号 for (String rc : requiredCols(table)) { if (masterCol.containsKey(rc) || resolver.isSystemColumn(rc)) { continue; } masterCol.put(rc, resolver.typeDefault(rc, masterTypes.get(rc))); } String masterId = erp.newUuid(identity.token()); masterCol.put("sId", masterId); masterCol.put("sFormId", formId); String billNo = resolver.nextBillNo(table, identity.brandsId()); if (billNo != null) { masterCol.put("sBillNo", billNo); } List> tables = new ArrayList<>(); tables.add(tableItem("quoquotationmaster", "master", masterCol)); // 印刷/部件从表:有从表字段或印刷备注就建一行(sCustomerId 从表 NOT-NULL) if (!slaveCol.isEmpty() || !notes.isEmpty()) { if (!notes.isEmpty()) { String memo = String.join(";", notes); Object exist = slaveCol.get("sMaterialsMemo"); slaveCol.put("sMaterialsMemo", exist == null ? memo : (exist + ";" + memo)); } slaveCol.put("sId", erp.newUuid(identity.token())); slaveCol.put("sParentId", masterId); slaveCol.put("sCustomerId", custId == null ? "" : custId); tables.add(tableItem("quoquotationslave", "slave", slaveCol)); } // 多数量报价:每个数量一行 for (String q : manyQtys) { Map mq = new LinkedHashMap<>(); mq.put("sId", erp.newUuid(identity.token())); mq.put("sParentId", masterId); mq.put("dManyQty", q); tables.add(tableItem("quoquotationmanyqtys", "slave", mq)); } Map wrap = new LinkedHashMap<>(); wrap.put("__tables__", tables); String payload; try { payload = mapper.writeValueAsString(wrap); } catch (Exception e) { return err("内部错误:" + e.getMessage()); } String description = "新增【报价】:" + String.join(",", descParts) + "(含印刷/多数量明细;价格请在 ERP 点【核价】计算)"; String opId = ops.createDraftPayload(identity.userId(), "create", formId, moduleId, table, payload, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的报价新增(主表+印刷明细+多数量),请点【确认】执行、或【取消】。价格在 ERP 里点【核价】计算。"); return toJson(out); } private Map tableItem(String sTable, String name, Map column) { Map m = new LinkedHashMap<>(); m.put("sTable", sTable); m.put("name", name); m.put("column", column); return m; } /** 中文名 -> {col, fk}(字段字典,先精确合并模糊,取使用度最高的一列)。 */ private Map resolveColumn(String table, String zh) { try { List> r = jdbc.queryForList( "SELECT sField col, MAX(sFkTable) fk FROM viw_kg_field_dict " + "WHERE sTable=? AND (sChinese=? OR sChinese LIKE ?) " + "GROUP BY sField ORDER BY SUM(iFormUses) DESC LIMIT 1", table, zh, "%" + zh + "%"); return r.isEmpty() ? null : r.get(0); } catch (Exception e) { return null; } } /** 目标表的 NOT-NULL 无默认列(排除 ERP 会自动注入的租户/制单人)。 */ private List requiredCols(String table) { List out = new ArrayList<>(); try { List> rows = jdbc.queryForList( "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? " + "AND IS_NULLABLE='NO' AND COLUMN_DEFAULT IS NULL AND EXTRA NOT LIKE '%auto_increment%'", table); Set skip = Set.of("sBrandsId", "sSubsidiaryId", "sMakePerson"); for (Map r : rows) { String c = str(r.get("COLUMN_NAME")); if (c != null && !skip.contains(c)) { out.add(c); } } } catch (Exception ignore) { } return out; } /** 外键兜底:该列现有的最常见非空值。列名来自 KG/info_schema(可信)。 */ private String commonValue(String table, String col) { try { return queryOne("SELECT `" + col + "` FROM `" + table + "` WHERE `" + col + "` IS NOT NULL AND `" + col + "`<>'' GROUP BY `" + col + "` ORDER BY COUNT(*) DESC LIMIT 1"); } catch (Exception e) { return null; } } /** 定位实体的可写主表——统一走 FormResolverService(与 collectForm 同源,含从属/参数表排除)。 */ private Map resolveForm(String entityKeyword) { return resolver.resolveMasterForm(entityKeyword); } private static String str(Object o) { return o == null ? null : o.toString(); } private String queryOne(String sql, Object... args) { try { List> r = jdbc.queryForList(sql, args); if (!r.isEmpty()) { Object v = r.get(0).values().iterator().next(); return v == null ? null : v.toString(); } } catch (Exception ignore) { } return null; } private static boolean isBlank(String s) { return s == null || s.isBlank(); } /** 归一化标签用于匹配:去掉括号提示与空白("多数量(逗号分隔)" ⇄ "多数量")。 */ private static String normLabel(String s) { if (s == null) { return ""; } return s.replaceAll("[((].*?[))]", "").replaceAll("\\s+", "").trim(); } private String err(String msg) { Map m = new LinkedHashMap<>(); m.put("error", msg); return toJson(m); } private String toJson(Map m) { try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{\"error\":\"内部错误\"}"; } } }