Commit bfa1689b539cba73b8ddaf80ca82d41a5fb47164

Authored by zichun
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 落入载荷;用户已填字段不被覆盖。
src/main/java/com/xly/service/FormRenderService.java
... ... @@ -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 281 * 把用户/模型给的字符串按目标列规范化:外键列名称→id(租户内,多条命中报错列候选、
260 282 * 唯一精确同名优先——绝不静默绑最短匹配),其余按列类型强转。目标列必须物理存在
261 283 * (字段字典由表单控件名构建,含大量幻影列;幻影列会让类型校验静默失效)。
... ... @@ -377,12 +399,15 @@ public class FormRenderService {
377 399 * **只构建不写入**:产物由调用方经 {@link OpService} 落 ai_op_queue。
378 400 */
379 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 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 411 CreateBuild out = new CreateBuild();
387 412 if (isBlank(entityKeyword)) {
388 413 out.error = "缺少单据类型。";
... ... @@ -401,7 +426,7 @@ public class FormRenderService {
401 426 return out;
402 427 }
403 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 432 Map<String, String> types = resolver.columnTypes(out.table);
... ... @@ -441,6 +466,7 @@ public class FormRenderService {
441 466 out.error = "请至少提供一个有效字段(如客户名称)。";
442 467 return out;
443 468 }
  469 + applyAssign(col, types, assign);
444 470 for (String rc : requiredCols(out.table)) {
445 471 if (col.containsKey(rc) || resolver.isSystemColumn(rc)) {
446 472 continue;
... ... @@ -469,8 +495,8 @@ public class FormRenderService {
469 495 * 字段按策展的 target 表分流;印刷/颜色/单双面无独立列 → 合进从表 sMaterialsMemo;多数量按逗号拆多行。
470 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 500 Map<String, String> masterTypes = resolver.columnTypes(out.table);
475 501 Map<String, String> slaveTypes = resolver.columnTypes("quoquotationslave");
476 502 Map<String, Map<String, Object>> labelMap = new LinkedHashMap<>();
... ... @@ -560,6 +586,7 @@ public class FormRenderService {
560 586 return out;
561 587 }
562 588  
  589 + applyAssign(masterCol, masterTypes, assign);
563 590 for (String rc : requiredCols(out.table)) {
564 591 if (masterCol.containsKey(rc) || resolver.isSystemColumn(rc)) {
565 592 continue;
... ...
src/main/java/com/xly/service/PreviewService.java
... ... @@ -490,14 +490,18 @@ public class PreviewService {
490 490  
491 491 /** collectForm 表单【保存】→ 构建校验 create 载荷 → 入队。返回 {queued,opIds,...} 或 {error}。 */
492 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 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 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 505 if (b.error != null) {
502 506 out.put("error", b.error);
503 507 return out;
... ...
src/main/java/com/xly/web/AgentChatController.java
... ... @@ -116,6 +116,7 @@ public class AgentChatController {
116 116 public String entity;
117 117 public Map<String, String> fields; // 字段中文名 -> 值
118 118 public Map<String, String> boundIds; // 外键字段技术名 -> 选择器里点中的记录 id
  119 + public Map<String, String> assign; // ERP 声明的联动回填:目标列技术名 -> 值
119 120 public String conversationId;
120 121 public String authorization;
121 122 }
... ... @@ -153,7 +154,7 @@ public class AgentChatController {
153 154 ledger.append(convId, "form_submit", Map.of("entity", entity, "fields", parts.toString()), identity);
154 155  
155 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 158 if (r.containsKey("error")) {
158 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 731 const controls = []; // {label, required, get()}
732 732 // 已选中记录的 id(字段名→id):ERP 的级联下拉要靠它过滤(如产品限定在已选客户名下)
733 733 const bound = {};
  734 + const assigned = {}; // 选中行带出的联动回填(字段名→{目标列:值}),由 ERP 的 assign 声明
734 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 746 (ev.fields || []).forEach(f => {
737 747 const wrap = el("div", "ffield");
... ... @@ -751,9 +761,10 @@ function addFormCard(ev) {
751 761 pick.onclick = () => openFkPicker(
752 762 { fkTable: f.fkTable, formId: ev.formId, name: f.name, ctx: ctxOf },
753 763 f.label || f.name,
754   - (name, id) => {
  764 + (name, id, assign) => {
755 765 inp.value = name;
756 766 if (id) bound[f.name] = id; else delete bound[f.name];
  767 + assigned[f.name] = assign || {};
757 768 });
758 769 row.appendChild(inp);
759 770 row.appendChild(pick);
... ... @@ -780,7 +791,7 @@ function addFormCard(ev) {
780 791 }
781 792 if (f.hint) wrap.appendChild(el("div", "hint", f.hint));
782 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 796 card.appendChild(grid);
786 797  
... ... @@ -821,6 +832,7 @@ function addFormCard(ev) {
821 832 entity: ev.entity,
822 833 fields: fields,
823 834 boundIds: bound,
  835 + assign: assignOf(),
824 836 conversationId: conversationId
825 837 })
826 838 });
... ... @@ -899,7 +911,15 @@ async function loadFkPage() {
899 911 cols.forEach(c => tr.appendChild(el("td", null, r[c.col] == null ? "" : String(r[c.col]))));
900 912 tr.onclick = () => {
901 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 924 tbody.appendChild(tr);
905 925 });
... ...