diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java index 25e9840..d97484e 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java @@ -670,7 +670,7 @@ public class MesXslDesktopAnonController { if (tpl == null) return Result.error("绑定的打印模板不存在"); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); JsonNode bizRoot = objectMapper.valueToTree(card); - ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping, tpl.getTemplateJson()); PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson()); Map out = new HashMap<>(8); out.put("cardId", card.getId()); @@ -697,7 +697,7 @@ public class MesXslDesktopAnonController { if (tpl == null) return Result.error("绑定的打印模板不存在"); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); JsonNode bizRoot = objectMapper.valueToTree(entry); - ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping, tpl.getTemplateJson()); PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson()); Map out = new HashMap<>(8); out.put("entryId", entry.getId()); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java index 453f123..bd38232 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java @@ -228,7 +228,7 @@ public class MesXslRawMaterialCardController extends JeecgController out = new HashMap<>(8); out.put("cardId", card.getId()); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialEntryController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialEntryController.java index 4872421..2abfe15 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialEntryController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialEntryController.java @@ -218,7 +218,7 @@ public class MesXslRawMaterialEntryController extends JeecgController out = new HashMap<>(8); out.put("entryId", entry.getId()); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/constant/MesMaterialPrintConstants.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/constant/MesMaterialPrintConstants.java new file mode 100644 index 0000000..b87a5ee --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/constant/MesMaterialPrintConstants.java @@ -0,0 +1,15 @@ +package org.jeecg.modules.mes.material.constant; + +/** + * 原材料检验标准等 MES 物料模块的打印业务编码:与「业务打印绑定」中 biz_code、print_biz_perm_entity.perm_id 一致。 + */ +public final class MesMaterialPrintConstants { + + /** 原材料检验标准列表页菜单(sys_permission.id,见 Flyway V3.9.2_62) */ + public static final String RAW_MATERIAL_INSPECT_STD_MENU_PERM_ID = "1900000000000000730"; + + /** 历史或手工绑定时可能使用的语义型 biz_code(服务端 prepareNativePrint 会作为次选查询) */ + public static final String RAW_MATERIAL_INSPECT_STD_SEMANTIC_BIZ_CODE = "MES_RAW_MATERIAL_INSPECT_STD"; + + private MesMaterialPrintConstants() {} +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/controller/MesRawMaterialInspectStdController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/controller/MesRawMaterialInspectStdController.java index e7e525f..229451f 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/controller/MesRawMaterialInspectStdController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/controller/MesRawMaterialInspectStdController.java @@ -3,22 +3,40 @@ package org.jeecg.modules.mes.material.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.jeecg.common.api.vo.Result; import org.jeecg.common.aspect.annotation.AutoLog; import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.modules.mes.material.constant.MesMaterialPrintConstants; import org.jeecg.modules.mes.material.entity.MesRawMaterialInspectStd; import org.jeecg.modules.mes.material.entity.MesRawMaterialInspectStdLine; import org.jeecg.modules.mes.material.service.IMesRawMaterialInspectStdService; import org.jeecg.modules.mes.material.vo.MesRawMaterialInspectStdPage; +import org.jeecg.modules.print.entity.PrintBizPermEntity; +import org.jeecg.modules.print.entity.PrintBizTemplateBind; +import org.jeecg.modules.print.entity.PrintTemplate; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.jeecg.modules.print.service.IPrintBizTemplateBindService; +import org.jeecg.modules.print.service.IPrintTemplateService; +import org.jeecg.modules.print.support.PrintServerEnvironmentService; +import org.jeecg.modules.print.support.PrintServerPdfJobService; +import org.jeecg.modules.print.util.PrintBizDataMappingUtil; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -31,6 +49,12 @@ import org.springframework.web.servlet.ModelAndView; public class MesRawMaterialInspectStdController extends JeecgController { @Autowired private IMesRawMaterialInspectStdService mesRawMaterialInspectStdService; + @Autowired private PrintServerEnvironmentService printServerEnvironmentService; + @Autowired private PrintServerPdfJobService printServerPdfJobService; + @Autowired private IPrintBizTemplateBindService printBizTemplateBindService; + @Autowired private IPrintBizPermEntityService printBizPermEntityService; + @Autowired private IPrintTemplateService printTemplateService; + @Autowired private ObjectMapper objectMapper; @GetMapping("/list") public Result> queryPageList( @@ -114,6 +138,136 @@ public class MesRawMaterialInspectStdController extends JeecgController> queryPrinters() { + return Result.OK(printServerEnvironmentService.buildPrinterQueryResult()); + } + + /** + * 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf(与原材料卡片 prepareNativePrint 一致)。 + */ + @Operation(summary = "MES-原材料检验标准-准备原生打印数据") + @GetMapping("/prepareNativePrint") + @RequiresPermissions("mes:mes_raw_material_inspect_std:edit") + public Result> prepareNativePrint(@RequestParam(name = "id") String id) { + try { + MesRawMaterialInspectStd main = mesRawMaterialInspectStdService.getById(id); + if (main == null) { + return Result.error("未找到原材料检验标准"); + } + PrintBizTemplateBind bind = resolveInspectStdPrintBind(); + if (bind == null) { + return Result.error( + "请先在「业务打印绑定」中配置原材料检验标准与打印模板;业务编码需为当前列表菜单的权限 id,或与 print_biz_perm_entity 中该业务实体一致"); + } + PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); + if (tpl == null) { + return Result.error("绑定的打印模板不存在"); + } + List lines = mesRawMaterialInspectStdService.selectLinesByStdId(id); + MesRawMaterialInspectStdPage pageVo = new MesRawMaterialInspectStdPage(); + BeanUtils.copyProperties(main, pageVo); + pageVo.setLineList(lines); + ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = objectMapper.valueToTree(pageVo); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping, tpl.getTemplateJson()); + PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson()); + Map out = new HashMap<>(8); + out.put("stdId", main.getId()); + out.put("templateCode", bind.getTemplateCode()); + out.put("templateJson", tpl.getTemplateJson()); + out.put("paperWidthMm", tpl.getPaperWidthMm()); + out.put("paperHeightMm", tpl.getPaperHeightMm()); + out.put("paperOrientation", tpl.getPaperOrientation()); + out.put("printData", objectMapper.convertValue(printData, Map.class)); + return Result.OK(out); + } catch (Exception e) { + log.error("原材料检验标准准备打印数据失败 id={}", id, e); + return Result.error("准备打印数据失败: " + e.getMessage()); + } + } + + /** + * 将前端生成的 PDF Base64 提交到服务器打印机(与原材料卡片 printPdf 一致)。 + */ + @AutoLog(value = "MES-原材料检验标准-PDF后端打印") + @Operation(summary = "MES-原材料检验标准-PDF后端打印") + @PostMapping("/printPdf") + @RequiresPermissions("mes:mes_raw_material_inspect_std:edit") + public Result printPdf(@RequestBody Map body) { + String sid = String.valueOf(body.getOrDefault("id", "")).trim(); + String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim(); + String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim(); + String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim(); + if (StringUtils.isBlank(sid)) { + return Result.error("id 不能为空"); + } + MesRawMaterialInspectStd std = mesRawMaterialInspectStdService.getById(sid); + if (std == null) { + return Result.error("未找到原材料检验标准"); + } + String prefix = + StringUtils.isNotBlank(std.getStandardNo()) ? std.getStandardNo() : std.getId(); + String fn = + StringUtils.isNotBlank(fileName) ? fileName : ("原材料检验标准-" + prefix + ".pdf"); + return printServerPdfJobService.submitPdfBase64( + printerName, pdfBase64, fn, "RAW_INSPECT_STD_" + prefix); + } + + private PrintBizTemplateBind resolveInspectStdPrintBind() { + PrintBizTemplateBind bind = + printBizTemplateBindService.getByBizCode(MesMaterialPrintConstants.RAW_MATERIAL_INSPECT_STD_MENU_PERM_ID); + if (bind != null) { + return bind; + } + bind = + printBizTemplateBindService.getByBizCode(MesMaterialPrintConstants.RAW_MATERIAL_INSPECT_STD_SEMANTIC_BIZ_CODE); + if (bind != null) { + return bind; + } + // 不同环境菜单 id 不一致:按 print_biz_perm_entity 中指向本实体的任意 perm_id 解析绑定 + String entityFqn = MesRawMaterialInspectStd.class.getName(); + List permRows = + printBizPermEntityService.lambdaQuery().eq(PrintBizPermEntity::getEntityClass, entityFqn).list(); + for (PrintBizPermEntity row : permRows) { + if (StringUtils.isBlank(row.getPermId())) { + continue; + } + bind = printBizTemplateBindService.getByBizCode(row.getPermId()); + if (bind != null) { + return bind; + } + } + // 最后兜底:业务名称与绑定页一致(避免 perm 映射表未同步时仍找不到) + List byName = + printBizTemplateBindService + .lambdaQuery() + .eq(PrintBizTemplateBind::getBizName, "原材料检验标准") + .orderByDesc(PrintBizTemplateBind::getCreateTime) + .last("LIMIT 8") + .list(); + if (byName.isEmpty()) { + return null; + } + if (byName.size() == 1) { + return byName.get(0); + } + for (PrintBizTemplateBind b : byName) { + PrintBizPermEntity pe = printBizPermEntityService.getByPermId(b.getBizCode()); + if (pe != null && entityFqn.equals(StringUtils.trimToEmpty(pe.getEntityClass()))) { + return b; + } + } + return byName.get(0); + } + @RequiresPermissions("mes:mes_raw_material_inspect_std:exportXls") @RequestMapping("/exportXls") public ModelAndView exportXls(HttpServletRequest request, MesRawMaterialInspectStd model) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/entity/MesRawMaterialInspectStd.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/entity/MesRawMaterialInspectStd.java index 7d9cae2..15d0847 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/entity/MesRawMaterialInspectStd.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/entity/MesRawMaterialInspectStd.java @@ -1,12 +1,14 @@ package org.jeecg.modules.mes.material.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; import java.util.Date; +import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; @@ -71,4 +73,9 @@ public class MesRawMaterialInspectStd implements Serializable { private Date updateTime; private Integer delFlag; + + /** 检验标准明细(子表 mes_raw_material_inspect_std_line,通过 stdId 关联本表 id;不参与主表入库映射) */ + @TableField(exist = false) + @Schema(description = "MES原材料检验标准-检验项明细列表") + private List lineList; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/vo/MesRawMaterialInspectStdPage.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/vo/MesRawMaterialInspectStdPage.java index b2cfbf5..34b753e 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/vo/MesRawMaterialInspectStdPage.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/mes/material/vo/MesRawMaterialInspectStdPage.java @@ -1,14 +1,10 @@ package org.jeecg.modules.mes.material.vo; -import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; import org.jeecg.modules.mes.material.entity.MesRawMaterialInspectStd; -import org.jeecg.modules.mes.material.entity.MesRawMaterialInspectStdLine; +/** 主子保存/编辑页 VO,继承主表实体(含 {@link MesRawMaterialInspectStd#lineList} 明细)。 */ @Data @EqualsAndHashCode(callSuper = true) -public class MesRawMaterialInspectStdPage extends MesRawMaterialInspectStd { - - private List lineList; -} +public class MesRawMaterialInspectStdPage extends MesRawMaterialInspectStd {} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java index 5d886f8..c034709 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java @@ -38,6 +38,7 @@ import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; import org.jeecg.modules.print.vo.PrintBizFieldItemVO; import org.jeecg.modules.print.vo.PrintBizTypeVO; import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; +import org.jeecg.modules.print.vo.PrintTemplateStructureVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.*; @@ -182,6 +183,23 @@ public class PrintBizTemplateBindController extends JeecgController parseTemplateStructure( + @RequestParam(name = "templateId") String templateId) { + if (StringUtils.isBlank(templateId)) { + return Result.error("templateId 不能为空"); + } + PrintTemplate tpl = printTemplateService.getById(templateId); + if (tpl == null) { + return Result.error("模板不存在"); + } + PrintTemplateStructureVO structure = + PrintNativeTemplateFieldExtractor.extractStructure(tpl.getTemplateJson()); + return Result.OK(structure); + } + @Operation(summary = "主实体上的明细槽位(List<明细实体> / 明细数组 / 嵌套对象),用于先选明细再反射明细字段") @GetMapping("/detailSlots") @RequiresPermissions("print:bizBind:list") @@ -258,12 +276,15 @@ public class PrintBizTemplateBindController extends JeecgController res = new HashMap<>(4); res.put("templateCode", bind.getTemplateCode()); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java index b1a320f..6250304 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java @@ -3,19 +3,35 @@ package org.jeecg.modules.print.util; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; -/** 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField) */ +/** + * 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField)。支持业务数组字段映射到模板明细表(如 List2.Field1)。 + * + *

模板 {@code dataBinding.detailTables} 中明细列统一为 {@code tableKey.fieldKey}(如 List1.Field1);无双缀旧映射仍可借助 + * {@link #resolveSyntheticDetailTemplateField} 参与列表展开。 + */ public final class PrintBizDataMappingUtil { private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final JsonNodeFactory NF = MAPPER.getNodeFactory(); private PrintBizDataMappingUtil() {} public static ObjectNode mapBizToPrintData(JsonNode bizRoot, ArrayNode mappingRules) { + return mapBizToPrintData(bizRoot, mappingRules, null); + } + + /** + * @param templateJson 原生模板 JSON;非空时可为无双缀明细列绑定补齐 tableKey,生成正确的明细数组结构 + */ + public static ObjectNode mapBizToPrintData(JsonNode bizRoot, ArrayNode mappingRules, String templateJson) { ObjectNode printData = MAPPER.createObjectNode(); if (bizRoot == null || mappingRules == null) { return printData; @@ -26,21 +42,109 @@ public final class PrintBizDataMappingUtil { } String templateField = text(rule, "templateField"); String bizField = text(rule, "bizField"); - // 仅要求模板字段名;业务字段为空表示「不参与取数」,仍向 printData 写入空字符串,避免模板占位符缺键 if (StringUtils.isBlank(templateField)) { continue; } + // 模板明细列多为「tableKey.columnKey」(如 List2.Field1),业务字段为「数组字段.列」(如 lineList.inspectItemId) + if (qualifiesForListExpansion(bizRoot, templateField, bizField)) { + continue; + } + // 无双缀占位(Field1)+ 业务侧为明细数组:交由列表展开写入 printData.List1[],避免落在根上导致表格 source 读不到 + if (shouldDeferShortDetailFieldForListExpansion(bizRoot, templateField, bizField, templateJson)) { + continue; + } JsonNode val; if (StringUtils.isBlank(bizField)) { - val = MAPPER.getNodeFactory().textNode(""); + val = NF.textNode(""); } else { val = resolvePath(bizRoot, bizField); } setPath(printData, templateField, val); } + applyListExpandedMappings(bizRoot, mappingRules, printData, templateJson); return printData; } + /** + * 在 {@code dataBinding.detailTables} 中按声明顺序查找首个包含 {@code fieldKey} 的明细表,返回 {@code tableKey.fieldKey}。 + * + *

与同模块抽取器中「明细列统一为 tableKey.fieldKey」一致;仍兼容历史绑定里保存的短键。 + */ + private static String resolveSyntheticDetailTemplateField(String templateJson, String shortFieldKey) { + if (StringUtils.isAnyBlank(templateJson, shortFieldKey)) { + return null; + } + try { + JsonNode root = MAPPER.readTree(templateJson); + JsonNode db = root.get("dataBinding"); + if (db == null || !db.isObject()) { + return null; + } + JsonNode tables = db.get("detailTables"); + if (tables == null || !tables.isArray()) { + return null; + } + for (JsonNode t : tables) { + if (t == null || !t.isObject()) { + continue; + } + String tableKey = text(t, "tableKey").trim(); + if (StringUtils.isBlank(tableKey)) { + continue; + } + JsonNode fields = t.get("fields"); + if (fields == null || !fields.isArray()) { + continue; + } + for (JsonNode f : fields) { + if (f != null && f.isObject() && shortFieldKey.equals(text(f, "key").trim())) { + return tableKey + "." + shortFieldKey; + } + } + } + return null; + } catch (Exception ignored) { + return null; + } + } + + private static String resolveEffectiveTemplateFieldForListExpansion( + String templateJson, String templateFieldRaw, String bizField, JsonNode bizRoot) { + if (StringUtils.isBlank(templateFieldRaw)) { + return templateFieldRaw; + } + String tf = templateFieldRaw.trim(); + if (splitFirstDotPair(tf) != null) { + return tf; + } + if (StringUtils.isBlank(templateJson) || StringUtils.isBlank(bizField)) { + return tf; + } + HeadTail biz = splitFirstDotPair(bizField.trim()); + if (biz == null) { + return tf; + } + JsonNode arr = bizRoot != null ? bizRoot.get(biz.head()) : null; + if (arr == null || !arr.isArray()) { + return tf; + } + String synthetic = resolveSyntheticDetailTemplateField(templateJson, tf); + return StringUtils.isNotBlank(synthetic) ? synthetic : tf; + } + + private static boolean shouldDeferShortDetailFieldForListExpansion( + JsonNode bizRoot, String templateField, String bizField, String templateJson) { + if (StringUtils.isBlank(templateJson) || StringUtils.isAnyBlank(templateField, bizField)) { + return false; + } + if (splitFirstDotPair(templateField) != null) { + return false; + } + String eff = + resolveEffectiveTemplateFieldForListExpansion(templateJson, templateField, bizField, bizRoot); + return qualifiesForListExpansion(bizRoot, eff, bizField); + } + /** * 按模板中已声明的绑定路径({@code dataBinding.params}、画布/表格等元素的 {@code bindField},与 * {@link PrintNativeTemplateFieldExtractor} 一致),向 printData 补齐缺失路径(空字符串)。 @@ -48,6 +152,15 @@ public final class PrintBizDataMappingUtil { *

避免字段映射未包含某键时 API 缺键,桌面端渲染把「设计稿占位 text」当成数据显示。 */ public static ObjectNode fillMissingDataBindingParamKeys(ObjectNode printData, String templateJson) { + return fillMissingDataBindingParamKeys(printData, templateJson, false); + } + + /** + * @param detailPreviewPlaceholderRow 为 true 时(如绑定预览):明细数据源无行数据时用模板列生成一行 {@code null} 占位,便于 JSON 里看出字段; + * 为 false 时(真实打印):保持 {@code []},避免出现空白假行。 + */ + public static ObjectNode fillMissingDataBindingParamKeys( + ObjectNode printData, String templateJson, boolean detailPreviewPlaceholderRow) { if (printData == null) { printData = MAPPER.createObjectNode(); } @@ -60,18 +173,260 @@ public final class PrintBizDataMappingUtil { if (item == null || StringUtils.isBlank(item.getBindField())) { continue; } + // 明细表列占位:必须由「明细数组映射」展开;不可用空串占位,否则会生成 List2 对象为 {Field1:""}, + // 前端 resolveTableRows 要求 List2 为数组(见原生 TableElement)。 + String et = StringUtils.defaultString(item.getElementType()).toLowerCase(); + if ("detailfield".equals(et) || "column".equals(et)) { + continue; + } String bf = item.getBindField().trim(); if (!hasPath(printData, bf)) { - setPath(printData, bf, MAPPER.getNodeFactory().textNode("")); + setPath(printData, bf, NF.textNode("")); } } } catch (Exception ignored) { // 模板解析异常时不阻断打印 } + try { + normalizeNativeDetailTableShapes(printData, templateJson, detailPreviewPlaceholderRow); + } catch (Exception ignored) { + // 明细形态规整失败不阻断 + } return printData; } - /** 判断 printData 上是否存在该点分路径(含嵌套对象) */ + /** + * 按模板 {@code dataBinding.detailTables} 将各明细数据源键规范为数组,并去掉首张明细表误落在根上的短列键。 + * + *

无业务数据时:真实打印侧保留空数组 {@code []};预览侧可选注入一行列占位(见 {@code detailPreviewPlaceholderRow})。 + */ + private static void normalizeNativeDetailTableShapes( + ObjectNode printData, String templateJson, boolean detailPreviewPlaceholderRow) { + if (printData == null || StringUtils.isBlank(templateJson)) { + return; + } + try { + JsonNode root = MAPPER.readTree(templateJson); + JsonNode db = root.get("dataBinding"); + if (db == null || !db.isObject()) { + return; + } + JsonNode tables = db.get("detailTables"); + if (tables == null || !tables.isArray()) { + return; + } + boolean firstTable = true; + LinkedHashSet firstTableColumnKeys = new LinkedHashSet<>(); + + for (JsonNode t : tables) { + if (t == null || !t.isObject()) { + continue; + } + String tk = text(t, "tableKey").trim(); + if (StringUtils.isBlank(tk)) { + continue; + } + JsonNode fields = t.get("fields"); + LinkedHashSet columnKeys = new LinkedHashSet<>(); + if (fields != null && fields.isArray()) { + for (JsonNode f : fields) { + if (f != null && f.isObject()) { + String k = text(f, "key").trim(); + if (StringUtils.isNotBlank(k)) { + columnKeys.add(k); + } + } + } + } + if (firstTable) { + firstTableColumnKeys.addAll(columnKeys); + firstTable = false; + } + + JsonNode cur = printData.get(tk); + if (cur == null || cur.isNull()) { + printData.set(tk, emptyDetailTableArray(columnKeys, detailPreviewPlaceholderRow)); + } else if (cur.isObject()) { + ObjectNode obj = (ObjectNode) cur; + if (obj.isEmpty() || objectLeavesOnlyEmptyPlaceholders(obj)) { + printData.set(tk, emptyDetailTableArray(columnKeys, detailPreviewPlaceholderRow)); + } else { + ArrayNode arr = MAPPER.createArrayNode(); + arr.add(obj.deepCopy()); + printData.set(tk, arr); + } + } else if (cur.isArray()) { + ArrayNode arr = (ArrayNode) cur; + if (arr.size() == 0) { + printData.set(tk, emptyDetailTableArray(columnKeys, detailPreviewPlaceholderRow)); + } + } else { + printData.set(tk, emptyDetailTableArray(columnKeys, detailPreviewPlaceholderRow)); + } + } + + for (String fk : firstTableColumnKeys) { + printData.remove(fk); + } + } catch (Exception ignored) { + // 忽略 + } + } + + /** + * 无明细行时的数组形态:预览用模板声明的列键生成一行 {@code null},便于看清字段;真实打印返回 {@code []}。 + */ + private static ArrayNode emptyDetailTableArray( + LinkedHashSet columnKeys, boolean detailPreviewPlaceholderRow) { + ArrayNode arr = MAPPER.createArrayNode(); + if (!detailPreviewPlaceholderRow || columnKeys == null || columnKeys.isEmpty()) { + return arr; + } + ObjectNode row = MAPPER.createObjectNode(); + for (String k : columnKeys) { + row.putNull(k); + } + arr.add(row); + return arr; + } + + /** + * 预览专用:占位明细行单元格为 {@code null}/空串时,按映射从业务 JSON 取数写入(明细数组取第一行)。 + */ + public static void fillPreviewDetailPlaceholderRowValues( + ObjectNode printData, JsonNode bizRoot, ArrayNode mappingRules, String templateJson) { + if (printData == null || bizRoot == null || mappingRules == null || StringUtils.isBlank(templateJson)) { + return; + } + LinkedHashSet declaredTables = loadDeclaredDetailTableKeys(templateJson); + if (declaredTables.isEmpty()) { + return; + } + try { + for (JsonNode rule : mappingRules) { + if (rule == null || !rule.isObject()) { + continue; + } + String tfRaw = text(rule, "templateField").trim(); + String bfRaw = text(rule, "bizField").trim(); + if (StringUtils.isBlank(tfRaw)) { + continue; + } + String tfEff = tfRaw; + if (splitFirstDotPair(tfEff) == null) { + String syn = resolveSyntheticDetailTemplateField(templateJson, tfEff); + if (StringUtils.isNotBlank(syn)) { + tfEff = syn; + } + } + HeadTail tpl = splitFirstDotPair(tfEff); + if (tpl == null || StringUtils.isAnyBlank(tpl.head(), tpl.tail())) { + continue; + } + if (!declaredTables.contains(tpl.head())) { + continue; + } + JsonNode arrNode = printData.get(tpl.head()); + if (!(arrNode instanceof ArrayNode arr) || arr.size() != 1) { + continue; + } + JsonNode rowNode = arr.get(0); + if (!(rowNode instanceof ObjectNode row)) { + continue; + } + String col = tpl.tail(); + if (!row.has(col)) { + continue; + } + if (!cellNeedsPreviewFill(row.get(col))) { + continue; + } + JsonNode val = resolveBizFieldPreviewCell(bizRoot, bfRaw); + putLeaf(row, col, val); + } + } catch (Exception ignored) { + // 预览填充失败不阻断 + } + } + + private static LinkedHashSet loadDeclaredDetailTableKeys(String templateJson) { + LinkedHashSet out = new LinkedHashSet<>(); + try { + JsonNode root = MAPPER.readTree(templateJson); + JsonNode db = root.get("dataBinding"); + if (db == null || !db.isObject()) { + return out; + } + JsonNode tables = db.get("detailTables"); + if (tables == null || !tables.isArray()) { + return out; + } + for (JsonNode t : tables) { + if (t != null && t.isObject()) { + String tk = text(t, "tableKey").trim(); + if (StringUtils.isNotBlank(tk)) { + out.add(tk); + } + } + } + } catch (Exception ignored) { + // 忽略 + } + return out; + } + + private static boolean cellNeedsPreviewFill(JsonNode cur) { + if (cur == null || cur.isNull()) { + return true; + } + return cur.isTextual() && cur.asText("").isEmpty(); + } + + private static JsonNode resolveBizFieldPreviewCell(JsonNode bizRoot, String bizField) { + if (bizRoot == null || StringUtils.isBlank(bizField)) { + return mappingValueOrEmptyText(null); + } + String bf = bizField.trim(); + HeadTail biz = splitFirstDotPair(bf); + if (biz != null) { + JsonNode arr = bizRoot.get(biz.head()); + if (arr != null && arr.isArray() && arr.size() > 0) { + JsonNode row = arr.get(0); + if (row != null && row.isObject()) { + JsonNode v = resolvePath(row, biz.tail()); + return mappingValueOrEmptyText(v); + } + } + } + JsonNode v = resolvePath(bizRoot, bf); + return mappingValueOrEmptyText(v); + } + + /** 对象仅含空占位(null / 空串),视为无明细行 */ + private static boolean objectLeavesOnlyEmptyPlaceholders(ObjectNode obj) { + if (obj == null || obj.isEmpty()) { + return true; + } + Iterator it = obj.elements(); + while (it.hasNext()) { + JsonNode v = it.next(); + if (v == null || v.isNull()) { + continue; + } + if (v.isTextual() && v.asText("").isEmpty()) { + continue; + } + if (v.isObject()) { + if (!objectLeavesOnlyEmptyPlaceholders((ObjectNode) v)) { + return false; + } + continue; + } + return false; + } + return true; + } + private static boolean hasPath(ObjectNode root, String path) { if (StringUtils.isBlank(path)) { return false; @@ -173,6 +528,105 @@ public final class PrintBizDataMappingUtil { } } + /** 第一段为 JSON 对象的键路径;余下段落(可含多级)相对该对象的 field 路径。 */ + private record HeadTail(String head, String tail) {} + + private static HeadTail splitFirstDotPair(String path) { + if (StringUtils.isBlank(path)) { + return null; + } + int dot = path.indexOf('.'); + if (dot <= 0 || dot >= path.length() - 1) { + return null; + } + String h = path.substring(0, dot).trim(); + String t = path.substring(dot + 1).trim(); + if (h.isEmpty() || t.isEmpty()) { + return null; + } + return new HeadTail(h, t); + } + + /** + * 业务字段首段指向 JSON 数组,且模板字段为「明细表绑定键.列」(如 {@code List2.Field1})时, + * 需要将每行业务对象展开为模板 {@code previewData[listKey][i].列},供原生表格 {@code source=listKey}。 + */ + private static boolean qualifiesForListExpansion(JsonNode bizRoot, String templateField, String bizField) { + if (bizRoot == null || StringUtils.isBlank(templateField) || StringUtils.isBlank(bizField)) { + return false; + } + HeadTail tpl = splitFirstDotPair(templateField); + HeadTail biz = splitFirstDotPair(bizField); + if (tpl == null || biz == null) { + return false; + } + JsonNode arr = bizRoot.get(biz.head()); + return arr != null && arr.isArray(); + } + + /** 将「业务明细数组 → 模板明细键」的规则写入 {@code printData}。 */ + private static void applyListExpandedMappings( + JsonNode bizRoot, ArrayNode mappingRules, ObjectNode printData, String templateJson) { + if (bizRoot == null || mappingRules == null || printData == null) { + return; + } + for (JsonNode rule : mappingRules) { + if (rule == null || !rule.isObject()) { + continue; + } + String tfRaw = text(rule, "templateField").trim(); + String bf = text(rule, "bizField").trim(); + String tfEff = resolveEffectiveTemplateFieldForListExpansion(templateJson, tfRaw, bf, bizRoot); + if (!qualifiesForListExpansion(bizRoot, tfEff, bf)) { + continue; + } + HeadTail tpl = splitFirstDotPair(tfEff); + HeadTail biz = splitFirstDotPair(bf); + if (tpl == null || biz == null) { + continue; + } + JsonNode srcArrNode = bizRoot.get(biz.head()); + if (srcArrNode == null || !srcArrNode.isArray()) { + continue; + } + ArrayNode tgtArr = ensureRowObjectArray(printData, tpl.head(), srcArrNode.size()); + for (int i = 0; i < srcArrNode.size(); i++) { + JsonNode srcRow = srcArrNode.get(i); + JsonNode val = + srcRow != null && srcRow.isObject() ? resolvePath(srcRow, biz.tail()) : null; + ObjectNode tgtRow = (ObjectNode) tgtArr.get(i); + setPath(tgtRow, tpl.tail(), mappingValueOrEmptyText(val)); + } + } + } + + /** 缺省占位与主映射一致:空则用空字符串。 */ + private static JsonNode mappingValueOrEmptyText(JsonNode val) { + if (val == null || val.isNull()) { + return NF.textNode(""); + } + return val; + } + + /** 保证 printData.{tableKey} 为数组,且长度为 rowCount(元素为展开行 Json 对象)。 */ + private static ArrayNode ensureRowObjectArray(ObjectNode printData, String tableKey, int rowCount) { + JsonNode existed = printData.get(tableKey); + ArrayNode arr; + if (existed instanceof ArrayNode a) { + arr = a; + } else { + if (existed != null) { + printData.remove(tableKey); + } + arr = MAPPER.createArrayNode(); + printData.set(tableKey, arr); + } + while (arr.size() < rowCount) { + arr.add(MAPPER.createObjectNode()); + } + return arr; + } + private static String text(JsonNode n, String key) { if (n == null || !n.isObject()) { return ""; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java index 3ff9035..c7e07dc 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java @@ -3,11 +3,16 @@ package org.jeecg.modules.print.util; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.vo.PrintTemplateDetailTableVO; import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; +import org.jeecg.modules.print.vo.PrintTemplateStructureVO; /** * 从原生打印模板 JSON 中收集所有 bindField / 表格列 field,供业务字段映射使用。 @@ -40,9 +45,206 @@ public final class PrintNativeTemplateFieldExtractor { } catch (Exception ignored) { return list; } + dedupeShortDetailFieldsWhenPrefixedExists(list); return list; } + /** + * dataBinding 已产出 {@code List1.Field1} 时,去掉画布扫描残留的同名短键 {@code Field1},避免映射表里重复一行。 + */ + private static void dedupeShortDetailFieldsWhenPrefixedExists(List list) { + HashSet columnSuffixes = new HashSet<>(); + for (PrintTemplateFieldItemVO vo : list) { + String bf = vo.getBindField(); + if (StringUtils.isBlank(bf)) { + continue; + } + int d = bf.indexOf('.'); + if (d > 0 && d < bf.length() - 1) { + columnSuffixes.add(bf.substring(d + 1).trim()); + } + } + list.removeIf( + vo -> { + String bf = vo.getBindField(); + if (StringUtils.isBlank(bf) || bf.indexOf('.') >= 0) { + return false; + } + String et = StringUtils.defaultString(vo.getElementType()).toLowerCase(); + if (!"detailfield".equals(et) && !"column".equals(et)) { + return false; + } + return columnSuffixes.contains(bf.trim()); + }); + } + + /** + * 将 {@link #extract(String)} 的扁平结果拆成「主表参数 + 按明细表分组」,供前端多标签映射。 + * + *

分组规则:带 {@code tableKey.} 前缀的占位归入对应明细表;无双缀短键按 dataBinding.detailTables 中字段声明归属; + * 若模板仅声明单个明细表则将短键归入该表;否则归入 {@code __general__}(其它画布/未归类占位)。 + */ + public static PrintTemplateStructureVO extractStructure(String templateJson) { + PrintTemplateStructureVO vo = new PrintTemplateStructureVO(); + if (StringUtils.isBlank(templateJson)) { + return vo; + } + JsonNode root; + try { + root = MAPPER.readTree(templateJson); + } catch (Exception e) { + return vo; + } + + List metaTables = readDetailTableMeta(root); + LinkedHashSet metaKeysOrdered = new LinkedHashSet<>(); + Map tableLabels = new LinkedHashMap<>(); + Map shortFieldOwner = new LinkedHashMap<>(); + for (DetailTableMeta m : metaTables) { + if (StringUtils.isNotBlank(m.tableKey)) { + metaKeysOrdered.add(m.tableKey.trim()); + if (StringUtils.isNotBlank(m.label)) { + tableLabels.put(m.tableKey.trim(), m.label.trim()); + } + for (String fk : m.fieldKeys) { + if (StringUtils.isNotBlank(fk)) { + shortFieldOwner.putIfAbsent(fk.trim(), m.tableKey.trim()); + } + } + } + } + + List flat = extract(templateJson); + List params = new ArrayList<>(); + List nonParams = new ArrayList<>(); + for (PrintTemplateFieldItemVO f : flat) { + if (f == null) { + continue; + } + if ("param".equalsIgnoreCase(StringUtils.defaultString(f.getElementType()))) { + params.add(f); + } else { + nonParams.add(f); + } + } + vo.setParams(params); + + LinkedHashSet dottedPrefixes = new LinkedHashSet<>(); + for (PrintTemplateFieldItemVO f : nonParams) { + String bf = StringUtils.trimToEmpty(f.getBindField()); + int dot = bf.indexOf('.'); + if (dot > 0) { + String prefix = bf.substring(0, dot).trim(); + if (StringUtils.isNotBlank(prefix)) { + dottedPrefixes.add(prefix); + } + } + } + + String soleMetaKey = metaKeysOrdered.size() == 1 ? metaKeysOrdered.iterator().next() : null; + + Map> groups = new LinkedHashMap<>(); + for (String k : metaKeysOrdered) { + groups.put(k, new ArrayList<>()); + } + for (String p : dottedPrefixes) { + groups.computeIfAbsent(p, key -> new ArrayList<>()); + } + + final String generalKey = "__general__"; + for (PrintTemplateFieldItemVO f : nonParams) { + String bf = StringUtils.trimToEmpty(f.getBindField()); + String groupKey; + int dot = bf.indexOf('.'); + if (dot > 0) { + groupKey = bf.substring(0, dot).trim(); + } else if (soleMetaKey != null) { + groupKey = soleMetaKey; + } else if (StringUtils.isNotBlank(bf) && shortFieldOwner.containsKey(bf)) { + groupKey = shortFieldOwner.get(bf); + } else if (metaKeysOrdered.isEmpty() && dottedPrefixes.size() == 1) { + groupKey = dottedPrefixes.iterator().next(); + } else { + groupKey = generalKey; + } + groups.computeIfAbsent(groupKey, key -> new ArrayList<>()); + groups.get(groupKey).add(f); + } + + List tables = new ArrayList<>(); + Set emitted = new LinkedHashSet<>(); + + for (DetailTableMeta m : metaTables) { + String tk = StringUtils.trimToEmpty(m.tableKey); + if (StringUtils.isBlank(tk)) { + continue; + } + emitted.add(tk); + List fields = groups.getOrDefault(tk, List.of()); + tables.add(new PrintTemplateDetailTableVO(tk, tableLabels.get(tk), new ArrayList<>(fields))); + } + + List extras = new ArrayList<>(); + for (String g : groups.keySet()) { + if (!emitted.contains(g) && !groups.get(g).isEmpty()) { + extras.add(g); + } + } + extras.sort(String::compareTo); + for (String g : extras) { + String lbl = + generalKey.equals(g) ? "其它(未归类画布/明细占位)" : null; + tables.add(new PrintTemplateDetailTableVO(g, lbl, new ArrayList<>(groups.get(g)))); + } + + vo.setDetailTables(tables); + return vo; + } + + private static final class DetailTableMeta { + String tableKey = ""; + String label = ""; + List fieldKeys = new ArrayList<>(); + } + + private static List readDetailTableMeta(JsonNode root) { + List out = new ArrayList<>(); + if (root == null || !root.isObject()) { + return out; + } + JsonNode db = root.get("dataBinding"); + if (db == null || !db.isObject()) { + return out; + } + JsonNode detailTables = db.get("detailTables"); + if (detailTables == null || !detailTables.isArray()) { + return out; + } + for (JsonNode t : detailTables) { + if (t == null || !t.isObject()) { + continue; + } + DetailTableMeta m = new DetailTableMeta(); + m.tableKey = text(t, "tableKey").trim(); + m.label = firstNonBlank(text(t, "label"), text(t, "title")).trim(); + JsonNode fields = t.get("fields"); + if (fields != null && fields.isArray()) { + for (JsonNode f : fields) { + if (f != null && f.isObject()) { + String k = text(f, "key").trim(); + if (StringUtils.isNotBlank(k)) { + m.fieldKeys.add(k); + } + } + } + } + if (StringUtils.isNotBlank(m.tableKey)) { + out.add(m); + } + } + return out; + } + /** 解析 schema.dataBinding:params(参数键)、detailTables(明细字段) */ private static void collectDataBinding(JsonNode root, Set seen, List list) { JsonNode db = root.get("dataBinding"); @@ -81,11 +283,8 @@ public final class PrintNativeTemplateFieldExtractor { if (StringUtils.isBlank(fk)) { continue; } - // 与画布列 bindField 一致时多为短 key;多表明细同字段再加 tableKey 前缀消歧 - String bindKey = fk; - if (seen.contains(bindKey) && StringUtils.isNotBlank(tableKey)) { - bindKey = tableKey + "." + fk; - } + // 统一使用 tableKey.fieldKey,与多表明细、列表展开、预览占位一致(避免首张表仍用短键 Field1) + String bindKey = StringUtils.isNotBlank(tableKey) ? tableKey + "." + fk : fk; if (seen.contains(bindKey) || !seen.add(bindKey)) { continue; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateDetailTableVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateDetailTableVO.java new file mode 100644 index 0000000..18d8398 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateDetailTableVO.java @@ -0,0 +1,26 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** 原生模板中单个明细表(对应 dataBinding.detailTables[].tableKey)及其占位字段 */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "打印模板明细表分组") +public class PrintTemplateDetailTableVO implements Serializable { + + @Schema(description = "明细表绑定键,与画布表格 source、占位前缀一致,如 List1、List2") + private String tableKey; + + @Schema(description = "设计器中配置的明细表显示名(可选)") + private String label; + + @Schema(description = "该明细表下的模板占位字段(bindField 已与全局解析逻辑一致)") + private List fields = new ArrayList<>(); +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateStructureVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateStructureVO.java new file mode 100644 index 0000000..a46845b --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateStructureVO.java @@ -0,0 +1,21 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 原生打印模板占位结构的结构化视图:主表参数 + 按明细表分组,供「业务打印绑定」弹窗标签页展示。 + */ +@Data +@Schema(description = "打印模板占位结构(参数 + 多明细表)") +public class PrintTemplateStructureVO implements Serializable { + + @Schema(description = "主表参数(dataBinding.params)") + private List params = new ArrayList<>(); + + @Schema(description = "明细表分组(顺序与模板 dataBinding.detailTables 一致,含画布推断的补充分组)") + private List detailTables = new ArrayList<>(); +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_62__mes_raw_material_inspect_std_menu_and_print_entity.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_62__mes_raw_material_inspect_std_menu_and_print_entity.sql new file mode 100644 index 0000000..40db17f --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_62__mes_raw_material_inspect_std_menu_and_print_entity.sql @@ -0,0 +1,88 @@ +-- 原材料检验标准:菜单(与前端 mes/rawmaterialinspectstd/index 一致)+ 打印实体映射 + 可选纳入打印白名单 +-- 若环境已存在同名菜单(不同 id),不会覆盖;打印绑定时请使用本脚本中的列表页 id 作为 biz_code,或沿用语义码 MES_RAW_MATERIAL_INSPECT_STD(后端支持双查询) + +-- ===================== 1. 菜单权限(父菜单:MES XSL 1900000000000000300)===================== +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000730', '1900000000000000300', '原材料检验标准', '/mes/rawMaterialInspectStd', 'mes/rawmaterialinspectstd/index', 1, NULL, NULL, 1, NULL, '0', 11.30, 0, 'ant-design:safety-outlined', 0, 1, 0, 0, 'MES原材料检验标准', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000730'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000731', '1900000000000000730', '添加', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:add', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000731'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000732', '1900000000000000730', '编辑', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:edit', '1', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000732'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000733', '1900000000000000730', '删除', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:delete', '1', 3.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000733'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000734', '1900000000000000730', '批量删除', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:deleteBatch', '1', 4.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000734'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000735', '1900000000000000730', '启用停用', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:enable', '1', 5.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000735'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000736', '1900000000000000730', '导出', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:exportXls', '1', 6.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000736'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000737', '1900000000000000730', '导入', NULL, NULL, 0, NULL, NULL, 2, 'mes:mes_raw_material_inspect_std:importExcel', '1', 7.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000737'); + +-- ===================== 2. 角色菜单授权(admin)===================== +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000730', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000730'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000731', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000731'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000732', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000732'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000733', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000733'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000734', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000734'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000735', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000735'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000736', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000736'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000737', NULL, NOW(), '127.0.0.1' +FROM `sys_role` r WHERE r.`role_code` = 'admin' + AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000737'); + +-- ===================== 3. 打印业务:菜单关联实体(含主子 lineList)===================== +INSERT INTO `print_biz_perm_entity` (`perm_id`, `entity_class`) +SELECT '1900000000000000730', 'org.jeecg.modules.mes.material.entity.MesRawMaterialInspectStd' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `print_biz_perm_entity` WHERE `perm_id` = '1900000000000000730'); + +-- ===================== 4. 纳入「业务打印绑定」可选范围(若表存在且尚未包含)===================== +INSERT INTO `print_biz_bind_perm_whitelist` (`perm_id`) +SELECT '1900000000000000730' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `print_biz_bind_perm_whitelist` WHERE `perm_id` = '1900000000000000730'); diff --git a/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStd.api.ts b/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStd.api.ts index 4a68419..5e406fa 100644 --- a/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStd.api.ts +++ b/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStd.api.ts @@ -12,6 +12,9 @@ enum Api { queryById = '/mes/material/rawMaterialInspectStd/queryById', queryLineList = '/mes/material/rawMaterialInspectStd/queryLineListByStdId', setEnable = '/mes/material/rawMaterialInspectStd/setEnable', + queryPrinters = '/mes/material/rawMaterialInspectStd/queryPrinters', + prepareNativePrint = '/mes/material/rawMaterialInspectStd/prepareNativePrint', + printPdf = '/mes/material/rawMaterialInspectStd/printPdf', } export const getExportUrl = Api.exportXls; @@ -37,3 +40,15 @@ export const batchDelete = (params, handleSuccess) => { export const saveOrUpdate = (params, isUpdate) => defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }); export const setEnable = (params) => defHttp.post({ url: Api.setEnable, params }); + +export const queryPrinters = () => defHttp.get({ url: Api.queryPrinters }); + +export const prepareNativePrint = (id: string) => + defHttp.get({ + url: Api.prepareNativePrint, + params: { id, _t: Date.now() }, + }); + +/** id + 前端生成的 pdfBase64;printerName 空则用默认队列(与原材料卡片一致) */ +export const printPdf = (data: { id: string; printerName?: string; pdfBase64: string; fileName?: string }) => + defHttp.post({ url: Api.printPdf, data, timeout: 3 * 60 * 1000 }); diff --git a/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStdList.vue b/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStdList.vue index bbe7851..a912469 100644 --- a/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStdList.vue +++ b/jeecgboot-vue3/src/views/mes/material/MesRawMaterialInspectStdList.vue @@ -2,32 +2,107 @@

+
+ + diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts index e05227c..4108c3d 100644 --- a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts @@ -9,6 +9,7 @@ enum Api { bizTypesForBinding = '/print/bizTemplateBind/bizTypesForBinding', permWhitelist = '/print/bizTemplateBind/permWhitelist', parseTemplateFields = '/print/bizTemplateBind/parseTemplateFields', + parseTemplateStructure = '/print/bizTemplateBind/parseTemplateStructure', previewMappedData = '/print/bizTemplateBind/previewMappedData', detailSlots = '/print/bizTemplateBind/detailSlots', bizFieldsForDetailSlot = '/print/bizTemplateBind/bizFieldsForDetailSlot', @@ -36,6 +37,16 @@ export const parseTemplateFields = (templateId: string) => params: { templateId, _t: Date.now() }, }); +/** 主表参数 + 按模板明细表分组(多标签映射) */ +export const parseTemplateStructure = (templateId: string) => + defHttp.get<{ + params: { bindField: string; elementType?: string; titleHint?: string }[]; + detailTables: { tableKey: string; label?: string; fields: { bindField: string; elementType?: string; titleHint?: string }[] }[]; + }>({ + url: Api.parseTemplateStructure, + params: { templateId, _t: Date.now() }, + }); + /** 预览映射后的打印数据 */ export const previewMappedData = (data: { bizCode: string; bizDataJson: Record }) => defHttp.post({ url: Api.previewMappedData, data }); diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue index 237682a..f9ac774 100644 --- a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue @@ -47,7 +47,7 @@ show-icon class="bind-alert" message="配置说明" - description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段;业务字段下拉第一项为「空占位符」,表示不参与业务 JSON 取值(等同输出空)。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。" + description="按卡片顺序操作:先选业务与模板 → 点击「解析模板占位字段」→ 主表参数映射主实体字段 → 若模板含多个明细表,在「明细与表格」标签页中逐表选择与模板明细键对应的业务明细集合,再映射列字段。业务字段下拉第一项为「空占位符」,表示不参与业务 JSON。明细列占位多为「模板明细键.列」(如 List2.Field1),业务侧选「明细属性.列」(如 lineList.qty),打印时会按数组展开。" /> @@ -89,30 +89,6 @@ - - - -

- 选择主实体类上的明细集合属性(如 List<明细实体>)或嵌套对象;系统将明细类字段并入下方「明细与表格」中的业务字段下拉。 -

- -
- @@ -269,7 +269,7 @@