原材料入库结存
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)───
|
||||
@@ -213,6 +213,9 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWarehouseArea/anon/**", "anon");
|
||||
// MES密炼物料管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
|
||||
// 打印模板免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/print/template/anon/**", "anon");
|
||||
filterChainDefinitionMap.put("/print/bizTemplateBind/anon/**", "anon");
|
||||
// 系统分类字典免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/sys/category/anon/**", "anon");
|
||||
// 桌面端用户反同步批量上报(Outbox -> /sys/sync/batch)
|
||||
|
||||
@@ -198,6 +198,36 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
|
||||
|
||||
/**
|
||||
* 免密-通过编码查询(供 WPF 桌面端匿名调用)
|
||||
*/
|
||||
@Operation(summary = "打印模板-免密通过编码查询(桌面端)")
|
||||
@GetMapping(value = "/anon/queryByCode")
|
||||
public Result<PrintTemplate> anonQueryByCode(@RequestParam(name = "code") String code) {
|
||||
PrintTemplate t = service.getByCode(code);
|
||||
if (t == null) {
|
||||
return Result.error("未找到模板: " + code);
|
||||
}
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 免密-分页列表(供 WPF 桌面端匿名调用,用于打印设置页选择模板)
|
||||
*/
|
||||
@Operation(summary = "打印模板-免密分页列表(桌面端)")
|
||||
@GetMapping(value = "/anon/list")
|
||||
public Result<IPage<PrintTemplate>> anonList(PrintTemplate query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByAsc("template_code");
|
||||
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
@AutoLog(value = "打印模板-图片分析生成原生JSON")
|
||||
@Operation(summary = "打印模板-上传图片分析为原生模板JSON(前端传 imageBase64;可接 OpenAI 兼容视觉模型)")
|
||||
@PostMapping(value = "/analyzeImageForNative")
|
||||
|
||||
@@ -18,5 +18,11 @@
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
</dependency>
|
||||
<!-- 复用打印模板模块:打印机枚举、业务绑定、PDF 提交队列 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-system-biz</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -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,16 @@
|
||||
package org.jeecg.modules.xslmes.capacity;
|
||||
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 库区展示用「实际存放量」数据源贡献者(可多实现并按 Spring Order 编排)
|
||||
*/
|
||||
public interface WarehouseAreaActualCapacityContribution {
|
||||
|
||||
/**
|
||||
* @param areas 通常为列表/分页的一段记录(内存回填,不写库)
|
||||
*/
|
||||
void contribute(List<MesXslWarehouseArea> areas);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.xslmes.capacity;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 编排库区「实际存放量」展示回填(可多 {@link WarehouseAreaActualCapacityContribution} 扩展)。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WarehouseAreaDisplayedActualCapacityMediator {
|
||||
|
||||
private final List<WarehouseAreaActualCapacityContribution> contributions;
|
||||
|
||||
public void apply(Collection<MesXslWarehouseArea> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<MesXslWarehouseArea> list = new ArrayList<>(areas);
|
||||
List<WarehouseAreaActualCapacityContribution> ordered = contributions.stream().sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
|
||||
for (WarehouseAreaActualCapacityContribution c : ordered) {
|
||||
c.contribute(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.jeecg.modules.xslmes.capacity.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.capacity.WarehouseAreaActualCapacityContribution;
|
||||
import org.jeecg.modules.xslmes.config.XslMesWarehouseAreaCapacityProperties;
|
||||
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
|
||||
import org.jeecg.modules.system.service.ISysCategoryService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用「原材料卡片」按库区(trim)汇总 remaining_quantity,回填展示字段 actual_capacity(不写库)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(100)
|
||||
@RequiredArgsConstructor
|
||||
public class RawMaterialCardRemainQtyCapacityContribution implements WarehouseAreaActualCapacityContribution {
|
||||
|
||||
private final XslMesWarehouseAreaCapacityProperties properties;
|
||||
private final MesXslRawMaterialCardMapper mesXslRawMaterialCardMapper;
|
||||
private final ISysCategoryService sysCategoryService;
|
||||
|
||||
@Override
|
||||
public void contribute(List<MesXslWarehouseArea> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!properties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowedCategories = resolveAllowedWarehouseCategoryIds();
|
||||
if (allowedCategories.isEmpty()) {
|
||||
log.debug("[WarehouseAreaActualCapacity] 原材料库分类编码/ID 均未解析到有效 warehouse_category,跳过 RawMaterialCard 策略");
|
||||
return;
|
||||
}
|
||||
|
||||
List<MesXslWarehouseArea> targets =
|
||||
areas.stream().filter(a -> a != null && allowedCategories.contains(normId(a.getWarehouseCategory())) && StringUtils.isNotBlank(a.getAreaCode()))
|
||||
.collect(Collectors.toList());
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Collection<String> codes =
|
||||
targets.stream().map(MesXslWarehouseArea::getAreaCode).map(String::trim).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
|
||||
|
||||
Map<String, Long> sumMap = loadSumMap(codes);
|
||||
for (MesXslWarehouseArea a : targets) {
|
||||
String ck = trimKey(a.getAreaCode());
|
||||
long sum = sumMap.getOrDefault(ck, 0L);
|
||||
int display;
|
||||
try {
|
||||
display = Math.toIntExact(Math.min(sum, Integer.MAX_VALUE));
|
||||
} catch (ArithmeticException ex) {
|
||||
display = Integer.MAX_VALUE;
|
||||
}
|
||||
a.setActualCapacity(display);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Long> loadSumMap(Collection<String> codes) {
|
||||
if (codes.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<MesXslAreaRemainQtySumDTO> rows = mesXslRawMaterialCardMapper.sumRemainingQtyByTrimmedWarehouseArea(codes);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Long> map = new HashMap<>(Math.max(16, rows.size() * 2));
|
||||
for (MesXslAreaRemainQtySumDTO row : rows) {
|
||||
if (row == null || row.getAreaCode() == null) {
|
||||
continue;
|
||||
}
|
||||
String k = row.getAreaCode().trim();
|
||||
if (StringUtils.isBlank(k)) {
|
||||
continue;
|
||||
}
|
||||
long v = row.getQtySum() == null ? 0L : row.getQtySum();
|
||||
map.put(k, v);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static String trimKey(String code) {
|
||||
return code == null ? "" : code.trim();
|
||||
}
|
||||
|
||||
private static String normId(String id) {
|
||||
return StringUtils.trimToEmpty(id);
|
||||
}
|
||||
|
||||
private static Set<String> normalizeIdSet(Collection<String> raw) {
|
||||
if (raw == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return raw.stream().filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** 配置的 id + 配置的 code 解析出的 id 合并 */
|
||||
private Set<String> resolveAllowedWarehouseCategoryIds() {
|
||||
Set<String> set = new HashSet<>(normalizeIdSet(properties.getRawMaterialWarehouseCategoryIds()));
|
||||
Collection<String> codes = properties.getRawMaterialWarehouseCategoryCodes();
|
||||
if (codes != null) {
|
||||
for (String c : codes) {
|
||||
if (StringUtils.isBlank(c)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String id = sysCategoryService.queryIdByCode(c.trim());
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
set.add(id.trim());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[WarehouseAreaActualCapacity] 解析原材料库分类编码失败 code={}: {}", c, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.jeecg.modules.xslmes.config;
|
||||
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.jeecg.modules.xslmes.constant.MesXslWarehouseCategory;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* 库区列表「实际存放量」展示口径配置(不写库)
|
||||
|
||||
* <p>
|
||||
|
||||
* 推荐使用 {@link #rawMaterialWarehouseCategoryCodes},启动时解析为 sys_category.id,
|
||||
|
||||
* 避免在配置里维护易变的雪花 id;也可用 {@link #rawMaterialWarehouseCategoryIds} 直接指定 id。
|
||||
|
||||
*/
|
||||
|
||||
@Data
|
||||
|
||||
@Component
|
||||
|
||||
@ConfigurationProperties(prefix = "xslmes.warehouse-area.display-actual-capacity")
|
||||
|
||||
public class XslMesWarehouseAreaCapacityProperties {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.constant;
|
||||
|
||||
/**
|
||||
* 打印业务绑定 biz_code 使用菜单 permission id;与 print_biz_perm_entity、Flyway 中原材料卡片菜单一致。
|
||||
*/
|
||||
public final class MesXslPrintConstants {
|
||||
|
||||
/** 原材料卡片页面菜单(sys_permission.id) */
|
||||
public static final String RAW_MATERIAL_CARD_PERM_ID = "1900000000000000540";
|
||||
|
||||
/** 原料入场记录页面菜单(sys_permission.id,与 Flyway 中 parent 菜单一致) */
|
||||
public static final String RAW_MATERIAL_ENTRY_PERM_ID = "1900000000000000530";
|
||||
|
||||
private MesXslPrintConstants() {}
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
package org.jeecg.modules.xslmes.constant;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MES 仓库分类(分类字典 sys_category,根编码 {@link #ROOT_CODE})
|
||||
*
|
||||
* <p>说明:历史版本曾在编码上使用 F1/F2 后缀;当前业务树已不再按楼层区分,
|
||||
* 「F1」「F2」仅可能作为编码后缀保留,请以 {@link #RAW_MATERIAL_CATEGORY_CODES}、
|
||||
* 客户/供应商库编码集合为准做兼容。</p>
|
||||
*/
|
||||
public final class MesXslWarehouseCategory {
|
||||
|
||||
@@ -10,8 +22,37 @@ public final class MesXslWarehouseCategory {
|
||||
/** 根节点编码,与 JCategorySelect 的 pcode 一致 */
|
||||
public static final String ROOT_CODE = "XSLMES_WH";
|
||||
|
||||
/** 客户库(二楼):须关联客户 */
|
||||
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F2_KH";
|
||||
/** 供应商库(二楼):须关联供应商 */
|
||||
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F2_GYS";
|
||||
/** 兼容历史:原材料库在库中可能出现的分类编码(新环境多为 F1,老数据可能仍为 F2) */
|
||||
public static final List<String> RAW_MATERIAL_CATEGORY_CODES =
|
||||
Collections.unmodifiableList(Arrays.asList("XSLMES_WH_F1_YCL", "XSLMES_WH_F2_YCL"));
|
||||
|
||||
/** 兼容历史编号的「客户库」分类编码集合 */
|
||||
private static final Set<String> CUSTOMER_CATEGORY_CODES =
|
||||
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_KH", "XSLMES_WH_F2_KH")));
|
||||
|
||||
/** 兼容历史编号的「供应商库」分类编码集合 */
|
||||
private static final Set<String> SUPPLIER_CATEGORY_CODES =
|
||||
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_GYS", "XSLMES_WH_F2_GYS")));
|
||||
|
||||
/**
|
||||
* @deprecated 请改用 {@link #isCustomerWarehouseCategoryCode(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F1_KH";
|
||||
|
||||
/**
|
||||
* @deprecated 请改用 {@link #isSupplierWarehouseCategoryCode(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F1_GYS";
|
||||
|
||||
/** 是否为「客户库」分类编码(兼容 XSLMES_WH_F1_KH / XSLMES_WH_F2_KH) */
|
||||
public static boolean isCustomerWarehouseCategoryCode(String code) {
|
||||
return CUSTOMER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
|
||||
}
|
||||
|
||||
/** 是否为「供应商库」分类编码(兼容 XSLMES_WH_F1_GYS / XSLMES_WH_F2_GYS) */
|
||||
public static boolean isSupplierWarehouseCategoryCode(String code) {
|
||||
return SUPPLIER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,20 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
@@ -33,6 +43,9 @@ import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -59,6 +72,9 @@ public class MesXslDesktopAnonController {
|
||||
private final IMesXslWarehouseService warehouseService;
|
||||
private final IMesXslWarehouseAreaService warehouseAreaService;
|
||||
private final MesXslStompNotifyService stompNotify;
|
||||
private final IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
private final IPrintTemplateService printTemplateService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// ═══════════════════════════ 车辆管理 ═══════════════════════════
|
||||
|
||||
@@ -606,6 +622,61 @@ public class MesXslDesktopAnonController {
|
||||
return Result.OK((int) count);
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "原材料卡片-免密准备原生打印数据(桌面端用)")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint")
|
||||
public Result<Map<String, Object>> rawMaterialCardAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialCard card = rawMaterialCardService.getById(id);
|
||||
if (card == null) return Result.error("未找到原材料卡片");
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
|
||||
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) return Result.error("绑定的打印模板不存在");
|
||||
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(card);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("cardId", card.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原材料卡片-免密准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密准备原生打印数据(桌面端用)")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint")
|
||||
public Result<Map<String, Object>> rawMaterialEntryAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialEntry entry = rawMaterialEntryService.getById(id);
|
||||
if (entry == null) return Result.error("未找到原料入场记录");
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
|
||||
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板");
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) return Result.error("绑定的打印模板不存在");
|
||||
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(entry);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("entryId", entry.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原料入场记录-免密准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════
|
||||
|
||||
@Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)")
|
||||
@@ -632,6 +703,7 @@ public class MesXslDesktopAnonController {
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWarehouseArea> qw = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
IPage<MesXslWarehouseArea> page = warehouseAreaService.page(new Page<>(pageNo, pageSize), qw);
|
||||
warehouseAreaService.enrichDisplayedActualCapacity(page.getRecords());
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@@ -639,7 +711,11 @@ public class MesXslDesktopAnonController {
|
||||
@GetMapping("/xslmes/mesXslWarehouseArea/anon/queryById")
|
||||
public Result<MesXslWarehouseArea> warehouseAreaAnonQueryById(@RequestParam(name = "id") String id) {
|
||||
MesXslWarehouseArea entity = warehouseAreaService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
warehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密添加")
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
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.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
|
||||
import org.jeecg.modules.print.support.PrintServerPdfJobService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
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.PutMapping;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @Description: 原材料卡片
|
||||
@@ -34,10 +57,21 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
@RequestMapping("/xslmes/mesXslRawMaterialCard")
|
||||
@Slf4j
|
||||
public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMaterialCard, IMesXslRawMaterialCardService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialCardService mesXslRawMaterialCardService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
@Autowired
|
||||
private PrintServerEnvironmentService printServerEnvironmentService;
|
||||
@Autowired
|
||||
private PrintServerPdfJobService printServerPdfJobService;
|
||||
@Autowired
|
||||
private IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
@Autowired
|
||||
private IPrintTemplateService printTemplateService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
@@ -48,7 +82,36 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
// 库区条件:去掉实体上的值避免 QueryGenerator 再拼一班;用 TRIM 与看板聚合一致(避免首尾空格导致「有汇总无明细」)
|
||||
String rawWarehouseArea = mesXslRawMaterialCard != null ? mesXslRawMaterialCard.getWarehouseArea() : null;
|
||||
String areaEq = oConvertUtils.isNotEmpty(rawWarehouseArea) ? rawWarehouseArea.trim() : "";
|
||||
boolean filterByWarehouseArea = !areaEq.isEmpty();
|
||||
if (filterByWarehouseArea) {
|
||||
mesXslRawMaterialCard.setWarehouseArea(null);
|
||||
}
|
||||
QueryWrapper<MesXslRawMaterialCard> queryWrapper = QueryGenerator.initQueryWrapper(mesXslRawMaterialCard, req.getParameterMap());
|
||||
if (filterByWarehouseArea) {
|
||||
queryWrapper.apply("TRIM(COALESCE(warehouse_area,'')) = {0}", areaEq);
|
||||
}
|
||||
// 看板等扩展查询:remaining_quantity「有剩余 / 无剩余」(非实体字段,单独拼条件)
|
||||
String remainQtyFilter = req.getParameter("remainQtyFilter");
|
||||
if ("has".equals(remainQtyFilter)) {
|
||||
queryWrapper.gt("remaining_quantity", 0);
|
||||
} else if ("none".equals(remainQtyFilter)) {
|
||||
queryWrapper.and(w -> w.isNull("remaining_quantity").or().le("remaining_quantity", 0));
|
||||
}
|
||||
String mixKw = req.getParameter("mixKeyword");
|
||||
if (StringUtils.isNotBlank(mixKw)) {
|
||||
final String kw = mixKw.trim();
|
||||
queryWrapper.and(
|
||||
w -> w.like("barcode", kw)
|
||||
.or()
|
||||
.like("batch_no", kw)
|
||||
.or()
|
||||
.like("material_name", kw)
|
||||
.or()
|
||||
.like("material_id", kw));
|
||||
}
|
||||
Page<MesXslRawMaterialCard> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslRawMaterialCard> pageList = mesXslRawMaterialCardService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
@@ -122,6 +185,88 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询可用打印机(与 /print/template/queryPrinters 返回结构一致)
|
||||
*/
|
||||
@Operation(summary = "原材料卡片-查询可用打印机")
|
||||
@GetMapping(value = "/queryPrinters")
|
||||
/** 有 list 可进页拉打印机;有 edit 可打印拉打印机,避免单权限缺另一项时 403 */
|
||||
@RequiresPermissions(
|
||||
value = {"xslmes:mes_xsl_raw_material_card:list", "xslmes:mes_xsl_raw_material_card:edit"},
|
||||
logical = Logical.OR)
|
||||
public Result<Map<String, Object>> queryPrinters() {
|
||||
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf
|
||||
*/
|
||||
@Operation(summary = "原材料卡片-准备原生打印数据")
|
||||
@GetMapping(value = "/prepareNativePrint")
|
||||
/** 与 printPdf 一致:准备模板与打印数据属于「打印」动作,使用 edit 权限 */
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
|
||||
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
|
||||
if (card == null) {
|
||||
return Result.error("未找到原材料卡片");
|
||||
}
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
|
||||
if (bind == null) {
|
||||
return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
}
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) {
|
||||
return Result.error("绑定的打印模板不存在");
|
||||
}
|
||||
ArrayNode mapping =
|
||||
PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(card);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("cardId", card.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("paperWidthMm", tpl.getPaperWidthMm());
|
||||
out.put("paperHeightMm", tpl.getPaperHeightMm());
|
||||
out.put("paperOrientation", tpl.getPaperOrientation());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原材料卡片准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端生成的 PDF Base64 提交到服务器打印机(与 /print/template/directPrintPdf 一致)
|
||||
*/
|
||||
@AutoLog(value = "原材料卡片-PDF后端打印")
|
||||
@Operation(summary = "原材料卡片-PDF后端打印")
|
||||
@PostMapping(value = "/printPdf")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
|
||||
public Result<String> printPdf(@RequestBody Map<String, Object> body) {
|
||||
String id = String.valueOf(body.getOrDefault("id", "")).trim();
|
||||
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
|
||||
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
|
||||
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return Result.error("id 不能为空");
|
||||
}
|
||||
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
|
||||
if (card == null) {
|
||||
return Result.error("未找到原材料卡片");
|
||||
}
|
||||
String prefix =
|
||||
StringUtils.isNotBlank(card.getBarcode()) ? card.getBarcode() : card.getId();
|
||||
String fn =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("原材料卡片-" + prefix + ".pdf");
|
||||
return printServerPdfJobService.submitPdfBase64(
|
||||
printerName, pdfBase64, fn, "RAW_CARD_" + prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*/
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
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.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
|
||||
import org.jeecg.modules.print.support.PrintServerPdfJobService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
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.PutMapping;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @Description: 原料入场记录
|
||||
@@ -40,6 +62,16 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
private IMesXslRawMaterialEntryService mesXslRawMaterialEntryService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
@Autowired
|
||||
private PrintServerEnvironmentService printServerEnvironmentService;
|
||||
@Autowired
|
||||
private PrintServerPdfJobService printServerPdfJobService;
|
||||
@Autowired
|
||||
private IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
@Autowired
|
||||
private IPrintTemplateService printTemplateService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Operation(summary = "原料入场记录-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
@@ -117,6 +149,79 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
return Result.OK(mesXslRawMaterialEntry);
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-查询可用打印机")
|
||||
@GetMapping(value = "/queryPrinters")
|
||||
@RequiresPermissions(
|
||||
value = {"xslmes:mes_xsl_raw_material_entry:list", "xslmes:mes_xsl_raw_material_entry:edit"},
|
||||
logical = Logical.OR)
|
||||
public Result<Map<String, Object>> queryPrinters() {
|
||||
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf
|
||||
*/
|
||||
@Operation(summary = "原料入场记录-准备原生打印数据")
|
||||
@GetMapping(value = "/prepareNativePrint")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
|
||||
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
|
||||
if (entry == null) {
|
||||
return Result.error("未找到原料入场记录");
|
||||
}
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
|
||||
if (bind == null) {
|
||||
return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板");
|
||||
}
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) {
|
||||
return Result.error("绑定的打印模板不存在");
|
||||
}
|
||||
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(entry);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("entryId", entry.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("paperWidthMm", tpl.getPaperWidthMm());
|
||||
out.put("paperHeightMm", tpl.getPaperHeightMm());
|
||||
out.put("paperOrientation", tpl.getPaperOrientation());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原料入场记录准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoLog(value = "原料入场记录-PDF后端打印")
|
||||
@Operation(summary = "原料入场记录-PDF后端打印")
|
||||
@PostMapping(value = "/printPdf")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
|
||||
public Result<String> printPdf(@RequestBody Map<String, Object> body) {
|
||||
String id = String.valueOf(body.getOrDefault("id", "")).trim();
|
||||
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
|
||||
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
|
||||
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return Result.error("id 不能为空");
|
||||
}
|
||||
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
|
||||
if (entry == null) {
|
||||
return Result.error("未找到原料入场记录");
|
||||
}
|
||||
String prefix =
|
||||
StringUtils.isNotBlank(entry.getBarcode()) ? entry.getBarcode() : entry.getId();
|
||||
String fn =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("原料入场记录-" + prefix + ".pdf");
|
||||
return printServerPdfJobService.submitPdfBase64(
|
||||
printerName, pdfBase64, fn, "RAW_ENTRY_" + prefix);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslRawMaterialEntry mesXslRawMaterialEntry) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 原材料库区看板:库区主数据 + 原材料卡片聚合
|
||||
*/
|
||||
@Tag(name = "原材料库区看板")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslRawMaterialWarehouseBoard")
|
||||
@Slf4j
|
||||
public class MesXslRawMaterialWarehouseBoardController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialWarehouseBoardService mesXslRawMaterialWarehouseBoardService;
|
||||
|
||||
@Operation(summary = "原材料库区看板-汇总数据")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_warehouse_board:list")
|
||||
@GetMapping(value = "/board")
|
||||
public Result<MesXslRawMaterialWarehouseBoardVO> board(
|
||||
@RequestParam(name = "warehouseId", required = false) String warehouseId,
|
||||
@RequestParam(name = "keyword", required = false) String keyword,
|
||||
@RequestParam(name = "measureType", required = false) String measureType) {
|
||||
MesXslRawMaterialWarehouseBoardVO data = mesXslRawMaterialWarehouseBoardService.queryBoard(warehouseId, keyword, measureType);
|
||||
return Result.OK(data);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 org.apache.shiro.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -13,15 +14,22 @@ 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.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -36,6 +44,9 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
@Autowired
|
||||
private IMesXslWarehouseAreaService mesXslWarehouseAreaService;
|
||||
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
|
||||
@@ -49,6 +60,7 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
Page<MesXslWarehouseArea> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslWarehouseArea> pageList = mesXslWarehouseAreaService.page(page, queryWrapper);
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(pageList.getRecords());
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@@ -166,13 +178,33 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslWarehouseArea mesXslWarehouseArea) {
|
||||
return super.exportXls(request, mesXslWarehouseArea, MesXslWarehouseArea.class, "MES库区管理");
|
||||
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String selections = request.getParameter("selections");
|
||||
if (oConvertUtils.isNotEmpty(selections)) {
|
||||
queryWrapper.in("id", Arrays.asList(selections.split(",")));
|
||||
}
|
||||
List<MesXslWarehouseArea> exportList = mesXslWarehouseAreaService.list(queryWrapper);
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(exportList);
|
||||
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||
mv.addObject(NormalExcelConstants.FILE_NAME, "MES库区管理");
|
||||
mv.addObject(NormalExcelConstants.CLASS, MesXslWarehouseArea.class);
|
||||
ExportParams exportParams = new ExportParams("MES库区管理报表", "导出人:" + sysUser.getRealname(), "MES库区管理", ExcelType.XSSF);
|
||||
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
||||
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
|
||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
||||
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
|
||||
if (oConvertUtils.isNotEmpty(exportFields)) {
|
||||
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:importExcel")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jeecg.modules.xslmes.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 按库区(trim 后编码)聚合剩余数量汇总
|
||||
*/
|
||||
@Data
|
||||
public class MesXslAreaRemainQtySumDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 库区编码(已 trim) */
|
||||
private String areaCode;
|
||||
|
||||
/** 剩余数量合计 */
|
||||
private Long qtySum;
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -1,7 +1,12 @@
|
||||
package org.jeecg.modules.xslmes.mapper;
|
||||
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 原材料卡片
|
||||
@@ -10,4 +15,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface MesXslRawMaterialCardMapper extends BaseMapper<MesXslRawMaterialCard> {
|
||||
|
||||
/**
|
||||
* 按 trim 后的库区编码汇总原材料卡片剩余数量(当前租户由拦截器补齐)
|
||||
*/
|
||||
List<MesXslAreaRemainQtySumDTO> sumRemainingQtyByTrimmedWarehouseArea(@Param("areaCodes") Collection<String> areaCodes);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper">
|
||||
<!-- 库区编码已与卡片 warehouse_area 按 TRIM 对齐(见库区/卡片筛选逻辑) -->
|
||||
<select id="sumRemainingQtyByTrimmedWarehouseArea"
|
||||
resultType="org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO">
|
||||
SELECT TRIM(t.warehouse_area) AS areaCode,
|
||||
COALESCE(SUM(COALESCE(t.remaining_quantity, 0)), 0) AS qtySum
|
||||
FROM mes_xsl_raw_material_card t
|
||||
WHERE t.warehouse_area IS NOT NULL
|
||||
AND TRIM(t.warehouse_area) != ''
|
||||
AND TRIM(t.warehouse_area) IN
|
||||
<foreach collection="areaCodes" item="c" open="(" separator="," close=")">
|
||||
#{c}
|
||||
</foreach>
|
||||
GROUP BY TRIM(t.warehouse_area)
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -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,18 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
|
||||
/**
|
||||
* 原材料库区看板(库区 + 原材料卡片聚合)
|
||||
*/
|
||||
public interface IMesXslRawMaterialWarehouseBoardService {
|
||||
|
||||
/**
|
||||
* 聚合看板数据
|
||||
*
|
||||
* @param warehouseId 所属仓库ID,可空表示全部启用库区
|
||||
* @param keyword 物料名称/条码/批次模糊筛选(可空);非空时仅展示仍有匹配卡片的库区
|
||||
* @param measureType quantity=占用率按剩余数量与 maxCapacity 对齐;weight=按剩余重量
|
||||
*/
|
||||
MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.jeecg.modules.xslmes.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@@ -12,4 +14,9 @@ public interface IMesXslWarehouseAreaService extends IService<MesXslWarehouseAre
|
||||
* 同租户下是否已存在相同库区编码(编辑时传 excludeId 排除自身)
|
||||
*/
|
||||
boolean existsSameAreaCode(String areaCode, String excludeId);
|
||||
|
||||
/**
|
||||
* 按配置策略回填列表中的「实际存放量」展示值(不写库)
|
||||
*/
|
||||
void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ public class MesXslStompNotifyService {
|
||||
publish("/topic/sync/mes-warehouse-areas", "MES_WAREHOUSE_AREA_CHANGED", "warehouseAreaId", warehouseAreaId, action);
|
||||
}
|
||||
|
||||
/** 广播打印模板变更事件到 /topic/sync/print-templates */
|
||||
public void publishPrintTemplateChanged(String action, String templateId) {
|
||||
publish("/topic/sync/print-templates", "PRINT_TEMPLATE_CHANGED", "templateId", templateId, action);
|
||||
}
|
||||
|
||||
// ─────────────────────────── 私有辅助 ────────────────────────────
|
||||
|
||||
private void publish(String topic, String cmd, String idKey, String idValue, String action) {
|
||||
|
||||
@@ -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,248 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardAreaCardVO;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardBandVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 原材料库区看板
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MesXslRawMaterialWarehouseBoardServiceImpl implements IMesXslRawMaterialWarehouseBoardService {
|
||||
|
||||
private static final String MEASURE_WEIGHT = "weight";
|
||||
/** 库区编码首段形如 1F、2P、03A 等,仅作看板分组带标题(与仓库分类层级无「楼层」对应关系) */
|
||||
private static final Pattern BAND_FIRST_SEGMENT = Pattern.compile("^\\d+[A-Za-z]+$");
|
||||
|
||||
private final IMesXslWarehouseAreaService mesXslWarehouseAreaService;
|
||||
private final IMesXslRawMaterialCardService mesXslRawMaterialCardService;
|
||||
|
||||
@Override
|
||||
public MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType) {
|
||||
String mt = StringUtils.defaultIfBlank(measureType, "quantity").toLowerCase(Locale.ROOT).trim();
|
||||
if (!MEASURE_WEIGHT.equals(mt)) {
|
||||
mt = "quantity";
|
||||
}
|
||||
MesXslRawMaterialWarehouseBoardVO vo = new MesXslRawMaterialWarehouseBoardVO();
|
||||
vo.setMeasureType(mt);
|
||||
|
||||
QueryWrapper<MesXslWarehouseArea> areaQw = new QueryWrapper<>();
|
||||
areaQw.eq("status", "0");
|
||||
if (StringUtils.isNotBlank(warehouseId)) {
|
||||
areaQw.eq("warehouse_id", warehouseId.trim());
|
||||
}
|
||||
areaQw.orderByAsc("area_code");
|
||||
List<MesXslWarehouseArea> areas = mesXslWarehouseAreaService.list(areaQw);
|
||||
if (areas.isEmpty()) {
|
||||
return vo;
|
||||
}
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(areas);
|
||||
|
||||
List<String> areaCodes = areas.stream().map(MesXslWarehouseArea::getAreaCode).filter(StringUtils::isNotBlank).map(String::trim).distinct().collect(Collectors.toList());
|
||||
|
||||
QueryWrapper<MesXslRawMaterialCard> cardQw = new QueryWrapper<>();
|
||||
cardQw.in("warehouse_area", areaCodes);
|
||||
cardQw.apply("(COALESCE(remaining_quantity,0) > 0 OR COALESCE(remaining_weight,0) > 0)");
|
||||
String kw = StringUtils.trimToNull(keyword);
|
||||
if (kw != null) {
|
||||
cardQw.and(w ->
|
||||
w.like("material_name", kw).or().like("barcode", kw).or().like("batch_no", kw));
|
||||
}
|
||||
List<MesXslRawMaterialCard> cards = mesXslRawMaterialCardService.list(cardQw);
|
||||
|
||||
Map<String, List<MesXslRawMaterialCard>> byArea =
|
||||
cards.stream().filter(c -> StringUtils.isNotBlank(c.getWarehouseArea())).collect(Collectors.groupingBy(c -> c.getWarehouseArea().trim(), Collectors.toList()));
|
||||
|
||||
boolean filterByKeyword = kw != null;
|
||||
Map<String, BoardBandVO> bandMap = new LinkedHashMap<>();
|
||||
|
||||
for (MesXslWarehouseArea area : areas) {
|
||||
String code = StringUtils.trimToEmpty(area.getAreaCode());
|
||||
if (code.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<MesXslRawMaterialCard> list = byArea.getOrDefault(code, new ArrayList<>());
|
||||
if (filterByKeyword && list.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
BoardAreaCardVO cardVo = aggregateArea(area, list, mt);
|
||||
BandKey bk = resolveBand(area.getAreaCode());
|
||||
BoardBandVO band = bandMap.computeIfAbsent(bk.key, k -> {
|
||||
BoardBandVO b = new BoardBandVO();
|
||||
b.setBandKey(bk.key);
|
||||
b.setBandLabel(bk.label);
|
||||
b.setBandSort(bk.sort);
|
||||
return b;
|
||||
});
|
||||
band.getAreas().add(cardVo);
|
||||
}
|
||||
|
||||
List<BoardBandVO> bands =
|
||||
bandMap.values().stream().sorted(Comparator.comparing(BoardBandVO::getBandSort, Comparator.nullsLast(Integer::compareTo)).thenComparing(BoardBandVO::getBandKey, Comparator.nullsLast(String::compareTo))).collect(Collectors.toList());
|
||||
|
||||
for (BoardBandVO band : bands) {
|
||||
band.getAreas().sort(Comparator.comparing(BoardAreaCardVO::getAreaCode, Comparator.nullsLast(String::compareTo)));
|
||||
}
|
||||
vo.setBands(bands);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private BoardAreaCardVO aggregateArea(MesXslWarehouseArea area, List<MesXslRawMaterialCard> list, String measureType) {
|
||||
BoardAreaCardVO vo = new BoardAreaCardVO();
|
||||
vo.setAreaId(area.getId());
|
||||
vo.setAreaCode(area.getAreaCode());
|
||||
vo.setAreaName(area.getAreaName());
|
||||
vo.setWarehouseId(area.getWarehouseId());
|
||||
vo.setWarehouseName(area.getWarehouseName());
|
||||
vo.setMaxCapacity(area.getMaxCapacity());
|
||||
vo.setActualCapacity(area.getActualCapacity());
|
||||
vo.setCardCount(list.size());
|
||||
|
||||
int sumQty = 0;
|
||||
BigDecimal sumW = BigDecimal.ZERO;
|
||||
LinkedHashSet<String> materialNames = new LinkedHashSet<>();
|
||||
for (MesXslRawMaterialCard c : list) {
|
||||
if (c.getRemainingQuantity() != null) {
|
||||
sumQty += c.getRemainingQuantity();
|
||||
}
|
||||
if (c.getRemainingWeight() != null) {
|
||||
sumW = sumW.add(c.getRemainingWeight());
|
||||
}
|
||||
if (StringUtils.isNotBlank(c.getMaterialName())) {
|
||||
materialNames.add(c.getMaterialName().trim());
|
||||
}
|
||||
}
|
||||
vo.setCurrentQuantity(sumQty);
|
||||
vo.setCurrentWeight(sumW.setScale(3, RoundingMode.HALF_UP));
|
||||
vo.setMaterialKindCount(materialNames.size());
|
||||
vo.setTopMaterialNames(materialNames.stream().limit(8).collect(Collectors.toList()));
|
||||
|
||||
Double usage = null;
|
||||
Integer maxCap = area.getMaxCapacity();
|
||||
if (maxCap != null && maxCap > 0) {
|
||||
if (MEASURE_WEIGHT.equals(measureType)) {
|
||||
usage = sumW.multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
|
||||
} else {
|
||||
usage = BigDecimal.valueOf(sumQty).multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
if (usage > 100) {
|
||||
usage = 100d;
|
||||
}
|
||||
if (usage < 0) {
|
||||
usage = 0d;
|
||||
}
|
||||
}
|
||||
vo.setUsagePercent(usage);
|
||||
vo.setAlertLevel(resolveAlert(usage, list.isEmpty()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String resolveAlert(Double usagePercent, boolean emptyCards) {
|
||||
if (emptyCards) {
|
||||
return "empty";
|
||||
}
|
||||
if (usagePercent == null) {
|
||||
return "unknown";
|
||||
}
|
||||
if (usagePercent <= 0) {
|
||||
return "empty";
|
||||
}
|
||||
if (usagePercent < 30) {
|
||||
return "low";
|
||||
}
|
||||
if (usagePercent < 90) {
|
||||
return "normal";
|
||||
}
|
||||
if (usagePercent < 100) {
|
||||
return "high";
|
||||
}
|
||||
return "full";
|
||||
}
|
||||
|
||||
private BandKey resolveBand(String areaCode) {
|
||||
if (StringUtils.isBlank(areaCode)) {
|
||||
return new BandKey("zzz_other", "其它", 999_000);
|
||||
}
|
||||
String trimmed = areaCode.trim();
|
||||
String head = trimmed.contains("-") ? trimmed.substring(0, trimmed.indexOf('-')) : trimmed;
|
||||
if (BAND_FIRST_SEGMENT.matcher(head).matches()) {
|
||||
String normalized = normalizeBandToken(head);
|
||||
int sort = bandSort(normalized);
|
||||
return new BandKey(normalized.toLowerCase(Locale.ROOT), normalized, sort);
|
||||
}
|
||||
String label = StringUtils.left(head, 16);
|
||||
if (label.isEmpty()) {
|
||||
label = trimmed.length() > 16 ? trimmed.substring(0, 16) : trimmed;
|
||||
}
|
||||
return new BandKey("z_" + label.toLowerCase(Locale.ROOT), label, 888_000 + Math.abs(label.hashCode() % 1000));
|
||||
}
|
||||
|
||||
private static String normalizeBandToken(String raw) {
|
||||
if (raw == null) {
|
||||
return "其它";
|
||||
}
|
||||
String s = raw.toUpperCase(Locale.ROOT);
|
||||
s = s.replace('F', 'F').replace('P', 'P');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单排序:按前缀中的数字升序(1F<2F<10F),末尾字母次之
|
||||
*/
|
||||
private static int bandSort(String label) {
|
||||
String digits = "";
|
||||
String tail = "";
|
||||
for (int i = 0; i < label.length(); i++) {
|
||||
char ch = label.charAt(i);
|
||||
if (Character.isDigit(ch)) {
|
||||
digits += ch;
|
||||
} else {
|
||||
tail = label.substring(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
int num = 0;
|
||||
try {
|
||||
if (!digits.isEmpty()) {
|
||||
num = Integer.parseInt(digits);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
num = 0;
|
||||
}
|
||||
return num * 100 + Math.min(Math.abs(tail.hashCode() % 99), 99);
|
||||
}
|
||||
|
||||
private static final class BandKey {
|
||||
final String key;
|
||||
final String label;
|
||||
final int sort;
|
||||
|
||||
BandKey(String key, String label, int sort) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.sort = sort;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.capacity.WarehouseAreaDisplayedActualCapacityMediator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseAreaMapper, MesXslWarehouseArea> implements IMesXslWarehouseAreaService {
|
||||
|
||||
private final WarehouseAreaDisplayedActualCapacityMediator displayedActualCapacityMediator;
|
||||
|
||||
@Override
|
||||
public boolean existsSameAreaCode(String areaCode, String excludeId) {
|
||||
if (StringUtils.isBlank(areaCode)) {
|
||||
@@ -24,4 +31,9 @@ public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseA
|
||||
.ne(StringUtils.isNotBlank(excludeId), MesXslWarehouseArea::getId, excludeId)
|
||||
.count() > 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas) {
|
||||
displayedActualCapacityMediator.apply(areas);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
|
||||
return;
|
||||
}
|
||||
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
|
||||
if (!MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
|
||||
if (!MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
|
||||
e.setCustomerId(null);
|
||||
e.setCustomerShortName(null);
|
||||
}
|
||||
if (!MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
|
||||
if (!MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
|
||||
e.setSupplierId(null);
|
||||
e.setSupplierShortName(null);
|
||||
}
|
||||
@@ -56,11 +56,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
|
||||
return;
|
||||
}
|
||||
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
|
||||
if (MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
|
||||
if (MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
|
||||
if (oConvertUtils.isEmpty(e.getCustomerId())) {
|
||||
throw new JeecgBootException("仓库分类为客户库时,请选择客户");
|
||||
}
|
||||
} else if (MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
|
||||
} else if (MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
|
||||
if (oConvertUtils.isEmpty(e.getSupplierId())) {
|
||||
throw new JeecgBootException("仓库分类为供应商库时,请选择供应商");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.jeecg.modules.xslmes.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 原材料库区看板:按库区编码前缀等规则形成的「分组带」汇总展示(不按业务楼层拆分)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "原材料库区看板")
|
||||
public class MesXslRawMaterialWarehouseBoardVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "计量口径回显:quantity=按剩余数量 weight=按剩余重量")
|
||||
private String measureType;
|
||||
|
||||
@Schema(description = "分组带列表")
|
||||
private List<BoardBandVO> bands = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Schema(description = "看板分组带(如 1F、2F)")
|
||||
public static class BoardBandVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "分组键(排序用)")
|
||||
private String bandKey;
|
||||
|
||||
@Schema(description = "分组标题")
|
||||
private String bandLabel;
|
||||
|
||||
@Schema(description = "排序号,越小越靠前")
|
||||
private Integer bandSort;
|
||||
|
||||
@Schema(description = "该组下的库区卡片")
|
||||
private List<BoardAreaCardVO> areas = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "单个库区卡片数据")
|
||||
public static class BoardAreaCardVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "库区主键")
|
||||
private String areaId;
|
||||
|
||||
@Schema(description = "库区编码")
|
||||
private String areaCode;
|
||||
|
||||
@Schema(description = "库区名称")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "所属仓库ID")
|
||||
private String warehouseId;
|
||||
|
||||
@Schema(description = "所属仓库名称")
|
||||
private String warehouseName;
|
||||
|
||||
@Schema(description = "最大存放量(库区主数据)")
|
||||
private Integer maxCapacity;
|
||||
|
||||
@Schema(description = "实际存放量(库区主数据快照)")
|
||||
private Integer actualCapacity;
|
||||
|
||||
@Schema(description = "在库卡片张数(有剩余量)")
|
||||
private Integer cardCount;
|
||||
|
||||
@Schema(description = "物料种类数(去重物料名称)")
|
||||
private Integer materialKindCount;
|
||||
|
||||
@Schema(description = "剩余数量合计")
|
||||
private Integer currentQuantity;
|
||||
|
||||
@Schema(description = "剩余重量合计")
|
||||
private BigDecimal currentWeight;
|
||||
|
||||
@Schema(description = "物料名称摘要(前若干种)")
|
||||
private List<String> topMaterialNames = new ArrayList<>();
|
||||
|
||||
@Schema(description = "占用率 0~100,无上限或上限为0时为 null")
|
||||
private Double usagePercent;
|
||||
|
||||
@Schema(description = "告警级别:empty/low/normal/high/full/unknown")
|
||||
private String alertLevel;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package org.jeecg.modules.print.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 com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.modules.print.catalog.IPrintBizEntityFieldCatalogProvider;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
|
||||
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;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 业务与打印模板绑定:可视化配置字段映射
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "业务打印绑定")
|
||||
@RestController
|
||||
@RequestMapping("/print/bizTemplateBind")
|
||||
public class PrintBizTemplateBindController extends JeecgController<PrintBizTemplateBind, IPrintBizTemplateBindService> {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Autowired private IPrintTemplateService printTemplateService;
|
||||
@Autowired private IPrintBizBindPermWhitelistService printBizBindPermWhitelistService;
|
||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||
@Autowired private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired(required = false)
|
||||
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||
|
||||
@Operation(summary = "业务打印绑定-分页列表")
|
||||
@GetMapping("/list")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<IPage<PrintBizTemplateBind>> list(
|
||||
PrintBizTemplateBind query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintBizTemplateBind> qw =
|
||||
QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
Page<PrintBizTemplateBind> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
@AutoLog(value = "业务打印绑定-添加")
|
||||
@Operation(summary = "业务打印绑定-添加")
|
||||
@PostMapping("/add")
|
||||
@RequiresPermissions("print:bizBind:add")
|
||||
public Result<String> add(@RequestBody PrintBizTemplateBind entity) {
|
||||
String err = validateAndFillTemplate(entity);
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
if (service.getByBizCode(entity.getBizCode()) != null) {
|
||||
return Result.error("该业务编码已存在绑定,请编辑原记录或先删除");
|
||||
}
|
||||
normalizeMappingJson(entity);
|
||||
service.save(entity);
|
||||
publishPrintBizTemplateBindChanged("add", entity.getId());
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "业务打印绑定-编辑")
|
||||
@Operation(summary = "业务打印绑定-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("print:bizBind:edit")
|
||||
public Result<String> edit(@RequestBody PrintBizTemplateBind entity) {
|
||||
PrintBizTemplateBind db = service.getById(entity.getId());
|
||||
if (db == null) {
|
||||
return Result.error("记录不存在");
|
||||
}
|
||||
String err = validateAndFillTemplate(entity);
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
PrintBizTemplateBind other = service.getByBizCode(entity.getBizCode());
|
||||
if (other != null && !other.getId().equals(entity.getId())) {
|
||||
return Result.error("业务编码与其他记录冲突");
|
||||
}
|
||||
normalizeMappingJson(entity);
|
||||
service.updateById(entity);
|
||||
publishPrintBizTemplateBindChanged("edit", entity.getId());
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "业务打印绑定-删除")
|
||||
@Operation(summary = "业务打印绑定-删除")
|
||||
@DeleteMapping("/delete")
|
||||
@RequiresPermissions("print:bizBind:delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
if (service.removeById(id)) {
|
||||
publishPrintBizTemplateBindChanged("delete", id);
|
||||
}
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "已配置的菜单-实体映射及反射字段(完整目录;biz_code 为菜单 id)")
|
||||
@GetMapping("/bizTypes")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizTypeVO>> bizTypes() {
|
||||
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 = "打印业务白名单:当前勾选的菜单 id(catalog 恒为空,避免全库反射超时)")
|
||||
@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)")
|
||||
@GetMapping("/parseTemplateFields")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintTemplateFieldItemVO>> parseTemplateFields(
|
||||
@RequestParam(name = "templateId") String templateId) {
|
||||
if (StringUtils.isBlank(templateId)) {
|
||||
return Result.error("templateId 不能为空");
|
||||
}
|
||||
PrintTemplate tpl = printTemplateService.getById(templateId);
|
||||
if (tpl == null) {
|
||||
return Result.error("模板不存在");
|
||||
}
|
||||
List<PrintTemplateFieldItemVO> fields =
|
||||
PrintNativeTemplateFieldExtractor.extract(tpl.getTemplateJson());
|
||||
return Result.OK(fields);
|
||||
}
|
||||
|
||||
@Operation(summary = "主实体上的明细槽位(List<明细实体> / 明细数组 / 嵌套对象),用于先选明细再反射明细字段")
|
||||
@GetMapping("/detailSlots")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizDetailSlotVO>> detailSlots(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
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())) {
|
||||
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 不能为空");
|
||||
}
|
||||
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())) {
|
||||
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")
|
||||
public Result<PrintBizTemplateBind> queryByBizCode(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
PrintBizTemplateBind row = service.getByBizCode(bizCode.trim());
|
||||
if (row == null) {
|
||||
return Result.error("未配置该业务的打印绑定");
|
||||
}
|
||||
return Result.OK(row);
|
||||
}
|
||||
|
||||
@Operation(summary = "预览:业务数据按映射转为模板打印 JSON")
|
||||
@PostMapping("/previewMappedData")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<Map<String, Object>> previewMappedData(@RequestBody PreviewMappedBody body) {
|
||||
if (body == null || StringUtils.isBlank(body.getBizCode())) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
PrintBizTemplateBind bind = service.getByBizCode(body.getBizCode().trim());
|
||||
if (bind == null) {
|
||||
return Result.error("未配置该业务的打印绑定");
|
||||
}
|
||||
try {
|
||||
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = PrintBizDataMappingUtil.parseBizJson(body.getBizDataJson());
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl != null && StringUtils.isNotBlank(tpl.getTemplateJson())) {
|
||||
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
|
||||
}
|
||||
Map<String, Object> res = new HashMap<>(4);
|
||||
res.put("templateCode", bind.getTemplateCode());
|
||||
res.put("templateId", bind.getTemplateId());
|
||||
res.put("printData", OBJECT_MAPPER.convertValue(printData, Map.class));
|
||||
return Result.OK(res);
|
||||
} catch (Exception e) {
|
||||
return Result.error("预览失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** @return 错误信息;null 表示校验通过 */
|
||||
private String validateAndFillTemplate(PrintBizTemplateBind entity) {
|
||||
if (entity == null || StringUtils.isBlank(entity.getBizCode())) {
|
||||
return "业务编码不能为空";
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getTemplateId())) {
|
||||
return "请选择打印模板";
|
||||
}
|
||||
PrintTemplate tpl = printTemplateService.getById(entity.getTemplateId());
|
||||
if (tpl == null) {
|
||||
return "打印模板不存在";
|
||||
}
|
||||
entity.setTemplateCode(tpl.getTemplateCode());
|
||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(entity.getBizCode());
|
||||
if (bizVo == null) {
|
||||
return "无效的业务:该菜单无法解析到实体类(需菜单 component 为如 xslmes/xxx/XxxList,或在 print_biz_perm_entity 中配置 entity_class;biz_code 为菜单 id)";
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getBizName())) {
|
||||
entity.setBizName(bizVo.getBizName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 确保 mapping JSON 为数组字符串 */
|
||||
private void normalizeMappingJson(PrintBizTemplateBind entity) {
|
||||
String raw = entity.getFieldMappingJson();
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
entity.setFieldMappingJson("[]");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JsonNode n = OBJECT_MAPPER.readTree(raw);
|
||||
if (!n.isArray()) {
|
||||
entity.setFieldMappingJson("[]");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
entity.setFieldMappingJson("[]");
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PreviewMappedBody {
|
||||
private String bizCode;
|
||||
/** 业务对象 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;
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 桌面端免密只读 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "业务打印绑定-免密分页列表(桌面端缓存同步)")
|
||||
@GetMapping("/anon/list")
|
||||
public Result<IPage<PrintBizTemplateBind>> anonList(
|
||||
PrintBizTemplateBind query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintBizTemplateBind> qw =
|
||||
QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
Page<PrintBizTemplateBind> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
@Operation(summary = "业务打印绑定-免密按 id 查询(桌面端)")
|
||||
@GetMapping("/anon/queryById")
|
||||
public Result<PrintBizTemplateBind> anonQueryById(@RequestParam(name = "id") String id) {
|
||||
PrintBizTemplateBind row = service.getById(id);
|
||||
return row != null ? Result.OK(row) : Result.error("未找到记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播到 /topic/sync/print-biz-binds,与桌面端 PrintBizTemplateBindSyncCoordinator 对应。
|
||||
*/
|
||||
private void publishPrintBizTemplateBindChanged(String action, String bindId) {
|
||||
try {
|
||||
Map<String, Object> event = new HashMap<>();
|
||||
event.put("cmd", "PRINT_BIZ_TEMPLATE_BIND_CHANGED");
|
||||
event.put("action", action);
|
||||
event.put("bindId", bindId);
|
||||
event.put("timestamp", System.currentTimeMillis());
|
||||
messagingTemplate.convertAndSend("/topic/sync/print-biz-binds", JSON.toJSONString(event));
|
||||
} catch (Exception e) {
|
||||
log.debug("广播 STOMP 事件失败 [PRINT_BIZ_TEMPLATE_BIND_CHANGED]: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,36 +6,19 @@ 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 java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.print.Doc;
|
||||
import javax.print.DocFlavor;
|
||||
import javax.print.DocPrintJob;
|
||||
import javax.print.PrintException;
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import javax.print.SimpleDoc;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterAbortException;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
@@ -44,10 +27,13 @@ import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
|
||||
import org.jeecg.modules.print.support.PrintServerPdfJobService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
|
||||
/**
|
||||
* 打印模板维护(Hiprint)
|
||||
@@ -57,12 +43,22 @@ import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
|
||||
@RestController
|
||||
@RequestMapping("/print/template")
|
||||
public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> {
|
||||
@Value("${print.network-printers:}")
|
||||
private String networkPrinters;
|
||||
@Autowired private PrintServerEnvironmentService printServerEnvironmentService;
|
||||
@Autowired private PrintServerPdfJobService printServerPdfJobService;
|
||||
|
||||
@Autowired
|
||||
private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService;
|
||||
|
||||
/**
|
||||
* STOMP 实时通知:广播打印模板变更到 /topic/sync/print-templates。
|
||||
* 直接用 SimpMessagingTemplate 内联推送,避免 jeecg-system-biz(核心模块)
|
||||
* 反向依赖 jeecg-module-xslmes(业务模块)造成的循环依赖。
|
||||
* 消息体格式与 MesXslStompNotifyService.publishPrintTemplateChanged 完全一致,
|
||||
* 桌面端订阅方无需任何改动。
|
||||
*/
|
||||
@Autowired
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Operation(summary = "打印模板-分页列表")
|
||||
@GetMapping(value = "/list")
|
||||
@RequiresPermissions("print:template:list")
|
||||
@@ -92,6 +88,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
entity.setTemplateJson("{}");
|
||||
}
|
||||
service.save(entity);
|
||||
publishPrintTemplateChanged("add", entity.getId());
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@@ -111,6 +108,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
}
|
||||
}
|
||||
service.updateById(entity);
|
||||
publishPrintTemplateChanged("edit", entity.getId());
|
||||
return Result.OK("修改成功");
|
||||
}
|
||||
|
||||
@@ -142,6 +140,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
@RequiresPermissions("print:template:delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
service.removeById(id);
|
||||
publishPrintTemplateChanged("delete", id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@@ -153,7 +152,9 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
if (StringUtils.isBlank(ids)) {
|
||||
return Result.error("参数 ids 不能为空");
|
||||
}
|
||||
service.removeByIds(java.util.Arrays.asList(ids.split(",")));
|
||||
List<String> idList = java.util.Arrays.asList(ids.split(","));
|
||||
service.removeByIds(idList);
|
||||
idList.forEach(id -> publishPrintTemplateChanged("delete", id.trim()));
|
||||
return Result.OK("批量删除成功");
|
||||
}
|
||||
|
||||
@@ -208,45 +209,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
@GetMapping(value = "/queryPrinters")
|
||||
@RequiresPermissions("print:template:list")
|
||||
public Result<Map<String, Object>> queryPrinters() {
|
||||
Map<String, Object> res = new HashMap<>(8);
|
||||
List<String> serverPrinters = new ArrayList<>();
|
||||
String serverDefaultPrinter = "";
|
||||
try {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService service : services) {
|
||||
if (service != null && StringUtils.isNotBlank(service.getName())) {
|
||||
serverPrinters.add(service.getName().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
|
||||
if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) {
|
||||
serverDefaultPrinter = defaultService.getName().trim();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询服务器打印机失败: {}", e.getMessage());
|
||||
}
|
||||
List<String> networkPrinterList =
|
||||
StringUtils.isBlank(networkPrinters)
|
||||
? new ArrayList<>()
|
||||
: java.util.Arrays.stream(networkPrinters.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> capability = new LinkedHashMap<>(4);
|
||||
capability.put("localSupported", false);
|
||||
capability.put("localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。");
|
||||
capability.put("serverSupported", true);
|
||||
capability.put("networkSupported", true);
|
||||
|
||||
res.put("capability", capability);
|
||||
res.put("serverPrinters", serverPrinters);
|
||||
res.put("serverDefaultPrinter", serverDefaultPrinter);
|
||||
res.put("networkPrinters", networkPrinterList);
|
||||
return Result.OK(res);
|
||||
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
|
||||
}
|
||||
|
||||
@AutoLog(value = "打印模板-服务端直打")
|
||||
@@ -269,23 +232,12 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
return Result.error("模板不存在: " + templateCode);
|
||||
}
|
||||
try {
|
||||
PrintService target = null;
|
||||
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService serviceItem : services) {
|
||||
if (serviceItem != null && printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
|
||||
target = serviceItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
return Result.error("未找到指定打印机: " + printerName);
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = PrintServiceLookup.lookupDefaultPrintService();
|
||||
PrintService target = printServerPdfJobService.resolvePrintService(printerName);
|
||||
if (StringUtils.isNotBlank(printerName)
|
||||
&& !"__system_default__".equals(printerName)
|
||||
&& target != null
|
||||
&& !printerName.equalsIgnoreCase(String.valueOf(target.getName()).trim())) {
|
||||
return Result.error("未找到指定打印机: " + printerName);
|
||||
}
|
||||
if (target == null) {
|
||||
return Result.error("未找到可用打印机,请检查服务器打印机配置");
|
||||
@@ -345,128 +297,48 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
if (StringUtils.isBlank(templateCode)) {
|
||||
return Result.error("templateCode 不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(pdfBase64)) {
|
||||
return Result.error("pdfBase64 不能为空");
|
||||
}
|
||||
String lastResolvedPrinterLabel = null;
|
||||
try {
|
||||
PrintService target = resolvePrintService(printerName);
|
||||
if (target == null) {
|
||||
return Result.error("未找到可用打印机,请检查服务器打印机配置");
|
||||
}
|
||||
final String resolvedPrinterLabel = target.getName();
|
||||
lastResolvedPrinterLabel = resolvedPrinterLabel;
|
||||
String base64Body = pdfBase64;
|
||||
int commaIdx = pdfBase64.indexOf(",");
|
||||
if (pdfBase64.startsWith("data:") && commaIdx > 0) {
|
||||
base64Body = pdfBase64.substring(commaIdx + 1);
|
||||
}
|
||||
byte[] pdfBytes = Base64.getDecoder().decode(base64Body);
|
||||
String printJobName = StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + templateCode + ".pdf");
|
||||
// 优先直送 PDF 字节,避免走 RasterPrinterJob(虚拟打印机/无界面会话下易触发 PrinterAbortException)
|
||||
if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) {
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
}
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
|
||||
PDFRenderer renderer = new PDFRenderer(document);
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(target);
|
||||
job.setJobName(printJobName);
|
||||
job.setPrintable(
|
||||
(graphics, pageFormat, pageIndex) -> {
|
||||
if (pageIndex >= document.getNumberOfPages()) {
|
||||
return Printable.NO_SUCH_PAGE;
|
||||
}
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = renderer.renderImageWithDPI(pageIndex, 150);
|
||||
} catch (Exception ex) {
|
||||
throw new PrinterException("PDF页面渲染失败: " + ex.getMessage());
|
||||
}
|
||||
Graphics2D g2 = (Graphics2D) graphics;
|
||||
double imageableX = pageFormat.getImageableX();
|
||||
double imageableY = pageFormat.getImageableY();
|
||||
double imageableWidth = pageFormat.getImageableWidth();
|
||||
double imageableHeight = pageFormat.getImageableHeight();
|
||||
double scale =
|
||||
Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight());
|
||||
int drawWidth = (int) Math.round(image.getWidth() * scale);
|
||||
int drawHeight = (int) Math.round(image.getHeight() * scale);
|
||||
int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2);
|
||||
int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2);
|
||||
g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null);
|
||||
return Printable.PAGE_EXISTS;
|
||||
});
|
||||
HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet();
|
||||
patts.add(new JobName(printJobName, Locale.getDefault()));
|
||||
job.print(patts);
|
||||
}
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
} catch (PrinterAbortException e) {
|
||||
log.error("PDF后端打印失败(PrinterAbortException)", e);
|
||||
return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel));
|
||||
} catch (Exception e) {
|
||||
log.error("PDF后端打印失败", e);
|
||||
return Result.error("PDF后端打印失败: " + e.getMessage());
|
||||
return printServerPdfJobService.submitPdfBase64(printerName, pdfBase64, fileName, templateCode);
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "打印模板-免密通过编码查询(桌面端)")
|
||||
@GetMapping(value = "/anon/queryByCode")
|
||||
public Result<PrintTemplate> anonQueryByCode(@RequestParam(name = "code") String code) {
|
||||
PrintTemplate t = service.getByCode(code);
|
||||
if (t == null) {
|
||||
return Result.error("未找到模板: " + code);
|
||||
}
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
@Operation(summary = "打印模板-免密分页列表(桌面端)")
|
||||
@GetMapping(value = "/anon/list")
|
||||
public Result<IPage<PrintTemplate>> anonList(PrintTemplate query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByAsc("template_code");
|
||||
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
/**
|
||||
* 若打印机声明支持 application/pdf,则通过 DocPrintJob 提交,通常比 AWT 栅格化更稳定。
|
||||
* 广播打印模板变更事件到 /topic/sync/print-templates,桌面端订阅同步刷新本地缓存。
|
||||
* 消息体格式 = MesXslStompNotifyService.publishPrintTemplateChanged 的输出,
|
||||
* 内联实现避免反向依赖业务模块。
|
||||
*/
|
||||
private boolean tryPrintPdfBytesWithDocFlavor(PrintService printService, byte[] pdfBytes, String jobName) {
|
||||
DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf");
|
||||
if (!printService.isDocFlavorSupported(flavor)) {
|
||||
return false;
|
||||
}
|
||||
private void publishPrintTemplateChanged(String action, String templateId) {
|
||||
try {
|
||||
DocPrintJob docJob = printService.createPrintJob();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes);
|
||||
Doc doc = new SimpleDoc(in, flavor, null);
|
||||
HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
|
||||
if (StringUtils.isNotBlank(jobName)) {
|
||||
attrs.add(new JobName(jobName, Locale.getDefault()));
|
||||
}
|
||||
docJob.print(doc, attrs);
|
||||
return true;
|
||||
} catch (PrintException e) {
|
||||
log.warn("PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}", printService.getName(), e.getMessage());
|
||||
return false;
|
||||
Map<String, Object> event = new HashMap<>();
|
||||
event.put("cmd", "PRINT_TEMPLATE_CHANGED");
|
||||
event.put("action", action);
|
||||
event.put("templateId", templateId);
|
||||
event.put("timestamp", System.currentTimeMillis());
|
||||
messagingTemplate.convertAndSend("/topic/sync/print-templates", JSON.toJSONString(event));
|
||||
} catch (Exception e) {
|
||||
log.debug("广播 STOMP 事件失败 [PRINT_TEMPLATE_CHANGED]: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildPdfPrinterAbortHint(String requestedPrinterName, String resolvedPrintQueueName) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("打印任务被系统取消(PrinterAbortException)。常见原因:");
|
||||
sb.append("1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机,在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName;");
|
||||
sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;");
|
||||
sb.append("3) 运行服务的 Windows 账户无权访问打印队列。");
|
||||
if (StringUtils.isNotBlank(resolvedPrintQueueName)) {
|
||||
sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("。");
|
||||
}
|
||||
if (StringUtils.isNotBlank(requestedPrinterName) && !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) {
|
||||
sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("。");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private PrintService resolvePrintService(String printerName) {
|
||||
PrintService target = null;
|
||||
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService serviceItem : services) {
|
||||
if (serviceItem != null
|
||||
&& printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
|
||||
target = serviceItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = PrintServiceLookup.lookupDefaultPrintService();
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.print.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
|
||||
/**
|
||||
* 业务与打印模板绑定(字段映射)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "业务打印模板绑定")
|
||||
@TableName("print_biz_template_bind")
|
||||
public class PrintBizTemplateBind extends JeecgEntity implements Serializable {
|
||||
|
||||
@Schema(description = "业务编码")
|
||||
@TableField("biz_code")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称")
|
||||
@TableField("biz_name")
|
||||
private String bizName;
|
||||
|
||||
@Schema(description = "打印模板主键")
|
||||
@TableField("template_id")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "打印模板编码")
|
||||
@TableField("template_code")
|
||||
private String templateCode;
|
||||
|
||||
@Schema(description = "字段映射 JSON:[{templateField,bizField}]")
|
||||
@TableField("field_mapping_json")
|
||||
private String fieldMappingJson;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@TableField("remark")
|
||||
private String remark;
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.print.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
|
||||
/** 业务打印模板绑定 Mapper */
|
||||
public interface PrintBizTemplateBindMapper extends BaseMapper<PrintBizTemplateBind> {}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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_permission(menu_type=1 且 component 非空),用表中真实的 component / component_name 推断实体类并写入
|
||||
* print_biz_perm_entity(不覆盖已有可加载的 entity_class)。
|
||||
*/
|
||||
void warmupMappingsFromSysPermissionTable();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jeecg.modules.print.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
|
||||
/** 业务打印模板绑定 */
|
||||
public interface IPrintBizTemplateBindService extends IService<PrintBizTemplateBind> {
|
||||
|
||||
PrintBizTemplateBind getByBizCode(String bizCode);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package org.jeecg.modules.print.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
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.catalog.IPrintBizEntityFieldCatalogProvider;
|
||||
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.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;
|
||||
|
||||
@Autowired(required = false)
|
||||
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||
|
||||
@Override
|
||||
public PrintBizPermEntity getByPermId(String permId) {
|
||||
if (StringUtils.isBlank(permId)) {
|
||||
return null;
|
||||
}
|
||||
return getById(permId.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrintBizTypeVO> listAllBizTypeVOs() {
|
||||
List<PrintBizPermEntity> rows = list();
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> permIds = new ArrayList<>(rows.size());
|
||||
for (PrintBizPermEntity row : rows) {
|
||||
if (row != null && StringUtils.isNotBlank(row.getPermId())) {
|
||||
permIds.add(row.getPermId().trim());
|
||||
}
|
||||
}
|
||||
Map<String, SysPermission> permMap = loadPermissionMap(permIds);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrintBizTypeVO> listBizTypeVOsFiltered(List<String> whitelistPermIds) {
|
||||
if (whitelistPermIds == null || whitelistPermIds.isEmpty()) {
|
||||
return listAllBizTypeVOs();
|
||||
}
|
||||
List<String> ids = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (String raw : whitelistPermIds) {
|
||||
String id = StringUtils.trimToEmpty(raw);
|
||||
if (id.isEmpty() || seen.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
ids.add(id);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@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 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) {
|
||||
if (StringUtils.isBlank(permId)) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
if (rowEff != null && StringUtils.isNotBlank(rowEff.getEntityClass())) {
|
||||
entityFqn = rowEff.getEntityClass().trim();
|
||||
} else {
|
||||
SysPermission p = permCache != null ? permCache.get(id) : sysPermissionService.getById(id);
|
||||
entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
||||
}
|
||||
boolean catalogOk =
|
||||
fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(id);
|
||||
if (StringUtils.isBlank(entityFqn)) {
|
||||
if (!catalogOk) {
|
||||
return null;
|
||||
}
|
||||
entityFqn = StringUtils.trimToEmpty(fieldCatalogProvider.getEntityClassFqn(id));
|
||||
}
|
||||
Class<?> clazz = null;
|
||||
if (!catalogOk) {
|
||||
clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
|
||||
if (clazz == null) {
|
||||
// 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导
|
||||
return null;
|
||||
}
|
||||
}
|
||||
PrintBizTypeVO vo = new PrintBizTypeVO();
|
||||
vo.setBizCode(id);
|
||||
vo.setLinkedPermissionId(id);
|
||||
vo.setBizName(resolveMenuName(id, permCache));
|
||||
String desc = StringUtils.isNotBlank(entityFqn) ? entityFqn : "";
|
||||
if (StringUtils.isBlank(desc) && fieldCatalogProvider != null) {
|
||||
desc = StringUtils.defaultString(fieldCatalogProvider.getEntityClassFqn(id));
|
||||
}
|
||||
vo.setDescription(desc);
|
||||
if (catalogOk) {
|
||||
vo.setFields(fieldCatalogProvider.listMainFields(id));
|
||||
} else {
|
||||
vo.setFields(PrintBizEntityFieldIntrospector.listFields(clazz));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String resolveMenuName(String permId, Map<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) {
|
||||
SysPermission p = sysPermissionService.getById(permId);
|
||||
if (p != null && StringUtils.isNotBlank(p.getName())) {
|
||||
return p.getName();
|
||||
}
|
||||
return permId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.print.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.mapper.PrintBizTemplateBindMapper;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PrintBizTemplateBindServiceImpl extends ServiceImpl<PrintBizTemplateBindMapper, PrintBizTemplateBind>
|
||||
implements IPrintBizTemplateBindService {
|
||||
|
||||
@Override
|
||||
public PrintBizTemplateBind getByBizCode(String bizCode) {
|
||||
if (bizCode == null || bizCode.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<PrintBizTemplateBind> q = new LambdaQueryWrapper<>();
|
||||
q.eq(PrintBizTemplateBind::getBizCode, bizCode.trim());
|
||||
q.last("LIMIT 1");
|
||||
return getOne(q);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.jeecg.modules.print.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 服务端打印机枚举能力(与 {@link org.jeecg.modules.print.controller.PrintTemplateController#queryPrinters} 一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PrintServerEnvironmentService {
|
||||
|
||||
@Value("${print.network-printers:}")
|
||||
private String networkPrinters;
|
||||
|
||||
/** 与打印模板页 /print/template/queryPrinters 返回结构完全一致 */
|
||||
public Map<String, Object> buildPrinterQueryResult() {
|
||||
Map<String, Object> res = new HashMap<>(8);
|
||||
List<String> serverPrinters = new ArrayList<>();
|
||||
String serverDefaultPrinter = "";
|
||||
try {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService service : services) {
|
||||
if (service != null && StringUtils.isNotBlank(service.getName())) {
|
||||
serverPrinters.add(service.getName().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
|
||||
if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) {
|
||||
serverDefaultPrinter = defaultService.getName().trim();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询服务器打印机失败: {}", e.getMessage());
|
||||
}
|
||||
List<String> networkPrinterList =
|
||||
StringUtils.isBlank(networkPrinters)
|
||||
? new ArrayList<>()
|
||||
: java.util.Arrays.stream(networkPrinters.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> capability = new LinkedHashMap<>(4);
|
||||
capability.put("localSupported", false);
|
||||
capability.put(
|
||||
"localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。");
|
||||
capability.put("serverSupported", true);
|
||||
capability.put("networkSupported", true);
|
||||
|
||||
res.put("capability", capability);
|
||||
res.put("serverPrinters", serverPrinters);
|
||||
res.put("serverDefaultPrinter", serverDefaultPrinter);
|
||||
res.put("networkPrinters", networkPrinterList);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.jeecg.modules.print.support;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterAbortException;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import javax.print.Doc;
|
||||
import javax.print.DocFlavor;
|
||||
import javax.print.DocPrintJob;
|
||||
import javax.print.PrintException;
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import javax.print.SimpleDoc;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 服务端将 PDF Base64 提交至打印队列(与打印模板 {@code /print/template/directPrintPdf} 逻辑一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PrintServerPdfJobService {
|
||||
|
||||
/**
|
||||
* @param templateCodeOrPrefix 用于默认作业名:{@code QH-MES-{code}.pdf}
|
||||
*/
|
||||
public Result<String> submitPdfBase64(
|
||||
String printerName, String pdfBase64, String fileName, String templateCodeOrPrefix) {
|
||||
if (StringUtils.isBlank(pdfBase64)) {
|
||||
return Result.error("pdfBase64 不能为空");
|
||||
}
|
||||
String lastResolvedPrinterLabel = null;
|
||||
try {
|
||||
PrintService target = resolvePrintService(printerName);
|
||||
if (target == null) {
|
||||
return Result.error("未找到可用打印机,请检查服务器打印机配置");
|
||||
}
|
||||
final String resolvedPrinterLabel = target.getName();
|
||||
lastResolvedPrinterLabel = resolvedPrinterLabel;
|
||||
String base64Body = pdfBase64;
|
||||
int commaIdx = pdfBase64.indexOf(",");
|
||||
if (pdfBase64.startsWith("data:") && commaIdx > 0) {
|
||||
base64Body = pdfBase64.substring(commaIdx + 1);
|
||||
}
|
||||
byte[] pdfBytes = Base64.getDecoder().decode(base64Body);
|
||||
String prefix = StringUtils.isNotBlank(templateCodeOrPrefix) ? templateCodeOrPrefix.trim() : "MES";
|
||||
String printJobName =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + prefix + ".pdf");
|
||||
if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) {
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
}
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
|
||||
PDFRenderer renderer = new PDFRenderer(document);
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(target);
|
||||
job.setJobName(printJobName);
|
||||
job.setPrintable(
|
||||
(graphics, pageFormat, pageIndex) -> {
|
||||
if (pageIndex >= document.getNumberOfPages()) {
|
||||
return Printable.NO_SUCH_PAGE;
|
||||
}
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = renderer.renderImageWithDPI(pageIndex, 150);
|
||||
} catch (Exception ex) {
|
||||
throw new PrinterException("PDF页面渲染失败: " + ex.getMessage());
|
||||
}
|
||||
Graphics2D g2 = (Graphics2D) graphics;
|
||||
double imageableX = pageFormat.getImageableX();
|
||||
double imageableY = pageFormat.getImageableY();
|
||||
double imageableWidth = pageFormat.getImageableWidth();
|
||||
double imageableHeight = pageFormat.getImageableHeight();
|
||||
double scale =
|
||||
Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight());
|
||||
int drawWidth = (int) Math.round(image.getWidth() * scale);
|
||||
int drawHeight = (int) Math.round(image.getHeight() * scale);
|
||||
int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2);
|
||||
int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2);
|
||||
g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null);
|
||||
return Printable.PAGE_EXISTS;
|
||||
});
|
||||
HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet();
|
||||
patts.add(new JobName(printJobName, Locale.getDefault()));
|
||||
job.print(patts);
|
||||
}
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
} catch (PrinterAbortException e) {
|
||||
log.error("PDF后端打印失败(PrinterAbortException)", e);
|
||||
return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel));
|
||||
} catch (Exception e) {
|
||||
log.error("PDF后端打印失败", e);
|
||||
return Result.error("PDF后端打印失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public PrintService resolvePrintService(String printerName) {
|
||||
PrintService target = null;
|
||||
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService serviceItem : services) {
|
||||
if (serviceItem != null
|
||||
&& printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
|
||||
target = serviceItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = PrintServiceLookup.lookupDefaultPrintService();
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private boolean tryPrintPdfBytesWithDocFlavor(
|
||||
PrintService printService, byte[] pdfBytes, String jobName) {
|
||||
DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf");
|
||||
if (!printService.isDocFlavorSupported(flavor)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
DocPrintJob docJob = printService.createPrintJob();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes);
|
||||
Doc doc = new SimpleDoc(in, flavor, null);
|
||||
HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
|
||||
if (StringUtils.isNotBlank(jobName)) {
|
||||
attrs.add(new JobName(jobName, Locale.getDefault()));
|
||||
}
|
||||
docJob.print(doc, attrs);
|
||||
return true;
|
||||
} catch (PrintException e) {
|
||||
log.warn(
|
||||
"PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}",
|
||||
printService.getName(),
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildPdfPrinterAbortHint(
|
||||
String requestedPrinterName, String resolvedPrintQueueName) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("打印任务被系统取消(PrinterAbortException)。常见原因:");
|
||||
sb.append(
|
||||
"1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机,在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName;");
|
||||
sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;");
|
||||
sb.append("3) 运行服务的 Windows 账户无权访问打印队列。");
|
||||
if (StringUtils.isNotBlank(resolvedPrintQueueName)) {
|
||||
sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("。");
|
||||
}
|
||||
if (StringUtils.isNotBlank(requestedPrinterName)
|
||||
&& !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) {
|
||||
sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("。");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
|
||||
|
||||
/** 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField) */
|
||||
public final class PrintBizDataMappingUtil {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private PrintBizDataMappingUtil() {}
|
||||
|
||||
public static ObjectNode mapBizToPrintData(JsonNode bizRoot, ArrayNode mappingRules) {
|
||||
ObjectNode printData = MAPPER.createObjectNode();
|
||||
if (bizRoot == null || mappingRules == null) {
|
||||
return printData;
|
||||
}
|
||||
for (JsonNode rule : mappingRules) {
|
||||
if (rule == null || !rule.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String templateField = text(rule, "templateField");
|
||||
String bizField = text(rule, "bizField");
|
||||
// 仅要求模板字段名;业务字段为空表示「不参与取数」,仍向 printData 写入空字符串,避免模板占位符缺键
|
||||
if (StringUtils.isBlank(templateField)) {
|
||||
continue;
|
||||
}
|
||||
JsonNode val;
|
||||
if (StringUtils.isBlank(bizField)) {
|
||||
val = MAPPER.getNodeFactory().textNode("");
|
||||
} else {
|
||||
val = resolvePath(bizRoot, bizField);
|
||||
}
|
||||
setPath(printData, templateField, val);
|
||||
}
|
||||
return printData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模板中已声明的绑定路径({@code dataBinding.params}、画布/表格等元素的 {@code bindField},与
|
||||
* {@link PrintNativeTemplateFieldExtractor} 一致),向 printData 补齐缺失路径(空字符串)。
|
||||
*
|
||||
* <p>避免字段映射未包含某键时 API 缺键,桌面端渲染把「设计稿占位 text」当成数据显示。
|
||||
*/
|
||||
public static ObjectNode fillMissingDataBindingParamKeys(ObjectNode printData, String templateJson) {
|
||||
if (printData == null) {
|
||||
printData = MAPPER.createObjectNode();
|
||||
}
|
||||
if (StringUtils.isBlank(templateJson)) {
|
||||
return printData;
|
||||
}
|
||||
try {
|
||||
List<PrintTemplateFieldItemVO> fields = PrintNativeTemplateFieldExtractor.extract(templateJson);
|
||||
for (PrintTemplateFieldItemVO item : fields) {
|
||||
if (item == null || StringUtils.isBlank(item.getBindField())) {
|
||||
continue;
|
||||
}
|
||||
String bf = item.getBindField().trim();
|
||||
if (!hasPath(printData, bf)) {
|
||||
setPath(printData, bf, MAPPER.getNodeFactory().textNode(""));
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 模板解析异常时不阻断打印
|
||||
}
|
||||
return printData;
|
||||
}
|
||||
|
||||
/** 判断 printData 上是否存在该点分路径(含嵌套对象) */
|
||||
private static boolean hasPath(ObjectNode root, String path) {
|
||||
if (StringUtils.isBlank(path)) {
|
||||
return false;
|
||||
}
|
||||
String[] parts = path.split("\\.");
|
||||
JsonNode cur = root;
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if (cur == null || !cur.isObject()) {
|
||||
return false;
|
||||
}
|
||||
ObjectNode obj = (ObjectNode) cur;
|
||||
String p = parts[i];
|
||||
if (p.isEmpty() || !obj.has(p)) {
|
||||
return false;
|
||||
}
|
||||
if (i == parts.length - 1) {
|
||||
return true;
|
||||
}
|
||||
cur = obj.get(p);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static JsonNode resolvePath(JsonNode root, String path) {
|
||||
if (root == null || StringUtils.isBlank(path)) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = path.split("\\.");
|
||||
JsonNode cur = root;
|
||||
for (String p : parts) {
|
||||
if (cur == null || p.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
String[] parts = path.split("\\.");
|
||||
if (parts.length == 1) {
|
||||
putLeaf(target, parts[0], value);
|
||||
return;
|
||||
}
|
||||
ObjectNode cur = target;
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
String p = parts[i];
|
||||
JsonNode next = cur.get(p);
|
||||
if (next == null || !next.isObject()) {
|
||||
ObjectNode created = MAPPER.createObjectNode();
|
||||
cur.set(p, created);
|
||||
cur = created;
|
||||
} else {
|
||||
cur = (ObjectNode) next;
|
||||
}
|
||||
}
|
||||
String leaf = parts[parts.length - 1];
|
||||
putLeaf(cur, leaf, value);
|
||||
}
|
||||
|
||||
private static void putLeaf(ObjectNode node, String key, JsonNode value) {
|
||||
if (value == null || value.isNull()) {
|
||||
node.putNull(key);
|
||||
} else if (value.isObject() || value.isArray()) {
|
||||
node.set(key, value);
|
||||
} else if (value.isTextual()) {
|
||||
node.put(key, value.asText());
|
||||
} else if (value.isBoolean()) {
|
||||
node.put(key, value.booleanValue());
|
||||
} else if (value.isNumber()) {
|
||||
node.put(key, value.doubleValue());
|
||||
} else {
|
||||
node.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode n, String key) {
|
||||
if (n == null || !n.isObject()) {
|
||||
return "";
|
||||
}
|
||||
JsonNode v = n.get(key);
|
||||
return v == null || v.isNull() ? "" : v.asText("");
|
||||
}
|
||||
|
||||
/** 将任意 JsonNode 转为可序列化的 JSON 树(复制) */
|
||||
public static JsonNode parseBizJson(Object bizDataJson) throws Exception {
|
||||
if (bizDataJson == null) {
|
||||
return MAPPER.createObjectNode();
|
||||
}
|
||||
if (bizDataJson instanceof String s) {
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return MAPPER.createObjectNode();
|
||||
}
|
||||
return MAPPER.readTree(s);
|
||||
}
|
||||
return MAPPER.valueToTree(bizDataJson);
|
||||
}
|
||||
|
||||
/** 规范化映射列表:解析字符串或数组 */
|
||||
public static ArrayNode parseMappingArray(String fieldMappingJson) throws Exception {
|
||||
if (StringUtils.isBlank(fieldMappingJson)) {
|
||||
return MAPPER.createArrayNode();
|
||||
}
|
||||
JsonNode n = MAPPER.readTree(fieldMappingJson);
|
||||
if (n.isArray()) {
|
||||
return (ArrayNode) n;
|
||||
}
|
||||
return MAPPER.createArrayNode();
|
||||
}
|
||||
|
||||
public static String mappingArrayToJson(ArrayNode arr) throws Exception {
|
||||
return MAPPER.writeValueAsString(arr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
|
||||
/**
|
||||
* 扫描主实体类上可作为「明细」的来源属性:{@code List<Entity>} / {@code Set<Entity>} / 数组 / 嵌套业务对象。
|
||||
*/
|
||||
public final class PrintBizDetailPropertyScanner {
|
||||
|
||||
private PrintBizDetailPropertyScanner() {}
|
||||
|
||||
/**
|
||||
* 根据已选明细槽位解析元素类型:LIST 取集合元素类或数组组件类型;OBJECT 取嵌套属性类型。
|
||||
*
|
||||
* @param slotKind LIST 或 OBJECT(与 {@link PrintBizDetailSlotVO#getSlotKind()} 一致)
|
||||
*/
|
||||
public static Class<?> resolveItemClassForSlot(
|
||||
Class<?> mainClazz, String propertyName, String slotKind) {
|
||||
if (mainClazz == null || StringUtils.isBlank(propertyName)) {
|
||||
return null;
|
||||
}
|
||||
Field f = findDeclaredField(mainClazz, propertyName.trim());
|
||||
if (f == null) {
|
||||
return null;
|
||||
}
|
||||
if ("OBJECT".equalsIgnoreCase(StringUtils.trimToEmpty(slotKind))) {
|
||||
Class<?> t = f.getType();
|
||||
return isLikelyBizBean(t) ? t : null;
|
||||
}
|
||||
Class<?> elem = resolveCollectionElementClass(f);
|
||||
if (elem != null) {
|
||||
return elem;
|
||||
}
|
||||
if (f.getType().isArray()) {
|
||||
Class<?> comp = f.getType().getComponentType();
|
||||
return isSimpleOrJdkValueType(comp) ? null : comp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Field findDeclaredField(Class<?> start, String name) {
|
||||
Class<?> c = start;
|
||||
while (c != null && c != Object.class) {
|
||||
try {
|
||||
return c.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 明细元素类型反射字段,fieldKey 已带「属性名.」前缀,便于与模板明细占位映射 */
|
||||
public static List<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(PrintBizFieldItemVO.copyWithPrefixedPath(x, path, label));
|
||||
}
|
||||
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<T> / Set<T> 的元素类型 T */
|
||||
private static Class<?> resolveCollectionElementClass(Field f) {
|
||||
Type gt = f.getGenericType();
|
||||
if (!(gt instanceof ParameterizedType)) {
|
||||
return null;
|
||||
}
|
||||
ParameterizedType pt = (ParameterizedType) gt;
|
||||
Type raw = pt.getRawType();
|
||||
if (!(raw instanceof Class<?> rc) || !Collection.class.isAssignableFrom(rc)) {
|
||||
return null;
|
||||
}
|
||||
Type[] args = pt.getActualTypeArguments();
|
||||
if (args.length != 1) {
|
||||
return null;
|
||||
}
|
||||
Type arg0 = args[0];
|
||||
if (arg0 instanceof Class<?>) {
|
||||
Class<?> ac = (Class<?>) arg0;
|
||||
return isSimpleOrJdkValueType(ac) ? null : ac;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isSimpleOrJdkValueType(Class<?> cl) {
|
||||
if (cl == null || cl.isPrimitive()) {
|
||||
return true;
|
||||
}
|
||||
if (cl.isEnum()) {
|
||||
return true;
|
||||
}
|
||||
String n = cl.getName();
|
||||
if (n.startsWith("java.lang") || n.startsWith("java.time")) {
|
||||
return true;
|
||||
}
|
||||
if (Number.class.isAssignableFrom(cl) || java.util.Date.class.isAssignableFrom(cl)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 非 JDK 简单值、非集合/Map 的自定义类型视为可映射明细实体 */
|
||||
private static boolean isLikelyBizBean(Class<?> cl) {
|
||||
return cl != null && !cl.isEnum() && !isSimpleOrJdkValueType(cl) && !Map.class.isAssignableFrom(cl);
|
||||
}
|
||||
|
||||
private static String resolveFieldLabel(Field f) {
|
||||
Schema schema = f.getAnnotation(Schema.class);
|
||||
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
||||
return schema.description().trim();
|
||||
}
|
||||
Excel excel = f.getAnnotation(Excel.class);
|
||||
if (excel != null && StringUtils.isNotBlank(excel.name())) {
|
||||
return excel.name().trim();
|
||||
}
|
||||
return f.getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。
|
||||
*
|
||||
* <p>写入缓存 JSON 时附带 {@code javaType}、{@code jdbcType}、{@code simpleKind}。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
PrintBizFieldItemVO vo =
|
||||
new PrintBizFieldItemVO(name, resolveLabel(f), "");
|
||||
fillJavaJdbcSimple(vo, f.getType());
|
||||
ordered.putIfAbsent(name, vo);
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
return new ArrayList<>(ordered.values());
|
||||
}
|
||||
|
||||
/** 将反射得到的 Java 类型写成 VO 上的三类提示字段 */
|
||||
public static void fillJavaJdbcSimple(PrintBizFieldItemVO vo, Class<?> declaredType) {
|
||||
if (vo == null || declaredType == null) {
|
||||
return;
|
||||
}
|
||||
vo.setJavaType(resolveJavaTypeFqn(declaredType));
|
||||
vo.setSimpleKind(resolveSimpleKind(declaredType));
|
||||
vo.setJdbcType(resolveJdbcTypeName(declaredType));
|
||||
}
|
||||
|
||||
private static String resolveJavaTypeFqn(Class<?> t) {
|
||||
String cn = t.getCanonicalName();
|
||||
return cn != null ? cn : t.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* STRING / BOOLEAN / INTEGER / LONG / DECIMAL / DATE / TIME / DATETIME / ENUM / BINARY / JAVA_OBJECT /
|
||||
* OTHER
|
||||
*/
|
||||
private static String resolveSimpleKind(Class<?> t) {
|
||||
if (t.isPrimitive()) {
|
||||
if (t == boolean.class) {
|
||||
return "BOOLEAN";
|
||||
}
|
||||
if (t == byte.class || t == short.class || t == int.class || t == char.class) {
|
||||
return "INTEGER";
|
||||
}
|
||||
if (t == long.class) {
|
||||
return "LONG";
|
||||
}
|
||||
if (t == float.class || t == double.class) {
|
||||
return "DECIMAL";
|
||||
}
|
||||
return "OTHER";
|
||||
}
|
||||
if (t == Boolean.class) {
|
||||
return "BOOLEAN";
|
||||
}
|
||||
if (t.isEnum()) {
|
||||
return "ENUM";
|
||||
}
|
||||
if (CharSequence.class.isAssignableFrom(t)) {
|
||||
return "STRING";
|
||||
}
|
||||
if (Number.class.isAssignableFrom(t)) {
|
||||
if (BigDecimal.class.isAssignableFrom(t)
|
||||
|| Float.class.isAssignableFrom(t)
|
||||
|| Double.class.isAssignableFrom(t)) {
|
||||
return "DECIMAL";
|
||||
}
|
||||
if (BigInteger.class.isAssignableFrom(t) || Long.class.isAssignableFrom(t)) {
|
||||
return "LONG";
|
||||
}
|
||||
return "INTEGER";
|
||||
}
|
||||
if (UUID.class.isAssignableFrom(t)) {
|
||||
return "STRING";
|
||||
}
|
||||
if (Date.class.isAssignableFrom(t)) {
|
||||
return "DATETIME";
|
||||
}
|
||||
if (LocalDate.class.isAssignableFrom(t)) {
|
||||
return "DATE";
|
||||
}
|
||||
if (LocalTime.class.isAssignableFrom(t)) {
|
||||
return "TIME";
|
||||
}
|
||||
if (LocalDateTime.class.isAssignableFrom(t)
|
||||
|| Instant.class.isAssignableFrom(t)
|
||||
|| ZonedDateTime.class.isAssignableFrom(t)
|
||||
|| OffsetDateTime.class.isAssignableFrom(t)) {
|
||||
return "DATETIME";
|
||||
}
|
||||
if (byte[].class == t) {
|
||||
return "BINARY";
|
||||
}
|
||||
if (t.getName().startsWith("java.") || t.getName().startsWith("javax.")) {
|
||||
return "JAVA_OBJECT";
|
||||
}
|
||||
return "JAVA_OBJECT";
|
||||
}
|
||||
|
||||
/**
|
||||
* 粗粒度 JDBC 名称(非穷尽);自定义 Bean 等返回 JAVA_OBJECT。
|
||||
*/
|
||||
private static String resolveJdbcTypeName(Class<?> t) {
|
||||
if (t.isPrimitive()) {
|
||||
if (t == boolean.class) {
|
||||
return "BOOLEAN";
|
||||
}
|
||||
if (t == byte.class) {
|
||||
return "TINYINT";
|
||||
}
|
||||
if (t == short.class) {
|
||||
return "SMALLINT";
|
||||
}
|
||||
if (t == int.class) {
|
||||
return "INTEGER";
|
||||
}
|
||||
if (t == long.class) {
|
||||
return "BIGINT";
|
||||
}
|
||||
if (t == float.class) {
|
||||
return "FLOAT";
|
||||
}
|
||||
if (t == double.class) {
|
||||
return "DOUBLE";
|
||||
}
|
||||
if (t == char.class) {
|
||||
return "CHAR";
|
||||
}
|
||||
return "OTHER";
|
||||
}
|
||||
if (t == Boolean.class) {
|
||||
return "BOOLEAN";
|
||||
}
|
||||
if (t.isEnum()) {
|
||||
return "VARCHAR";
|
||||
}
|
||||
if (String.class == t || CharSequence.class.isAssignableFrom(t)) {
|
||||
return "VARCHAR";
|
||||
}
|
||||
if (UUID.class.isAssignableFrom(t)) {
|
||||
return "CHAR";
|
||||
}
|
||||
if (Integer.class == t || Short.class == t || Byte.class == t) {
|
||||
return "INTEGER";
|
||||
}
|
||||
if (Long.class == t || BigInteger.class.isAssignableFrom(t)) {
|
||||
return "BIGINT";
|
||||
}
|
||||
if (BigDecimal.class.isAssignableFrom(t)) {
|
||||
return "DECIMAL";
|
||||
}
|
||||
if (Float.class == t || Double.class == t) {
|
||||
return "DOUBLE";
|
||||
}
|
||||
if (Date.class.isAssignableFrom(t)) {
|
||||
return "TIMESTAMP";
|
||||
}
|
||||
if (LocalDate.class.isAssignableFrom(t)) {
|
||||
return "DATE";
|
||||
}
|
||||
if (LocalTime.class.isAssignableFrom(t)) {
|
||||
return "TIME";
|
||||
}
|
||||
if (LocalDateTime.class.isAssignableFrom(t)
|
||||
|| Instant.class.isAssignableFrom(t)
|
||||
|| ZonedDateTime.class.isAssignableFrom(t)
|
||||
|| OffsetDateTime.class.isAssignableFrom(t)) {
|
||||
return "TIMESTAMP";
|
||||
}
|
||||
if (byte[].class == t) {
|
||||
return "BLOB";
|
||||
}
|
||||
return "JAVA_OBJECT";
|
||||
}
|
||||
|
||||
private static String resolveLabel(Field f) {
|
||||
Schema schema = f.getAnnotation(Schema.class);
|
||||
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Vue:component 常为 mes/xxx/index,实体类名在 component_name(如 MesMaterialList)
|
||||
if (c.startsWith("mes/") && c.endsWith("/index")) {
|
||||
String fromMesIndex =
|
||||
tryInferMesMaterialModuleFromComponentName(permission.getComponentName());
|
||||
if (StringUtils.isNotBlank(fromMesIndex)) {
|
||||
return fromMesIndex;
|
||||
}
|
||||
}
|
||||
|
||||
String[] segs = c.split("/");
|
||||
if (segs.length < 2) {
|
||||
return null;
|
||||
}
|
||||
String last = segs[segs.length - 1];
|
||||
if (StringUtils.isBlank(last)) {
|
||||
return null;
|
||||
}
|
||||
String simple =
|
||||
last.endsWith("List") ? last.substring(0, last.length() - 4) : last;
|
||||
if (simple.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String module = segs[0];
|
||||
if (StringUtils.isBlank(module)) {
|
||||
return null;
|
||||
}
|
||||
String trial = "org.jeecg.modules." + module + ".entity." + simple;
|
||||
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
|
||||
return trial;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** mes 模块下 index 路由:用 componentName(MesXxxList)推断 mes.material.entity.MesXxx */
|
||||
private static String tryInferMesMaterialModuleFromComponentName(String componentName) {
|
||||
if (StringUtils.isBlank(componentName)) {
|
||||
return null;
|
||||
}
|
||||
String cn = componentName.trim();
|
||||
if (!cn.endsWith("List")) {
|
||||
return null;
|
||||
}
|
||||
String simple = cn.substring(0, cn.length() - 4);
|
||||
if (simple.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String trial = MES_MATERIAL_ENTITY_PKG + simple;
|
||||
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
|
||||
return trial;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
|
||||
|
||||
/**
|
||||
* 从原生打印模板 JSON 中收集所有 bindField / 表格列 field,供业务字段映射使用。
|
||||
*
|
||||
* <p>原生设计器在 {@code dataBinding.params} 中维护「参数」列表(如 Parameter1~Parameter10),画布元素通过
|
||||
* bindField 引用这些 key;仅扫描 elements 会漏掉未拖放到画布上的参数,故必须先合并 dataBinding。
|
||||
*/
|
||||
public final class PrintNativeTemplateFieldExtractor {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private PrintNativeTemplateFieldExtractor() {}
|
||||
|
||||
public static List<PrintTemplateFieldItemVO> extract(String templateJson) {
|
||||
List<PrintTemplateFieldItemVO> list = new ArrayList<>();
|
||||
if (StringUtils.isBlank(templateJson)) {
|
||||
return list;
|
||||
}
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(templateJson);
|
||||
// 优先收录设计器「参数/明细字段」目录,保证显示名与组件库一致,且不被后续元素扫描覆盖
|
||||
collectDataBinding(root, seen, list);
|
||||
JsonNode elements = root.get("elements");
|
||||
if (elements != null && elements.isArray()) {
|
||||
for (JsonNode el : elements) {
|
||||
collectFromElement(el, seen, list);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return list;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 解析 schema.dataBinding:params(参数键)、detailTables(明细字段) */
|
||||
private static void collectDataBinding(JsonNode root, Set<String> seen, List<PrintTemplateFieldItemVO> list) {
|
||||
JsonNode db = root.get("dataBinding");
|
||||
if (db == null || !db.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode params = db.get("params");
|
||||
if (params != null && params.isArray()) {
|
||||
for (JsonNode p : params) {
|
||||
if (p == null || !p.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String key = text(p, "key").trim();
|
||||
if (StringUtils.isBlank(key) || !seen.add(key)) {
|
||||
continue;
|
||||
}
|
||||
list.add(new PrintTemplateFieldItemVO(key, "param", text(p, "label")));
|
||||
}
|
||||
}
|
||||
JsonNode detailTables = db.get("detailTables");
|
||||
if (detailTables != null && detailTables.isArray()) {
|
||||
for (JsonNode t : detailTables) {
|
||||
if (t == null || !t.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String tableKey = text(t, "tableKey").trim();
|
||||
JsonNode fields = t.get("fields");
|
||||
if (fields == null || !fields.isArray()) {
|
||||
continue;
|
||||
}
|
||||
for (JsonNode f : fields) {
|
||||
if (f == null || !f.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String fk = text(f, "key").trim();
|
||||
if (StringUtils.isBlank(fk)) {
|
||||
continue;
|
||||
}
|
||||
// 与画布列 bindField 一致时多为短 key;多表明细同字段再加 tableKey 前缀消歧
|
||||
String bindKey = fk;
|
||||
if (seen.contains(bindKey) && StringUtils.isNotBlank(tableKey)) {
|
||||
bindKey = tableKey + "." + fk;
|
||||
}
|
||||
if (seen.contains(bindKey) || !seen.add(bindKey)) {
|
||||
continue;
|
||||
}
|
||||
list.add(new PrintTemplateFieldItemVO(bindKey, "detailField", text(f, "label")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectFromElement(JsonNode el, Set<String> seen, List<PrintTemplateFieldItemVO> list) {
|
||||
if (el == null || !el.isObject()) {
|
||||
return;
|
||||
}
|
||||
String bindField = text(el, "bindField");
|
||||
if (StringUtils.isNotBlank(bindField) && seen.add(bindField.trim())) {
|
||||
list.add(
|
||||
new PrintTemplateFieldItemVO(
|
||||
bindField.trim(),
|
||||
text(el, "type"),
|
||||
firstNonBlank(text(el, "title"), text(el, "text"))));
|
||||
}
|
||||
JsonNode cols = el.get("columns");
|
||||
if (cols != null && cols.isArray()) {
|
||||
for (JsonNode c : cols) {
|
||||
String field = firstNonBlank(text(c, "bindField"), text(c, "field"));
|
||||
if (StringUtils.isNotBlank(field) && seen.add(field.trim())) {
|
||||
list.add(new PrintTemplateFieldItemVO(field.trim(), "column", text(c, "title")));
|
||||
}
|
||||
}
|
||||
}
|
||||
JsonNode nested = el.get("elements");
|
||||
if (nested != null && nested.isArray()) {
|
||||
for (JsonNode child : nested) {
|
||||
collectFromElement(child, seen, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode n, String key) {
|
||||
if (n == null || !n.isObject()) {
|
||||
return "";
|
||||
}
|
||||
JsonNode v = n.get(key);
|
||||
return v == null || v.isNull() ? "" : v.asText("");
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String a, String b) {
|
||||
if (StringUtils.isNotBlank(a)) {
|
||||
return a;
|
||||
}
|
||||
return StringUtils.isNotBlank(b) ? b : "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.jeecg.modules.print.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "业务实体可选字段")
|
||||
public class PrintBizFieldItemVO implements Serializable {
|
||||
|
||||
@Schema(description = "字段路径(支持 a.b,与 JSON 一致)")
|
||||
private String fieldKey;
|
||||
|
||||
@Schema(description = "展示名称")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "说明")
|
||||
private String description;
|
||||
|
||||
/** 声明类型全限定名(如 java.lang.String、java.time.LocalDateTime) */
|
||||
@Schema(description = "Java 声明类型全限定名")
|
||||
private String javaType;
|
||||
|
||||
/**
|
||||
* 与 JDBC 习惯对齐的类型名(如 VARCHAR、BIGINT、DECIMAL、TIMESTAMP);非 JDBC 标量填 JAVA_OBJECT。
|
||||
*/
|
||||
@Schema(description = "粗粒度 JDBC 风格类型名")
|
||||
private String jdbcType;
|
||||
|
||||
/**
|
||||
* 粗分类:STRING、BOOLEAN、INTEGER、LONG、DECIMAL、DATE、TIME、DATETIME、ENUM、BINARY、OTHER、JAVA_OBJECT
|
||||
*/
|
||||
@Schema(description = "简化种类,便于前端格式化")
|
||||
private String simpleKind;
|
||||
|
||||
/** 兼容旧三参构造(类型字段为空) */
|
||||
public PrintBizFieldItemVO(String fieldKey, String label, String description) {
|
||||
this.fieldKey = fieldKey;
|
||||
this.label = label;
|
||||
this.description = description == null ? "" : description;
|
||||
}
|
||||
|
||||
/** 复制前缀明细字段时保留类型元数据 */
|
||||
public static PrintBizFieldItemVO copyWithPrefixedPath(
|
||||
PrintBizFieldItemVO src, String prefixedFieldKey, String prefixedLabel) {
|
||||
PrintBizFieldItemVO o = new PrintBizFieldItemVO();
|
||||
o.setFieldKey(prefixedFieldKey);
|
||||
o.setLabel(prefixedLabel);
|
||||
o.setDescription(src != null ? src.getDescription() : "");
|
||||
if (src != null) {
|
||||
o.setJavaType(src.getJavaType());
|
||||
o.setJdbcType(src.getJdbcType());
|
||||
o.setSimpleKind(src.getSimpleKind());
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/** 仅占位字符串数组解析出来的单项(视为字符串) */
|
||||
public static PrintBizFieldItemVO plainStringField(String fieldKey) {
|
||||
PrintBizFieldItemVO o = new PrintBizFieldItemVO(fieldKey, fieldKey, "");
|
||||
o.setJavaType(String.class.getName());
|
||||
o.setJdbcType("VARCHAR");
|
||||
o.setSimpleKind("STRING");
|
||||
return o;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jeecg.modules.print.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "可配置的业务类型")
|
||||
public class PrintBizTypeVO implements Serializable {
|
||||
@Schema(description = "业务编码(与菜单权限 id、绑定表 biz_code 一致)")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称")
|
||||
private String bizName;
|
||||
|
||||
@Schema(description = "说明")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 与「系统菜单」中的菜单主键(sys_permission.id)对应;用于「打印业务白名单」按菜单勾选。
|
||||
* 未设置表示未挂菜单,白名单生效时仍可出现于下拉(避免仅后端注册、尚未配菜单的业务被误过滤)。
|
||||
*/
|
||||
@Schema(description = "关联菜单权限ID(sys_permission.id)")
|
||||
private String linkedPermissionId;
|
||||
|
||||
@Schema(description = "业务侧可用字段目录")
|
||||
private List<PrintBizFieldItemVO> fields;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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 PrintTemplateFieldItemVO implements Serializable {
|
||||
@Schema(description = "模板 bindField 路径")
|
||||
private String bindField;
|
||||
|
||||
@Schema(description = "元素类型")
|
||||
private String elementType;
|
||||
|
||||
@Schema(description = "元素标题/提示")
|
||||
private String titleHint;
|
||||
}
|
||||
@@ -416,13 +416,7 @@ public class SysCategoryController {
|
||||
* 递归求子节点 同步加载用到
|
||||
*/
|
||||
private void loadAllCategoryChildren(List<TreeSelectModel> ls) {
|
||||
for (TreeSelectModel tsm : ls) {
|
||||
List<TreeSelectModel> temp = this.sysCategoryService.queryListByPid(tsm.getKey());
|
||||
if(temp!=null && temp.size()>0) {
|
||||
tsm.setChildren(temp);
|
||||
loadAllCategoryChildren(temp);
|
||||
}
|
||||
}
|
||||
this.sysCategoryService.fillCategoryChildrenBatch(ls);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,11 @@ public interface SysCategoryMapper extends BaseMapper<SysCategory> {
|
||||
*/
|
||||
public List<TreeSelectModel> queryListByPid(@Param("pid") String pid,@Param("query") Map<String, String> query);
|
||||
|
||||
/**
|
||||
* 批量按父 id 查询子节点(树同步加载按层拉取,避免 N+1)
|
||||
*/
|
||||
List<TreeSelectModel> queryListByPidIn(@Param("pids") List<String> pids, @Param("query") Map<String, String> query);
|
||||
|
||||
/**
|
||||
* 通过code查询分类字典表
|
||||
* @param code
|
||||
|
||||
@@ -33,5 +33,36 @@
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="queryListByPidIn" resultType="org.jeecg.modules.system.model.TreeSelectModel">
|
||||
select code,
|
||||
name as "title",
|
||||
id as "key",
|
||||
(case when has_child = '1' then 0 else 1 end) as isLeaf,
|
||||
pid as parentId
|
||||
from sys_category
|
||||
where pid in
|
||||
<foreach collection="pids" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
<if test="query!= null">
|
||||
<if test="query.code !=null and query.code != ''">
|
||||
and code = #{query.code}
|
||||
</if>
|
||||
<if test="query.name !=null and query.name != ''">
|
||||
and name = #{query.name}
|
||||
</if>
|
||||
<if test="query.id !=null and query.id != ''">
|
||||
and id = #{query.id}
|
||||
</if>
|
||||
<if test="query.createBy !=null and query.createBy != ''">
|
||||
and create_by = #{query.createBy}
|
||||
</if>
|
||||
<if test="query.sysOrgCode !=null and query.sysOrgCode != ''">
|
||||
and sys_org_code = #{query.sysOrgCode}
|
||||
</if>
|
||||
</if>
|
||||
order by code
|
||||
</select>
|
||||
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -98,4 +98,11 @@ public interface ISysCategoryService extends IService<SysCategory> {
|
||||
*/
|
||||
List<String> loadDictItemByNames(String names, boolean delNotExist);
|
||||
|
||||
/**
|
||||
* 按层批量补全子节点(替代逐节点递归 queryListByPid,显著减少数据库往返)
|
||||
*
|
||||
* @param nodes 首层节点列表(通常来自 {@link #queryListByCode})
|
||||
*/
|
||||
void fillCategoryChildrenBatch(List<TreeSelectModel> nodes);
|
||||
|
||||
}
|
||||
|
||||
@@ -126,6 +126,37 @@ public class SysCategoryServiceImpl extends ServiceImpl<SysCategoryMapper, SysCa
|
||||
return baseMapper.queryListByPid(pid,condition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillCategoryChildrenBatch(List<TreeSelectModel> nodes) {
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<TreeSelectModel> frontier = new ArrayList<>(nodes);
|
||||
while (!frontier.isEmpty()) {
|
||||
List<String> pids = frontier.stream().map(TreeSelectModel::getKey).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
if (pids.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
List<TreeSelectModel> allChildren = baseMapper.queryListByPidIn(pids, null);
|
||||
if (allChildren == null || allChildren.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
Map<String, List<TreeSelectModel>> byParent = allChildren.stream().collect(Collectors.groupingBy(TreeSelectModel::getParentId));
|
||||
List<TreeSelectModel> nextFrontier = new ArrayList<>();
|
||||
for (TreeSelectModel parent : frontier) {
|
||||
List<TreeSelectModel> ch = byParent.get(parent.getKey());
|
||||
if (ch != null && !ch.isEmpty()) {
|
||||
parent.setChildren(ch);
|
||||
nextFrontier.addAll(ch);
|
||||
}
|
||||
}
|
||||
if (nextFrontier.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
frontier = nextFrontier;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryIdByCode(String code) {
|
||||
return baseMapper.queryIdByCode(code);
|
||||
|
||||
@@ -48,6 +48,7 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
|
||||
String port = env.getProperty("server.port");
|
||||
String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path"));
|
||||
log.info("\n----------------------------------------------------------\n\t" +
|
||||
|
||||
"Application Jeecg-Boot is running! Access URLs:\n\t" +
|
||||
"Local: \t\thttp://localhost:" + port + path + "\n\t" +
|
||||
"External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" +
|
||||
|
||||
@@ -2,6 +2,8 @@ spring:
|
||||
application:
|
||||
name: jeecg-system
|
||||
config:
|
||||
import: optional:classpath:config/application-liteflow.yml
|
||||
import:
|
||||
- optional:classpath:config/application-liteflow.yml
|
||||
- optional:classpath:config/application-xslmes-warehouse-area.yml
|
||||
profiles:
|
||||
active: '@profile.name@'
|
||||
@@ -0,0 +1,11 @@
|
||||
# MES XSL — 库区「实际存放量」展示回填(不写库,可覆盖)
|
||||
xslmes:
|
||||
warehouse-area:
|
||||
display-actual-capacity:
|
||||
enabled: true
|
||||
# 按「原材料库」分类编码解析 sys_category.id(推荐;业务树已无楼层语义,编码中 F1/F2 后缀若变化请改此处)
|
||||
raw-material-warehouse-category-codes:
|
||||
- XSLMES_WH_F1_YCL
|
||||
- XSLMES_WH_F2_YCL
|
||||
# 可选:直接写 warehouse_category=id,与上面的编码解析结果合并
|
||||
raw-material-warehouse-category-ids: []
|
||||
@@ -0,0 +1,43 @@
|
||||
-- 业务与打印模板绑定(字段映射可视化配置)
|
||||
CREATE TABLE IF NOT EXISTS `print_biz_template_bind` (
|
||||
`id` varchar(36) NOT NULL COMMENT '主键',
|
||||
`biz_code` varchar(64) NOT NULL COMMENT '业务编码(如 MES_RAW_MATERIAL_CARD)',
|
||||
`biz_name` varchar(128) DEFAULT NULL COMMENT '业务名称(冗余展示)',
|
||||
`template_id` varchar(36) NOT NULL COMMENT '打印模板主键',
|
||||
`template_code` varchar(64) NOT NULL COMMENT '打印模板编码(冗余,便于调用方查询)',
|
||||
`field_mapping_json` longtext COMMENT '字段映射 JSON:[{templateField,bizField}],templateField 对应模板 bindField',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_print_biz_template_bind_biz` (`biz_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务打印模板绑定';
|
||||
|
||||
-- 菜单:打印管理下「业务打印绑定」
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000120', '1900000000000000100', '业务打印绑定', '/print/bizTemplateBind', 'print/bizTemplateBind/index', 1, 'PrintBizTemplateBind', NULL, 1, NULL, '0', 3.00, 0, 'ant-design:link-outlined', 1, 1, 0, 0, '业务与打印模板、字段映射配置', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000120');
|
||||
|
||||
-- 按钮权限
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000121', '1900000000000000120', '业务打印绑定-查询', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:list', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000121');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000122', '1900000000000000120', '业务打印绑定-添加', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:add', '1', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000122');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000123', '1900000000000000120', '业务打印绑定-编辑', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:edit', '1', 3.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000123');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000124', '1900000000000000120', '业务打印绑定-删除', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:delete', '1', 4.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000124');
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 打印业务可选范围:白名单(勾选 sys_permission.id,对应 PrintBizTypeCatalog 中 linkedPermissionId)
|
||||
CREATE TABLE IF NOT EXISTS `print_biz_bind_perm_whitelist` (
|
||||
`perm_id` varchar(36) NOT NULL COMMENT 'sys_permission 主键(菜单/功能)',
|
||||
PRIMARY KEY (`perm_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务打印绑定-可选业务白名单(权限菜单)';
|
||||
|
||||
-- 按钮:打印业务白名单
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000125', '1900000000000000120', '打印业务白名单', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:whitelist', '1', 5.00, 0, NULL, 1, 0, 0, 0, '配置哪些菜单关联的打印业务可出现在「新增业务打印绑定」中', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000125');
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 菜单权限与打印业务实体类映射(biz_code / 绑定表业务编码 = perm_id)
|
||||
CREATE TABLE IF NOT EXISTS `print_biz_perm_entity` (
|
||||
`perm_id` varchar(36) NOT NULL COMMENT 'sys_permission.id',
|
||||
`entity_class` varchar(512) NOT NULL COMMENT '实体类全限定名(用于反射字段)',
|
||||
PRIMARY KEY (`perm_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印业务-菜单与实体映射';
|
||||
|
||||
-- 原材料卡片菜单 -> MesXslRawMaterialCard
|
||||
INSERT INTO `print_biz_perm_entity` (`perm_id`, `entity_class`)
|
||||
SELECT '1900000000000000540', 'org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `print_biz_perm_entity` WHERE `perm_id` = '1900000000000000540');
|
||||
|
||||
-- 历史绑定:业务编码由语义码改为菜单 id(与白名单、映射表一致)
|
||||
UPDATE `print_biz_template_bind`
|
||||
SET `biz_code` = '1900000000000000540'
|
||||
WHERE `biz_code` = 'MES_RAW_MATERIAL_CARD';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 允许 entity_class 为空:白名单勾选的菜单优先落库占位,无法按 component 推断时再手工补全
|
||||
ALTER TABLE `print_biz_perm_entity`
|
||||
MODIFY COLUMN `entity_class` varchar(512) NULL COMMENT '实体类全限定名;为空表示仅勾选占位,需手工配置或菜单不符合推断规则';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 原材料库区看板:菜单与查询权限(父菜单 MES XSL 1900000000000000300)
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000580', '1900000000000000300', '原材料库区看板', '/xslmes/mesXslRawMaterialWarehouseBoard', 'xslmes/mesXslRawMaterialWarehouseBoard/MesXslRawMaterialWarehouseBoard', 1, NULL, NULL, 1, NULL, '0', 12.50, 0, 'ant-design:layout-outlined', 0, 1, 0, 0, '按库区聚合展示原材料卡片库存', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000580');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000581', '1900000000000000580', '查询', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_warehouse_board:list', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000581');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000580', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000580');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000581', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000581');
|
||||
Reference in New Issue
Block a user