diff --git a/jeecg-boot/db/mes-xsl-biz-entity-field-detail-alter-slot-columns.sql b/jeecg-boot/db/mes-xsl-biz-entity-field-detail-alter-slot-columns.sql new file mode 100644 index 00000000..7d716f73 --- /dev/null +++ b/jeecg-boot/db/mes-xsl-biz-entity-field-detail-alter-slot-columns.sql @@ -0,0 +1,28 @@ +-- 升级脚本:仅为缺失的列执行 ADD,可重复执行(不会因「列已存在」报错) +-- 适用:旧库 mes_xsl_biz_entity_field_detail 缺少 detail_property_name / detail_slot_kind + +SET NAMES utf8mb4; + +SELECT COUNT(*) INTO @jeecg_chk_dpn FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_biz_entity_field_detail' + AND COLUMN_NAME = 'detail_property_name'; + +SET @jeecg_sql_dpn := IF(@jeecg_chk_dpn = 0, + 'ALTER TABLE mes_xsl_biz_entity_field_detail ADD COLUMN detail_property_name varchar(128) DEFAULT NULL COMMENT ''主实体明细属性名(与打印绑定 detailProperty 一致)'' AFTER profile_id', + 'SELECT ''detail_property_name 已存在,跳过'' AS msg'); +PREPARE jeecg_stmt_dpn FROM @jeecg_sql_dpn; +EXECUTE jeecg_stmt_dpn; +DEALLOCATE PREPARE jeecg_stmt_dpn; + +SELECT COUNT(*) INTO @jeecg_chk_dsk FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_biz_entity_field_detail' + AND COLUMN_NAME = 'detail_slot_kind'; + +SET @jeecg_sql_dsk := IF(@jeecg_chk_dsk = 0, + 'ALTER TABLE mes_xsl_biz_entity_field_detail ADD COLUMN detail_slot_kind varchar(16) DEFAULT NULL COMMENT ''LIST 或 OBJECT'' AFTER detail_property_name', + 'SELECT ''detail_slot_kind 已存在,跳过'' AS msg'); +PREPARE jeecg_stmt_dsk FROM @jeecg_sql_dsk; +EXECUTE jeecg_stmt_dsk; +DEALLOCATE PREPARE jeecg_stmt_dsk; diff --git a/jeecg-boot/db/mes-xsl-biz-entity-field-profile.sql b/jeecg-boot/db/mes-xsl-biz-entity-field-profile.sql new file mode 100644 index 00000000..816960d2 --- /dev/null +++ b/jeecg-boot/db/mes-xsl-biz-entity-field-profile.sql @@ -0,0 +1,38 @@ +-- 业务实体字段配置(主表 + 明细:每条明细对应一类「明细表」及其字段列表) +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `mes_xsl_biz_entity_field_profile` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `business_name` varchar(200) NOT NULL COMMENT '业务名称', + `business_code` varchar(64) NOT NULL COMMENT '业务编码(菜单 permission id,与打印 biz_code 一致,唯一)', + `entity_class_name` varchar(512) DEFAULT NULL COMMENT '主实体 Java 全限定类名', + `main_fields_json` text COMMENT '主表实体字段列表(JSON 数组,元素可为字符串字段名或含 name/comment/javaType 的对象)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `tenant_id` int DEFAULT NULL COMMENT '租户ID', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_mxbefp_bcode` (`business_code`), + KEY `idx_mxbefp_tenant` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES业务实体字段配置-主表'; + +CREATE TABLE IF NOT EXISTS `mes_xsl_biz_entity_field_detail` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `profile_id` varchar(32) NOT NULL COMMENT '主表ID', + `detail_property_name` varchar(128) DEFAULT NULL COMMENT '主实体明细属性名(与打印绑定 detailProperty 一致)', + `detail_slot_kind` varchar(16) DEFAULT NULL COMMENT 'LIST 或 OBJECT', + `detail_name` varchar(200) DEFAULT NULL COMMENT '明细展示名称', + `detail_entity_class_name` varchar(512) DEFAULT NULL COMMENT '明细实体 Java 全限定类名', + `detail_fields_json` text COMMENT '明细表字段列表(JSON 数组,规则同 main_fields_json)', + `sort_no` int DEFAULT NULL COMMENT '排序号', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_mxbefd_profile` (`profile_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES业务实体字段配置-明细表字段清单'; + +-- ─── 旧表缺列时执行(见 db/mes-xsl-biz-entity-field-detail-alter-slot-columns.sql 或 Flyway V3.9.2_55)─── diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/bootstrap/BizEntityFieldCatalogSyncRunner.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/bootstrap/BizEntityFieldCatalogSyncRunner.java new file mode 100644 index 00000000..f11b61a2 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/bootstrap/BizEntityFieldCatalogSyncRunner.java @@ -0,0 +1,164 @@ +package org.jeecg.modules.xslmes.bootstrap; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.entity.PrintBizPermEntity; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner; +import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector; +import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.system.entity.SysPermission; +import org.jeecg.modules.system.service.ISysPermissionService; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail; +import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService; +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 print_biz_perm_entity} 中已配置实体类的业务,反射主表与明细槽位字段并写入 {@code mes_xsl_biz_entity_field_*}, + * 供「业务打印绑定」弹窗读取(避免运行时频繁反射)。 + * + *

关闭:{@code jeecg.print.biz-entity-field-catalog-sync=false} + * + *

建议在 {@link org.jeecg.modules.print.bootstrap.PrintBizPermEntityWarmupRunner}(Order 2000)之后执行。 + */ +@Slf4j +@Component +@Order(2100) +public class BizEntityFieldCatalogSyncRunner implements ApplicationRunner { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Resource + private IPrintBizPermEntityService printBizPermEntityService; + + @Resource + private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService; + + @Resource + private ISysPermissionService sysPermissionService; + + @Value("${jeecg.print.biz-entity-field-catalog-sync:true}") + private boolean syncEnabled; + + /** 延迟执行秒数,便于晚于「打印菜单实体映射预热」写完 print_biz_perm_entity */ + @Value("${jeecg.print.biz-entity-field-catalog-sync-delay-seconds:10}") + private int catalogSyncDelaySeconds; + + @Override + public void run(ApplicationArguments args) { + if (!syncEnabled) { + log.info("业务实体字段缓存同步已关闭(jeecg.print.biz-entity-field-catalog-sync=false)"); + return; + } + log.info( + "业务实体字段缓存:将在 {} 秒后异步同步(来源 print_biz_perm_entity → mes_xsl_biz_entity_field_*)", + Math.max(0, catalogSyncDelaySeconds)); + CompletableFuture.runAsync( + this::doSync, + CompletableFuture.delayedExecutor( + Math.max(0, catalogSyncDelaySeconds), + TimeUnit.SECONDS, + ForkJoinPool.commonPool())); + } + + private void doSync() { + try { + log.info("业务实体字段缓存:开始异步同步(来源 print_biz_perm_entity)"); + List rows = printBizPermEntityService.list(); + if (rows == null || rows.isEmpty()) { + log.info("业务实体字段缓存:print_biz_perm_entity 无数据,跳过"); + return; + } + int ok = 0; + int skip = 0; + for (PrintBizPermEntity row : rows) { + if (row == null || StringUtils.isBlank(row.getPermId())) { + skip++; + continue; + } + String permId = row.getPermId().trim(); + String entityFqn = StringUtils.trimToNull(row.getEntityClass()); + if (entityFqn == null) { + skip++; + continue; + } + Class clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn); + if (clazz == null) { + skip++; + continue; + } + try { + syncOne(permId, clazz, entityFqn); + ok++; + } catch (Exception ex) { + log.warn("业务实体字段缓存同步失败 permId={} entity={}", permId, entityFqn, ex); + skip++; + } + } + log.info("业务实体字段缓存同步完成:成功 {} 条,跳过 {} 条", ok, skip); + } catch (Exception e) { + log.warn("业务实体字段缓存同步异常(不影响系统启动)", e); + } + } + + private void syncOne(String permId, Class clazz, String entityFqn) throws Exception { + List mainFields = PrintBizEntityFieldIntrospector.listFields(clazz); + String mainJson = OBJECT_MAPPER.writeValueAsString(mainFields); + + List slots = PrintBizDetailPropertyScanner.listSlots(clazz); + List detailRows = new ArrayList<>(); + int sort = 0; + Date now = new Date(); + for (PrintBizDetailSlotVO slot : slots) { + Class itemClazz = + PrintBizDetailPropertyScanner.resolveItemClassForSlot( + clazz, slot.getPropertyName(), slot.getSlotKind()); + if (itemClazz == null) { + continue; + } + List itemFields = PrintBizEntityFieldIntrospector.listFields(itemClazz); + MesXslBizEntityFieldDetail d = new MesXslBizEntityFieldDetail(); + d.setDetailPropertyName(slot.getPropertyName()); + d.setDetailSlotKind(slot.getSlotKind()); + d.setDetailName(slot.getLabel()); + d.setDetailEntityClassName(fqn(itemClazz)); + d.setDetailFieldsJson(OBJECT_MAPPER.writeValueAsString(itemFields)); + d.setSortNo(sort++); + d.setCreateTime(now); + d.setUpdateTime(now); + detailRows.add(d); + } + + String bizName = resolveMenuName(permId); + bizEntityFieldProfileService.upsertScannedProfile(permId, bizName, entityFqn, mainJson, detailRows); + } + + private static String fqn(Class c) { + if (c == null) { + return null; + } + String cn = c.getCanonicalName(); + return cn != null ? cn : c.getName(); + } + + private String resolveMenuName(String permId) { + SysPermission p = sysPermissionService.getById(permId); + if (p != null && StringUtils.isNotBlank(p.getName())) { + return p.getName().trim(); + } + return permId; + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslBizEntityFieldProfileController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslBizEntityFieldProfileController.java new file mode 100644 index 00000000..4fbed799 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslBizEntityFieldProfileController.java @@ -0,0 +1,128 @@ +package org.jeecg.modules.xslmes.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 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 lombok.extern.slf4j.Slf4j; +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.common.util.oConvertUtils; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile; +import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +/** + * 业务实体字段配置:主表存业务名称与主实体字段 JSON;子表存各明细表的字段 JSON。 + */ +@Tag(name = "业务实体字段配置") +@RestController +@RequestMapping("/xslmes/mesXslBizEntityFieldProfile") +@Slf4j +public class MesXslBizEntityFieldProfileController extends JeecgController { + + @Autowired + private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService; + + @Operation(summary = "分页列表(不含明细,减轻负载)") + @GetMapping(value = "/list") + public Result> queryPageList( + MesXslBizEntityFieldProfile query, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = bizEntityFieldProfileService.page(page, qw); + return Result.OK(pageList); + } + + @AutoLog(value = "业务实体字段配置-添加") + @Operation(summary = "添加(请求体可含 detailList)") + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:add") + @PostMapping(value = "/add") + public Result add(@RequestBody MesXslBizEntityFieldProfile entity) { + if (oConvertUtils.isEmpty(entity.getBusinessName())) { + return Result.error("业务名称不能为空"); + } + if (oConvertUtils.isEmpty(entity.getBusinessCode())) { + return Result.error("业务编码不能为空(建议填写菜单 permission id)"); + } + bizEntityFieldProfileService.saveWithDetails(entity); + return Result.OK("添加成功"); + } + + @AutoLog(value = "业务实体字段配置-编辑") + @Operation(summary = "编辑(明细全量替换)") + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody MesXslBizEntityFieldProfile entity) { + if (oConvertUtils.isEmpty(entity.getId())) { + return Result.error("主键不能为空"); + } + if (oConvertUtils.isEmpty(entity.getBusinessName())) { + return Result.error("业务名称不能为空"); + } + if (oConvertUtils.isEmpty(entity.getBusinessCode())) { + return Result.error("业务编码不能为空(建议填写菜单 permission id)"); + } + bizEntityFieldProfileService.updateWithDetails(entity); + return Result.OK("编辑成功"); + } + + @AutoLog(value = "业务实体字段配置-删除") + @Operation(summary = "删除") + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + bizEntityFieldProfileService.removeWithDetails(id); + return Result.OK("删除成功"); + } + + @AutoLog(value = "业务实体字段配置-批量删除") + @Operation(summary = "批量删除") + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + bizEntityFieldProfileService.removeBatchWithDetails(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功"); + } + + @Operation(summary = "按 id 查询(含 detailList)") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + MesXslBizEntityFieldProfile entity = bizEntityFieldProfileService.getByIdWithDetails(id); + if (entity == null) { + return Result.error("未找到对应数据"); + } + return Result.OK(entity); + } + + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, MesXslBizEntityFieldProfile query) { + return super.exportXls(request, query, MesXslBizEntityFieldProfile.class, "业务实体字段配置"); + } + + @RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, MesXslBizEntityFieldProfile.class); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldDetail.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldDetail.java new file mode 100644 index 00000000..2c70743e --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldDetail.java @@ -0,0 +1,68 @@ +package org.jeecg.modules.xslmes.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +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 lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * 业务实体字段配置子表:一类明细表对应一行,字段清单存 JSON。 + */ +@Data +@TableName("mes_xsl_biz_entity_field_detail") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "业务实体字段配置-明细表字段清单") +public class MesXslBizEntityFieldDetail implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + @Schema(description = "主表ID") + private String profileId; + + @Schema(description = "主实体上明细属性名(与打印绑定 detailProperty 一致,如 lines)") + private String detailPropertyName; + + @Schema(description = "槽位类型:LIST 或 OBJECT") + private String detailSlotKind; + + @Schema(description = "明细展示名称") + private String detailName; + + @Schema(description = "明细实体 Java 全限定类名") + private String detailEntityClassName; + + /** 明细表字段列表 JSON */ + @Schema(description = "明细表字段列表(JSON 数组)") + private String detailFieldsJson; + + @Schema(description = "排序号") + private Integer sortNo; + + @Schema(description = "创建人") + private String createBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "更新人") + private String updateBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldProfile.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldProfile.java new file mode 100644 index 00000000..c3bf52c7 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslBizEntityFieldProfile.java @@ -0,0 +1,72 @@ +package org.jeecg.modules.xslmes.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; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * 业务实体字段配置主表:业务名称、主实体类名、主表字段 JSON;明细通过 detailList 关联子表。 + */ +@Data +@TableName("mes_xsl_biz_entity_field_profile") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "业务实体字段配置主表") +public class MesXslBizEntityFieldProfile implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + @Schema(description = "业务名称") + private String businessName; + + @Schema(description = "业务编码(菜单 permission id,与业务打印绑定 biz_code、print_biz_perm_entity.perm_id 一致)") + private String businessCode; + + @Schema(description = "主实体 Java 全限定类名") + private String entityClassName; + + /** 主表实体字段列表 JSON */ + @Schema(description = "主表实体字段列表(JSON 数组)") + private String mainFieldsJson; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "创建人") + private String createBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "更新人") + private String updateBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; + + /** 各明细表对应的字段清单(不落主表) */ + @TableField(exist = false) + @Schema(description = "明细表字段配置列表") + private List detailList; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldDetailMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldDetailMapper.java new file mode 100644 index 00000000..e05176ec --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldDetailMapper.java @@ -0,0 +1,8 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail; + +@Mapper +public interface MesXslBizEntityFieldDetailMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldProfileMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldProfileMapper.java new file mode 100644 index 00000000..d1a73091 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslBizEntityFieldProfileMapper.java @@ -0,0 +1,8 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile; + +@Mapper +public interface MesXslBizEntityFieldProfileMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/print/catalog/PrintBizEntityFieldCatalogProviderImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/print/catalog/PrintBizEntityFieldCatalogProviderImpl.java new file mode 100644 index 00000000..994e3950 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/print/catalog/PrintBizEntityFieldCatalogProviderImpl.java @@ -0,0 +1,226 @@ +package org.jeecg.modules.xslmes.print.catalog; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.catalog.IPrintBizEntityFieldCatalogProvider; +import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile; +import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldDetailMapper; +import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService; +import org.springframework.stereotype.Component; + +/** + * 将 mes_xsl_biz_entity_field_* 中的缓存提供给打印绑定接口(SPI 实现)。 + * + *

bizCode = {@code print_biz_perm_entity.perm_id}。 + * + *

「新增绑定」下拉组装时对每条业务若单独查库会产生严重 N+1;{@link #beginBulkLookup(Collection)} 在同一请求线程内改为单次 IN 查询。 + */ +@Slf4j +@Component +public class PrintBizEntityFieldCatalogProviderImpl implements IPrintBizEntityFieldCatalogProvider { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** null=非批量模式;非 null=批量模式(可能为空 Map) */ + private static final ThreadLocal> BULK_PROFILE_BY_CODE = + new ThreadLocal<>(); + + @Resource + private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService; + + @Resource + private MesXslBizEntityFieldDetailMapper detailMapper; + + @Override + public void beginBulkLookup(Collection bizCodes) { + endBulkLookup(); + if (bizCodes == null || bizCodes.isEmpty()) { + BULK_PROFILE_BY_CODE.set(Collections.emptyMap()); + return; + } + List ids = + bizCodes.stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + if (ids.isEmpty()) { + BULK_PROFILE_BY_CODE.set(Collections.emptyMap()); + return; + } + List list = + bizEntityFieldProfileService + .lambdaQuery() + .in(MesXslBizEntityFieldProfile::getBusinessCode, ids) + .list(); + Map map = new HashMap<>(Math.max(16, list.size() * 2)); + for (MesXslBizEntityFieldProfile p : list) { + if (p != null && StringUtils.isNotBlank(p.getBusinessCode())) { + map.put(p.getBusinessCode().trim(), p); + } + } + BULK_PROFILE_BY_CODE.set(map); + } + + @Override + public void endBulkLookup() { + BULK_PROFILE_BY_CODE.remove(); + } + + /** 批量模式下读线程 Map;否则单次按编码查询 */ + private MesXslBizEntityFieldProfile resolveProfile(String bizCode) { + if (StringUtils.isBlank(bizCode)) { + return null; + } + Map bulk = BULK_PROFILE_BY_CODE.get(); + if (bulk != null) { + return bulk.get(bizCode.trim()); + } + return bizEntityFieldProfileService.getByBusinessCode(bizCode); + } + + @Override + public String getEntityClassFqn(String bizCode) { + MesXslBizEntityFieldProfile p = resolveProfile(bizCode); + return p != null ? StringUtils.trimToNull(p.getEntityClassName()) : null; + } + + @Override + public boolean hasCatalogForBiz(String bizCode) { + return resolveProfile(bizCode) != null; + } + + @Override + public List listMainFields(String bizCode) { + MesXslBizEntityFieldProfile p = resolveProfile(bizCode); + if (p == null) { + return Collections.emptyList(); + } + return parseFieldItems(p.getMainFieldsJson()); + } + + @Override + public List listDetailSlots(String bizCode) { + MesXslBizEntityFieldProfile p = resolveProfile(bizCode); + if (p == null || StringUtils.isBlank(p.getId())) { + return Collections.emptyList(); + } + List lines = + detailMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslBizEntityFieldDetail::getProfileId, p.getId()) + .orderByAsc(MesXslBizEntityFieldDetail::getSortNo) + .orderByAsc(MesXslBizEntityFieldDetail::getId)); + List out = new ArrayList<>(); + for (MesXslBizEntityFieldDetail line : lines) { + if (StringUtils.isBlank(line.getDetailPropertyName())) { + continue; + } + out.add( + new PrintBizDetailSlotVO( + line.getDetailPropertyName(), + StringUtils.defaultString(line.getDetailEntityClassName()), + StringUtils.defaultIfBlank(line.getDetailSlotKind(), "LIST"), + StringUtils.defaultIfBlank(line.getDetailName(), line.getDetailPropertyName()))); + } + return out; + } + + @Override + public List listPrefixedDetailFields(String bizCode, String detailProperty, String slotKind) { + if (StringUtils.isAnyBlank(bizCode, detailProperty)) { + return Collections.emptyList(); + } + MesXslBizEntityFieldProfile p = resolveProfile(bizCode); + if (p == null || StringUtils.isBlank(p.getId())) { + return Collections.emptyList(); + } + String prop = detailProperty.trim(); + String reqKind = StringUtils.trimToEmpty(slotKind); + List lines = + detailMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslBizEntityFieldDetail::getProfileId, p.getId())); + MesXslBizEntityFieldDetail hit = null; + for (MesXslBizEntityFieldDetail line : lines) { + if (!prop.equals(line.getDetailPropertyName())) { + continue; + } + if (!slotKindMatch(reqKind, line.getDetailSlotKind())) { + continue; + } + hit = line; + break; + } + if (hit == null) { + return Collections.emptyList(); + } + List raw = parseFieldItems(hit.getDetailFieldsJson()); + List out = new ArrayList<>(raw.size()); + for (PrintBizFieldItemVO x : raw) { + String path = prop + "." + x.getFieldKey(); + String label = "明细「" + prop + "」→ " + x.getLabel(); + out.add(PrintBizFieldItemVO.copyWithPrefixedPath(x, path, label)); + } + return out; + } + + private static boolean slotKindMatch(String requested, String stored) { + String r = StringUtils.trimToEmpty(requested); + String s = StringUtils.trimToEmpty(stored); + if (StringUtils.isBlank(s)) { + return true; + } + if (StringUtils.isBlank(r)) { + return true; + } + return r.equalsIgnoreCase(s); + } + + /** 解析 JSON 数组:支持 VO 对象元素或纯字符串字段名 */ + private List parseFieldItems(String json) { + if (StringUtils.isBlank(json)) { + return Collections.emptyList(); + } + try { + JsonNode root = OBJECT_MAPPER.readTree(json); + if (!root.isArray()) { + return Collections.emptyList(); + } + List out = new ArrayList<>(); + for (JsonNode n : root) { + if (n.isTextual()) { + String k = n.asText(); + if (StringUtils.isNotBlank(k)) { + out.add(PrintBizFieldItemVO.plainStringField(k)); + } + continue; + } + if (n.isObject()) { + PrintBizFieldItemVO vo = OBJECT_MAPPER.treeToValue(n, PrintBizFieldItemVO.class); + if (vo != null && StringUtils.isNotBlank(vo.getFieldKey())) { + out.add(vo); + } + } + } + return out; + } catch (Exception e) { + log.warn("解析业务实体字段缓存 JSON 失败: {}", e.getMessage()); + return Collections.emptyList(); + } + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslBizEntityFieldProfileService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslBizEntityFieldProfileService.java new file mode 100644 index 00000000..2d8065a9 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslBizEntityFieldProfileService.java @@ -0,0 +1,40 @@ +package org.jeecg.modules.xslmes.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile; + +/** 业务实体字段配置(主子表) */ +public interface IMesXslBizEntityFieldProfileService extends IService { + + /** 按业务编码(菜单 permission id)查询主表,不含 detailList */ + MesXslBizEntityFieldProfile getByBusinessCode(String businessCode); + + /** + * 按业务编码 upsert(用于启动扫描写入);明细全量替换。 + * + * @param businessCode 与 print_biz_perm_entity.perm_id、biz_code 一致 + */ + void upsertScannedProfile( + String businessCode, + String businessName, + String entityFqn, + String mainFieldsJson, + List detailRows); + + /** 新增主表并保存明细 */ + void saveWithDetails(MesXslBizEntityFieldProfile profile); + + /** 更新主表并重写明细 */ + void updateWithDetails(MesXslBizEntityFieldProfile profile); + + /** 按主键删除主表及明细 */ + void removeWithDetails(String id); + + /** 批量删除主表及明细 */ + void removeBatchWithDetails(java.util.Collection ids); + + /** 查询主表并填充 detailList */ + MesXslBizEntityFieldProfile getByIdWithDetails(String id); +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslBizEntityFieldProfileServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslBizEntityFieldProfileServiceImpl.java new file mode 100644 index 00000000..06387d2b --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslBizEntityFieldProfileServiceImpl.java @@ -0,0 +1,164 @@ +package org.jeecg.modules.xslmes.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail; +import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile; +import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldDetailMapper; +import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldProfileMapper; +import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class MesXslBizEntityFieldProfileServiceImpl extends ServiceImpl + implements IMesXslBizEntityFieldProfileService { + + /** 标记为启动任务根据 print_biz_perm_entity 写入,便于区分手工维护数据 */ + private static final String REMARK_PRINT_PERM_SCAN = "print_biz_perm_entity 启动异步扫描"; + + @Resource + private MesXslBizEntityFieldDetailMapper detailMapper; + + @Override + public MesXslBizEntityFieldProfile getByBusinessCode(String businessCode) { + if (StringUtils.isBlank(businessCode)) { + return null; + } + return this.lambdaQuery() + .eq(MesXslBizEntityFieldProfile::getBusinessCode, businessCode.trim()) + .last("LIMIT 1") + .one(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void upsertScannedProfile( + String businessCode, + String businessName, + String entityFqn, + String mainFieldsJson, + List detailRows) { + if (StringUtils.isBlank(businessCode)) { + return; + } + String code = businessCode.trim(); + Date now = new Date(); + MesXslBizEntityFieldProfile existing = getByBusinessCode(code); + if (existing == null) { + MesXslBizEntityFieldProfile p = new MesXslBizEntityFieldProfile(); + p.setBusinessCode(code); + p.setBusinessName(StringUtils.defaultIfBlank(businessName, code)); + p.setEntityClassName(entityFqn); + p.setMainFieldsJson(mainFieldsJson); + p.setRemark(REMARK_PRINT_PERM_SCAN); + p.setCreateTime(now); + p.setUpdateTime(now); + p.setDetailList(detailRows != null ? detailRows : List.of()); + saveWithDetails(p); + return; + } + existing.setBusinessName(StringUtils.defaultIfBlank(businessName, existing.getBusinessName())); + existing.setEntityClassName(entityFqn); + existing.setMainFieldsJson(mainFieldsJson); + existing.setRemark(REMARK_PRINT_PERM_SCAN); + existing.setUpdateTime(now); + existing.setDetailList(detailRows != null ? detailRows : List.of()); + updateWithDetails(existing); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveWithDetails(MesXslBizEntityFieldProfile profile) { + this.save(profile); + insertDetailRows(profile); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWithDetails(MesXslBizEntityFieldProfile profile) { + this.updateById(profile); + detailMapper.delete(new LambdaQueryWrapper().eq(MesXslBizEntityFieldDetail::getProfileId, profile.getId())); + insertDetailRows(profile); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeWithDetails(String id) { + if (StringUtils.isBlank(id)) { + return; + } + detailMapper.delete(new LambdaQueryWrapper().eq(MesXslBizEntityFieldDetail::getProfileId, id)); + this.removeById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeBatchWithDetails(Collection ids) { + if (ids == null || ids.isEmpty()) { + return; + } + List trimmed = new ArrayList<>(); + for (String id : ids) { + if (StringUtils.isNotBlank(id)) { + String t = id.trim(); + trimmed.add(t); + detailMapper.delete(new LambdaQueryWrapper().eq(MesXslBizEntityFieldDetail::getProfileId, t)); + } + } + if (!trimmed.isEmpty()) { + this.removeByIds(trimmed); + } + } + + @Override + public MesXslBizEntityFieldProfile getByIdWithDetails(String id) { + MesXslBizEntityFieldProfile profile = this.getById(id); + if (profile == null) { + return null; + } + List lines = + detailMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslBizEntityFieldDetail::getProfileId, id) + .orderByAsc(MesXslBizEntityFieldDetail::getSortNo) + .orderByAsc(MesXslBizEntityFieldDetail::getId)); + profile.setDetailList(lines); + return profile; + } + + /** 写入子表(编辑时已清空旧数据) */ + private void insertDetailRows(MesXslBizEntityFieldProfile profile) { + if (profile.getDetailList() == null || profile.getDetailList().isEmpty()) { + return; + } + Date now = new Date(); + int seq = 0; + for (MesXslBizEntityFieldDetail row : profile.getDetailList()) { + row.setId(null); + row.setProfileId(profile.getId()); + if (row.getSortNo() == null) { + row.setSortNo(seq++); + } + if (row.getCreateTime() == null) { + row.setCreateTime(now); + } + if (row.getUpdateTime() == null) { + row.setUpdateTime(now); + } + if (StringUtils.isBlank(row.getCreateBy())) { + row.setCreateBy(profile.getCreateBy()); + } + if (StringUtils.isBlank(row.getUpdateBy())) { + row.setUpdateBy(profile.getUpdateBy()); + } + detailMapper.insert(row); + } + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/IPrintBizEntityFieldCatalogProvider.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/IPrintBizEntityFieldCatalogProvider.java new file mode 100644 index 00000000..20166d74 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/IPrintBizEntityFieldCatalogProvider.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.print.catalog; + +import java.util.Collection; +import java.util.List; +import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; + +/** + * 业务实体字段缓存(mes_xsl_biz_entity_field_*)供打印绑定使用;实现类位于 jeecg-module-xslmes,避免 system-biz 依赖业务模块。 + * + *

bizCode 与 {@code print_biz_perm_entity.perm_id}、打印绑定 {@code biz_code} 一致。 + */ +public interface IPrintBizEntityFieldCatalogProvider { + + /** + * 在一次接口内批量组装多个业务的 VO 前调用,将mes_xsl_biz_entity_field_profile 一次查出放入线程缓存,避免按业务 N + * 次查询;必须与 {@link #endBulkLookup()} 成对(建议 finally)。 + */ + default void beginBulkLookup(Collection bizCodes) {} + + /** 结束批量查找,清理线程缓存 */ + default void endBulkLookup() {} + + /** 缓存中的主实体全限定名(无记录返回 null) */ + String getEntityClassFqn(String bizCode); + + /** 是否已有缓存记录(存在即优先走缓存,即使字段为空) */ + boolean hasCatalogForBiz(String bizCode); + + /** 主实体可选字段(与反射接口字段结构一致) */ + List listMainFields(String bizCode); + + /** 明细数据来源槽位 */ + List listDetailSlots(String bizCode); + + /** + * 明细槽位对应字段,fieldKey 已带「属性名.」前缀(与 {@link org.jeecg.modules.print.util.PrintBizDetailPropertyScanner#listPrefixedDetailFields} + * 一致)。 + */ + List listPrefixedDetailFields(String bizCode, String detailProperty, String slotKind); +} 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 5fd7402b..5d886f88 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 @@ -23,6 +23,7 @@ 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.IPrintBizEntityFieldCatalogProvider; import org.jeecg.modules.print.entity.PrintBizTemplateBind; import org.jeecg.modules.print.entity.PrintTemplate; import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService; @@ -57,6 +58,9 @@ public class PrintBizTemplateBindController extends JeecgController listAllBizTypeVOs() { - List out = new ArrayList<>(); - // 仅使用 print_biz_perm_entity,避免全库扫菜单+反射导致接口超时;由「保存白名单」时 upsert 写入表 - for (PrintBizPermEntity row : list()) { - if (row == null || StringUtils.isBlank(row.getPermId())) { - continue; + List rows = list(); + if (rows == null || rows.isEmpty()) { + return new ArrayList<>(); + } + List permIds = new ArrayList<>(rows.size()); + for (PrintBizPermEntity row : rows) { + if (row != null && StringUtils.isNotBlank(row.getPermId())) { + permIds.add(row.getPermId().trim()); } - PrintBizTypeVO vo = buildVoForPermId(row.getPermId()); - if (vo != null) { - out.add(vo); + } + Map permMap = loadPermissionMap(permIds); + List out = new ArrayList<>(); + try { + if (fieldCatalogProvider != null) { + fieldCatalogProvider.beginBulkLookup(permIds); + } + for (PrintBizPermEntity row : rows) { + if (row == null || StringUtils.isBlank(row.getPermId())) { + continue; + } + PrintBizTypeVO vo = buildVoForPermId(row.getPermId().trim(), row, permMap); + if (vo != null) { + out.add(vo); + } + } + } finally { + if (fieldCatalogProvider != null) { + fieldCatalogProvider.endBulkLookup(); } } return out; @@ -60,15 +83,40 @@ public class PrintBizPermEntityServiceImpl if (whitelistPermIds == null || whitelistPermIds.isEmpty()) { return listAllBizTypeVOs(); } - List out = new ArrayList<>(); + List ids = new ArrayList<>(); + Set seen = new HashSet<>(); for (String raw : whitelistPermIds) { String id = StringUtils.trimToEmpty(raw); - if (id.isEmpty()) { + if (id.isEmpty() || seen.contains(id)) { continue; } - PrintBizTypeVO vo = buildVoForPermId(id); - if (vo != null) { - out.add(vo); + seen.add(id); + ids.add(id); + } + if (ids.isEmpty()) { + return new ArrayList<>(); + } + Map permMap = loadPermissionMap(ids); + Map entityMap = new HashMap<>(ids.size()); + for (PrintBizPermEntity e : listByIds(ids)) { + if (e != null && StringUtils.isNotBlank(e.getPermId())) { + entityMap.put(e.getPermId().trim(), e); + } + } + List out = new ArrayList<>(); + try { + if (fieldCatalogProvider != null) { + fieldCatalogProvider.beginBulkLookup(ids); + } + for (String id : ids) { + PrintBizTypeVO vo = buildVoForPermId(id, entityMap.get(id), permMap); + if (vo != null) { + out.add(vo); + } + } + } finally { + if (fieldCatalogProvider != null) { + fieldCatalogProvider.endBulkLookup(); } } return out; @@ -203,37 +251,86 @@ public class PrintBizPermEntityServiceImpl /** * 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。 */ + private Map loadPermissionMap(List permIds) { + if (permIds == null || permIds.isEmpty()) { + return Collections.emptyMap(); + } + List plist = sysPermissionService.listByIds(permIds); + Map map = new HashMap<>(Math.max(16, plist.size() * 2)); + for (SysPermission p : plist) { + if (p != null && StringUtils.isNotBlank(p.getId())) { + map.put(p.getId().trim(), p); + } + } + return map; + } + private PrintBizTypeVO buildVoForPermId(String permId) { if (StringUtils.isBlank(permId)) { return null; } - PrintBizPermEntity row = getById(permId.trim()); + String id = permId.trim(); + return buildVoForPermId(id, getById(id), null); + } + + private PrintBizTypeVO buildVoForPermId( + String permId, PrintBizPermEntity row, Map permCache) { + if (StringUtils.isBlank(permId)) { + return null; + } + String id = permId.trim(); + PrintBizPermEntity rowEff = row != null ? row : getById(id); String entityFqn = null; - if (row != null && StringUtils.isNotBlank(row.getEntityClass())) { - entityFqn = row.getEntityClass().trim(); + if (rowEff != null && StringUtils.isNotBlank(rowEff.getEntityClass())) { + entityFqn = rowEff.getEntityClass().trim(); } else { - SysPermission p = sysPermissionService.getById(permId.trim()); + SysPermission p = permCache != null ? permCache.get(id) : sysPermissionService.getById(id); entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p); } + boolean catalogOk = + fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(id); if (StringUtils.isBlank(entityFqn)) { - return null; + if (!catalogOk) { + return null; + } + entityFqn = StringUtils.trimToEmpty(fieldCatalogProvider.getEntityClassFqn(id)); } - Class clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn); - List fields = - clazz == null ? new ArrayList<>() : PrintBizEntityFieldIntrospector.listFields(clazz); - if (clazz == null) { - // 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导 - return null; + Class clazz = null; + if (!catalogOk) { + clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn); + 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); + vo.setBizCode(id); + vo.setLinkedPermissionId(id); + vo.setBizName(resolveMenuName(id, permCache)); + String desc = StringUtils.isNotBlank(entityFqn) ? entityFqn : ""; + if (StringUtils.isBlank(desc) && fieldCatalogProvider != null) { + desc = StringUtils.defaultString(fieldCatalogProvider.getEntityClassFqn(id)); + } + vo.setDescription(desc); + if (catalogOk) { + vo.setFields(fieldCatalogProvider.listMainFields(id)); + } else { + vo.setFields(PrintBizEntityFieldIntrospector.listFields(clazz)); + } return vo; } + private String resolveMenuName(String permId, Map permCache) { + if (permCache != null) { + SysPermission p = permCache.get(permId); + if (p != null && StringUtils.isNotBlank(p.getName())) { + return p.getName().trim(); + } + return permId; + } + return resolveMenuName(permId); + } + private String resolveMenuName(String permId) { SysPermission p = sysPermissionService.getById(permId); if (p != null && StringUtils.isNotBlank(p.getName())) { 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 index 8546759d..dad9c8e5 100644 --- 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 @@ -77,7 +77,7 @@ public final class PrintBizDetailPropertyScanner { for (PrintBizFieldItemVO x : raw) { String path = prefix + "." + x.getFieldKey(); String label = "明细「" + prefix + "」→ " + x.getLabel(); - out.add(new PrintBizFieldItemVO(path, label, x.getDescription())); + out.add(PrintBizFieldItemVO.copyWithPrefixedPath(x, path, label)); } return out; } 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 index a3659234..4911b657 100644 --- 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 @@ -3,16 +3,28 @@ 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.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.jeecg.modules.print.vo.PrintBizFieldItemVO; import org.jeecgframework.poi.excel.annotation.Excel; /** * 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。 + * + *

写入缓存 JSON 时附带 {@code javaType}、{@code jdbcType}、{@code simpleKind}。 */ public final class PrintBizEntityFieldIntrospector { @@ -31,13 +43,174 @@ public final class PrintBizEntityFieldIntrospector { if ("serialVersionUID".equals(name)) { continue; } - ordered.putIfAbsent(name, new PrintBizFieldItemVO(name, resolveLabel(f), "")); + PrintBizFieldItemVO vo = + new PrintBizFieldItemVO(name, resolveLabel(f), ""); + fillJavaJdbcSimple(vo, f.getType()); + ordered.putIfAbsent(name, vo); } c = c.getSuperclass(); } return new ArrayList<>(ordered.values()); } + /** 将反射得到的 Java 类型写成 VO 上的三类提示字段 */ + public static void fillJavaJdbcSimple(PrintBizFieldItemVO vo, Class declaredType) { + if (vo == null || declaredType == null) { + return; + } + vo.setJavaType(resolveJavaTypeFqn(declaredType)); + vo.setSimpleKind(resolveSimpleKind(declaredType)); + vo.setJdbcType(resolveJdbcTypeName(declaredType)); + } + + private static String resolveJavaTypeFqn(Class t) { + String cn = t.getCanonicalName(); + return cn != null ? cn : t.getName(); + } + + /** + * STRING / BOOLEAN / INTEGER / LONG / DECIMAL / DATE / TIME / DATETIME / ENUM / BINARY / JAVA_OBJECT / + * OTHER + */ + private static String resolveSimpleKind(Class t) { + if (t.isPrimitive()) { + if (t == boolean.class) { + return "BOOLEAN"; + } + if (t == byte.class || t == short.class || t == int.class || t == char.class) { + return "INTEGER"; + } + if (t == long.class) { + return "LONG"; + } + if (t == float.class || t == double.class) { + return "DECIMAL"; + } + return "OTHER"; + } + if (t == Boolean.class) { + return "BOOLEAN"; + } + if (t.isEnum()) { + return "ENUM"; + } + if (CharSequence.class.isAssignableFrom(t)) { + return "STRING"; + } + if (Number.class.isAssignableFrom(t)) { + if (BigDecimal.class.isAssignableFrom(t) + || Float.class.isAssignableFrom(t) + || Double.class.isAssignableFrom(t)) { + return "DECIMAL"; + } + if (BigInteger.class.isAssignableFrom(t) || Long.class.isAssignableFrom(t)) { + return "LONG"; + } + return "INTEGER"; + } + if (UUID.class.isAssignableFrom(t)) { + return "STRING"; + } + if (Date.class.isAssignableFrom(t)) { + return "DATETIME"; + } + if (LocalDate.class.isAssignableFrom(t)) { + return "DATE"; + } + if (LocalTime.class.isAssignableFrom(t)) { + return "TIME"; + } + if (LocalDateTime.class.isAssignableFrom(t) + || Instant.class.isAssignableFrom(t) + || ZonedDateTime.class.isAssignableFrom(t) + || OffsetDateTime.class.isAssignableFrom(t)) { + return "DATETIME"; + } + if (byte[].class == t) { + return "BINARY"; + } + if (t.getName().startsWith("java.") || t.getName().startsWith("javax.")) { + return "JAVA_OBJECT"; + } + return "JAVA_OBJECT"; + } + + /** + * 粗粒度 JDBC 名称(非穷尽);自定义 Bean 等返回 JAVA_OBJECT。 + */ + private static String resolveJdbcTypeName(Class t) { + if (t.isPrimitive()) { + if (t == boolean.class) { + return "BOOLEAN"; + } + if (t == byte.class) { + return "TINYINT"; + } + if (t == short.class) { + return "SMALLINT"; + } + if (t == int.class) { + return "INTEGER"; + } + if (t == long.class) { + return "BIGINT"; + } + if (t == float.class) { + return "FLOAT"; + } + if (t == double.class) { + return "DOUBLE"; + } + if (t == char.class) { + return "CHAR"; + } + return "OTHER"; + } + if (t == Boolean.class) { + return "BOOLEAN"; + } + if (t.isEnum()) { + return "VARCHAR"; + } + if (String.class == t || CharSequence.class.isAssignableFrom(t)) { + return "VARCHAR"; + } + if (UUID.class.isAssignableFrom(t)) { + return "CHAR"; + } + if (Integer.class == t || Short.class == t || Byte.class == t) { + return "INTEGER"; + } + if (Long.class == t || BigInteger.class.isAssignableFrom(t)) { + return "BIGINT"; + } + if (BigDecimal.class.isAssignableFrom(t)) { + return "DECIMAL"; + } + if (Float.class == t || Double.class == t) { + return "DOUBLE"; + } + if (Date.class.isAssignableFrom(t)) { + return "TIMESTAMP"; + } + if (LocalDate.class.isAssignableFrom(t)) { + return "DATE"; + } + if (LocalTime.class.isAssignableFrom(t)) { + return "TIME"; + } + if (LocalDateTime.class.isAssignableFrom(t) + || Instant.class.isAssignableFrom(t) + || ZonedDateTime.class.isAssignableFrom(t) + || OffsetDateTime.class.isAssignableFrom(t)) { + return "TIMESTAMP"; + } + if (byte[].class == t) { + return "BLOB"; + } + return "JAVA_OBJECT"; + } + private static String resolveLabel(Field f) { Schema schema = f.getAnnotation(Schema.class); if (schema != null && StringUtils.isNotBlank(schema.description())) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java index f1b0f914..c7db51e4 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java @@ -2,15 +2,14 @@ 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 PrintBizFieldItemVO implements Serializable { + @Schema(description = "字段路径(支持 a.b,与 JSON 一致)") private String fieldKey; @@ -19,4 +18,51 @@ public class PrintBizFieldItemVO implements Serializable { @Schema(description = "说明") private String description; + + /** 声明类型全限定名(如 java.lang.String、java.time.LocalDateTime) */ + @Schema(description = "Java 声明类型全限定名") + private String javaType; + + /** + * 与 JDBC 习惯对齐的类型名(如 VARCHAR、BIGINT、DECIMAL、TIMESTAMP);非 JDBC 标量填 JAVA_OBJECT。 + */ + @Schema(description = "粗粒度 JDBC 风格类型名") + private String jdbcType; + + /** + * 粗分类:STRING、BOOLEAN、INTEGER、LONG、DECIMAL、DATE、TIME、DATETIME、ENUM、BINARY、OTHER、JAVA_OBJECT + */ + @Schema(description = "简化种类,便于前端格式化") + private String simpleKind; + + /** 兼容旧三参构造(类型字段为空) */ + public PrintBizFieldItemVO(String fieldKey, String label, String description) { + this.fieldKey = fieldKey; + this.label = label; + this.description = description == null ? "" : description; + } + + /** 复制前缀明细字段时保留类型元数据 */ + public static PrintBizFieldItemVO copyWithPrefixedPath( + PrintBizFieldItemVO src, String prefixedFieldKey, String prefixedLabel) { + PrintBizFieldItemVO o = new PrintBizFieldItemVO(); + o.setFieldKey(prefixedFieldKey); + o.setLabel(prefixedLabel); + o.setDescription(src != null ? src.getDescription() : ""); + if (src != null) { + o.setJavaType(src.getJavaType()); + o.setJdbcType(src.getJdbcType()); + o.setSimpleKind(src.getSimpleKind()); + } + return o; + } + + /** 仅占位字符串数组解析出来的单项(视为字符串) */ + public static PrintBizFieldItemVO plainStringField(String fieldKey) { + PrintBizFieldItemVO o = new PrintBizFieldItemVO(fieldKey, fieldKey, ""); + o.setJavaType(String.class.getName()); + o.setJdbcType("VARCHAR"); + o.setSimpleKind("STRING"); + return o; + } } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_54__mes_xsl_biz_entity_field_catalog.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_54__mes_xsl_biz_entity_field_catalog.sql new file mode 100644 index 00000000..02c8a066 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_54__mes_xsl_biz_entity_field_catalog.sql @@ -0,0 +1,35 @@ +-- 业务实体字段缓存表:供「业务打印绑定」下拉读取;数据由启动任务根据 print_biz_perm_entity 异步扫描写入 + +CREATE TABLE IF NOT EXISTS `mes_xsl_biz_entity_field_profile` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `business_name` varchar(200) NOT NULL COMMENT '业务名称', + `business_code` varchar(64) NOT NULL COMMENT '业务编码(菜单 permission id,与 print 绑定 biz_code 一致)', + `entity_class_name` varchar(512) DEFAULT NULL COMMENT '主实体 Java 全限定类名', + `main_fields_json` text COMMENT '主表字段列表 JSON(PrintBizFieldItemVO 数组)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `tenant_id` int DEFAULT NULL COMMENT '租户ID', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_mxbefp_bcode` (`business_code`), + KEY `idx_mxbefp_tenant` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES业务实体字段配置-主表'; + +CREATE TABLE IF NOT EXISTS `mes_xsl_biz_entity_field_detail` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `profile_id` varchar(32) NOT NULL COMMENT '主表ID', + `detail_property_name` varchar(128) DEFAULT NULL COMMENT '主实体明细属性名(如 lines,与打印绑定 detailProperty 一致)', + `detail_slot_kind` varchar(16) DEFAULT NULL COMMENT 'LIST 或 OBJECT', + `detail_name` varchar(200) DEFAULT NULL COMMENT '明细展示名称', + `detail_entity_class_name` varchar(512) DEFAULT NULL COMMENT '明细元素类型全限定名', + `detail_fields_json` text COMMENT '明细元素类字段列表 JSON(无前缀,PrintBizFieldItemVO 数组)', + `sort_no` int DEFAULT NULL COMMENT '排序号', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_mxbefd_profile` (`profile_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES业务实体字段配置-明细槽位字段清单'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_55__mes_xsl_biz_entity_field_detail_slot_columns.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_55__mes_xsl_biz_entity_field_detail_slot_columns.sql new file mode 100644 index 00000000..50a06e0d --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_55__mes_xsl_biz_entity_field_detail_slot_columns.sql @@ -0,0 +1,25 @@ +-- 旧库升级:明细表若早于完整脚本创建,可能缺少 detail_property_name / detail_slot_kind(兼容 MySQL 5.7+) + +SELECT COUNT(*) INTO @jeecg_chk_dpn FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_biz_entity_field_detail' + AND COLUMN_NAME = 'detail_property_name'; + +SET @jeecg_sql_dpn := IF(@jeecg_chk_dpn = 0, + 'ALTER TABLE mes_xsl_biz_entity_field_detail ADD COLUMN detail_property_name varchar(128) DEFAULT NULL COMMENT ''主实体明细属性名(与打印绑定 detailProperty 一致)'' AFTER profile_id', + 'SELECT 1'); +PREPARE jeecg_stmt_dpn FROM @jeecg_sql_dpn; +EXECUTE jeecg_stmt_dpn; +DEALLOCATE PREPARE jeecg_stmt_dpn; + +SELECT COUNT(*) INTO @jeecg_chk_dsk FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_biz_entity_field_detail' + AND COLUMN_NAME = 'detail_slot_kind'; + +SET @jeecg_sql_dsk := IF(@jeecg_chk_dsk = 0, + 'ALTER TABLE mes_xsl_biz_entity_field_detail ADD COLUMN detail_slot_kind varchar(16) DEFAULT NULL COMMENT ''LIST 或 OBJECT'' AFTER detail_property_name', + 'SELECT 1'); +PREPARE jeecg_stmt_dsk FROM @jeecg_sql_dsk; +EXECUTE jeecg_stmt_dsk; +DEALLOCATE PREPARE jeecg_stmt_dsk; diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts index 975f348d..e05227cc 100644 --- a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts @@ -22,8 +22,9 @@ 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 }); +/** 新增/编辑绑定时可选业务(受打印业务白名单过滤);后端批量查缓存与菜单,数据多时延长超时 */ +export const bizTypesForBinding = () => + defHttp.get({ url: Api.bizTypesForBinding, timeout: 120000 }); /** 白名单:已勾选菜单 id + 完整业务目录 */ export const getPermWhitelist = () => defHttp.get({ url: Api.permWhitelist }); /** 勾选菜单多时后端需批量 upsert,默认 10s 易超时 */ diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue index abf9032e..237682a5 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="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。" + description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段;业务字段下拉第一项为「空占位符」,表示不参与业务 JSON 取值(等同输出空)。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。" /> @@ -57,7 +57,7 @@ 同名自动匹配 - 添加空占位 @@ -285,6 +284,9 @@ const { createMessage } = useMessage(); const { t } = useI18n(); + /** 下拉「空占位符」选项值(落库 fieldMappingJson 的 bizField 转为 '') */ + const EMPTY_BIZ_FIELD_SENTINEL = '__PRINT_BIND_EMPTY_BIZ__'; + interface BizTypeItem { bizCode: string; bizName: string; @@ -361,15 +363,18 @@ })), ); - const bizFieldOptionsMain = computed(() => - unref(bizFields).map((f) => ({ + const bizFieldOptionsMain = computed(() => { + const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }]; + const rest = unref(bizFields).map((f) => ({ label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey, value: f.fieldKey, - })), - ); + })); + return [...head, ...rest]; + }); /** 主表 + 明细前缀字段(用于明细/表格占位) */ const bizFieldOptions = computed(() => { + const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }]; const main = unref(bizFields).map((f) => ({ label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey, value: f.fieldKey, @@ -378,9 +383,25 @@ label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey, value: f.fieldKey, })); - return [...main, ...detail]; + return [...head, ...main, ...detail]; }); + /** 已保存的空字符串映射为下拉哨兵,便于展示「空占位符」项 */ + function normalizeBizFieldForUi(raw?: string) { + if (raw === undefined || raw === null || raw === '') { + return EMPTY_BIZ_FIELD_SENTINEL; + } + return raw; + } + + /** 提交前哨兵还原为空字符串 */ + function denormalizeBizFieldForSave(v?: string) { + if (v === EMPTY_BIZ_FIELD_SENTINEL || v === undefined || v === null || v === '') { + return ''; + } + return v; + } + /** 主表参数行:模板 elementType 为 param */ const mappingRowsParam = computed(() => unref(mappingRows).filter((r) => (r.elementType || '') === 'param'), @@ -615,7 +636,7 @@ const hit = saved.find((x) => x.templateField === templateField); return { templateField, - bizField: hit?.bizField ?? '', + bizField: hit !== undefined ? normalizeBizFieldForUi(hit.bizField) : undefined, elementType: t?.elementType || 'param', titleHint: t?.titleHint || @@ -637,26 +658,13 @@ mappingRows.value = [...unref(mappingRows)]; } - /** 手动增加仅输出空值的模板占位(写入 savedMappingRef 并重建行,避免再次「解析模板」时丢失) */ - function addPlaceholderParamRow() { - const raw = window.prompt( - '请输入模板参数 bindField(如 Parameter3)。业务字段将固定为空字符串,不参与业务 JSON 取值。', - 'Parameter3', - ); - const k = (raw || '').trim(); - if (!k) return; - if (unref(savedMappingRef).some((x) => x.templateField === k) || unref(mappingRows).some((r) => r.templateField === k)) { - createMessage.warning('该占位已存在'); - return; - } - savedMappingRef.value = [...unref(savedMappingRef), { templateField: k, bizField: '' }]; - rebuildMappingRows(); - } - function buildFieldMappingJson() { const arr = unref(mappingRows) .filter((r) => r.templateField) - .map((r) => ({ templateField: r.templateField, bizField: r.bizField ?? '' })); + .map((r) => ({ + templateField: r.templateField, + bizField: denormalizeBizFieldForSave(r.bizField), + })); return JSON.stringify(arr); }