新增业务实体字段配置功能,包含主表和明细表的数据库结构定义,支持业务打印绑定的字段映射。实现字段配置的增删改查操作,优化打印数据生成逻辑,提升系统的可维护性和扩展性。同时,新增异步同步功能以支持打印模板与业务数据的实时更新。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user