Merge branch 'main' into 生产基础资料

This commit is contained in:
2026-05-15 17:21:36 +08:00
2550 changed files with 63705 additions and 498 deletions

View File

@@ -46,7 +46,7 @@ public class MesMixerMaterial implements Serializable {
@Excel(name = "物料别名", width = 15)
private String aliasName;
@Excel(name = "状态", width = 12, replace = {"在投管_1", "未投管_0"})
@Excel(name = "状态", width = 12, replace = {"在投管_1", "未投管_0"})
private Integer feedManageStatus;
@Excel(name = "使用状态", width = 12, replace = {"使用中_1", "停用_0"})
private Integer useStatus;

View File

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

View File

@@ -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);
}

View File

@@ -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 = "打印业务白名单:当前勾选的菜单 idcatalog 恒为空,避免全库反射超时)")
@GetMapping("/permWhitelist")
@RequiresPermissions("print:bizBind:whitelist")
public Result<PrintBizPermWhitelistVO> getPermWhitelist() {
PrintBizPermWhitelistVO vo = new PrintBizPermWhitelistVO();
vo.setPermIds(printBizBindPermWhitelistService.listPermIds());
vo.setCatalog(Collections.emptyList());
return Result.OK(vo);
}
@AutoLog(value = "业务打印绑定-保存打印业务白名单")
@Operation(summary = "保存打印业务白名单(按菜单 permission id")
@PostMapping("/permWhitelist")
@RequiresPermissions("print:bizBind:whitelist")
public Result<String> savePermWhitelist(@RequestBody PermWhitelistBody body) {
printBizBindPermWhitelistService.replacePermIds(body == null ? null : body.getPermIds());
return Result.OK("保存成功");
}
@Operation(summary = "解析原生模板中的占位字段bindField")
@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&lt;明细实体&gt; / 明细数组 / 嵌套对象),用于先选明细再反射明细字段")
@GetMapping("/detailSlots")
@RequiresPermissions("print:bizBind:list")
public Result<List<PrintBizDetailSlotVO>> detailSlots(@RequestParam(name = "bizCode") String bizCode) {
if (StringUtils.isBlank(bizCode)) {
return Result.error("bizCode 不能为空");
}
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_classbiz_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());
}
}
}

View File

@@ -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,8 +27,9 @@ 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;
@@ -59,8 +43,8 @@ import com.alibaba.fastjson.JSON;
@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;
@@ -225,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 = "打印模板-服务端直打")
@@ -286,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("未找到可用打印机,请检查服务器打印机配置");
@@ -362,109 +297,7 @@ 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());
}
}
/**
* 若打印机声明支持 application/pdf则通过 DocPrintJob 提交,通常比 AWT 栅格化更稳定。
*/
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();
return printServerPdfJobService.submitPdfBase64(printerName, pdfBase64, fileName, templateCode);
}
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
@@ -491,26 +324,6 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
return Result.OK(service.page(page, qw));
}
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;
}
/**
* 广播打印模板变更事件到 /topic/sync/print-templates桌面端订阅同步刷新本地缓存。
* 消息体格式 = MesXslStompNotifyService.publishPrintTemplateChanged 的输出,

View File

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

View File

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

View File

@@ -0,0 +1,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;
}

View File

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

View File

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

View File

@@ -0,0 +1,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> {}

View File

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

View File

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

View File

@@ -0,0 +1,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);
}

View File

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

View File

@@ -0,0 +1,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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -0,0 +1,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;
}
}
}

View File

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

View File

@@ -0,0 +1,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} 中维护「参数」列表(如 Parameter1Parameter10画布元素通过
* 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.dataBindingparams参数键、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 : "";
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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 = "关联菜单权限IDsys_permission.id")
private String linkedPermissionId;
@Schema(description = "业务侧可用字段目录")
private List<PrintBizFieldItemVO> fields;
}

View File

@@ -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;
}

View File

@@ -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);
}
/**

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -39,7 +39,7 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
SpringApplication app = new SpringApplication(JeecgSystemApplication.class);
Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("management.health.elasticsearch.enabled", false);
app.setDefaultProperties(defaultProperties);
app.setDefaultProperties(defaultProperties);
log.info("[JEECG] Elasticsearch Health Check Enabled: false" );
ConfigurableApplicationContext application = app.run(args);;
@@ -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" +

View File

@@ -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@'

View File

@@ -0,0 +1,11 @@
# MES XSL — 库区「实际存放量」兜底配置(不写库的可选 YAML
# 当前租户若在表 mes_xsl_warehouse_area_capacity_cfg 中已保存「匹配仓库」配置,则<strong>仅以库内配置为准</strong>
# 本文件的 codes/ids/enabled 仅在该租户尚无库内记录时作为兜底生效。
xslmes:
warehouse-area:
display-actual-capacity:
enabled: true
raw-material-warehouse-category-codes:
- XSLMES_WH_F1_YCL
- XSLMES_WH_F2_YCL
raw-material-warehouse-category-ids: []

View File

@@ -0,0 +1,45 @@
-- 原材料库存建表 + 菜单权限幂等
-- ===================== 1. 建表 =====================
CREATE TABLE IF NOT EXISTS `mes_xsl_raw_material_inventory` (
`id` varchar(32) NOT NULL COMMENT '主键',
`warehouse_name` varchar(200) DEFAULT NULL COMMENT '所在仓库',
`material_name` varchar(200) DEFAULT NULL COMMENT '物料名称',
`material_code` varchar(100) DEFAULT NULL COMMENT '物料编码',
`test_result` varchar(10) DEFAULT NULL COMMENT '状态检验字典 xslmes_test_result',
`total_packages` decimal(18,3) DEFAULT 0 COMMENT '总包数',
`total_weight` decimal(18,3) DEFAULT 0 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 '更新时间',
`tenant_id` int DEFAULT 1002 COMMENT '租户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rmi_wh_mat_result` (`warehouse_name`, `material_code`, `test_result`),
KEY `idx_rmi_material_code` (`material_code`),
KEY `idx_rmi_test_result` (`test_result`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原材料库存';
-- ===================== 2. 菜单权限 =====================
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 '1900000000000000600', '1900000000000000300', '原材料库存', '/xslmes/mesXslRawMaterialInventory', 'xslmes/mesXslRawMaterialInventory/MesXslRawMaterialInventoryList', 1, NULL, NULL, 1, NULL, '0', 12.00, 0, 'ant-design:inbox-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` = '1900000000000000600');
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 '1900000000000000601', '1900000000000000600', '导出', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_inventory:exportXls', '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` = '1900000000000000601');
-- 默认管理员角色授权
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', '1900000000000000600', NULL, NOW(), '127.0.0.1'
WHERE NOT EXISTS (
SELECT 1 FROM `sys_role_permission`
WHERE `role_id` = 'f6817f48af4fb3af11b9e8bf182f618b' AND `permission_id` = '1900000000000000600'
);
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', '1900000000000000601', NULL, NOW(), '127.0.0.1'
WHERE NOT EXISTS (
SELECT 1 FROM `sys_role_permission`
WHERE `role_id` = 'f6817f48af4fb3af11b9e8bf182f618b' AND `permission_id` = '1900000000000000601'
);

View File

@@ -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');

View File

@@ -0,0 +1,53 @@
-- 原材料库存改为保存物料ID关联 mes_mixer_material.id并调整唯一键口径
-- 1) 新增 material_id 字段若不存在
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND COLUMN_NAME = 'material_id'
);
SET @ddl := IF(@col_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD COLUMN `material_id` varchar(32) DEFAULT NULL COMMENT ''物料ID关联 mes_mixer_material.id'' AFTER `warehouse_name`',
'SELECT 1'
);
PREPARE s1 FROM @ddl; EXECUTE s1; DEALLOCATE PREPARE s1;
-- 2) 删除旧唯一键若存在
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wh_mat_result'
);
SET @ddl := IF(@idx_exists > 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` DROP INDEX `uk_rmi_wh_mat_result`',
'SELECT 1'
);
PREPARE s2 FROM @ddl; EXECUTE s2; DEALLOCATE PREPARE s2;
-- 3) 新唯一键所在仓库 + 物料ID + 状态检验
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wh_mid_result'
);
SET @ddl := IF(@idx_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD UNIQUE KEY `uk_rmi_wh_mid_result` (`warehouse_name`, `material_id`, `test_result`)',
'SELECT 1'
);
PREPARE s3 FROM @ddl; EXECUTE s3; DEALLOCATE PREPARE s3;
-- 4) 查询索引material_id
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'idx_rmi_material_id'
);
SET @ddl := IF(@idx_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD KEY `idx_rmi_material_id` (`material_id`)',
'SELECT 1'
);
PREPARE s4 FROM @ddl; EXECUTE s4; DEALLOCATE PREPARE s4;

View File

@@ -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');

View File

@@ -0,0 +1,72 @@
-- 原材料库存历史数据回填 material_id material_code 关联 mes_mixer_material
-- 处理策略
-- 1) material_id 为空且可按编码匹配的库存行先聚合后写入遇唯一键冲突自动累加
-- 2) 删除已迁移的旧空 material_id 避免重复数据
-- 3) 对已有 material_id 但编码/名称为空的数据做展示字段补齐
DROP TEMPORARY TABLE IF EXISTS `tmp_rmi_backfill`;
CREATE TEMPORARY TABLE `tmp_rmi_backfill` AS
SELECT
i.warehouse_name,
m.id AS material_id,
i.test_result,
MAX(COALESCE(NULLIF(i.material_code, ''), m.material_code)) AS material_code,
MAX(COALESCE(NULLIF(i.material_name, ''), m.material_name)) AS material_name,
SUM(IFNULL(i.total_packages, 0)) AS total_packages,
SUM(IFNULL(i.total_weight, 0)) AS total_weight,
MAX(i.tenant_id) AS tenant_id
FROM mes_xsl_raw_material_inventory i
INNER JOIN mes_mixer_material m
ON m.material_code = i.material_code
AND (m.del_flag = 0 OR m.del_flag IS NULL)
WHERE (i.material_id IS NULL OR i.material_id = '')
AND i.material_code IS NOT NULL
AND i.material_code <> ''
GROUP BY i.warehouse_name, m.id, i.test_result;
INSERT INTO mes_xsl_raw_material_inventory
(`id`, `warehouse_name`, `material_id`, `material_name`, `material_code`, `test_result`, `total_packages`, `total_weight`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`)
SELECT
REPLACE(UUID(), '-', ''),
t.warehouse_name,
t.material_id,
t.material_name,
t.material_code,
t.test_result,
t.total_packages,
t.total_weight,
'admin',
NOW(),
'admin',
NOW(),
IFNULL(t.tenant_id, 1002)
FROM tmp_rmi_backfill t
ON DUPLICATE KEY UPDATE
total_packages = IFNULL(total_packages, 0) + VALUES(total_packages),
total_weight = IFNULL(total_weight, 0) + VALUES(total_weight),
material_code = CASE WHEN material_code IS NULL OR material_code = '' THEN VALUES(material_code) ELSE material_code END,
material_name = CASE WHEN material_name IS NULL OR material_name = '' THEN VALUES(material_name) ELSE material_name END,
update_by = 'admin',
update_time = NOW();
DELETE i
FROM mes_xsl_raw_material_inventory i
INNER JOIN mes_mixer_material m
ON m.material_code = i.material_code
AND (m.del_flag = 0 OR m.del_flag IS NULL)
WHERE (i.material_id IS NULL OR i.material_id = '')
AND i.material_code IS NOT NULL
AND i.material_code <> '';
UPDATE mes_xsl_raw_material_inventory i
INNER JOIN mes_mixer_material m
ON m.id = i.material_id
AND (m.del_flag = 0 OR m.del_flag IS NULL)
SET
i.material_code = CASE WHEN i.material_code IS NULL OR i.material_code = '' THEN m.material_code ELSE i.material_code END,
i.material_name = CASE WHEN i.material_name IS NULL OR i.material_name = '' THEN m.material_name ELSE i.material_name END,
i.update_by = 'admin',
i.update_time = NOW()
WHERE (i.material_code IS NULL OR i.material_code = '' OR i.material_name IS NULL OR i.material_name = '');
DROP TEMPORARY TABLE IF EXISTS `tmp_rmi_backfill`;

View File

@@ -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';

View File

@@ -0,0 +1,63 @@
-- 原材料库存改为保存 warehouse_id关联 mes_xsl_warehouse.id并调整唯一键口径
-- 1) 新增 warehouse_id 字段若不存在
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND COLUMN_NAME = 'warehouse_id'
);
SET @ddl := IF(@col_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD COLUMN `warehouse_id` varchar(32) DEFAULT NULL COMMENT ''所在仓库ID关联 mes_xsl_warehouse.id'' AFTER `warehouse_name`',
'SELECT 1'
);
PREPARE s1 FROM @ddl; EXECUTE s1; DEALLOCATE PREPARE s1;
-- 2) 回填 warehouse_id按仓库名称关联
UPDATE mes_xsl_raw_material_inventory i
INNER JOIN mes_xsl_warehouse w
ON w.warehouse_name = i.warehouse_name
AND (w.del_flag = 0 OR w.del_flag IS NULL)
SET i.warehouse_id = w.id
WHERE (i.warehouse_id IS NULL OR i.warehouse_id = '')
AND i.warehouse_name IS NOT NULL
AND i.warehouse_name <> '';
-- 3) 删除旧唯一键若存在
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wh_mid_result'
);
SET @ddl := IF(@idx_exists > 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` DROP INDEX `uk_rmi_wh_mid_result`',
'SELECT 1'
);
PREPARE s2 FROM @ddl; EXECUTE s2; DEALLOCATE PREPARE s2;
-- 4) 新唯一键仓库ID + 物料ID + 状态检验
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wid_mid_result'
);
SET @ddl := IF(@idx_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD UNIQUE KEY `uk_rmi_wid_mid_result` (`warehouse_id`, `material_id`, `test_result`)',
'SELECT 1'
);
PREPARE s3 FROM @ddl; EXECUTE s3; DEALLOCATE PREPARE s3;
-- 5) 查询索引warehouse_id
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'idx_rmi_warehouse_id'
);
SET @ddl := IF(@idx_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD KEY `idx_rmi_warehouse_id` (`warehouse_id`)',
'SELECT 1'
);
PREPARE s4 FROM @ddl; EXECUTE s4; DEALLOCATE PREPARE s4;

View File

@@ -0,0 +1,3 @@
-- 允许 entity_class 为空白名单勾选的菜单优先落库占位无法按 component 推断时再手工补全
ALTER TABLE `print_biz_perm_entity`
MODIFY COLUMN `entity_class` varchar(512) NULL COMMENT '实体类全限定名为空表示仅勾选占位需手工配置或菜单不符合推断规则';

View File

@@ -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 '主表字段列表 JSONPrintBizFieldItemVO 数组',
`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业务实体字段配置-明细槽位字段清单';

View File

@@ -0,0 +1,86 @@
-- 原材料库存所属仓库 + 物料唯一数量/重量累计
-- 处理内容
-- 1) 删除旧唯一键 test_result 维度
-- 2) 合并历史重复行同仓库同物料累计 total_packages/total_weight
-- 3) 新建唯一键warehouse_id + material_id
-- 1) 删除旧唯一键若存在
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wid_mid_result'
);
SET @ddl := IF(@idx_exists > 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` DROP INDEX `uk_rmi_wid_mid_result`',
'SELECT 1'
);
PREPARE s1 FROM @ddl; EXECUTE s1; DEALLOCATE PREPARE s1;
-- 兼容旧脚本里的唯一键名
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wh_mid_result'
);
SET @ddl := IF(@idx_exists > 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` DROP INDEX `uk_rmi_wh_mid_result`',
'SELECT 1'
);
PREPARE s2 FROM @ddl; EXECUTE s2; DEALLOCATE PREPARE s2;
-- 2) 合并历史数据
DROP TEMPORARY TABLE IF EXISTS `tmp_rmi_merge`;
CREATE TEMPORARY TABLE `tmp_rmi_merge` AS
SELECT
-- 用空串承载未回填ID避免唯一键上的 NULL 语义导致重复
COALESCE(NULLIF(MAX(NULLIF(warehouse_id, '')), ''), '') AS warehouse_id,
COALESCE(NULLIF(MAX(NULLIF(material_id, '')), ''), '') AS material_id,
MAX(COALESCE(NULLIF(warehouse_name, ''), '')) AS warehouse_name,
MAX(COALESCE(NULLIF(material_code, ''), '')) AS material_code,
MAX(COALESCE(NULLIF(material_name, ''), '')) AS material_name,
MAX(COALESCE(NULLIF(test_result, ''), '')) AS test_result,
SUM(IFNULL(total_packages, 0)) AS total_packages,
SUM(IFNULL(total_weight, 0)) AS total_weight,
MAX(tenant_id) AS tenant_id
FROM mes_xsl_raw_material_inventory
GROUP BY
COALESCE(NULLIF(warehouse_id, ''), CONCAT('NAME:', IFNULL(warehouse_name, ''))),
COALESCE(NULLIF(material_id, ''), CONCAT('CODE:', IFNULL(material_code, '')));
TRUNCATE TABLE mes_xsl_raw_material_inventory;
INSERT INTO mes_xsl_raw_material_inventory
(`id`, `warehouse_name`, `warehouse_id`, `material_id`, `material_name`, `material_code`, `test_result`, `total_packages`, `total_weight`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`)
SELECT
REPLACE(UUID(), '-', ''),
warehouse_name,
warehouse_id,
material_id,
material_name,
material_code,
test_result,
total_packages,
total_weight,
'admin',
NOW(),
'admin',
NOW(),
IFNULL(tenant_id, 1002)
FROM tmp_rmi_merge;
DROP TEMPORARY TABLE IF EXISTS `tmp_rmi_merge`;
-- 3) 新唯一键warehouse_id + material_id
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_inventory'
AND INDEX_NAME = 'uk_rmi_wid_mid'
);
SET @ddl := IF(@idx_exists = 0,
'ALTER TABLE `mes_xsl_raw_material_inventory` ADD UNIQUE KEY `uk_rmi_wid_mid` (`warehouse_id`, `material_id`)',
'SELECT 1'
);
PREPARE s3 FROM @ddl; EXECUTE s3; DEALLOCATE PREPARE s3;

View File

@@ -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;

View File

@@ -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');

View File

@@ -0,0 +1,26 @@
-- 库区实际存放量回填规则按租户页面配置原材料卡片汇总所匹配的仓库分类
CREATE TABLE IF NOT EXISTS `mes_xsl_warehouse_area_capacity_cfg` (
`id` varchar(32) NOT NULL COMMENT '主键',
`enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用回填 0否 1是',
`warehouse_category_ids` varchar(2000) DEFAULT NULL COMMENT 'MES仓库分类(sys_category.id)逗号分隔',
`warehouse_category_codes` varchar(2000) DEFAULT NULL COMMENT 'MES仓库分类编码逗号分隔 IDs 合并解析',
`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 '更新时间',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_xsl_wacc_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES库区实际存放量回填规则租户';
-- 按钮权限匹配仓库
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 '1900000000000000558', '1900000000000000550', '匹配仓库', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:capacityMatch', '1', 8.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` = '1900000000000000558');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000558', 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` = '1900000000000000558');

View File

@@ -0,0 +1,11 @@
-- 一级菜单MES原料仓储目录默认重定向至已有原材料库区看板
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 '1900000000000000700', '', 'MES原料仓储', '/xslmesRawStorage', 'layouts/default/index', 1, NULL, '/xslmes/mesXslRawMaterialWarehouseBoard', 0, NULL, '0', 81.50, 0, 'ant-design:inbox-outlined', 0, 0, 0, 0, 'MES 原料仓储一级目录子菜单可后续挂接或从系统管理中调整', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000700');
-- admin 角色授权幂等
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000700', 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` = '1900000000000000700');

View File

@@ -0,0 +1,17 @@
-- PrintDot WebSocket 地址字典item_text 展示名称item_value 为完整 ws 地址可在系统字典中维护多条
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'PrintDot WS地址', 'xslmes_print_dot_ws', '原材料卡片等页面桥接器 WebSocket 地址文本为说明值为完整 ws URL', 0, 'admin', NOW(), 0, 1002
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_print_dot_ws' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '本机 PrintDot', 'ws://127.0.0.1:1122/ws', '本机默认端口', 1, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_print_dot_ws'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'ws://127.0.0.1:1122/ws');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '本机(备用回环)', 'ws://localhost:1122/ws', '与 127.0.0.1 等价场景', 2, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_print_dot_ws'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'ws://localhost:1122/ws');

View File

@@ -0,0 +1,53 @@
-- 原材料卡片修改日志 + 菜单挂到MES原料仓储1900000000000000700
CREATE TABLE IF NOT EXISTS `mes_xsl_raw_material_card_edit_log` (
`id` varchar(32) NOT NULL COMMENT '主键',
`card_id` varchar(32) NOT NULL COMMENT '原材料卡片ID',
`barcode` varchar(100) DEFAULT NULL COMMENT '条码',
`batch_no` varchar(100) DEFAULT NULL COMMENT '批次号',
`material_name` varchar(200) DEFAULT NULL COMMENT '物料名称',
`material_id` varchar(32) DEFAULT NULL COMMENT '物料ID',
`before_remaining_weight` decimal(12,3) DEFAULT NULL COMMENT '修改前剩余重量',
`before_remaining_qty` int DEFAULT NULL COMMENT '修改前剩余数量',
`after_remaining_weight` decimal(12,3) DEFAULT NULL COMMENT '修改后剩余重量',
`before_warehouse_area` varchar(100) DEFAULT NULL COMMENT '修改前库区',
`after_remaining_qty` int DEFAULT NULL COMMENT '修改后剩余数量',
`after_warehouse_area` varchar(100) DEFAULT NULL COMMENT '修改后库区',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`modify_by_name` varchar(100) DEFAULT NULL COMMENT '修改人姓名',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
`data_source` varchar(100) DEFAULT NULL COMMENT '数值来源Web端/桌面端等',
PRIMARY KEY (`id`),
KEY `idx_rmcel_card_id` (`card_id`),
KEY `idx_rmcel_barcode` (`barcode`),
KEY `idx_rmcel_modify` (`modify_time`),
KEY `idx_rmcel_tenant` (`tenant_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 '1900000000000000710', '1900000000000000700', '原材料卡片修改日志', '/xslmes/mesXslRawMaterialCardEditLog', 'xslmes/mesXslRawMaterialCardEditLog/MesXslRawMaterialCardEditLogList', 1, NULL, NULL, 1, NULL, '0', 13.00, 0, 'ant-design:history-outlined', 0, 1, 0, 0, '记录 Web/桌面端编辑原材料卡片剩余量与库区', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000710');
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 '1900000000000000711', '1900000000000000710', '查询', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card_edit_log: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` = '1900000000000000711');
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 '1900000000000000712', '1900000000000000710', '导出', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card_edit_log:exportXls', '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` = '1900000000000000712');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000710', 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` = '1900000000000000710');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000711', 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` = '1900000000000000711');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000712', 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` = '1900000000000000712');

View File

@@ -0,0 +1,45 @@
-- 原料入场记录逻辑删除字段 + 原料入场删除日志只读菜单MES原料仓储下
-- 说明删除改为逻辑删除del_flag=1本页面仅查询已删除记录展示不另建日志表
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
AND COLUMN_NAME = 'del_flag'
);
SET @ddl := IF(@col_exists > 0,
'SELECT 1',
'ALTER TABLE `mes_xsl_raw_material_entry` ADD COLUMN `del_flag` int NOT NULL DEFAULT 0 COMMENT ''逻辑删除0正常 1已删除'' AFTER `tenant_id`'
);
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
AND INDEX_NAME = 'idx_rme_del_flag'
);
SET @idx_ddl := IF(@idx_exists > 0,
'SELECT 1',
'ALTER TABLE `mes_xsl_raw_material_entry` ADD KEY `idx_rme_del_flag` (`del_flag`)'
);
PREPARE s2 FROM @idx_ddl; EXECUTE s2; DEALLOCATE PREPARE s2;
-- 菜单
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 '1900000000000000720', '1900000000000000700', '原料入场删除日志', '/xslmes/mesXslRawMaterialEntryDeleteLog', 'xslmes/mesXslRawMaterialEntryDeleteLog/MesXslRawMaterialEntryDeleteLogList', 1, NULL, NULL, 1, NULL, '0', 14.00, 0, 'ant-design:delete-row-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` = '1900000000000000720');
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 '1900000000000000721', '1900000000000000720', '查询', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_entry_delete_log: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` = '1900000000000000721');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000720', 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` = '1900000000000000720');
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000721', 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` = '1900000000000000721');