新增业务实体字段配置功能,包含主表和明细表的数据库结构定义,支持业务打印绑定的字段映射。实现字段配置的增删改查操作,优化打印数据生成逻辑,提升系统的可维护性和扩展性。同时,新增异步同步功能以支持打印模板与业务数据的实时更新。
This commit is contained in:
@@ -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;
|
||||||
38
jeecg-boot/db/mes-xsl-biz-entity-field-profile.sql
Normal file
38
jeecg-boot/db/mes-xsl-biz-entity-field-profile.sql
Normal file
@@ -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)───
|
||||||
@@ -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_*},
|
||||||
|
* 供「业务打印绑定」弹窗读取(避免运行时频繁反射)。
|
||||||
|
*
|
||||||
|
* <p>关闭:{@code jeecg.print.biz-entity-field-catalog-sync=false}
|
||||||
|
*
|
||||||
|
* <p>建议在 {@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<PrintBizPermEntity> 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<PrintBizFieldItemVO> mainFields = PrintBizEntityFieldIntrospector.listFields(clazz);
|
||||||
|
String mainJson = OBJECT_MAPPER.writeValueAsString(mainFields);
|
||||||
|
|
||||||
|
List<PrintBizDetailSlotVO> slots = PrintBizDetailPropertyScanner.listSlots(clazz);
|
||||||
|
List<MesXslBizEntityFieldDetail> 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<PrintBizFieldItemVO> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MesXslBizEntityFieldProfile, IMesXslBizEntityFieldProfileService> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页列表(不含明细,减轻负载)")
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<MesXslBizEntityFieldProfile>> queryPageList(
|
||||||
|
MesXslBizEntityFieldProfile query,
|
||||||
|
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<MesXslBizEntityFieldProfile> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||||
|
Page<MesXslBizEntityFieldProfile> page = new Page<>(pageNo, pageSize);
|
||||||
|
IPage<MesXslBizEntityFieldProfile> 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<String> 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<String> 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<String> 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<String> 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<MesXslBizEntityFieldProfile> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<MesXslBizEntityFieldDetail> detailList;
|
||||||
|
}
|
||||||
@@ -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<MesXslBizEntityFieldDetail> {}
|
||||||
@@ -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<MesXslBizEntityFieldProfile> {}
|
||||||
@@ -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 实现)。
|
||||||
|
*
|
||||||
|
* <p>bizCode = {@code print_biz_perm_entity.perm_id}。
|
||||||
|
*
|
||||||
|
* <p>「新增绑定」下拉组装时对每条业务若单独查库会产生严重 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<Map<String, MesXslBizEntityFieldProfile>> BULK_PROFILE_BY_CODE =
|
||||||
|
new ThreadLocal<>();
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MesXslBizEntityFieldDetailMapper detailMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginBulkLookup(Collection<String> bizCodes) {
|
||||||
|
endBulkLookup();
|
||||||
|
if (bizCodes == null || bizCodes.isEmpty()) {
|
||||||
|
BULK_PROFILE_BY_CODE.set(Collections.emptyMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> 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<MesXslBizEntityFieldProfile> list =
|
||||||
|
bizEntityFieldProfileService
|
||||||
|
.lambdaQuery()
|
||||||
|
.in(MesXslBizEntityFieldProfile::getBusinessCode, ids)
|
||||||
|
.list();
|
||||||
|
Map<String, MesXslBizEntityFieldProfile> 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<String, MesXslBizEntityFieldProfile> 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<PrintBizFieldItemVO> listMainFields(String bizCode) {
|
||||||
|
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
|
||||||
|
if (p == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return parseFieldItems(p.getMainFieldsJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PrintBizDetailSlotVO> listDetailSlots(String bizCode) {
|
||||||
|
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
|
||||||
|
if (p == null || StringUtils.isBlank(p.getId())) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<MesXslBizEntityFieldDetail> lines =
|
||||||
|
detailMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
|
||||||
|
.eq(MesXslBizEntityFieldDetail::getProfileId, p.getId())
|
||||||
|
.orderByAsc(MesXslBizEntityFieldDetail::getSortNo)
|
||||||
|
.orderByAsc(MesXslBizEntityFieldDetail::getId));
|
||||||
|
List<PrintBizDetailSlotVO> 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<PrintBizFieldItemVO> 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<MesXslBizEntityFieldDetail> lines =
|
||||||
|
detailMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
|
||||||
|
.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<PrintBizFieldItemVO> raw = parseFieldItems(hit.getDetailFieldsJson());
|
||||||
|
List<PrintBizFieldItemVO> 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<PrintBizFieldItemVO> parseFieldItems(String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonNode root = OBJECT_MAPPER.readTree(json);
|
||||||
|
if (!root.isArray()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<PrintBizFieldItemVO> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MesXslBizEntityFieldProfile> {
|
||||||
|
|
||||||
|
/** 按业务编码(菜单 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<MesXslBizEntityFieldDetail> detailRows);
|
||||||
|
|
||||||
|
/** 新增主表并保存明细 */
|
||||||
|
void saveWithDetails(MesXslBizEntityFieldProfile profile);
|
||||||
|
|
||||||
|
/** 更新主表并重写明细 */
|
||||||
|
void updateWithDetails(MesXslBizEntityFieldProfile profile);
|
||||||
|
|
||||||
|
/** 按主键删除主表及明细 */
|
||||||
|
void removeWithDetails(String id);
|
||||||
|
|
||||||
|
/** 批量删除主表及明细 */
|
||||||
|
void removeBatchWithDetails(java.util.Collection<String> ids);
|
||||||
|
|
||||||
|
/** 查询主表并填充 detailList */
|
||||||
|
MesXslBizEntityFieldProfile getByIdWithDetails(String id);
|
||||||
|
}
|
||||||
@@ -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<MesXslBizEntityFieldProfileMapper, MesXslBizEntityFieldProfile>
|
||||||
|
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<MesXslBizEntityFieldDetail> 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<MesXslBizEntityFieldDetail>().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<MesXslBizEntityFieldDetail>().eq(MesXslBizEntityFieldDetail::getProfileId, id));
|
||||||
|
this.removeById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void removeBatchWithDetails(Collection<String> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> trimmed = new ArrayList<>();
|
||||||
|
for (String id : ids) {
|
||||||
|
if (StringUtils.isNotBlank(id)) {
|
||||||
|
String t = id.trim();
|
||||||
|
trimmed.add(t);
|
||||||
|
detailMapper.delete(new LambdaQueryWrapper<MesXslBizEntityFieldDetail>().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<MesXslBizEntityFieldDetail> lines =
|
||||||
|
detailMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 依赖业务模块。
|
||||||
|
*
|
||||||
|
* <p>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<String> bizCodes) {}
|
||||||
|
|
||||||
|
/** 结束批量查找,清理线程缓存 */
|
||||||
|
default void endBulkLookup() {}
|
||||||
|
|
||||||
|
/** 缓存中的主实体全限定名(无记录返回 null) */
|
||||||
|
String getEntityClassFqn(String bizCode);
|
||||||
|
|
||||||
|
/** 是否已有缓存记录(存在即优先走缓存,即使字段为空) */
|
||||||
|
boolean hasCatalogForBiz(String bizCode);
|
||||||
|
|
||||||
|
/** 主实体可选字段(与反射接口字段结构一致) */
|
||||||
|
List<PrintBizFieldItemVO> listMainFields(String bizCode);
|
||||||
|
|
||||||
|
/** 明细数据来源槽位 */
|
||||||
|
List<PrintBizDetailSlotVO> listDetailSlots(String bizCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细槽位对应字段,fieldKey 已带「属性名.」前缀(与 {@link org.jeecg.modules.print.util.PrintBizDetailPropertyScanner#listPrefixedDetailFields}
|
||||||
|
* 一致)。
|
||||||
|
*/
|
||||||
|
List<PrintBizFieldItemVO> listPrefixedDetailFields(String bizCode, String detailProperty, String slotKind);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import org.jeecg.common.api.vo.Result;
|
|||||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
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.PrintBizTemplateBind;
|
||||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||||
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
|
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
|
||||||
@@ -57,6 +58,9 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
|||||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||||
@Autowired private SimpMessagingTemplate messagingTemplate;
|
@Autowired private SimpMessagingTemplate messagingTemplate;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||||
|
|
||||||
@Operation(summary = "业务打印绑定-分页列表")
|
@Operation(summary = "业务打印绑定-分页列表")
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@RequiresPermissions("print:bizBind:list")
|
@RequiresPermissions("print:bizBind:list")
|
||||||
@@ -185,7 +189,11 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
|||||||
if (StringUtils.isBlank(bizCode)) {
|
if (StringUtils.isBlank(bizCode)) {
|
||||||
return Result.error("bizCode 不能为空");
|
return Result.error("bizCode 不能为空");
|
||||||
}
|
}
|
||||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
|
String code = bizCode.trim();
|
||||||
|
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||||
|
return Result.OK(fieldCatalogProvider.listDetailSlots(code));
|
||||||
|
}
|
||||||
|
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||||
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
||||||
return Result.OK(Collections.emptyList());
|
return Result.OK(Collections.emptyList());
|
||||||
}
|
}
|
||||||
@@ -206,7 +214,12 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
|||||||
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
|
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
|
||||||
return Result.error("bizCode 与 detailProperty 不能为空");
|
return Result.error("bizCode 与 detailProperty 不能为空");
|
||||||
}
|
}
|
||||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
|
String code = bizCode.trim();
|
||||||
|
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||||
|
return Result.OK(
|
||||||
|
fieldCatalogProvider.listPrefixedDetailFields(code, detailProperty.trim(), slotKind.trim()));
|
||||||
|
}
|
||||||
|
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||||
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
||||||
return Result.OK(Collections.emptyList());
|
return Result.OK(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.jeecg.modules.print.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -10,11 +11,11 @@ import java.util.Set;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.modules.print.entity.PrintBizPermEntity;
|
import org.jeecg.modules.print.entity.PrintBizPermEntity;
|
||||||
|
import org.jeecg.modules.print.catalog.IPrintBizEntityFieldCatalogProvider;
|
||||||
import org.jeecg.modules.print.mapper.PrintBizPermEntityMapper;
|
import org.jeecg.modules.print.mapper.PrintBizPermEntityMapper;
|
||||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||||
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
|
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
|
||||||
import org.jeecg.modules.print.util.PrintBizMenuEntityInference;
|
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.print.vo.PrintBizTypeVO;
|
||||||
import org.jeecg.modules.system.entity.SysPermission;
|
import org.jeecg.modules.system.entity.SysPermission;
|
||||||
import org.jeecg.modules.system.service.ISysPermissionService;
|
import org.jeecg.modules.system.service.ISysPermissionService;
|
||||||
@@ -31,6 +32,9 @@ public class PrintBizPermEntityServiceImpl
|
|||||||
|
|
||||||
@Autowired private ISysPermissionService sysPermissionService;
|
@Autowired private ISysPermissionService sysPermissionService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PrintBizPermEntity getByPermId(String permId) {
|
public PrintBizPermEntity getByPermId(String permId) {
|
||||||
if (StringUtils.isBlank(permId)) {
|
if (StringUtils.isBlank(permId)) {
|
||||||
@@ -41,15 +45,34 @@ public class PrintBizPermEntityServiceImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PrintBizTypeVO> listAllBizTypeVOs() {
|
public List<PrintBizTypeVO> listAllBizTypeVOs() {
|
||||||
List<PrintBizTypeVO> out = new ArrayList<>();
|
List<PrintBizPermEntity> rows = list();
|
||||||
// 仅使用 print_biz_perm_entity,避免全库扫菜单+反射导致接口超时;由「保存白名单」时 upsert 写入表
|
if (rows == null || rows.isEmpty()) {
|
||||||
for (PrintBizPermEntity row : list()) {
|
return new ArrayList<>();
|
||||||
if (row == null || StringUtils.isBlank(row.getPermId())) {
|
}
|
||||||
continue;
|
List<String> 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) {
|
Map<String, SysPermission> permMap = loadPermissionMap(permIds);
|
||||||
out.add(vo);
|
List<PrintBizTypeVO> 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;
|
return out;
|
||||||
@@ -60,15 +83,40 @@ public class PrintBizPermEntityServiceImpl
|
|||||||
if (whitelistPermIds == null || whitelistPermIds.isEmpty()) {
|
if (whitelistPermIds == null || whitelistPermIds.isEmpty()) {
|
||||||
return listAllBizTypeVOs();
|
return listAllBizTypeVOs();
|
||||||
}
|
}
|
||||||
List<PrintBizTypeVO> out = new ArrayList<>();
|
List<String> ids = new ArrayList<>();
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
for (String raw : whitelistPermIds) {
|
for (String raw : whitelistPermIds) {
|
||||||
String id = StringUtils.trimToEmpty(raw);
|
String id = StringUtils.trimToEmpty(raw);
|
||||||
if (id.isEmpty()) {
|
if (id.isEmpty() || seen.contains(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PrintBizTypeVO vo = buildVoForPermId(id);
|
seen.add(id);
|
||||||
if (vo != null) {
|
ids.add(id);
|
||||||
out.add(vo);
|
}
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
Map<String, SysPermission> permMap = loadPermissionMap(ids);
|
||||||
|
Map<String, PrintBizPermEntity> entityMap = new HashMap<>(ids.size());
|
||||||
|
for (PrintBizPermEntity e : listByIds(ids)) {
|
||||||
|
if (e != null && StringUtils.isNotBlank(e.getPermId())) {
|
||||||
|
entityMap.put(e.getPermId().trim(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PrintBizTypeVO> 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;
|
return out;
|
||||||
@@ -203,37 +251,86 @@ public class PrintBizPermEntityServiceImpl
|
|||||||
/**
|
/**
|
||||||
* 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。
|
* 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。
|
||||||
*/
|
*/
|
||||||
|
private Map<String, SysPermission> loadPermissionMap(List<String> permIds) {
|
||||||
|
if (permIds == null || permIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<SysPermission> plist = sysPermissionService.listByIds(permIds);
|
||||||
|
Map<String, SysPermission> 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) {
|
private PrintBizTypeVO buildVoForPermId(String permId) {
|
||||||
if (StringUtils.isBlank(permId)) {
|
if (StringUtils.isBlank(permId)) {
|
||||||
return null;
|
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<String, SysPermission> permCache) {
|
||||||
|
if (StringUtils.isBlank(permId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String id = permId.trim();
|
||||||
|
PrintBizPermEntity rowEff = row != null ? row : getById(id);
|
||||||
String entityFqn = null;
|
String entityFqn = null;
|
||||||
if (row != null && StringUtils.isNotBlank(row.getEntityClass())) {
|
if (rowEff != null && StringUtils.isNotBlank(rowEff.getEntityClass())) {
|
||||||
entityFqn = row.getEntityClass().trim();
|
entityFqn = rowEff.getEntityClass().trim();
|
||||||
} else {
|
} else {
|
||||||
SysPermission p = sysPermissionService.getById(permId.trim());
|
SysPermission p = permCache != null ? permCache.get(id) : sysPermissionService.getById(id);
|
||||||
entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
||||||
}
|
}
|
||||||
|
boolean catalogOk =
|
||||||
|
fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(id);
|
||||||
if (StringUtils.isBlank(entityFqn)) {
|
if (StringUtils.isBlank(entityFqn)) {
|
||||||
return null;
|
if (!catalogOk) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entityFqn = StringUtils.trimToEmpty(fieldCatalogProvider.getEntityClassFqn(id));
|
||||||
}
|
}
|
||||||
Class<?> clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
|
Class<?> clazz = null;
|
||||||
List<PrintBizFieldItemVO> fields =
|
if (!catalogOk) {
|
||||||
clazz == null ? new ArrayList<>() : PrintBizEntityFieldIntrospector.listFields(clazz);
|
clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
|
||||||
if (clazz == null) {
|
if (clazz == null) {
|
||||||
// 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导
|
// 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PrintBizTypeVO vo = new PrintBizTypeVO();
|
PrintBizTypeVO vo = new PrintBizTypeVO();
|
||||||
vo.setBizCode(permId.trim());
|
vo.setBizCode(id);
|
||||||
vo.setLinkedPermissionId(permId.trim());
|
vo.setLinkedPermissionId(id);
|
||||||
vo.setBizName(resolveMenuName(permId.trim()));
|
vo.setBizName(resolveMenuName(id, permCache));
|
||||||
vo.setDescription(entityFqn);
|
String desc = StringUtils.isNotBlank(entityFqn) ? entityFqn : "";
|
||||||
vo.setFields(fields);
|
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;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveMenuName(String permId, Map<String, SysPermission> 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) {
|
private String resolveMenuName(String permId) {
|
||||||
SysPermission p = sysPermissionService.getById(permId);
|
SysPermission p = sysPermissionService.getById(permId);
|
||||||
if (p != null && StringUtils.isNotBlank(p.getName())) {
|
if (p != null && StringUtils.isNotBlank(p.getName())) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public final class PrintBizDetailPropertyScanner {
|
|||||||
for (PrintBizFieldItemVO x : raw) {
|
for (PrintBizFieldItemVO x : raw) {
|
||||||
String path = prefix + "." + x.getFieldKey();
|
String path = prefix + "." + x.getFieldKey();
|
||||||
String label = "明细「" + prefix + "」→ " + x.getLabel();
|
String label = "明细「" + prefix + "」→ " + x.getLabel();
|
||||||
out.add(new PrintBizFieldItemVO(path, label, x.getDescription()));
|
out.add(PrintBizFieldItemVO.copyWithPrefixedPath(x, path, label));
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,28 @@ package org.jeecg.modules.print.util;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Modifier;
|
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.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。
|
* 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。
|
||||||
|
*
|
||||||
|
* <p>写入缓存 JSON 时附带 {@code javaType}、{@code jdbcType}、{@code simpleKind}。
|
||||||
*/
|
*/
|
||||||
public final class PrintBizEntityFieldIntrospector {
|
public final class PrintBizEntityFieldIntrospector {
|
||||||
|
|
||||||
@@ -31,13 +43,174 @@ public final class PrintBizEntityFieldIntrospector {
|
|||||||
if ("serialVersionUID".equals(name)) {
|
if ("serialVersionUID".equals(name)) {
|
||||||
continue;
|
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();
|
c = c.getSuperclass();
|
||||||
}
|
}
|
||||||
return new ArrayList<>(ordered.values());
|
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) {
|
private static String resolveLabel(Field f) {
|
||||||
Schema schema = f.getAnnotation(Schema.class);
|
Schema schema = f.getAnnotation(Schema.class);
|
||||||
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package org.jeecg.modules.print.vo;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "业务实体可选字段")
|
@Schema(description = "业务实体可选字段")
|
||||||
public class PrintBizFieldItemVO implements Serializable {
|
public class PrintBizFieldItemVO implements Serializable {
|
||||||
|
|
||||||
@Schema(description = "字段路径(支持 a.b,与 JSON 一致)")
|
@Schema(description = "字段路径(支持 a.b,与 JSON 一致)")
|
||||||
private String fieldKey;
|
private String fieldKey;
|
||||||
|
|
||||||
@@ -19,4 +18,51 @@ public class PrintBizFieldItemVO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "说明")
|
@Schema(description = "说明")
|
||||||
private String 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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业务实体字段配置-明细槽位字段清单';
|
||||||
@@ -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;
|
||||||
@@ -22,8 +22,9 @@ export const deleteOne = (params, handleSuccess?) =>
|
|||||||
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess?.());
|
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess?.());
|
||||||
|
|
||||||
export const bizTypes = () => defHttp.get({ url: Api.bizTypes });
|
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 + 完整业务目录 */
|
/** 白名单:已勾选菜单 id + 完整业务目录 */
|
||||||
export const getPermWhitelist = () => defHttp.get({ url: Api.permWhitelist });
|
export const getPermWhitelist = () => defHttp.get({ url: Api.permWhitelist });
|
||||||
/** 勾选菜单多时后端需批量 upsert,默认 10s 易超时 */
|
/** 勾选菜单多时后端需批量 upsert,默认 10s 易超时 */
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
show-icon
|
show-icon
|
||||||
class="bind-alert"
|
class="bind-alert"
|
||||||
message="配置说明"
|
message="配置说明"
|
||||||
description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。"
|
description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段;业务字段下拉第一项为「空占位符」,表示不参与业务 JSON 取值(等同输出空)。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a-card title="基础信息" size="small" :bordered="true" class="bind-card">
|
<a-card title="基础信息" size="small" :bordered="true" class="bind-card">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<a-form-item
|
<a-form-item
|
||||||
label="业务"
|
label="业务"
|
||||||
required
|
required
|
||||||
extra="业务编码为菜单 id;后端按 print_biz_perm_entity 或菜单 component 推断主实体并反射主表字段。"
|
extra="业务编码为菜单 id;业务字段优先从缓存表读取(启动任务根据 print_biz_perm_entity 异步写入 mes_xsl_biz_entity_field_*),无缓存时再反射实体类。"
|
||||||
>
|
>
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="form.bizCode"
|
v-model:value="form.bizCode"
|
||||||
@@ -129,7 +129,6 @@
|
|||||||
>
|
>
|
||||||
同名自动匹配
|
同名自动匹配
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button size="small" @click="addPlaceholderParamRow">添加空占位</a-button>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -285,6 +284,9 @@
|
|||||||
const { createMessage } = useMessage();
|
const { createMessage } = useMessage();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
/** 下拉「空占位符」选项值(落库 fieldMappingJson 的 bizField 转为 '') */
|
||||||
|
const EMPTY_BIZ_FIELD_SENTINEL = '__PRINT_BIND_EMPTY_BIZ__';
|
||||||
|
|
||||||
interface BizTypeItem {
|
interface BizTypeItem {
|
||||||
bizCode: string;
|
bizCode: string;
|
||||||
bizName: string;
|
bizName: string;
|
||||||
@@ -361,15 +363,18 @@
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bizFieldOptionsMain = computed(() =>
|
const bizFieldOptionsMain = computed(() => {
|
||||||
unref(bizFields).map((f) => ({
|
const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }];
|
||||||
|
const rest = unref(bizFields).map((f) => ({
|
||||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||||
value: f.fieldKey,
|
value: f.fieldKey,
|
||||||
})),
|
}));
|
||||||
);
|
return [...head, ...rest];
|
||||||
|
});
|
||||||
|
|
||||||
/** 主表 + 明细前缀字段(用于明细/表格占位) */
|
/** 主表 + 明细前缀字段(用于明细/表格占位) */
|
||||||
const bizFieldOptions = computed(() => {
|
const bizFieldOptions = computed(() => {
|
||||||
|
const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }];
|
||||||
const main = unref(bizFields).map((f) => ({
|
const main = unref(bizFields).map((f) => ({
|
||||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||||
value: f.fieldKey,
|
value: f.fieldKey,
|
||||||
@@ -378,9 +383,25 @@
|
|||||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||||
value: 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 */
|
/** 主表参数行:模板 elementType 为 param */
|
||||||
const mappingRowsParam = computed(() =>
|
const mappingRowsParam = computed(() =>
|
||||||
unref(mappingRows).filter((r) => (r.elementType || '') === 'param'),
|
unref(mappingRows).filter((r) => (r.elementType || '') === 'param'),
|
||||||
@@ -615,7 +636,7 @@
|
|||||||
const hit = saved.find((x) => x.templateField === templateField);
|
const hit = saved.find((x) => x.templateField === templateField);
|
||||||
return {
|
return {
|
||||||
templateField,
|
templateField,
|
||||||
bizField: hit?.bizField ?? '',
|
bizField: hit !== undefined ? normalizeBizFieldForUi(hit.bizField) : undefined,
|
||||||
elementType: t?.elementType || 'param',
|
elementType: t?.elementType || 'param',
|
||||||
titleHint:
|
titleHint:
|
||||||
t?.titleHint ||
|
t?.titleHint ||
|
||||||
@@ -637,26 +658,13 @@
|
|||||||
mappingRows.value = [...unref(mappingRows)];
|
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() {
|
function buildFieldMappingJson() {
|
||||||
const arr = unref(mappingRows)
|
const arr = unref(mappingRows)
|
||||||
.filter((r) => r.templateField)
|
.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);
|
return JSON.stringify(arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user