diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/constant/MesXslPrintConstants.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/constant/MesXslPrintConstants.java new file mode 100644 index 0000000..4dc5c3a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/constant/MesXslPrintConstants.java @@ -0,0 +1,15 @@ +package org.jeecg.modules.xslmes.constant; + +/** + * 打印业务绑定 biz_code 使用菜单 permission id;与 print_biz_perm_entity、Flyway 中原材料卡片菜单一致。 + */ +public final class MesXslPrintConstants { + + /** 原材料卡片页面菜单(sys_permission.id) */ + public static final String RAW_MATERIAL_CARD_PERM_ID = "1900000000000000540"; + + /** 原料入场记录页面菜单(sys_permission.id,与 Flyway 中 parent 菜单一致) */ + public static final String RAW_MATERIAL_ENTRY_PERM_ID = "1900000000000000530"; + + private MesXslPrintConstants() {} +} 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 d0b0813..0067ff9 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 @@ -22,6 +22,7 @@ import org.jeecg.modules.print.service.IPrintBizTemplateBindService; import org.jeecg.modules.print.service.IPrintTemplateService; import org.jeecg.modules.print.util.PrintBizDataMappingUtil; import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus; +import org.jeecg.modules.xslmes.constant.MesXslPrintConstants; import org.jeecg.modules.xslmes.entity.MesXslCustomer; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry; @@ -620,8 +621,6 @@ public class MesXslDesktopAnonController { return Result.OK((int) count); } - /** 与 PrintBizTypeCatalog / 业务打印绑定一致 */ - private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD"; @Operation(summary = "原材料卡片-免密准备原生打印数据(桌面端用)") @GetMapping("/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint") @@ -629,7 +628,8 @@ public class MesXslDesktopAnonController { try { MesXslRawMaterialCard card = rawMaterialCardService.getById(id); if (card == null) return Result.error("未找到原材料卡片"); - PrintBizTemplateBind bind = printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE); + PrintBizTemplateBind bind = + printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID); if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板"); PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); if (tpl == null) return Result.error("绑定的打印模板不存在"); @@ -648,6 +648,32 @@ public class MesXslDesktopAnonController { } } + @Operation(summary = "原料入场记录-免密准备原生打印数据(桌面端用)") + @GetMapping("/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint") + public Result> rawMaterialEntryAnonPrepareNativePrint(@RequestParam(name = "id") String id) { + try { + MesXslRawMaterialEntry entry = rawMaterialEntryService.getById(id); + if (entry == null) return Result.error("未找到原料入场记录"); + PrintBizTemplateBind bind = + printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID); + if (bind == null) return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板"); + PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); + if (tpl == null) return Result.error("绑定的打印模板不存在"); + ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = objectMapper.valueToTree(entry); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + Map out = new HashMap<>(8); + out.put("entryId", entry.getId()); + out.put("templateCode", bind.getTemplateCode()); + out.put("templateJson", tpl.getTemplateJson()); + 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()); + } + } + // ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════ @Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)") 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 3ca108d..1b38b30 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 @@ -29,6 +29,7 @@ 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.jeecg.modules.xslmes.constant.MesXslPrintConstants; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; @@ -56,9 +57,6 @@ import org.springframework.web.servlet.ModelAndView; @Slf4j public class MesXslRawMaterialCardController extends JeecgController { - /** 与 PrintBizTypeCatalog / 业务打印绑定一致 */ - private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD"; - @Autowired private IMesXslRawMaterialCardService mesXslRawMaterialCardService; @Autowired @@ -184,7 +182,7 @@ public class MesXslRawMaterialCardController extends JeecgController> queryPrinters() { + return Result.OK(printServerEnvironmentService.buildPrinterQueryResult()); + } + + /** + * 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf + */ + @Operation(summary = "原料入场记录-准备原生打印数据") + @GetMapping(value = "/prepareNativePrint") + @RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit") + public Result> prepareNativePrint(@RequestParam(name = "id") String id) { + try { + MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id); + if (entry == null) { + return Result.error("未找到原料入场记录"); + } + PrintBizTemplateBind bind = + printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID); + if (bind == null) { + return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板"); + } + PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); + if (tpl == null) { + return Result.error("绑定的打印模板不存在"); + } + ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = objectMapper.valueToTree(entry); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + Map out = new HashMap<>(8); + out.put("entryId", entry.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()); + } + } + + @AutoLog(value = "原料入场记录-PDF后端打印") + @Operation(summary = "原料入场记录-PDF后端打印") + @PostMapping(value = "/printPdf") + @RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit") + public Result printPdf(@RequestBody Map body) { + String id = 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(id)) { + return Result.error("id 不能为空"); + } + MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id); + if (entry == null) { + return Result.error("未找到原料入场记录"); + } + String prefix = + StringUtils.isNotBlank(entry.getBarcode()) ? entry.getBarcode() : entry.getId(); + String fn = + StringUtils.isNotBlank(fileName) ? fileName : ("原料入场记录-" + prefix + ".pdf"); + return printServerPdfJobService.submitPdfBase64( + printerName, pdfBase64, fn, "RAW_ENTRY_" + prefix); + } + @RequiresPermissions("xslmes:mes_xsl_raw_material_entry:exportXls") @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, MesXslRawMaterialEntry mesXslRawMaterialEntry) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/bootstrap/PrintBizPermEntityWarmupRunner.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/bootstrap/PrintBizPermEntityWarmupRunner.java new file mode 100644 index 0000000..beee855 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/bootstrap/PrintBizPermEntityWarmupRunner.java @@ -0,0 +1,43 @@ +package org.jeecg.modules.print.bootstrap; + +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 项目启动后异步从数据库 {@code sys_permission} 读取真实 component / component_name,预热 + * {@code print_biz_perm_entity},不依赖人工白名单操作。关闭:{@code jeecg.print.biz-perm-warmup=false} + */ +@Slf4j +@Component +@Order(2000) +public class PrintBizPermEntityWarmupRunner implements ApplicationRunner { + + @Autowired private IPrintBizPermEntityService printBizPermEntityService; + + @Value("${jeecg.print.biz-perm-warmup:true}") + private boolean warmupEnabled; + + @Override + public void run(ApplicationArguments args) { + if (!warmupEnabled) { + log.info("打印菜单-实体映射预热已关闭(jeecg.print.biz-perm-warmup=false)"); + return; + } + CompletableFuture.runAsync( + () -> { + try { + log.info("打印菜单-实体映射:开始异步预热(来源 sys_permission 表)"); + printBizPermEntityService.warmupMappingsFromSysPermissionTable(); + } catch (Exception e) { + log.warn("打印菜单-实体映射预热失败(不影响系统启动)", e); + } + }); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java deleted file mode 100644 index 4cb3c06..0000000 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.jeecg.modules.print.catalog; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.jeecg.modules.print.vo.PrintBizFieldItemVO; -import org.jeecg.modules.print.vo.PrintBizTypeVO; - -/** - * 业务类型及可映射字段目录(与实体 JSON 字段名一致,camelCase)。 - * 新增业务:在此注册 bizCode 与字段列表。 - */ -public final class PrintBizTypeCatalog { - - private static final Map REGISTRY = new LinkedHashMap<>(); - - static { - registerRawMaterialCard(); - } - - private PrintBizTypeCatalog() {} - - private static void registerRawMaterialCard() { - PrintBizTypeVO vo = new PrintBizTypeVO(); - vo.setBizCode("MES_RAW_MATERIAL_CARD"); - vo.setBizName("原材料卡片"); - vo.setDescription("mes_xsl_raw_material_card / MesXslRawMaterialCard"); - List fields = new ArrayList<>(); - fields.add(f("id", "主键")); - fields.add(f("barcode", "条码")); - fields.add(f("splitDetailId", "拆码明细行ID")); - fields.add(f("batchNo", "批次号")); - fields.add(f("entryDate", "入场日期")); - fields.add(f("materialId", "物料ID")); - fields.add(f("materialName", "物料名称")); - fields.add(f("materialDesc", "物料描述")); - fields.add(f("supplierId", "供应商ID")); - fields.add(f("supplierName", "供应商名称")); - fields.add(f("manufacturerMaterialName", "厂家物料名称")); - fields.add(f("shelfLife", "保质期")); - fields.add(f("totalWeight", "总重")); - fields.add(f("remainingWeight", "剩余重量")); - fields.add(f("remainingQuantity", "剩余数量")); - fields.add(f("status", "状态(字典)")); - fields.add(f("testResult", "检测结果(字典)")); - fields.add(f("warehouseArea", "库区/库位")); - fields.add(f("unloadOperator", "卸货操作人")); - fields.add(f("priorityPickup", "优先出库")); - fields.add(f("createBy", "创建人")); - fields.add(f("createTime", "创建时间")); - fields.add(f("updateBy", "更新人")); - fields.add(f("updateTime", "更新时间")); - vo.setFields(Collections.unmodifiableList(fields)); - REGISTRY.put(vo.getBizCode(), vo); - } - - private static PrintBizFieldItemVO f(String key, String label) { - return new PrintBizFieldItemVO(key, label, ""); - } - - public static List listAll() { - return new ArrayList<>(REGISTRY.values()); - } - - public static PrintBizTypeVO getByCode(String bizCode) { - if (bizCode == null) { - return null; - } - return REGISTRY.get(bizCode.trim()); - } -} 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 2239253..231c0d3 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 @@ -10,6 +10,7 @@ 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 java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -20,13 +21,18 @@ 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.print.catalog.PrintBizTypeCatalog; import org.jeecg.modules.print.entity.PrintBizTemplateBind; import org.jeecg.modules.print.entity.PrintTemplate; +import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService; +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.util.PrintBizDataMappingUtil; +import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner; +import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector; import org.jeecg.modules.print.util.PrintNativeTemplateFieldExtractor; +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.springframework.beans.factory.annotation.Autowired; @@ -43,6 +49,8 @@ public class PrintBizTemplateBindController extends JeecgController> bizTypes() { - return Result.OK(PrintBizTypeCatalog.listAll()); + return Result.OK(printBizPermEntityService.listAllBizTypeVOs()); + } + + /** + * 「新增/编辑业务打印绑定」业务下拉:print_biz_perm_entity 中的映射,并按「打印业务白名单」过滤。 + * 白名单为空表示不过滤;非空则仅保留菜单 id 在白名单内的项。 + */ + @Operation(summary = "可选业务类型(白名单过滤后)") + @GetMapping("/bizTypesForBinding") + @RequiresPermissions("print:bizBind:list") + public Result> bizTypesForBinding() { + return Result.OK(printBizBindPermWhitelistService.listBizTypesForBinding()); + } + + @Operation(summary = "打印业务白名单:当前勾选的菜单 id(catalog 恒为空,避免全库反射超时)") + @GetMapping("/permWhitelist") + @RequiresPermissions("print:bizBind:whitelist") + public Result getPermWhitelist() { + PrintBizPermWhitelistVO vo = new PrintBizPermWhitelistVO(); + vo.setPermIds(printBizBindPermWhitelistService.listPermIds()); + vo.setCatalog(Collections.emptyList()); + return Result.OK(vo); + } + + @AutoLog(value = "业务打印绑定-保存打印业务白名单") + @Operation(summary = "保存打印业务白名单(按菜单 permission id)") + @PostMapping("/permWhitelist") + @RequiresPermissions("print:bizBind:whitelist") + public Result savePermWhitelist(@RequestBody PermWhitelistBody body) { + printBizBindPermWhitelistService.replacePermIds(body == null ? null : body.getPermIds()); + return Result.OK("保存成功"); } @Operation(summary = "解析原生模板中的占位字段(bindField)") @@ -131,6 +169,47 @@ public class PrintBizTemplateBindController extends JeecgController> detailSlots(@RequestParam(name = "bizCode") String bizCode) { + if (StringUtils.isBlank(bizCode)) { + return Result.error("bizCode 不能为空"); + } + PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim()); + if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) { + return Result.OK(Collections.emptyList()); + } + Class main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim()); + if (main == null) { + return Result.OK(Collections.emptyList()); + } + return Result.OK(PrintBizDetailPropertyScanner.listSlots(main)); + } + + @Operation(summary = "反射指定明细槽位元素类的字段(fieldKey 带「属性名.」前缀,用于与模板明细占位绑定)") + @GetMapping("/bizFieldsForDetailSlot") + @RequiresPermissions("print:bizBind:list") + public Result> bizFieldsForDetailSlot( + @RequestParam(name = "bizCode") String bizCode, + @RequestParam(name = "detailProperty") String detailProperty, + @RequestParam(name = "slotKind", defaultValue = "LIST") String slotKind) { + if (StringUtils.isAnyBlank(bizCode, detailProperty)) { + return Result.error("bizCode 与 detailProperty 不能为空"); + } + PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim()); + if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) { + return Result.OK(Collections.emptyList()); + } + Class main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim()); + if (main == null) { + return Result.OK(Collections.emptyList()); + } + return Result.OK( + PrintBizDetailPropertyScanner.listPrefixedDetailFields( + main, detailProperty.trim(), slotKind.trim())); + } + @Operation(summary = "按业务编码查询绑定(供打印调用)") @GetMapping("/queryByBizCode") @RequiresPermissions("print:bizBind:list") @@ -183,9 +262,12 @@ public class PrintBizTemplateBindController extends JeecgController permIds; + /** 完整打印业务目录(含 linkedPermissionId,便于对照菜单) */ + private java.util.List catalog; + } + + @Data + public static class PermWhitelistBody { + private java.util.List permIds; + } } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizBindPermWhitelist.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizBindPermWhitelist.java new file mode 100644 index 0000000..d8e6371 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizBindPermWhitelist.java @@ -0,0 +1,16 @@ +package org.jeecg.modules.print.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; +import lombok.Data; + +/** 业务打印绑定可选范围:白名单(sys_permission.id) */ +@Data +@TableName("print_biz_bind_perm_whitelist") +public class PrintBizBindPermWhitelist implements Serializable { + + @TableId(type = IdType.INPUT) + private String permId; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizPermEntity.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizPermEntity.java new file mode 100644 index 0000000..aad2917 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizPermEntity.java @@ -0,0 +1,24 @@ +package org.jeecg.modules.print.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +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 java.io.Serializable; +import lombok.Data; + +/** 打印业务:菜单权限与实体类映射(biz_code 使用 perm_id) */ +@Data +@TableName("print_biz_perm_entity") +public class PrintBizPermEntity implements Serializable { + + @TableId(value = "perm_id", type = IdType.INPUT) + private String permId; + + /** + * 实体类全限定名;为空表示占位。ALWAYS 保证 update 时带上 entity_class(含置 null),避免仅主键导致无 SET 子句。 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String entityClass; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizBindPermWhitelistMapper.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizBindPermWhitelistMapper.java new file mode 100644 index 0000000..e0a7fec --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizBindPermWhitelistMapper.java @@ -0,0 +1,7 @@ +package org.jeecg.modules.print.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist; + +/** 打印业务菜单白名单 */ +public interface PrintBizBindPermWhitelistMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizPermEntityMapper.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizPermEntityMapper.java new file mode 100644 index 0000000..eff3f4a --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizPermEntityMapper.java @@ -0,0 +1,7 @@ +package org.jeecg.modules.print.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.print.entity.PrintBizPermEntity; + +/** 菜单-实体映射 */ +public interface PrintBizPermEntityMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizBindPermWhitelistService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizBindPermWhitelistService.java new file mode 100644 index 0000000..d985977 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizBindPermWhitelistService.java @@ -0,0 +1,19 @@ +package org.jeecg.modules.print.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist; +import org.jeecg.modules.print.vo.PrintBizTypeVO; + +/** 打印业务与系统菜单白名单 */ +public interface IPrintBizBindPermWhitelistService extends IService { + + /** 当前白名单中的菜单 id(空集合表示未配置,放行全部目录中的打印业务) */ + List listPermIds(); + + /** 全量替换白名单 */ + void replacePermIds(List permIds); + + /** 「新增业务打印绑定」下拉里可用的业务类型(print_biz_perm_entity + 反射字段,受白名单过滤) */ + List listBizTypesForBinding(); +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizPermEntityService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizPermEntityService.java new file mode 100644 index 0000000..27e3ac5 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizPermEntityService.java @@ -0,0 +1,36 @@ +package org.jeecg.modules.print.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import org.jeecg.modules.print.entity.PrintBizPermEntity; +import org.jeecg.modules.print.vo.PrintBizTypeVO; + +/** 菜单与实体映射 + 反射生成业务类型 VO */ +public interface IPrintBizPermEntityService extends IService { + + PrintBizPermEntity getByPermId(String permId); + + /** 全部映射对应的业务类型(含反射字段);用于目录展示与白名单弹窗 catalog */ + List listAllBizTypeVOs(); + + /** + * 按白名单过滤:permIds 为空表示不过滤(返回全部映射);非空则仅保留 perm_id 在集合内的项。 + */ + List listBizTypeVOsFiltered(List whitelistPermIds); + + /** + * 解析单个菜单对应的业务类型:优先 print_biz_perm_entity,否则按菜单 component 推断实体类并反射字段。 + */ + PrintBizTypeVO resolveBizTypeVo(String permId); + + /** + * 将白名单勾选的菜单写入/补全 print_biz_perm_entity:已有可加载的 entity_class 不覆盖;否则按菜单 component 推断并插入或更新。 + */ + void upsertMappingsForWhitelist(List permIds); + + /** + * 启动预热:扫描数据库 sys_permission(menu_type=1 且 component 非空),用表中真实的 component / component_name 推断实体类并写入 + * print_biz_perm_entity(不覆盖已有可加载的 entity_class)。 + */ + void warmupMappingsFromSysPermissionTable(); +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizBindPermWhitelistServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizBindPermWhitelistServiceImpl.java new file mode 100644 index 0000000..86e0c8f --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizBindPermWhitelistServiceImpl.java @@ -0,0 +1,67 @@ +package org.jeecg.modules.print.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist; +import org.jeecg.modules.print.mapper.PrintBizBindPermWhitelistMapper; +import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.jeecg.modules.print.vo.PrintBizTypeVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class PrintBizBindPermWhitelistServiceImpl + extends ServiceImpl + implements IPrintBizBindPermWhitelistService { + + @Autowired private IPrintBizPermEntityService printBizPermEntityService; + + @Override + public List listPermIds() { + return list().stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void replacePermIds(List permIds) { + remove( + Wrappers.lambdaQuery() + .isNotNull(PrintBizBindPermWhitelist::getPermId)); + if (permIds == null || permIds.isEmpty()) { + return; + } + Set seen = new HashSet<>(); + List batch = new ArrayList<>(); + for (String raw : permIds) { + String id = StringUtils.trimToEmpty(raw); + if (id.isEmpty() || seen.contains(id)) { + continue; + } + seen.add(id); + PrintBizBindPermWhitelist row = new PrintBizBindPermWhitelist(); + row.setPermId(id); + batch.add(row); + } + if (!batch.isEmpty()) { + // 分段写入,避免单次 SQL 包过大 + saveBatch(batch, 500); + List savedIds = + batch.stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList()); + printBizPermEntityService.upsertMappingsForWhitelist(savedIds); + } + } + + @Override + public List listBizTypesForBinding() { + List whitelist = listPermIds(); + return printBizPermEntityService.listBizTypeVOsFiltered(whitelist); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizPermEntityServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizPermEntityServiceImpl.java new file mode 100644 index 0000000..60b745e --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizPermEntityServiceImpl.java @@ -0,0 +1,244 @@ +package org.jeecg.modules.print.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.entity.PrintBizPermEntity; +import org.jeecg.modules.print.mapper.PrintBizPermEntityMapper; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector; +import org.jeecg.modules.print.util.PrintBizMenuEntityInference; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.print.vo.PrintBizTypeVO; +import org.jeecg.modules.system.entity.SysPermission; +import org.jeecg.modules.system.service.ISysPermissionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class PrintBizPermEntityServiceImpl + extends ServiceImpl + implements IPrintBizPermEntityService { + + @Autowired private ISysPermissionService sysPermissionService; + + @Override + public PrintBizPermEntity getByPermId(String permId) { + if (StringUtils.isBlank(permId)) { + return null; + } + return getById(permId.trim()); + } + + @Override + public List listAllBizTypeVOs() { + List out = new ArrayList<>(); + // 仅使用 print_biz_perm_entity,避免全库扫菜单+反射导致接口超时;由「保存白名单」时 upsert 写入表 + for (PrintBizPermEntity row : list()) { + if (row == null || StringUtils.isBlank(row.getPermId())) { + continue; + } + PrintBizTypeVO vo = buildVoForPermId(row.getPermId()); + if (vo != null) { + out.add(vo); + } + } + return out; + } + + @Override + public List listBizTypeVOsFiltered(List whitelistPermIds) { + if (whitelistPermIds == null || whitelistPermIds.isEmpty()) { + return listAllBizTypeVOs(); + } + List out = new ArrayList<>(); + for (String raw : whitelistPermIds) { + String id = StringUtils.trimToEmpty(raw); + if (id.isEmpty()) { + continue; + } + PrintBizTypeVO vo = buildVoForPermId(id); + if (vo != null) { + out.add(vo); + } + } + return out; + } + + @Override + public PrintBizTypeVO resolveBizTypeVo(String permId) { + return buildVoForPermId(permId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void upsertMappingsForWhitelist(List permIds) { + if (permIds == null || permIds.isEmpty()) { + return; + } + List ids = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String raw : permIds) { + String id = StringUtils.trimToEmpty(raw); + if (id.isEmpty() || seen.contains(id)) { + continue; + } + seen.add(id); + ids.add(id); + } + if (ids.isEmpty()) { + return; + } + // 批量查询,避免勾选数百条时 N 次 getById 导致超时 + Map existingMap = new HashMap<>(ids.size()); + for (PrintBizPermEntity e : listByIds(ids)) { + if (e != null && StringUtils.isNotBlank(e.getPermId())) { + existingMap.put(e.getPermId(), e); + } + } + Map permMap = new HashMap<>(ids.size()); + for (SysPermission p : sysPermissionService.listByIds(ids)) { + if (p != null && StringUtils.isNotBlank(p.getId())) { + permMap.put(p.getId(), p); + } + } + List toSave = new ArrayList<>(); + for (String id : ids) { + PrintBizPermEntity existing = existingMap.get(id); + // 已有可加载的实体类配置则保留,不再覆盖 + if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) { + Class loaded = + PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim()); + if (loaded != null) { + continue; + } + } + SysPermission p = permMap.get(id); + String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p); + PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity(); + row.setPermId(id); + if (StringUtils.isNotBlank(inferred) + && PrintBizEntityFieldIntrospector.tryLoadClass(inferred) != null) { + row.setEntityClass(inferred); + } else { + // 勾选即落库(与勾选数量一致);无法推断或类不在 classpath 时占位 NULL,可手工 UPDATE + row.setEntityClass(null); + } + toSave.add(row); + } + if (!toSave.isEmpty()) { + saveOrUpdateBatch(toSave, 500); + } + } + + @Override + public void warmupMappingsFromSysPermissionTable() { + List menus = + sysPermissionService + .lambdaQuery() + .eq(SysPermission::getMenuType, 1) + .isNotNull(SysPermission::getComponent) + .list(); + if (menus == null || menus.isEmpty()) { + log.info("打印菜单-实体映射预热:无子菜单数据"); + return; + } + List ids = + menus.stream() + .map(SysPermission::getId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + Map existingMap = new HashMap<>(ids.size()); + for (PrintBizPermEntity e : listByIds(ids)) { + if (e != null && StringUtils.isNotBlank(e.getPermId())) { + existingMap.put(e.getPermId(), e); + } + } + List toSave = new ArrayList<>(); + for (SysPermission p : menus) { + if (p == null || StringUtils.isBlank(p.getId())) { + continue; + } + String comp = p.getComponent(); + if (StringUtils.isBlank(comp) + || comp.contains("layouts") + || comp.contains("RouteView") + || comp.contains("ParentView")) { + continue; + } + PrintBizPermEntity existing = existingMap.get(p.getId()); + if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) { + if (PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim()) != null) { + continue; + } + } + String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p); + if (StringUtils.isBlank(inferred) + || PrintBizEntityFieldIntrospector.tryLoadClass(inferred) == null) { + continue; + } + PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity(); + row.setPermId(p.getId()); + row.setEntityClass(inferred); + toSave.add(row); + } + if (toSave.isEmpty()) { + log.info("打印菜单-实体映射预热:无新增可解析项"); + return; + } + saveOrUpdateBatch(toSave, 500); + log.info("打印菜单-实体映射预热完成,本次写入/更新 {} 条(数据来自 sys_permission 表中的 component/component_name)", toSave.size()); + } + + /** + * 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。 + */ + private PrintBizTypeVO buildVoForPermId(String permId) { + if (StringUtils.isBlank(permId)) { + return null; + } + PrintBizPermEntity row = getById(permId.trim()); + String entityFqn = null; + if (row != null && StringUtils.isNotBlank(row.getEntityClass())) { + entityFqn = row.getEntityClass().trim(); + } else { + SysPermission p = sysPermissionService.getById(permId.trim()); + entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p); + } + if (StringUtils.isBlank(entityFqn)) { + return null; + } + Class clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn); + List fields = + clazz == null ? new ArrayList<>() : PrintBizEntityFieldIntrospector.listFields(clazz); + if (clazz == null) { + // 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导 + return null; + } + PrintBizTypeVO vo = new PrintBizTypeVO(); + vo.setBizCode(permId.trim()); + vo.setLinkedPermissionId(permId.trim()); + vo.setBizName(resolveMenuName(permId.trim())); + vo.setDescription(entityFqn); + vo.setFields(fields); + return vo; + } + + private String resolveMenuName(String permId) { + SysPermission p = sysPermissionService.getById(permId); + if (p != null && StringUtils.isNotBlank(p.getName())) { + return p.getName(); + } + return permId; + } +} 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 7bee392..4b94e59 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 @@ -43,11 +43,33 @@ public final class PrintBizDataMappingUtil { if (cur == null || p.isEmpty()) { return null; } - cur = cur.get(p); + if (cur.isArray()) { + if (isNonNegativeIntString(p)) { + cur = cur.get(Integer.parseInt(p)); + } else { + JsonNode first = cur.size() > 0 ? cur.get(0) : null; + cur = first != null ? first.get(p) : null; + } + } else { + cur = cur.get(p); + } } return cur; } + /** 全数字段按数组下标解析,否则在 JSON 数组上取首行再取属性(如 lines.qty 表示第一行明细的 qty) */ + private static boolean isNonNegativeIntString(String p) { + if (p == null || p.isEmpty()) { + return false; + } + for (int i = 0; i < p.length(); i++) { + if (!Character.isDigit(p.charAt(i))) { + return false; + } + } + return true; + } + private static void setPath(ObjectNode target, String path, JsonNode value) { if (target == null || StringUtils.isBlank(path)) { return; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDetailPropertyScanner.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDetailPropertyScanner.java new file mode 100644 index 0000000..8546759 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDetailPropertyScanner.java @@ -0,0 +1,185 @@ +package org.jeecg.modules.print.util; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; + +/** + * 扫描主实体类上可作为「明细」的来源属性:{@code List<Entity>} / {@code Set<Entity>} / 数组 / 嵌套业务对象。 + */ +public final class PrintBizDetailPropertyScanner { + + private PrintBizDetailPropertyScanner() {} + + /** + * 根据已选明细槽位解析元素类型:LIST 取集合元素类或数组组件类型;OBJECT 取嵌套属性类型。 + * + * @param slotKind LIST 或 OBJECT(与 {@link PrintBizDetailSlotVO#getSlotKind()} 一致) + */ + public static Class resolveItemClassForSlot( + Class mainClazz, String propertyName, String slotKind) { + if (mainClazz == null || StringUtils.isBlank(propertyName)) { + return null; + } + Field f = findDeclaredField(mainClazz, propertyName.trim()); + if (f == null) { + return null; + } + if ("OBJECT".equalsIgnoreCase(StringUtils.trimToEmpty(slotKind))) { + Class t = f.getType(); + return isLikelyBizBean(t) ? t : null; + } + Class elem = resolveCollectionElementClass(f); + if (elem != null) { + return elem; + } + if (f.getType().isArray()) { + Class comp = f.getType().getComponentType(); + return isSimpleOrJdkValueType(comp) ? null : comp; + } + return null; + } + + private static Field findDeclaredField(Class start, String name) { + Class c = start; + while (c != null && c != Object.class) { + try { + return c.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + c = c.getSuperclass(); + } + } + return null; + } + + /** 明细元素类型反射字段,fieldKey 已带「属性名.」前缀,便于与模板明细占位映射 */ + public static List listPrefixedDetailFields( + Class mainClazz, String propertyName, String slotKind) { + Class item = resolveItemClassForSlot(mainClazz, propertyName, slotKind); + if (item == null) { + return Collections.emptyList(); + } + List raw = PrintBizEntityFieldIntrospector.listFields(item); + String prefix = propertyName.trim(); + List out = new ArrayList<>(raw.size()); + for (PrintBizFieldItemVO x : raw) { + String path = prefix + "." + x.getFieldKey(); + String label = "明细「" + prefix + "」→ " + x.getLabel(); + out.add(new PrintBizFieldItemVO(path, label, x.getDescription())); + } + return out; + } + + public static List listSlots(Class mainClazz) { + Map ordered = new LinkedHashMap<>(); + Class c = mainClazz; + while (c != null && c != Object.class) { + for (Field f : c.getDeclaredFields()) { + int mod = f.getModifiers(); + if (Modifier.isStatic(mod) || f.isSynthetic()) { + continue; + } + String name = f.getName(); + if ("serialVersionUID".equals(name)) { + continue; + } + Class elem = resolveCollectionElementClass(f); + if (elem != null && isLikelyBizBean(elem)) { + ordered.putIfAbsent( + name, + new PrintBizDetailSlotVO(name, elem.getName(), "LIST", resolveFieldLabel(f))); + continue; + } + Class ft = f.getType(); + if (ft.isArray() && !isSimpleOrJdkValueType(ft.getComponentType())) { + Class comp = ft.getComponentType(); + ordered.putIfAbsent( + name, new PrintBizDetailSlotVO(name, comp.getName(), "LIST", resolveFieldLabel(f))); + continue; + } + if (!ft.isPrimitive() + && !ft.getName().startsWith("java.lang") + && !Number.class.isAssignableFrom(ft) + && !java.util.Date.class.isAssignableFrom(ft) + && !ft.getName().startsWith("java.time") + && !Map.class.isAssignableFrom(ft) + && !Collection.class.isAssignableFrom(ft) + && isLikelyBizBean(ft)) { + ordered.putIfAbsent( + name, new PrintBizDetailSlotVO(name, ft.getName(), "OBJECT", resolveFieldLabel(f))); + } + } + c = c.getSuperclass(); + } + return new ArrayList<>(ordered.values()); + } + + /** 解析 List<T> / Set<T> 的元素类型 T */ + private static Class resolveCollectionElementClass(Field f) { + Type gt = f.getGenericType(); + if (!(gt instanceof ParameterizedType)) { + return null; + } + ParameterizedType pt = (ParameterizedType) gt; + Type raw = pt.getRawType(); + if (!(raw instanceof Class rc) || !Collection.class.isAssignableFrom(rc)) { + return null; + } + Type[] args = pt.getActualTypeArguments(); + if (args.length != 1) { + return null; + } + Type arg0 = args[0]; + if (arg0 instanceof Class) { + Class ac = (Class) arg0; + return isSimpleOrJdkValueType(ac) ? null : ac; + } + return null; + } + + private static boolean isSimpleOrJdkValueType(Class cl) { + if (cl == null || cl.isPrimitive()) { + return true; + } + if (cl.isEnum()) { + return true; + } + String n = cl.getName(); + if (n.startsWith("java.lang") || n.startsWith("java.time")) { + return true; + } + if (Number.class.isAssignableFrom(cl) || java.util.Date.class.isAssignableFrom(cl)) { + return true; + } + return false; + } + + /** 非 JDK 简单值、非集合/Map 的自定义类型视为可映射明细实体 */ + private static boolean isLikelyBizBean(Class cl) { + return cl != null && !cl.isEnum() && !isSimpleOrJdkValueType(cl) && !Map.class.isAssignableFrom(cl); + } + + private static String resolveFieldLabel(Field f) { + Schema schema = f.getAnnotation(Schema.class); + if (schema != null && StringUtils.isNotBlank(schema.description())) { + return schema.description().trim(); + } + Excel excel = f.getAnnotation(Excel.class); + if (excel != null && StringUtils.isNotBlank(excel.name())) { + return excel.name().trim(); + } + return f.getName(); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizEntityFieldIntrospector.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizEntityFieldIntrospector.java new file mode 100644 index 0000000..a365923 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizEntityFieldIntrospector.java @@ -0,0 +1,64 @@ +package org.jeecg.modules.print.util; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。 + */ +public final class PrintBizEntityFieldIntrospector { + + private PrintBizEntityFieldIntrospector() {} + + public static List listFields(Class clazz) { + Map ordered = new LinkedHashMap<>(); + Class c = clazz; + while (c != null && c != Object.class) { + for (Field f : c.getDeclaredFields()) { + int mod = f.getModifiers(); + if (Modifier.isStatic(mod) || f.isSynthetic()) { + continue; + } + String name = f.getName(); + if ("serialVersionUID".equals(name)) { + continue; + } + ordered.putIfAbsent(name, new PrintBizFieldItemVO(name, resolveLabel(f), "")); + } + c = c.getSuperclass(); + } + return new ArrayList<>(ordered.values()); + } + + private static String resolveLabel(Field f) { + Schema schema = f.getAnnotation(Schema.class); + if (schema != null && StringUtils.isNotBlank(schema.description())) { + return schema.description().trim(); + } + Excel excel = f.getAnnotation(Excel.class); + if (excel != null && StringUtils.isNotBlank(excel.name())) { + return excel.name().trim(); + } + return f.getName(); + } + + /** 按全限定类名加载 Class,失败返回 null */ + public static Class tryLoadClass(String entityClassFqn) { + if (StringUtils.isBlank(entityClassFqn)) { + return null; + } + try { + return Class.forName(entityClassFqn.trim()); + } catch (Throwable e) { + return null; + } + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizMenuEntityInference.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizMenuEntityInference.java new file mode 100644 index 0000000..52d2e63 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizMenuEntityInference.java @@ -0,0 +1,94 @@ +package org.jeecg.modules.print.util; + +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.system.entity.SysPermission; + +/** + * 根据菜单 component / componentName 推断实体类全名。
+ * 典型:
+ * - xslmes/mesXslWarehouseArea/MesXslWarehouseAreaList
+ * - mes/materialinfo/index + componentName=MesMaterialList → org.jeecg.modules.mes.material.entity.MesMaterial + */ +public final class PrintBizMenuEntityInference { + + /** MES 物料等菜单实体默认落在该包下(与工程 mes.material.entity 一致) */ + private static final String MES_MATERIAL_ENTITY_PKG = "org.jeecg.modules.mes.material.entity."; + + private PrintBizMenuEntityInference() {} + + /** + * @return 实体类全限定名;无法推断时返回 null(按钮、目录等) + */ + public static String inferEntityClassFqn(SysPermission permission) { + if (permission == null) { + return null; + } + if (Objects.equals(permission.getMenuType(), 2)) { + return null; + } + String comp = permission.getComponent(); + if (StringUtils.isBlank(comp)) { + return null; + } + String c = comp.trim(); + if (c.contains("layouts") + || c.contains("RouteView") + || c.contains("ParentView") + || c.startsWith("http")) { + return null; + } + + // Jeecg Vue:component 常为 mes/xxx/index,实体类名在 component_name(如 MesMaterialList) + if (c.startsWith("mes/") && c.endsWith("/index")) { + String fromMesIndex = + tryInferMesMaterialModuleFromComponentName(permission.getComponentName()); + if (StringUtils.isNotBlank(fromMesIndex)) { + return fromMesIndex; + } + } + + String[] segs = c.split("/"); + if (segs.length < 2) { + return null; + } + String last = segs[segs.length - 1]; + if (StringUtils.isBlank(last)) { + return null; + } + String simple = + last.endsWith("List") ? last.substring(0, last.length() - 4) : last; + if (simple.isEmpty()) { + return null; + } + String module = segs[0]; + if (StringUtils.isBlank(module)) { + return null; + } + String trial = "org.jeecg.modules." + module + ".entity." + simple; + if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) { + return trial; + } + return null; + } + + /** mes 模块下 index 路由:用 componentName(MesXxxList)推断 mes.material.entity.MesXxx */ + private static String tryInferMesMaterialModuleFromComponentName(String componentName) { + if (StringUtils.isBlank(componentName)) { + return null; + } + String cn = componentName.trim(); + if (!cn.endsWith("List")) { + return null; + } + String simple = cn.substring(0, cn.length() - 4); + if (simple.isEmpty()) { + return null; + } + String trial = MES_MATERIAL_ENTITY_PKG + simple; + if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) { + return trial; + } + return null; + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizDetailSlotVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizDetailSlotVO.java new file mode 100644 index 0000000..8de95ab --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizDetailSlotVO.java @@ -0,0 +1,27 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** 主实体上可作为「明细数据来源」的属性(集合元素类型或嵌套对象类型) */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "业务实体明细槽位") +public class PrintBizDetailSlotVO implements Serializable { + + @Schema(description = "Java 属性名(JSON 路径前缀,如 lines、headerExt)") + private String propertyName; + + @Schema(description = "明细元素类型全限定名") + private String itemEntityClassFqn; + + @Schema(description = "LIST=集合明细一行映射;OBJECT=嵌套对象字段映射") + private String slotKind; + + @Schema(description = "展示名") + private String label; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java index 852f9ff..9804e3c 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java @@ -8,7 +8,7 @@ import lombok.Data; @Data @Schema(description = "可配置的业务类型") public class PrintBizTypeVO implements Serializable { - @Schema(description = "业务编码") + @Schema(description = "业务编码(与菜单权限 id、绑定表 biz_code 一致)") private String bizCode; @Schema(description = "业务名称") @@ -17,6 +17,13 @@ public class PrintBizTypeVO implements Serializable { @Schema(description = "说明") private String description; + /** + * 与「系统菜单」中的菜单主键(sys_permission.id)对应;用于「打印业务白名单」按菜单勾选。 + * 未设置表示未挂菜单,白名单生效时仍可出现于下拉(避免仅后端注册、尚未配菜单的业务被误过滤)。 + */ + @Schema(description = "关联菜单权限ID(sys_permission.id)") + private String linkedPermissionId; + @Schema(description = "业务侧可用字段目录") private List fields; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_51__print_biz_bind_perm_whitelist.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_51__print_biz_bind_perm_whitelist.sql new file mode 100644 index 0000000..1e7c40e --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_51__print_biz_bind_perm_whitelist.sql @@ -0,0 +1,11 @@ +-- 打印业务可选范围:白名单(勾选 sys_permission.id,对应 PrintBizTypeCatalog 中 linkedPermissionId) +CREATE TABLE IF NOT EXISTS `print_biz_bind_perm_whitelist` ( + `perm_id` varchar(36) NOT NULL COMMENT 'sys_permission 主键(菜单/功能)', + PRIMARY KEY (`perm_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务打印绑定-可选业务白名单(权限菜单)'; + +-- 按钮:打印业务白名单 +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 '1900000000000000125', '1900000000000000120', '打印业务白名单', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:whitelist', '1', 5.00, 0, NULL, 1, 0, 0, 0, '配置哪些菜单关联的打印业务可出现在「新增业务打印绑定」中', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000125'); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_52__print_biz_perm_entity.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_52__print_biz_perm_entity.sql new file mode 100644 index 0000000..649e40d --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_52__print_biz_perm_entity.sql @@ -0,0 +1,17 @@ +-- 菜单权限与打印业务实体类映射(biz_code / 绑定表业务编码 = perm_id) +CREATE TABLE IF NOT EXISTS `print_biz_perm_entity` ( + `perm_id` varchar(36) NOT NULL COMMENT 'sys_permission.id', + `entity_class` varchar(512) NOT NULL COMMENT '实体类全限定名(用于反射字段)', + PRIMARY KEY (`perm_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印业务-菜单与实体映射'; + +-- 原材料卡片菜单 -> MesXslRawMaterialCard +INSERT INTO `print_biz_perm_entity` (`perm_id`, `entity_class`) +SELECT '1900000000000000540', 'org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `print_biz_perm_entity` WHERE `perm_id` = '1900000000000000540'); + +-- 历史绑定:业务编码由语义码改为菜单 id(与白名单、映射表一致) +UPDATE `print_biz_template_bind` +SET `biz_code` = '1900000000000000540' +WHERE `biz_code` = 'MES_RAW_MATERIAL_CARD'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_53__print_biz_perm_entity_nullable_entity.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_53__print_biz_perm_entity_nullable_entity.sql new file mode 100644 index 0000000..ef93f7c --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_53__print_biz_perm_entity_nullable_entity.sql @@ -0,0 +1,3 @@ +-- 允许 entity_class 为空:白名单勾选的菜单优先落库占位,无法按 component 推断时再手工补全 +ALTER TABLE `print_biz_perm_entity` + MODIFY COLUMN `entity_class` varchar(512) NULL COMMENT '实体类全限定名;为空表示仅勾选占位,需手工配置或菜单不符合推断规则'; diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts index 056f26e..975f348 100644 --- a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts @@ -6,8 +6,12 @@ enum Api { edit = '/print/bizTemplateBind/edit', deleteOne = '/print/bizTemplateBind/delete', bizTypes = '/print/bizTemplateBind/bizTypes', + bizTypesForBinding = '/print/bizTemplateBind/bizTypesForBinding', + permWhitelist = '/print/bizTemplateBind/permWhitelist', parseTemplateFields = '/print/bizTemplateBind/parseTemplateFields', previewMappedData = '/print/bizTemplateBind/previewMappedData', + detailSlots = '/print/bizTemplateBind/detailSlots', + bizFieldsForDetailSlot = '/print/bizTemplateBind/bizFieldsForDetailSlot', } export const list = (params) => defHttp.get({ url: Api.list, params }); @@ -18,6 +22,13 @@ export const deleteOne = (params, handleSuccess?) => defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess?.()); export const bizTypes = () => defHttp.get({ url: Api.bizTypes }); +/** 新增/编辑绑定时可选业务(受打印业务白名单过滤) */ +export const bizTypesForBinding = () => defHttp.get({ url: Api.bizTypesForBinding }); +/** 白名单:已勾选菜单 id + 完整业务目录 */ +export const getPermWhitelist = () => defHttp.get({ url: Api.permWhitelist }); +/** 勾选菜单多时后端需批量 upsert,默认 10s 易超时 */ +export const savePermWhitelist = (data: { permIds: string[] }) => + defHttp.post({ url: Api.permWhitelist, data, timeout: 3 * 60 * 1000 }); export const parseTemplateFields = (templateId: string) => defHttp.get({ url: Api.parseTemplateFields, @@ -27,3 +38,21 @@ export const parseTemplateFields = (templateId: string) => /** 预览映射后的打印数据 */ export const previewMappedData = (data: { bizCode: string; bizDataJson: Record }) => defHttp.post({ url: Api.previewMappedData, data }); + +/** 主实体上可作为明细的数据属性(List/数组/嵌套对象) */ +export const detailSlots = (bizCode: string) => + defHttp.get<{ propertyName: string; itemEntityClassFqn: string; slotKind: string; label: string }[]>({ + url: Api.detailSlots, + params: { bizCode }, + }); + +/** 反射明细元素类字段,fieldKey 已带「属性名.」前缀 */ +export const bizFieldsForDetailSlot = (params: { + bizCode: string; + detailProperty: string; + slotKind?: string; +}) => + defHttp.get<{ fieldKey: string; label: string; description?: string }[]>({ + url: Api.bizFieldsForDetailSlot, + params, + }); diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue index 2a5f2a3..919a24b 100644 --- a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue @@ -2,7 +2,12 @@
+ +