新增打印业务绑定功能,整合原材料卡片和入场记录的打印模板配置,优化打印数据准备逻辑。新增打印机查询接口,提升打印服务的灵活性和用户体验。同时,重构相关控制器以支持新的打印常量定义,增强系统的可维护性和扩展性。

This commit is contained in:
geht
2026-05-13 17:25:13 +08:00
parent c3f8190537
commit 642cecb04d
29 changed files with 2265 additions and 217 deletions

View File

@@ -0,0 +1,43 @@
package org.jeecg.modules.print.bootstrap;
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 项目启动后异步从数据库 {@code sys_permission} 读取真实 component / component_name预热
* {@code print_biz_perm_entity},不依赖人工白名单操作。关闭:{@code jeecg.print.biz-perm-warmup=false}
*/
@Slf4j
@Component
@Order(2000)
public class PrintBizPermEntityWarmupRunner implements ApplicationRunner {
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
@Value("${jeecg.print.biz-perm-warmup:true}")
private boolean warmupEnabled;
@Override
public void run(ApplicationArguments args) {
if (!warmupEnabled) {
log.info("打印菜单-实体映射预热已关闭jeecg.print.biz-perm-warmup=false");
return;
}
CompletableFuture.runAsync(
() -> {
try {
log.info("打印菜单-实体映射:开始异步预热(来源 sys_permission 表)");
printBizPermEntityService.warmupMappingsFromSysPermissionTable();
} catch (Exception e) {
log.warn("打印菜单-实体映射预热失败(不影响系统启动)", e);
}
});
}
}

View File

@@ -1,73 +0,0 @@
package org.jeecg.modules.print.catalog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
/**
* 业务类型及可映射字段目录(与实体 JSON 字段名一致camelCase
* 新增业务:在此注册 bizCode 与字段列表。
*/
public final class PrintBizTypeCatalog {
private static final Map<String, PrintBizTypeVO> REGISTRY = new LinkedHashMap<>();
static {
registerRawMaterialCard();
}
private PrintBizTypeCatalog() {}
private static void registerRawMaterialCard() {
PrintBizTypeVO vo = new PrintBizTypeVO();
vo.setBizCode("MES_RAW_MATERIAL_CARD");
vo.setBizName("原材料卡片");
vo.setDescription("mes_xsl_raw_material_card / MesXslRawMaterialCard");
List<PrintBizFieldItemVO> fields = new ArrayList<>();
fields.add(f("id", "主键"));
fields.add(f("barcode", "条码"));
fields.add(f("splitDetailId", "拆码明细行ID"));
fields.add(f("batchNo", "批次号"));
fields.add(f("entryDate", "入场日期"));
fields.add(f("materialId", "物料ID"));
fields.add(f("materialName", "物料名称"));
fields.add(f("materialDesc", "物料描述"));
fields.add(f("supplierId", "供应商ID"));
fields.add(f("supplierName", "供应商名称"));
fields.add(f("manufacturerMaterialName", "厂家物料名称"));
fields.add(f("shelfLife", "保质期"));
fields.add(f("totalWeight", "总重"));
fields.add(f("remainingWeight", "剩余重量"));
fields.add(f("remainingQuantity", "剩余数量"));
fields.add(f("status", "状态(字典)"));
fields.add(f("testResult", "检测结果(字典)"));
fields.add(f("warehouseArea", "库区/库位"));
fields.add(f("unloadOperator", "卸货操作人"));
fields.add(f("priorityPickup", "优先出库"));
fields.add(f("createBy", "创建人"));
fields.add(f("createTime", "创建时间"));
fields.add(f("updateBy", "更新人"));
fields.add(f("updateTime", "更新时间"));
vo.setFields(Collections.unmodifiableList(fields));
REGISTRY.put(vo.getBizCode(), vo);
}
private static PrintBizFieldItemVO f(String key, String label) {
return new PrintBizFieldItemVO(key, label, "");
}
public static List<PrintBizTypeVO> listAll() {
return new ArrayList<>(REGISTRY.values());
}
public static PrintBizTypeVO getByCode(String bizCode) {
if (bizCode == null) {
return null;
}
return REGISTRY.get(bizCode.trim());
}
}

View File

@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -20,13 +21,18 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.catalog.PrintBizTypeCatalog;
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
import org.jeecg.modules.print.service.IPrintTemplateService;
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner;
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
import org.jeecg.modules.print.util.PrintNativeTemplateFieldExtractor;
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
import org.springframework.beans.factory.annotation.Autowired;
@@ -43,6 +49,8 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Autowired private IPrintTemplateService printTemplateService;
@Autowired private IPrintBizBindPermWhitelistService printBizBindPermWhitelistService;
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
@Operation(summary = "业务打印绑定-分页列表")
@GetMapping("/list")
@@ -107,11 +115,41 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
return Result.OK("删除成功");
}
@Operation(summary = "注册的业务类型及字段目录")
@Operation(summary = "配置的菜单-实体映射及反射字段完整目录biz_code 为菜单 id")
@GetMapping("/bizTypes")
@RequiresPermissions("print:bizBind:list")
public Result<List<PrintBizTypeVO>> bizTypes() {
return Result.OK(PrintBizTypeCatalog.listAll());
return Result.OK(printBizPermEntityService.listAllBizTypeVOs());
}
/**
* 「新增/编辑业务打印绑定」业务下拉print_biz_perm_entity 中的映射,并按「打印业务白名单」过滤。
* 白名单为空表示不过滤;非空则仅保留菜单 id 在白名单内的项。
*/
@Operation(summary = "可选业务类型(白名单过滤后)")
@GetMapping("/bizTypesForBinding")
@RequiresPermissions("print:bizBind:list")
public Result<List<PrintBizTypeVO>> bizTypesForBinding() {
return Result.OK(printBizBindPermWhitelistService.listBizTypesForBinding());
}
@Operation(summary = "打印业务白名单:当前勾选的菜单 idcatalog 恒为空,避免全库反射超时)")
@GetMapping("/permWhitelist")
@RequiresPermissions("print:bizBind:whitelist")
public Result<PrintBizPermWhitelistVO> getPermWhitelist() {
PrintBizPermWhitelistVO vo = new PrintBizPermWhitelistVO();
vo.setPermIds(printBizBindPermWhitelistService.listPermIds());
vo.setCatalog(Collections.emptyList());
return Result.OK(vo);
}
@AutoLog(value = "业务打印绑定-保存打印业务白名单")
@Operation(summary = "保存打印业务白名单(按菜单 permission id")
@PostMapping("/permWhitelist")
@RequiresPermissions("print:bizBind:whitelist")
public Result<String> savePermWhitelist(@RequestBody PermWhitelistBody body) {
printBizBindPermWhitelistService.replacePermIds(body == null ? null : body.getPermIds());
return Result.OK("保存成功");
}
@Operation(summary = "解析原生模板中的占位字段bindField")
@@ -131,6 +169,47 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
return Result.OK(fields);
}
@Operation(summary = "主实体上的明细槽位List&lt;明细实体&gt; / 明细数组 / 嵌套对象),用于先选明细再反射明细字段")
@GetMapping("/detailSlots")
@RequiresPermissions("print:bizBind:list")
public Result<List<PrintBizDetailSlotVO>> detailSlots(@RequestParam(name = "bizCode") String bizCode) {
if (StringUtils.isBlank(bizCode)) {
return Result.error("bizCode 不能为空");
}
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
return Result.OK(Collections.emptyList());
}
Class<?> main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim());
if (main == null) {
return Result.OK(Collections.emptyList());
}
return Result.OK(PrintBizDetailPropertyScanner.listSlots(main));
}
@Operation(summary = "反射指定明细槽位元素类的字段fieldKey 带「属性名.」前缀,用于与模板明细占位绑定)")
@GetMapping("/bizFieldsForDetailSlot")
@RequiresPermissions("print:bizBind:list")
public Result<List<PrintBizFieldItemVO>> bizFieldsForDetailSlot(
@RequestParam(name = "bizCode") String bizCode,
@RequestParam(name = "detailProperty") String detailProperty,
@RequestParam(name = "slotKind", defaultValue = "LIST") String slotKind) {
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
return Result.error("bizCode 与 detailProperty 不能为空");
}
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
return Result.OK(Collections.emptyList());
}
Class<?> main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim());
if (main == null) {
return Result.OK(Collections.emptyList());
}
return Result.OK(
PrintBizDetailPropertyScanner.listPrefixedDetailFields(
main, detailProperty.trim(), slotKind.trim()));
}
@Operation(summary = "按业务编码查询绑定(供打印调用)")
@GetMapping("/queryByBizCode")
@RequiresPermissions("print:bizBind:list")
@@ -183,9 +262,12 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
return "打印模板不存在";
}
entity.setTemplateCode(tpl.getTemplateCode());
if (PrintBizTypeCatalog.getByCode(entity.getBizCode()) != null
&& StringUtils.isBlank(entity.getBizName())) {
entity.setBizName(PrintBizTypeCatalog.getByCode(entity.getBizCode()).getBizName());
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(entity.getBizCode());
if (bizVo == null) {
return "无效的业务:该菜单无法解析到实体类(需菜单 component 为如 xslmes/xxx/XxxList或在 print_biz_perm_entity 中配置 entity_classbiz_code 为菜单 id";
}
if (StringUtils.isBlank(entity.getBizName())) {
entity.setBizName(bizVo.getBizName());
}
return null;
}
@@ -213,4 +295,17 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
/** 业务对象 JSON对象或可解析字符串 */
private Object bizDataJson;
}
@Data
public static class PrintBizPermWhitelistVO {
/** 已勾选的 sys_permission.id空集合表示未配置不限制可选业务 */
private java.util.List<String> permIds;
/** 完整打印业务目录(含 linkedPermissionId便于对照菜单 */
private java.util.List<PrintBizTypeVO> catalog;
}
@Data
public static class PermWhitelistBody {
private java.util.List<String> permIds;
}
}

View File

@@ -0,0 +1,16 @@
package org.jeecg.modules.print.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
/** 业务打印绑定可选范围白名单sys_permission.id */
@Data
@TableName("print_biz_bind_perm_whitelist")
public class PrintBizBindPermWhitelist implements Serializable {
@TableId(type = IdType.INPUT)
private String permId;
}

View File

@@ -0,0 +1,24 @@
package org.jeecg.modules.print.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
/** 打印业务菜单权限与实体类映射biz_code 使用 perm_id */
@Data
@TableName("print_biz_perm_entity")
public class PrintBizPermEntity implements Serializable {
@TableId(value = "perm_id", type = IdType.INPUT)
private String permId;
/**
* 实体类全限定名为空表示占位。ALWAYS 保证 update 时带上 entity_class含置 null避免仅主键导致无 SET 子句。
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String entityClass;
}

View File

@@ -0,0 +1,7 @@
package org.jeecg.modules.print.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
/** 打印业务菜单白名单 */
public interface PrintBizBindPermWhitelistMapper extends BaseMapper<PrintBizBindPermWhitelist> {}

View File

@@ -0,0 +1,7 @@
package org.jeecg.modules.print.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.print.entity.PrintBizPermEntity;
/** 菜单-实体映射 */
public interface PrintBizPermEntityMapper extends BaseMapper<PrintBizPermEntity> {}

View File

@@ -0,0 +1,19 @@
package org.jeecg.modules.print.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
/** 打印业务与系统菜单白名单 */
public interface IPrintBizBindPermWhitelistService extends IService<PrintBizBindPermWhitelist> {
/** 当前白名单中的菜单 id空集合表示未配置放行全部目录中的打印业务 */
List<String> listPermIds();
/** 全量替换白名单 */
void replacePermIds(List<String> permIds);
/** 「新增业务打印绑定」下拉里可用的业务类型print_biz_perm_entity + 反射字段,受白名单过滤) */
List<PrintBizTypeVO> listBizTypesForBinding();
}

View File

@@ -0,0 +1,36 @@
package org.jeecg.modules.print.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import org.jeecg.modules.print.entity.PrintBizPermEntity;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
/** 菜单与实体映射 + 反射生成业务类型 VO */
public interface IPrintBizPermEntityService extends IService<PrintBizPermEntity> {
PrintBizPermEntity getByPermId(String permId);
/** 全部映射对应的业务类型(含反射字段);用于目录展示与白名单弹窗 catalog */
List<PrintBizTypeVO> listAllBizTypeVOs();
/**
* 按白名单过滤permIds 为空表示不过滤(返回全部映射);非空则仅保留 perm_id 在集合内的项。
*/
List<PrintBizTypeVO> listBizTypeVOsFiltered(List<String> whitelistPermIds);
/**
* 解析单个菜单对应的业务类型:优先 print_biz_perm_entity否则按菜单 component 推断实体类并反射字段。
*/
PrintBizTypeVO resolveBizTypeVo(String permId);
/**
* 将白名单勾选的菜单写入/补全 print_biz_perm_entity已有可加载的 entity_class 不覆盖;否则按菜单 component 推断并插入或更新。
*/
void upsertMappingsForWhitelist(List<String> permIds);
/**
* 启动预热:扫描数据库 sys_permissionmenu_type=1 且 component 非空),用表中真实的 component / component_name 推断实体类并写入
* print_biz_perm_entity不覆盖已有可加载的 entity_class
*/
void warmupMappingsFromSysPermissionTable();
}

View File

@@ -0,0 +1,67 @@
package org.jeecg.modules.print.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
import org.jeecg.modules.print.mapper.PrintBizBindPermWhitelistMapper;
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PrintBizBindPermWhitelistServiceImpl
extends ServiceImpl<PrintBizBindPermWhitelistMapper, PrintBizBindPermWhitelist>
implements IPrintBizBindPermWhitelistService {
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
@Override
public List<String> listPermIds() {
return list().stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void replacePermIds(List<String> permIds) {
remove(
Wrappers.<PrintBizBindPermWhitelist>lambdaQuery()
.isNotNull(PrintBizBindPermWhitelist::getPermId));
if (permIds == null || permIds.isEmpty()) {
return;
}
Set<String> seen = new HashSet<>();
List<PrintBizBindPermWhitelist> batch = new ArrayList<>();
for (String raw : permIds) {
String id = StringUtils.trimToEmpty(raw);
if (id.isEmpty() || seen.contains(id)) {
continue;
}
seen.add(id);
PrintBizBindPermWhitelist row = new PrintBizBindPermWhitelist();
row.setPermId(id);
batch.add(row);
}
if (!batch.isEmpty()) {
// 分段写入,避免单次 SQL 包过大
saveBatch(batch, 500);
List<String> savedIds =
batch.stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList());
printBizPermEntityService.upsertMappingsForWhitelist(savedIds);
}
}
@Override
public List<PrintBizTypeVO> listBizTypesForBinding() {
List<String> whitelist = listPermIds();
return printBizPermEntityService.listBizTypeVOsFiltered(whitelist);
}
}

View File

@@ -0,0 +1,244 @@
package org.jeecg.modules.print.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.entity.PrintBizPermEntity;
import org.jeecg.modules.print.mapper.PrintBizPermEntityMapper;
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
import org.jeecg.modules.print.util.PrintBizMenuEntityInference;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.print.vo.PrintBizTypeVO;
import org.jeecg.modules.system.entity.SysPermission;
import org.jeecg.modules.system.service.ISysPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class PrintBizPermEntityServiceImpl
extends ServiceImpl<PrintBizPermEntityMapper, PrintBizPermEntity>
implements IPrintBizPermEntityService {
@Autowired private ISysPermissionService sysPermissionService;
@Override
public PrintBizPermEntity getByPermId(String permId) {
if (StringUtils.isBlank(permId)) {
return null;
}
return getById(permId.trim());
}
@Override
public List<PrintBizTypeVO> listAllBizTypeVOs() {
List<PrintBizTypeVO> out = new ArrayList<>();
// 仅使用 print_biz_perm_entity避免全库扫菜单+反射导致接口超时;由「保存白名单」时 upsert 写入表
for (PrintBizPermEntity row : list()) {
if (row == null || StringUtils.isBlank(row.getPermId())) {
continue;
}
PrintBizTypeVO vo = buildVoForPermId(row.getPermId());
if (vo != null) {
out.add(vo);
}
}
return out;
}
@Override
public List<PrintBizTypeVO> listBizTypeVOsFiltered(List<String> whitelistPermIds) {
if (whitelistPermIds == null || whitelistPermIds.isEmpty()) {
return listAllBizTypeVOs();
}
List<PrintBizTypeVO> out = new ArrayList<>();
for (String raw : whitelistPermIds) {
String id = StringUtils.trimToEmpty(raw);
if (id.isEmpty()) {
continue;
}
PrintBizTypeVO vo = buildVoForPermId(id);
if (vo != null) {
out.add(vo);
}
}
return out;
}
@Override
public PrintBizTypeVO resolveBizTypeVo(String permId) {
return buildVoForPermId(permId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void upsertMappingsForWhitelist(List<String> permIds) {
if (permIds == null || permIds.isEmpty()) {
return;
}
List<String> ids = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (String raw : permIds) {
String id = StringUtils.trimToEmpty(raw);
if (id.isEmpty() || seen.contains(id)) {
continue;
}
seen.add(id);
ids.add(id);
}
if (ids.isEmpty()) {
return;
}
// 批量查询,避免勾选数百条时 N 次 getById 导致超时
Map<String, PrintBizPermEntity> existingMap = new HashMap<>(ids.size());
for (PrintBizPermEntity e : listByIds(ids)) {
if (e != null && StringUtils.isNotBlank(e.getPermId())) {
existingMap.put(e.getPermId(), e);
}
}
Map<String, SysPermission> permMap = new HashMap<>(ids.size());
for (SysPermission p : sysPermissionService.listByIds(ids)) {
if (p != null && StringUtils.isNotBlank(p.getId())) {
permMap.put(p.getId(), p);
}
}
List<PrintBizPermEntity> toSave = new ArrayList<>();
for (String id : ids) {
PrintBizPermEntity existing = existingMap.get(id);
// 已有可加载的实体类配置则保留,不再覆盖
if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) {
Class<?> loaded =
PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim());
if (loaded != null) {
continue;
}
}
SysPermission p = permMap.get(id);
String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p);
PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity();
row.setPermId(id);
if (StringUtils.isNotBlank(inferred)
&& PrintBizEntityFieldIntrospector.tryLoadClass(inferred) != null) {
row.setEntityClass(inferred);
} else {
// 勾选即落库(与勾选数量一致);无法推断或类不在 classpath 时占位 NULL可手工 UPDATE
row.setEntityClass(null);
}
toSave.add(row);
}
if (!toSave.isEmpty()) {
saveOrUpdateBatch(toSave, 500);
}
}
@Override
public void warmupMappingsFromSysPermissionTable() {
List<SysPermission> menus =
sysPermissionService
.lambdaQuery()
.eq(SysPermission::getMenuType, 1)
.isNotNull(SysPermission::getComponent)
.list();
if (menus == null || menus.isEmpty()) {
log.info("打印菜单-实体映射预热:无子菜单数据");
return;
}
List<String> ids =
menus.stream()
.map(SysPermission::getId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
Map<String, PrintBizPermEntity> existingMap = new HashMap<>(ids.size());
for (PrintBizPermEntity e : listByIds(ids)) {
if (e != null && StringUtils.isNotBlank(e.getPermId())) {
existingMap.put(e.getPermId(), e);
}
}
List<PrintBizPermEntity> toSave = new ArrayList<>();
for (SysPermission p : menus) {
if (p == null || StringUtils.isBlank(p.getId())) {
continue;
}
String comp = p.getComponent();
if (StringUtils.isBlank(comp)
|| comp.contains("layouts")
|| comp.contains("RouteView")
|| comp.contains("ParentView")) {
continue;
}
PrintBizPermEntity existing = existingMap.get(p.getId());
if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) {
if (PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim()) != null) {
continue;
}
}
String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p);
if (StringUtils.isBlank(inferred)
|| PrintBizEntityFieldIntrospector.tryLoadClass(inferred) == null) {
continue;
}
PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity();
row.setPermId(p.getId());
row.setEntityClass(inferred);
toSave.add(row);
}
if (toSave.isEmpty()) {
log.info("打印菜单-实体映射预热:无新增可解析项");
return;
}
saveOrUpdateBatch(toSave, 500);
log.info("打印菜单-实体映射预热完成,本次写入/更新 {} 条(数据来自 sys_permission 表中的 component/component_name", toSave.size());
}
/**
* 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。
*/
private PrintBizTypeVO buildVoForPermId(String permId) {
if (StringUtils.isBlank(permId)) {
return null;
}
PrintBizPermEntity row = getById(permId.trim());
String entityFqn = null;
if (row != null && StringUtils.isNotBlank(row.getEntityClass())) {
entityFqn = row.getEntityClass().trim();
} else {
SysPermission p = sysPermissionService.getById(permId.trim());
entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p);
}
if (StringUtils.isBlank(entityFqn)) {
return null;
}
Class<?> clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
List<PrintBizFieldItemVO> fields =
clazz == null ? new ArrayList<>() : PrintBizEntityFieldIntrospector.listFields(clazz);
if (clazz == null) {
// 类不在 classpath模块未引入时不生成下拉项避免空字段误导
return null;
}
PrintBizTypeVO vo = new PrintBizTypeVO();
vo.setBizCode(permId.trim());
vo.setLinkedPermissionId(permId.trim());
vo.setBizName(resolveMenuName(permId.trim()));
vo.setDescription(entityFqn);
vo.setFields(fields);
return vo;
}
private String resolveMenuName(String permId) {
SysPermission p = sysPermissionService.getById(permId);
if (p != null && StringUtils.isNotBlank(p.getName())) {
return p.getName();
}
return permId;
}
}

View File

@@ -43,11 +43,33 @@ public final class PrintBizDataMappingUtil {
if (cur == null || p.isEmpty()) {
return null;
}
cur = cur.get(p);
if (cur.isArray()) {
if (isNonNegativeIntString(p)) {
cur = cur.get(Integer.parseInt(p));
} else {
JsonNode first = cur.size() > 0 ? cur.get(0) : null;
cur = first != null ? first.get(p) : null;
}
} else {
cur = cur.get(p);
}
}
return cur;
}
/** 全数字段按数组下标解析,否则在 JSON 数组上取首行再取属性(如 lines.qty 表示第一行明细的 qty */
private static boolean isNonNegativeIntString(String p) {
if (p == null || p.isEmpty()) {
return false;
}
for (int i = 0; i < p.length(); i++) {
if (!Character.isDigit(p.charAt(i))) {
return false;
}
}
return true;
}
private static void setPath(ObjectNode target, String path, JsonNode value) {
if (target == null || StringUtils.isBlank(path)) {
return;

View File

@@ -0,0 +1,185 @@
package org.jeecg.modules.print.util;
import io.swagger.v3.oas.annotations.media.Schema;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
/**
* 扫描主实体类上可作为「明细」的来源属性:{@code List&lt;Entity&gt;} / {@code Set&lt;Entity&gt;} / 数组 / 嵌套业务对象。
*/
public final class PrintBizDetailPropertyScanner {
private PrintBizDetailPropertyScanner() {}
/**
* 根据已选明细槽位解析元素类型LIST 取集合元素类或数组组件类型OBJECT 取嵌套属性类型。
*
* @param slotKind LIST 或 OBJECT与 {@link PrintBizDetailSlotVO#getSlotKind()} 一致)
*/
public static Class<?> resolveItemClassForSlot(
Class<?> mainClazz, String propertyName, String slotKind) {
if (mainClazz == null || StringUtils.isBlank(propertyName)) {
return null;
}
Field f = findDeclaredField(mainClazz, propertyName.trim());
if (f == null) {
return null;
}
if ("OBJECT".equalsIgnoreCase(StringUtils.trimToEmpty(slotKind))) {
Class<?> t = f.getType();
return isLikelyBizBean(t) ? t : null;
}
Class<?> elem = resolveCollectionElementClass(f);
if (elem != null) {
return elem;
}
if (f.getType().isArray()) {
Class<?> comp = f.getType().getComponentType();
return isSimpleOrJdkValueType(comp) ? null : comp;
}
return null;
}
private static Field findDeclaredField(Class<?> start, String name) {
Class<?> c = start;
while (c != null && c != Object.class) {
try {
return c.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
c = c.getSuperclass();
}
}
return null;
}
/** 明细元素类型反射字段fieldKey 已带「属性名.」前缀,便于与模板明细占位映射 */
public static List<PrintBizFieldItemVO> listPrefixedDetailFields(
Class<?> mainClazz, String propertyName, String slotKind) {
Class<?> item = resolveItemClassForSlot(mainClazz, propertyName, slotKind);
if (item == null) {
return Collections.emptyList();
}
List<PrintBizFieldItemVO> raw = PrintBizEntityFieldIntrospector.listFields(item);
String prefix = propertyName.trim();
List<PrintBizFieldItemVO> out = new ArrayList<>(raw.size());
for (PrintBizFieldItemVO x : raw) {
String path = prefix + "." + x.getFieldKey();
String label = "明细「" + prefix + "」→ " + x.getLabel();
out.add(new PrintBizFieldItemVO(path, label, x.getDescription()));
}
return out;
}
public static List<PrintBizDetailSlotVO> listSlots(Class<?> mainClazz) {
Map<String, PrintBizDetailSlotVO> ordered = new LinkedHashMap<>();
Class<?> c = mainClazz;
while (c != null && c != Object.class) {
for (Field f : c.getDeclaredFields()) {
int mod = f.getModifiers();
if (Modifier.isStatic(mod) || f.isSynthetic()) {
continue;
}
String name = f.getName();
if ("serialVersionUID".equals(name)) {
continue;
}
Class<?> elem = resolveCollectionElementClass(f);
if (elem != null && isLikelyBizBean(elem)) {
ordered.putIfAbsent(
name,
new PrintBizDetailSlotVO(name, elem.getName(), "LIST", resolveFieldLabel(f)));
continue;
}
Class<?> ft = f.getType();
if (ft.isArray() && !isSimpleOrJdkValueType(ft.getComponentType())) {
Class<?> comp = ft.getComponentType();
ordered.putIfAbsent(
name, new PrintBizDetailSlotVO(name, comp.getName(), "LIST", resolveFieldLabel(f)));
continue;
}
if (!ft.isPrimitive()
&& !ft.getName().startsWith("java.lang")
&& !Number.class.isAssignableFrom(ft)
&& !java.util.Date.class.isAssignableFrom(ft)
&& !ft.getName().startsWith("java.time")
&& !Map.class.isAssignableFrom(ft)
&& !Collection.class.isAssignableFrom(ft)
&& isLikelyBizBean(ft)) {
ordered.putIfAbsent(
name, new PrintBizDetailSlotVO(name, ft.getName(), "OBJECT", resolveFieldLabel(f)));
}
}
c = c.getSuperclass();
}
return new ArrayList<>(ordered.values());
}
/** 解析 List&lt;T&gt; / Set&lt;T&gt; 的元素类型 T */
private static Class<?> resolveCollectionElementClass(Field f) {
Type gt = f.getGenericType();
if (!(gt instanceof ParameterizedType)) {
return null;
}
ParameterizedType pt = (ParameterizedType) gt;
Type raw = pt.getRawType();
if (!(raw instanceof Class<?> rc) || !Collection.class.isAssignableFrom(rc)) {
return null;
}
Type[] args = pt.getActualTypeArguments();
if (args.length != 1) {
return null;
}
Type arg0 = args[0];
if (arg0 instanceof Class<?>) {
Class<?> ac = (Class<?>) arg0;
return isSimpleOrJdkValueType(ac) ? null : ac;
}
return null;
}
private static boolean isSimpleOrJdkValueType(Class<?> cl) {
if (cl == null || cl.isPrimitive()) {
return true;
}
if (cl.isEnum()) {
return true;
}
String n = cl.getName();
if (n.startsWith("java.lang") || n.startsWith("java.time")) {
return true;
}
if (Number.class.isAssignableFrom(cl) || java.util.Date.class.isAssignableFrom(cl)) {
return true;
}
return false;
}
/** 非 JDK 简单值、非集合/Map 的自定义类型视为可映射明细实体 */
private static boolean isLikelyBizBean(Class<?> cl) {
return cl != null && !cl.isEnum() && !isSimpleOrJdkValueType(cl) && !Map.class.isAssignableFrom(cl);
}
private static String resolveFieldLabel(Field f) {
Schema schema = f.getAnnotation(Schema.class);
if (schema != null && StringUtils.isNotBlank(schema.description())) {
return schema.description().trim();
}
Excel excel = f.getAnnotation(Excel.class);
if (excel != null && StringUtils.isNotBlank(excel.name())) {
return excel.name().trim();
}
return f.getName();
}
}

View File

@@ -0,0 +1,64 @@
package org.jeecg.modules.print.util;
import io.swagger.v3.oas.annotations.media.Schema;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecgframework.poi.excel.annotation.Excel;
/**
* 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。
*/
public final class PrintBizEntityFieldIntrospector {
private PrintBizEntityFieldIntrospector() {}
public static List<PrintBizFieldItemVO> listFields(Class<?> clazz) {
Map<String, PrintBizFieldItemVO> ordered = new LinkedHashMap<>();
Class<?> c = clazz;
while (c != null && c != Object.class) {
for (Field f : c.getDeclaredFields()) {
int mod = f.getModifiers();
if (Modifier.isStatic(mod) || f.isSynthetic()) {
continue;
}
String name = f.getName();
if ("serialVersionUID".equals(name)) {
continue;
}
ordered.putIfAbsent(name, new PrintBizFieldItemVO(name, resolveLabel(f), ""));
}
c = c.getSuperclass();
}
return new ArrayList<>(ordered.values());
}
private static String resolveLabel(Field f) {
Schema schema = f.getAnnotation(Schema.class);
if (schema != null && StringUtils.isNotBlank(schema.description())) {
return schema.description().trim();
}
Excel excel = f.getAnnotation(Excel.class);
if (excel != null && StringUtils.isNotBlank(excel.name())) {
return excel.name().trim();
}
return f.getName();
}
/** 按全限定类名加载 Class失败返回 null */
public static Class<?> tryLoadClass(String entityClassFqn) {
if (StringUtils.isBlank(entityClassFqn)) {
return null;
}
try {
return Class.forName(entityClassFqn.trim());
} catch (Throwable e) {
return null;
}
}
}

View File

@@ -0,0 +1,94 @@
package org.jeecg.modules.print.util;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.system.entity.SysPermission;
/**
* 根据菜单 component / componentName 推断实体类全名。<br>
* 典型:<br>
* - xslmes/mesXslWarehouseArea/MesXslWarehouseAreaList<br>
* - mes/materialinfo/index + componentName=MesMaterialList → org.jeecg.modules.mes.material.entity.MesMaterial
*/
public final class PrintBizMenuEntityInference {
/** MES 物料等菜单实体默认落在该包下(与工程 mes.material.entity 一致) */
private static final String MES_MATERIAL_ENTITY_PKG = "org.jeecg.modules.mes.material.entity.";
private PrintBizMenuEntityInference() {}
/**
* @return 实体类全限定名;无法推断时返回 null按钮、目录等
*/
public static String inferEntityClassFqn(SysPermission permission) {
if (permission == null) {
return null;
}
if (Objects.equals(permission.getMenuType(), 2)) {
return null;
}
String comp = permission.getComponent();
if (StringUtils.isBlank(comp)) {
return null;
}
String c = comp.trim();
if (c.contains("layouts")
|| c.contains("RouteView")
|| c.contains("ParentView")
|| c.startsWith("http")) {
return null;
}
// Jeecg Vuecomponent 常为 mes/xxx/index实体类名在 component_name如 MesMaterialList
if (c.startsWith("mes/") && c.endsWith("/index")) {
String fromMesIndex =
tryInferMesMaterialModuleFromComponentName(permission.getComponentName());
if (StringUtils.isNotBlank(fromMesIndex)) {
return fromMesIndex;
}
}
String[] segs = c.split("/");
if (segs.length < 2) {
return null;
}
String last = segs[segs.length - 1];
if (StringUtils.isBlank(last)) {
return null;
}
String simple =
last.endsWith("List") ? last.substring(0, last.length() - 4) : last;
if (simple.isEmpty()) {
return null;
}
String module = segs[0];
if (StringUtils.isBlank(module)) {
return null;
}
String trial = "org.jeecg.modules." + module + ".entity." + simple;
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
return trial;
}
return null;
}
/** mes 模块下 index 路由:用 componentNameMesXxxList推断 mes.material.entity.MesXxx */
private static String tryInferMesMaterialModuleFromComponentName(String componentName) {
if (StringUtils.isBlank(componentName)) {
return null;
}
String cn = componentName.trim();
if (!cn.endsWith("List")) {
return null;
}
String simple = cn.substring(0, cn.length() - 4);
if (simple.isEmpty()) {
return null;
}
String trial = MES_MATERIAL_ENTITY_PKG + simple;
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
return trial;
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
package org.jeecg.modules.print.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** 主实体上可作为「明细数据来源」的属性(集合元素类型或嵌套对象类型) */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "业务实体明细槽位")
public class PrintBizDetailSlotVO implements Serializable {
@Schema(description = "Java 属性名JSON 路径前缀,如 lines、headerExt")
private String propertyName;
@Schema(description = "明细元素类型全限定名")
private String itemEntityClassFqn;
@Schema(description = "LIST=集合明细一行映射OBJECT=嵌套对象字段映射")
private String slotKind;
@Schema(description = "展示名")
private String label;
}

View File

@@ -8,7 +8,7 @@ import lombok.Data;
@Data
@Schema(description = "可配置的业务类型")
public class PrintBizTypeVO implements Serializable {
@Schema(description = "业务编码")
@Schema(description = "业务编码(与菜单权限 id、绑定表 biz_code 一致)")
private String bizCode;
@Schema(description = "业务名称")
@@ -17,6 +17,13 @@ public class PrintBizTypeVO implements Serializable {
@Schema(description = "说明")
private String description;
/**
* 与「系统菜单」中的菜单主键sys_permission.id对应用于「打印业务白名单」按菜单勾选。
* 未设置表示未挂菜单,白名单生效时仍可出现于下拉(避免仅后端注册、尚未配菜单的业务被误过滤)。
*/
@Schema(description = "关联菜单权限IDsys_permission.id")
private String linkedPermissionId;
@Schema(description = "业务侧可用字段目录")
private List<PrintBizFieldItemVO> fields;
}