Commit bfa1689b539cba73b8ddaf80ca82d41a5fb47164
1 parent
edc349ba
feat: 接入 ERP 声明的 assign 联动回填(选中客户带出客户编号/名称)
ERP /ai/fieldOptions 已上线(feature/ai @5d4ff83),其返回的 assign 声明了 选中一行后还应带出哪些列。前端在选中时按 assign 收集该行对应值随提交回传, buildCreate/buildQuote 用 applyAssign 合并:只填用户没填过的列、且必须是目标表 真实存在的非系统列——用户已写的备注不会被客户档案的备注覆盖(ERP 回执 4.1 提醒)。 修复 ERP 指出的实际缺陷:我们建的 10 张报价 sCustomerName 全空,而原生单会填。 实测:assign 提交后 sCustomerName/sCustomerNo 落入载荷;用户已填字段不被覆盖。
Showing
4 changed files
with
66 additions
and
14 deletions
src/main/java/com/xly/service/FormRenderService.java
| @@ -256,6 +256,28 @@ public class FormRenderService { | @@ -256,6 +256,28 @@ public class FormRenderService { | ||
| 256 | } | 256 | } |
| 257 | 257 | ||
| 258 | /** | 258 | /** |
| 259 | + * 联动回填:用户在选择器里选中一行后,ERP 按其控件配置的 {@code sAssignField} 声明了该行还应带出哪些列 | ||
| 260 | + * (客户→客户编号/客户名称等),前端把值一并回传。**只填用户没填过的列**——用户在表单里写了备注, | ||
| 261 | + * 就不该被客户档案里的备注覆盖;同时只认目标表真实存在的非系统列。 | ||
| 262 | + */ | ||
| 263 | + private void applyAssign(Map<String, Object> col, Map<String, String> types, Map<String, String> assign) { | ||
| 264 | + if (assign == null || assign.isEmpty()) { | ||
| 265 | + return; | ||
| 266 | + } | ||
| 267 | + for (Map.Entry<String, String> e : assign.entrySet()) { | ||
| 268 | + String target = e.getKey(); | ||
| 269 | + String v = e.getValue(); | ||
| 270 | + if (v == null || v.isBlank() || col.containsKey(target)) { | ||
| 271 | + continue; | ||
| 272 | + } | ||
| 273 | + if (!types.containsKey(target) || resolver.isSystemColumn(target)) { | ||
| 274 | + continue; | ||
| 275 | + } | ||
| 276 | + col.put(target, v); | ||
| 277 | + } | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + /** | ||
| 259 | * 把用户/模型给的字符串按目标列规范化:外键列名称→id(租户内,多条命中报错列候选、 | 281 | * 把用户/模型给的字符串按目标列规范化:外键列名称→id(租户内,多条命中报错列候选、 |
| 260 | * 唯一精确同名优先——绝不静默绑最短匹配),其余按列类型强转。目标列必须物理存在 | 282 | * 唯一精确同名优先——绝不静默绑最短匹配),其余按列类型强转。目标列必须物理存在 |
| 261 | * (字段字典由表单控件名构建,含大量幻影列;幻影列会让类型校验静默失效)。 | 283 | * (字段字典由表单控件名构建,含大量幻影列;幻影列会让类型校验静默失效)。 |
| @@ -377,12 +399,15 @@ public class FormRenderService { | @@ -377,12 +399,15 @@ public class FormRenderService { | ||
| 377 | * **只构建不写入**:产物由调用方经 {@link OpService} 落 ai_op_queue。 | 399 | * **只构建不写入**:产物由调用方经 {@link OpService} 落 ai_op_queue。 |
| 378 | */ | 400 | */ |
| 379 | public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map<String, String> fields) { | 401 | public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map<String, String> fields) { |
| 380 | - return buildCreate(identity, entityKeyword, fields, null); | 402 | + return buildCreate(identity, entityKeyword, fields, null, null); |
| 381 | } | 403 | } |
| 382 | 404 | ||
| 383 | - /** 同上;{@code boundIds}(技术列名→id)来自选择器里点中的行,FK 列据此直接绑定,不按名称反查。 */ | 405 | + /** |
| 406 | + * 同上;{@code boundIds}(技术列名→id)来自选择器里点中的行,FK 列据此直接绑定,不按名称反查; | ||
| 407 | + * {@code assign}(技术列名→值)是 ERP 声明的联动回填,只补用户没填过的列。 | ||
| 408 | + */ | ||
| 384 | public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map<String, String> fields, | 409 | public CreateBuild buildCreate(AgentIdentity identity, String entityKeyword, Map<String, String> fields, |
| 385 | - Map<String, String> boundIds) { | 410 | + Map<String, String> boundIds, Map<String, String> assign) { |
| 386 | CreateBuild out = new CreateBuild(); | 411 | CreateBuild out = new CreateBuild(); |
| 387 | if (isBlank(entityKeyword)) { | 412 | if (isBlank(entityKeyword)) { |
| 388 | out.error = "缺少单据类型。"; | 413 | out.error = "缺少单据类型。"; |
| @@ -401,7 +426,7 @@ public class FormRenderService { | @@ -401,7 +426,7 @@ public class FormRenderService { | ||
| 401 | return out; | 426 | return out; |
| 402 | } | 427 | } |
| 403 | if ("quoquotationmaster".equalsIgnoreCase(out.table)) { | 428 | if ("quoquotationmaster".equalsIgnoreCase(out.table)) { |
| 404 | - return buildQuote(identity, entityKeyword, fields, boundIds, out); | 429 | + return buildQuote(identity, entityKeyword, fields, boundIds, assign, out); |
| 405 | } | 430 | } |
| 406 | 431 | ||
| 407 | Map<String, String> types = resolver.columnTypes(out.table); | 432 | Map<String, String> types = resolver.columnTypes(out.table); |
| @@ -441,6 +466,7 @@ public class FormRenderService { | @@ -441,6 +466,7 @@ public class FormRenderService { | ||
| 441 | out.error = "请至少提供一个有效字段(如客户名称)。"; | 466 | out.error = "请至少提供一个有效字段(如客户名称)。"; |
| 442 | return out; | 467 | return out; |
| 443 | } | 468 | } |
| 469 | + applyAssign(col, types, assign); | ||
| 444 | for (String rc : requiredCols(out.table)) { | 470 | for (String rc : requiredCols(out.table)) { |
| 445 | if (col.containsKey(rc) || resolver.isSystemColumn(rc)) { | 471 | if (col.containsKey(rc) || resolver.isSystemColumn(rc)) { |
| 446 | continue; | 472 | continue; |
| @@ -469,8 +495,8 @@ public class FormRenderService { | @@ -469,8 +495,8 @@ public class FormRenderService { | ||
| 469 | * 字段按策展的 target 表分流;印刷/颜色/单双面无独立列 → 合进从表 sMaterialsMemo;多数量按逗号拆多行。 | 495 | * 字段按策展的 target 表分流;印刷/颜色/单双面无独立列 → 合进从表 sMaterialsMemo;多数量按逗号拆多行。 |
| 470 | * 价格由 ERP【核价】计算,只落主-从明细。 | 496 | * 价格由 ERP【核价】计算,只落主-从明细。 |
| 471 | */ | 497 | */ |
| 472 | - private CreateBuild buildQuote(AgentIdentity identity, String entityKeyword, | ||
| 473 | - Map<String, String> fields, Map<String, String> boundIds, CreateBuild out) { | 498 | + private CreateBuild buildQuote(AgentIdentity identity, String entityKeyword, Map<String, String> fields, |
| 499 | + Map<String, String> boundIds, Map<String, String> assign, CreateBuild out) { | ||
| 474 | Map<String, String> masterTypes = resolver.columnTypes(out.table); | 500 | Map<String, String> masterTypes = resolver.columnTypes(out.table); |
| 475 | Map<String, String> slaveTypes = resolver.columnTypes("quoquotationslave"); | 501 | Map<String, String> slaveTypes = resolver.columnTypes("quoquotationslave"); |
| 476 | Map<String, Map<String, Object>> labelMap = new LinkedHashMap<>(); | 502 | Map<String, Map<String, Object>> labelMap = new LinkedHashMap<>(); |
| @@ -560,6 +586,7 @@ public class FormRenderService { | @@ -560,6 +586,7 @@ public class FormRenderService { | ||
| 560 | return out; | 586 | return out; |
| 561 | } | 587 | } |
| 562 | 588 | ||
| 589 | + applyAssign(masterCol, masterTypes, assign); | ||
| 563 | for (String rc : requiredCols(out.table)) { | 590 | for (String rc : requiredCols(out.table)) { |
| 564 | if (masterCol.containsKey(rc) || resolver.isSystemColumn(rc)) { | 591 | if (masterCol.containsKey(rc) || resolver.isSystemColumn(rc)) { |
| 565 | continue; | 592 | continue; |
src/main/java/com/xly/service/PreviewService.java
| @@ -490,14 +490,18 @@ public class PreviewService { | @@ -490,14 +490,18 @@ public class PreviewService { | ||
| 490 | 490 | ||
| 491 | /** collectForm 表单【保存】→ 构建校验 create 载荷 → 入队。返回 {queued,opIds,...} 或 {error}。 */ | 491 | /** collectForm 表单【保存】→ 构建校验 create 载荷 → 入队。返回 {queued,opIds,...} 或 {error}。 */ |
| 492 | public Map<String, Object> saveCreate(AgentIdentity who, String convId, String entity, Map<String, String> fields) { | 492 | public Map<String, Object> saveCreate(AgentIdentity who, String convId, String entity, Map<String, String> fields) { |
| 493 | - return saveCreate(who, convId, entity, fields, null); | 493 | + return saveCreate(who, convId, entity, fields, null, null); |
| 494 | } | 494 | } |
| 495 | 495 | ||
| 496 | - /** 同上;boundIds = 外键字段在选择器里点中的记录 id(字段技术名→id)。 */ | 496 | + /** |
| 497 | + * 同上;boundIds = 外键字段在选择器里点中的记录 id(字段技术名→id); | ||
| 498 | + * assign = ERP 声明的联动回填(选中客户带出客户编号等),只补用户没填过的列。 | ||
| 499 | + */ | ||
| 497 | public Map<String, Object> saveCreate(AgentIdentity who, String convId, String entity, | 500 | public Map<String, Object> saveCreate(AgentIdentity who, String convId, String entity, |
| 498 | - Map<String, String> fields, Map<String, String> boundIds) { | 501 | + Map<String, String> fields, Map<String, String> boundIds, |
| 502 | + Map<String, String> assign) { | ||
| 499 | Map<String, Object> out = new LinkedHashMap<>(); | 503 | Map<String, Object> out = new LinkedHashMap<>(); |
| 500 | - FormRenderService.CreateBuild b = render.buildCreate(who, entity, fields, boundIds); | 504 | + FormRenderService.CreateBuild b = render.buildCreate(who, entity, fields, boundIds, assign); |
| 501 | if (b.error != null) { | 505 | if (b.error != null) { |
| 502 | out.put("error", b.error); | 506 | out.put("error", b.error); |
| 503 | return out; | 507 | return out; |
src/main/java/com/xly/web/AgentChatController.java
| @@ -116,6 +116,7 @@ public class AgentChatController { | @@ -116,6 +116,7 @@ public class AgentChatController { | ||
| 116 | public String entity; | 116 | public String entity; |
| 117 | public Map<String, String> fields; // 字段中文名 -> 值 | 117 | public Map<String, String> fields; // 字段中文名 -> 值 |
| 118 | public Map<String, String> boundIds; // 外键字段技术名 -> 选择器里点中的记录 id | 118 | public Map<String, String> boundIds; // 外键字段技术名 -> 选择器里点中的记录 id |
| 119 | + public Map<String, String> assign; // ERP 声明的联动回填:目标列技术名 -> 值 | ||
| 119 | public String conversationId; | 120 | public String conversationId; |
| 120 | public String authorization; | 121 | public String authorization; |
| 121 | } | 122 | } |
| @@ -153,7 +154,7 @@ public class AgentChatController { | @@ -153,7 +154,7 @@ public class AgentChatController { | ||
| 153 | ledger.append(convId, "form_submit", Map.of("entity", entity, "fields", parts.toString()), identity); | 154 | ledger.append(convId, "form_submit", Map.of("entity", entity, "fields", parts.toString()), identity); |
| 154 | 155 | ||
| 155 | try { | 156 | try { |
| 156 | - Map<String, Object> r = previews.saveCreate(identity, convId, entity, fields, req.boundIds); | 157 | + Map<String, Object> r = previews.saveCreate(identity, convId, entity, fields, req.boundIds, req.assign); |
| 157 | if (r.containsKey("error")) { | 158 | if (r.containsKey("error")) { |
| 158 | ledger.append(convId, "assistant", Map.of("text", String.valueOf(r.get("error"))), identity); | 159 | ledger.append(convId, "assistant", Map.of("text", String.valueOf(r.get("error"))), identity); |
| 159 | } | 160 | } |
src/main/resources/templates/chat.html
| @@ -731,7 +731,17 @@ function addFormCard(ev) { | @@ -731,7 +731,17 @@ function addFormCard(ev) { | ||
| 731 | const controls = []; // {label, required, get()} | 731 | const controls = []; // {label, required, get()} |
| 732 | // 已选中记录的 id(字段名→id):ERP 的级联下拉要靠它过滤(如产品限定在已选客户名下) | 732 | // 已选中记录的 id(字段名→id):ERP 的级联下拉要靠它过滤(如产品限定在已选客户名下) |
| 733 | const bound = {}; | 733 | const bound = {}; |
| 734 | + const assigned = {}; // 选中行带出的联动回填(字段名→{目标列:值}),由 ERP 的 assign 声明 | ||
| 734 | const ctxOf = () => Object.assign({}, bound); | 735 | const ctxOf = () => Object.assign({}, bound); |
| 736 | + // 用户自己填了的列不参与回填——已写的备注不该被客户档案里的备注覆盖 | ||
| 737 | + const assignOf = () => { | ||
| 738 | + const typed = new Set(controls.filter(c => String(c.get() || "").trim()).map(c => c.name)); | ||
| 739 | + const out = {}; | ||
| 740 | + Object.values(assigned).forEach(m => Object.entries(m).forEach(([k, v]) => { | ||
| 741 | + if (!typed.has(k)) out[k] = v; | ||
| 742 | + })); | ||
| 743 | + return out; | ||
| 744 | + }; | ||
| 735 | 745 | ||
| 736 | (ev.fields || []).forEach(f => { | 746 | (ev.fields || []).forEach(f => { |
| 737 | const wrap = el("div", "ffield"); | 747 | const wrap = el("div", "ffield"); |
| @@ -751,9 +761,10 @@ function addFormCard(ev) { | @@ -751,9 +761,10 @@ function addFormCard(ev) { | ||
| 751 | pick.onclick = () => openFkPicker( | 761 | pick.onclick = () => openFkPicker( |
| 752 | { fkTable: f.fkTable, formId: ev.formId, name: f.name, ctx: ctxOf }, | 762 | { fkTable: f.fkTable, formId: ev.formId, name: f.name, ctx: ctxOf }, |
| 753 | f.label || f.name, | 763 | f.label || f.name, |
| 754 | - (name, id) => { | 764 | + (name, id, assign) => { |
| 755 | inp.value = name; | 765 | inp.value = name; |
| 756 | if (id) bound[f.name] = id; else delete bound[f.name]; | 766 | if (id) bound[f.name] = id; else delete bound[f.name]; |
| 767 | + assigned[f.name] = assign || {}; | ||
| 757 | }); | 768 | }); |
| 758 | row.appendChild(inp); | 769 | row.appendChild(inp); |
| 759 | row.appendChild(pick); | 770 | row.appendChild(pick); |
| @@ -780,7 +791,7 @@ function addFormCard(ev) { | @@ -780,7 +791,7 @@ function addFormCard(ev) { | ||
| 780 | } | 791 | } |
| 781 | if (f.hint) wrap.appendChild(el("div", "hint", f.hint)); | 792 | if (f.hint) wrap.appendChild(el("div", "hint", f.hint)); |
| 782 | grid.appendChild(wrap); | 793 | grid.appendChild(wrap); |
| 783 | - controls.push({ label: f.label || f.name, required: !!f.required, get: getter }); | 794 | + controls.push({ name: f.name, label: f.label || f.name, required: !!f.required, get: getter }); |
| 784 | }); | 795 | }); |
| 785 | card.appendChild(grid); | 796 | card.appendChild(grid); |
| 786 | 797 | ||
| @@ -821,6 +832,7 @@ function addFormCard(ev) { | @@ -821,6 +832,7 @@ function addFormCard(ev) { | ||
| 821 | entity: ev.entity, | 832 | entity: ev.entity, |
| 822 | fields: fields, | 833 | fields: fields, |
| 823 | boundIds: bound, | 834 | boundIds: bound, |
| 835 | + assign: assignOf(), | ||
| 824 | conversationId: conversationId | 836 | conversationId: conversationId |
| 825 | }) | 837 | }) |
| 826 | }); | 838 | }); |
| @@ -899,7 +911,15 @@ async function loadFkPage() { | @@ -899,7 +911,15 @@ async function loadFkPage() { | ||
| 899 | cols.forEach(c => tr.appendChild(el("td", null, r[c.col] == null ? "" : String(r[c.col])))); | 911 | cols.forEach(c => tr.appendChild(el("td", null, r[c.col] == null ? "" : String(r[c.col])))); |
| 900 | tr.onclick = () => { | 912 | tr.onclick = () => { |
| 901 | $("#modalMask").classList.remove("show"); | 913 | $("#modalMask").classList.remove("show"); |
| 902 | - if (fk.onPick && nameCol) fk.onPick(String(r[nameCol] || ""), r[valueCol] == null ? null : String(r[valueCol])); | 914 | + // ERP 声明的联动回填(目标列→结果列):把选中行对应的值一并带出,和网页选中客户的行为一致 |
| 915 | + const assign = {}; | ||
| 916 | + Object.entries(d.assign || {}).forEach(([target, src]) => { | ||
| 917 | + const v = r[src]; | ||
| 918 | + if (v != null && String(v) !== "") assign[target] = String(v); | ||
| 919 | + }); | ||
| 920 | + if (fk.onPick && nameCol) { | ||
| 921 | + fk.onPick(String(r[nameCol] || ""), r[valueCol] == null ? null : String(r[valueCol]), assign); | ||
| 922 | + } | ||
| 903 | }; | 923 | }; |
| 904 | tbody.appendChild(tr); | 924 | tbody.appendChild(tr); |
| 905 | }); | 925 | }); |