diff --git a/src/main/java/com/xly/service/ErpClient.java b/src/main/java/com/xly/service/ErpClient.java index f31eaa8..7fb240f 100644 --- a/src/main/java/com/xly/service/ErpClient.java +++ b/src/main/java/com/xly/service/ErpClient.java @@ -293,6 +293,49 @@ public class ErpClient { return root; } + /** + * 多表联动新增(主-从:报价 = 主表 + 印刷/部件从表 + 多数量)。{@code tables} 每项 = {@code {sTable, name, column(map)}}, + * 本方法给每行补 handleType=add 并一次性 POST addUpdateDelBusinessData,从表靠 sParentId 关联主表。 + */ + public JsonNode createMulti(String authToken, String moduleId, List> tables) { + JsonNode root = doCreateMulti(moduleId, tables, resolveToken(authToken)); + if (root.path("code").asInt() == -2 && canRelogin(authToken)) { + login(); + root = doCreateMulti(moduleId, tables, resolveToken(authToken)); + } + return root; + } + + @SuppressWarnings("unchecked") + private JsonNode doCreateMulti(String moduleId, List> tables, String tok) { + try { + String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId; + List> data = new ArrayList<>(); + for (Map t : tables) { + Map col = new LinkedHashMap<>((Map) t.get("column")); + col.put("handleType", "add"); + Map item = new LinkedHashMap<>(); + item.put("sTable", t.get("sTable")); + item.put("name", t.get("name")); + item.put("column", List.of(col)); + data.add(item); + } + Map body = new LinkedHashMap<>(); + body.put("sModelsId", moduleId); + body.put("data", data); + HttpRequest req = HttpRequest.newBuilder(URI.create(url)) + .header("Content-Type", "application/json;charset=UTF-8") + .header("Authorization", tok) + .timeout(Duration.ofSeconds(40)) + .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body), StandardCharsets.UTF_8)) + .build(); + HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return mapper.readTree(resp.body()); + } catch (Exception e) { + throw new RuntimeException("ERP 多表新增异常: " + e.getMessage(), e); + } + } + private JsonNode doCreate(String moduleId, String table, Map columns, String tok) { try { String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId; diff --git a/src/main/java/com/xly/service/FormResolverService.java b/src/main/java/com/xly/service/FormResolverService.java index 2fd9585..075b5f2 100644 --- a/src/main/java/com/xly/service/FormResolverService.java +++ b/src/main/java/com/xly/service/FormResolverService.java @@ -131,26 +131,41 @@ public class FormResolverService { return null; } if (table.equalsIgnoreCase("quoquotationmaster")) { + // 报价是跨表主-从表单:主表(quoquotationmaster) + 印刷/部件明细(quoquotationslave) + 多数量(quoquotationmanyqtys)。 + // 每个字段标 target 表;印刷/颜色/单双面无独立列,作为备注写进从表 sMaterialsMemo(note)。价格由 ERP【核价】算。 List> l = new ArrayList<>(); - l.add(bf("sCustomerId", "客户名称", "elecustomer")); - l.add(bf("sProductId", "产品名称", "eleproduct")); - l.add(bf("dQty", "数量", null)); - l.add(bf("dLength", "长(L)", null)); - l.add(bf("dWidth", "宽(W)", null)); - l.add(bf("dHeight", "高(D)", null)); - l.add(bf("dPrice", "单价", null)); + l.add(bf("sCustomerId", "客户名称", "elecustomer", "master")); + l.add(bf("sProductId", "产品名称", "eleproduct", "master")); + l.add(bf("sMaterialsId", "物料名称", "elematerials", "master")); + l.add(bf("sProductUnit", "单位", null, "slave")); + l.add(bf("dLength", "长(L)", null, "master")); + l.add(bf("dWidth", "宽(W)", null, "master")); + l.add(bf("dHeight", "高(D)", null, "master")); + l.add(bf("dQty", "数量", null, "master")); + l.add(bf("sPrint", "印刷", null, "note")); + l.add(bf("sColor", "颜色", null, "note")); + l.add(bf("sSingleDouble", "单双面", null, "note")); + l.add(bf("dProductLength", "部件长", null, "slave")); + l.add(bf("dProductWidth", "部件宽", null, "slave")); + l.add(bf("dPrice", "单价", null, "master")); + l.add(bf("dCoefficient", "系数", null, "slave")); + l.add(bf("sMaterialsMemo", "材料备注", null, "slave")); + l.add(bf("dManyQty", "多数量(逗号分隔)", null, "manyqtys")); return l; } return null; } - private Map bf(String col, String label, String fk) { + private Map bf(String col, String label, String fk, String targetTable) { Map m = new LinkedHashMap<>(); m.put("col", col); m.put("label", label); if (fk != null) { m.put("fk", fk); } + if (targetTable != null) { + m.put("table", targetTable); + } return m; } diff --git a/src/main/java/com/xly/tool/ProposeWriteTool.java b/src/main/java/com/xly/tool/ProposeWriteTool.java index b3b16b1..7068fbc 100644 --- a/src/main/java/com/xly/tool/ProposeWriteTool.java +++ b/src/main/java/com/xly/tool/ProposeWriteTool.java @@ -212,6 +212,11 @@ public class ProposeWriteTool { 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<>(); @@ -360,6 +365,155 @@ public class ProposeWriteTool { 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(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(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) { + return err("找不到名为「" + v + "」的" + zh + ",请确认是否已存在。"); + } + 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 { diff --git a/src/main/java/com/xly/web/OpController.java b/src/main/java/com/xly/web/OpController.java index a4b8a10..4cd460c 100644 --- a/src/main/java/com/xly/web/OpController.java +++ b/src/main/java/com/xly/web/OpController.java @@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -81,9 +82,16 @@ public class OpController { String opType = str(op.get("sOpType")); JsonNode r; if ("create".equals(opType)) { - @SuppressWarnings("unchecked") - Map columns = mapper.readValue(str(op.get("sPayload")), Map.class); - r = erp.createForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), columns); + JsonNode payload = mapper.readTree(str(op.get("sPayload"))); + if (payload.has("__tables__")) { // 报价等多表主-从创建 + @SuppressWarnings("unchecked") + List> tables = mapper.convertValue(payload.get("__tables__"), List.class); + r = erp.createMulti(authToken, str(op.get("sTargetModuleId")), tables); + } else { + @SuppressWarnings("unchecked") + Map columns = mapper.convertValue(payload, Map.class); + r = erp.createForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), columns); + } } else if ("delete".equals(opType)) { r = erp.deleteForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), str(op.get("sTargetBillId"))); } else if ("examine".equals(opType)) {