package com.xly.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.Intent; import org.springframework.stereotype.Service; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 新增单据的**确定性槽位映射**:把意图门已抽好的「带角色实体」+ 正则解析的尺寸/数量, * 按表单真实字段落位,产出 collectForm 的预填 JSON。 * *

为什么不用 LLM 做这步:实测 qwen3:14b 能可靠区分「产品 vs 客户」(意图门已给出角色), * 但对「长和宽50cm,高5cm」这类**尺寸拆分**始终出错(塞进备注/单价)。这类窄任务用 Java 正则 + * 角色映射远比让模型受约束填槽可靠——把不确定的判断交给能确定处理的代码,是治『不智能』的关键手法之一。 */ @Service public class SlotFillService { private final ObjectMapper mapper; public SlotFillService(ObjectMapper mapper) { this.mapper = mapper; } // 标注式尺寸:"长和宽50"、"高5cm"、"长:50" private static final Pattern DIM_LABELED = Pattern.compile("([长宽高厚深](?:[和与、,,及/][长宽高厚深])*)\\s*[::是为]?\\s*(\\d+(?:\\.\\d+)?)"); // 位置式尺寸:"50*30*5"、"50x30x5"、"50×30×5" private static final Pattern DIM_POS = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?)(?:\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?))?"); /** * 依据意图门的角色实体 + 正则,确定性地构建新增表单的预填字段(中文字段名 -> 值)。 * * @param utterance 用户原话(用于解析尺寸) * @param entity 单据/实体类型(如 报价 / 客户) * @param fields {@code FormResolverService.businessFields}(含 label/fk/type) * @param it 意图门结果(提供带角色的实体) */ public Map buildCreateFields(String utterance, String entity, List> fields, Intent it) { Map known = new LinkedHashMap<>(); if (fields == null || fields.isEmpty()) { return known; } // 干净字段名 -> 原始 label(供尺寸/数量按名定位) Map cleanToLabel = new LinkedHashMap<>(); for (Map f : fields) { String label = str(f.get("label")); if (label != null && !label.isBlank()) { cleanToLabel.putIfAbsent(cleanLabel(label), label); } } String cust = it == null ? null : it.firstValueByRole("客户"); String prod = it == null ? null : it.firstValueByRole("产品"); String mat = it == null ? null : it.firstValueByRole("物料"); String sup = it == null ? null : it.firstValueByRole("供应商"); // 1) 外键角色字段:客户/产品/物料/供应商 → 对应 FK 字段 for (Map f : fields) { String fk = str(f.get("fk")); String label = str(f.get("label")); if (fk == null || label == null) continue; if ("elecustomer".equals(fk) && cust != null) known.putIfAbsent(label, cust); else if ("eleproduct".equals(fk) && prod != null) known.putIfAbsent(label, prod); else if ("elematerials".equals(fk) && mat != null) known.putIfAbsent(label, mat); else if ("elesupplier".equals(fk) && sup != null) known.putIfAbsent(label, sup); } // 2) 数量:意图门角色=数量 → 只留数字 String qty = it == null ? null : it.firstValueByRole("数量"); if (qty != null) { String lbl = cleanToLabel.get("数量"); if (lbl != null) { String d = qty.replaceAll("[^0-9.]", ""); if (!d.isEmpty()) known.putIfAbsent(lbl, d); } } // 3) 尺寸:正则从原话解析 长/宽/高 Map dims = parseDimensions(utterance); for (Map.Entry e : dims.entrySet()) { String lbl = cleanToLabel.get(e.getKey()); // "长"/"宽"/"高" if (lbl != null) known.putIfAbsent(lbl, e.getValue()); } // 4) 名称字段:新增某实体本身(如"新增客户叫X")时,实体名要落到它的名称字段 // (客户表本身没有 elecustomer 外键列,故不会被上面的 FK 环填上,需在此补)。 String nameLabel = findNameField(fields); if (nameLabel != null && !known.containsKey(nameLabel)) { String selfRole = danjuToRole(entity); String nameVal = (selfRole != null && it != null) ? it.firstValueByRole(selfRole) : null; if (nameVal == null && cust == null && prod == null && mat == null && sup == null) { nameVal = firstEntityValue(it); // 兜底:单据类型不含角色词时用首个实体 } if (nameVal != null) { known.put(nameLabel, nameVal); } } return known; } /** 单据/实体类型 → 它自身的业务角色(新增该实体时,该角色的实体值即其名称)。 */ private static String danjuToRole(String entity) { if (entity == null) return null; if (entity.contains("客户")) return "客户"; if (entity.contains("供应商")) return "供应商"; if (entity.contains("物料")) return "物料"; if (entity.contains("产品")) return "产品"; return null; } /** 解析尺寸 → {长,宽,高} 的数字(cm/mm 单位忽略,只取数值)。 */ public Map parseDimensions(String utterance) { Map out = new LinkedHashMap<>(); if (utterance == null || utterance.isBlank()) return out; Matcher m = DIM_LABELED.matcher(utterance); boolean any = false; while (m.find()) { String labels = m.group(1); String val = m.group(2); for (int i = 0; i < labels.length(); i++) { char c = labels.charAt(i); String key = dimKey(c); if (key != null) { out.put(key, val); any = true; } } } if (!any) { Matcher p = DIM_POS.matcher(utterance); if (p.find()) { out.put("长", p.group(1)); out.put("宽", p.group(2)); if (p.group(3) != null) out.put("高", p.group(3)); } } return out; } private static String dimKey(char c) { switch (c) { case '长': return "长"; case '宽': return "宽"; case '高': case '厚': case '深': return "高"; default: return null; } } /** 名称字段:优先 label 以「名称」结尾且非外键的字段;否则第一个 label 含「名称」的。 */ private static String findNameField(List> fields) { for (Map f : fields) { String label = str(f.get("label")); String fk = str(f.get("fk")); if (label != null && label.endsWith("名称") && (fk == null || fk.isBlank())) { return label; } } for (Map f : fields) { String label = str(f.get("label")); if (label != null && label.contains("名称")) return label; } return null; } private static String firstEntityValue(Intent it) { if (it == null) return null; for (Intent.Entity e : it.entities) { if (e != null && e.value != null && !e.value.isBlank()) return e.value.trim(); } return null; } /** 序列化为 knownFieldsJson,供 {@code FormCollectTool.collectForm} 预填。 */ public String toJson(Map known) { try { return mapper.writeValueAsString(known); } catch (Exception e) { return "{}"; } } private static String cleanLabel(String s) { if (s == null) return ""; return s.replaceAll("[((].*?[))]", "").replaceAll("\\s+", "").trim(); } private static String str(Object o) { return o == null ? null : o.toString(); } }