diff --git a/src/main/java/com/xly/service/FormRenderService.java b/src/main/java/com/xly/service/FormRenderService.java index 19436bc..f63135b 100644 --- a/src/main/java/com/xly/service/FormRenderService.java +++ b/src/main/java/com/xly/service/FormRenderService.java @@ -256,6 +256,28 @@ public class FormRenderService { } /** + * 联动回填:用户在选择器里选中一行后,ERP 按其控件配置的 {@code sAssignField} 声明了该行还应带出哪些列 + * (客户→客户编号/客户名称等),前端把值一并回传。**只填用户没填过的列**——用户在表单里写了备注, + * 就不该被客户档案里的备注覆盖;同时只认目标表真实存在的非系统列。 + */ + private void applyAssign(Map col, Map types, Map assign) { + if (assign == null || assign.isEmpty()) { + return; + } + for (Map.Entry e : assign.entrySet()) { + String target = e.getKey(); + String v = e.getValue(); + if (v == null || v.isBlank() || col.containsKey(target)) { + continue; + } + if (!types.containsKey(target) || resolver.isSystemColumn(target)) { + continue; + } + col.put(target, v); + } + } + + /** * 把用户/模型给的字符串按目标列规范化:外键列名称→id(租户内,多条命中报错列候选、 * 唯一精确同名优先——绝不静默绑最短匹配),其余按列类型强转。目标列必须物理存在 * (字段字典由表单控件名构建,含大量幻影列;幻影列会让类型校验静默失效)。 @@ -377,12 +399,15 @@ public class FormRenderService { * **只构建不写入**:产物由调用方经 {@link OpService} 落 ai_op_queue。 */ public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map fields) { - return buildCreate(identity, entityKeyword, fields, null); + return buildCreate(identity, entityKeyword, fields, null, null); } - /** 同上;{@code boundIds}(技术列名→id)来自选择器里点中的行,FK 列据此直接绑定,不按名称反查。 */ + /** + * 同上;{@code boundIds}(技术列名→id)来自选择器里点中的行,FK 列据此直接绑定,不按名称反查; + * {@code assign}(技术列名→值)是 ERP 声明的联动回填,只补用户没填过的列。 + */ public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map fields, - Map boundIds) { + Map boundIds, Map assign) { CreateBuild out = new CreateBuild(); if (isBlank(entityKeyword)) { out.error = "缺少单据类型。"; @@ -401,7 +426,7 @@ public class FormRenderService { return out; } if ("quoquotationmaster".equalsIgnoreCase(out.table)) { - return buildQuote(identity, entityKeyword, fields, boundIds, out); + return buildQuote(identity, entityKeyword, fields, boundIds, assign, out); } Map types = resolver.columnTypes(out.table); @@ -441,6 +466,7 @@ public class FormRenderService { out.error = "请至少提供一个有效字段(如客户名称)。"; return out; } + applyAssign(col, types, assign); for (String rc : requiredCols(out.table)) { if (col.containsKey(rc) || resolver.isSystemColumn(rc)) { continue; @@ -469,8 +495,8 @@ public class FormRenderService { * 字段按策展的 target 表分流;印刷/颜色/单双面无独立列 → 合进从表 sMaterialsMemo;多数量按逗号拆多行。 * 价格由 ERP【核价】计算,只落主-从明细。 */ - private CreateBuild buildQuote(AgentIdentity identity, String entityKeyword, - Map fields, Map boundIds, CreateBuild out) { + private CreateBuild buildQuote(AgentIdentity identity, String entityKeyword, Map fields, + Map boundIds, Map assign, CreateBuild out) { Map masterTypes = resolver.columnTypes(out.table); Map slaveTypes = resolver.columnTypes("quoquotationslave"); Map> labelMap = new LinkedHashMap<>(); @@ -560,6 +586,7 @@ public class FormRenderService { return out; } + applyAssign(masterCol, masterTypes, assign); for (String rc : requiredCols(out.table)) { if (masterCol.containsKey(rc) || resolver.isSystemColumn(rc)) { continue; diff --git a/src/main/java/com/xly/service/PreviewService.java b/src/main/java/com/xly/service/PreviewService.java index 1814ad0..32abbb3 100644 --- a/src/main/java/com/xly/service/PreviewService.java +++ b/src/main/java/com/xly/service/PreviewService.java @@ -490,14 +490,18 @@ public class PreviewService { /** collectForm 表单【保存】→ 构建校验 create 载荷 → 入队。返回 {queued,opIds,...} 或 {error}。 */ public Map saveCreate(AgentIdentity who, String convId, String entity, Map fields) { - return saveCreate(who, convId, entity, fields, null); + return saveCreate(who, convId, entity, fields, null, null); } - /** 同上;boundIds = 外键字段在选择器里点中的记录 id(字段技术名→id)。 */ + /** + * 同上;boundIds = 外键字段在选择器里点中的记录 id(字段技术名→id); + * assign = ERP 声明的联动回填(选中客户带出客户编号等),只补用户没填过的列。 + */ public Map saveCreate(AgentIdentity who, String convId, String entity, - Map fields, Map boundIds) { + Map fields, Map boundIds, + Map assign) { Map out = new LinkedHashMap<>(); - FormRenderService.CreateBuild b = render.buildCreate(who, entity, fields, boundIds); + FormRenderService.CreateBuild b = render.buildCreate(who, entity, fields, boundIds, assign); if (b.error != null) { out.put("error", b.error); return out; diff --git a/src/main/java/com/xly/web/AgentChatController.java b/src/main/java/com/xly/web/AgentChatController.java index ef7c072..a586d6c 100644 --- a/src/main/java/com/xly/web/AgentChatController.java +++ b/src/main/java/com/xly/web/AgentChatController.java @@ -116,6 +116,7 @@ public class AgentChatController { public String entity; public Map fields; // 字段中文名 -> 值 public Map boundIds; // 外键字段技术名 -> 选择器里点中的记录 id + public Map assign; // ERP 声明的联动回填:目标列技术名 -> 值 public String conversationId; public String authorization; } @@ -153,7 +154,7 @@ public class AgentChatController { ledger.append(convId, "form_submit", Map.of("entity", entity, "fields", parts.toString()), identity); try { - Map r = previews.saveCreate(identity, convId, entity, fields, req.boundIds); + Map r = previews.saveCreate(identity, convId, entity, fields, req.boundIds, req.assign); if (r.containsKey("error")) { ledger.append(convId, "assistant", Map.of("text", String.valueOf(r.get("error"))), identity); } diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html index 19bb64b..84afe9e 100644 --- a/src/main/resources/templates/chat.html +++ b/src/main/resources/templates/chat.html @@ -731,7 +731,17 @@ function addFormCard(ev) { const controls = []; // {label, required, get()} // 已选中记录的 id(字段名→id):ERP 的级联下拉要靠它过滤(如产品限定在已选客户名下) const bound = {}; + const assigned = {}; // 选中行带出的联动回填(字段名→{目标列:值}),由 ERP 的 assign 声明 const ctxOf = () => Object.assign({}, bound); + // 用户自己填了的列不参与回填——已写的备注不该被客户档案里的备注覆盖 + const assignOf = () => { + const typed = new Set(controls.filter(c => String(c.get() || "").trim()).map(c => c.name)); + const out = {}; + Object.values(assigned).forEach(m => Object.entries(m).forEach(([k, v]) => { + if (!typed.has(k)) out[k] = v; + })); + return out; + }; (ev.fields || []).forEach(f => { const wrap = el("div", "ffield"); @@ -751,9 +761,10 @@ function addFormCard(ev) { pick.onclick = () => openFkPicker( { fkTable: f.fkTable, formId: ev.formId, name: f.name, ctx: ctxOf }, f.label || f.name, - (name, id) => { + (name, id, assign) => { inp.value = name; if (id) bound[f.name] = id; else delete bound[f.name]; + assigned[f.name] = assign || {}; }); row.appendChild(inp); row.appendChild(pick); @@ -780,7 +791,7 @@ function addFormCard(ev) { } if (f.hint) wrap.appendChild(el("div", "hint", f.hint)); grid.appendChild(wrap); - controls.push({ label: f.label || f.name, required: !!f.required, get: getter }); + controls.push({ name: f.name, label: f.label || f.name, required: !!f.required, get: getter }); }); card.appendChild(grid); @@ -821,6 +832,7 @@ function addFormCard(ev) { entity: ev.entity, fields: fields, boundIds: bound, + assign: assignOf(), conversationId: conversationId }) }); @@ -899,7 +911,15 @@ async function loadFkPage() { cols.forEach(c => tr.appendChild(el("td", null, r[c.col] == null ? "" : String(r[c.col])))); tr.onclick = () => { $("#modalMask").classList.remove("show"); - if (fk.onPick && nameCol) fk.onPick(String(r[nameCol] || ""), r[valueCol] == null ? null : String(r[valueCol])); + // ERP 声明的联动回填(目标列→结果列):把选中行对应的值一并带出,和网页选中客户的行为一致 + const assign = {}; + Object.entries(d.assign || {}).forEach(([target, src]) => { + const v = r[src]; + if (v != null && String(v) !== "") assign[target] = String(v); + }); + if (fk.onPick && nameCol) { + fk.onPick(String(r[nameCol] || ""), r[valueCol] == null ? null : String(r[valueCol]), assign); + } }; tbody.appendChild(tr); });