新增打印模板绑定功能,支持业务与打印模板的映射配置。实现打印模板的增删改查操作,优化打印数据的生成逻辑,提升打印模板的灵活性和用户体验。同时,新增打印机查询接口,增强打印服务的可用性和实时性。
This commit is contained in:
@@ -18,5 +18,11 @@
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
</dependency>
|
||||
<!-- 复用打印模板模块:打印机枚举、业务绑定、PDF 提交队列 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-system-biz</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -9,9 +9,18 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
@@ -33,6 +42,8 @@ import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -59,6 +70,9 @@ public class MesXslDesktopAnonController {
|
||||
private final IMesXslWarehouseService warehouseService;
|
||||
private final IMesXslWarehouseAreaService warehouseAreaService;
|
||||
private final MesXslStompNotifyService stompNotify;
|
||||
private final IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
private final IPrintTemplateService printTemplateService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// ═══════════════════════════ 车辆管理 ═══════════════════════════
|
||||
|
||||
@@ -606,6 +620,34 @@ public class MesXslDesktopAnonController {
|
||||
return Result.OK((int) count);
|
||||
}
|
||||
|
||||
/** 与 PrintBizTypeCatalog / 业务打印绑定一致 */
|
||||
private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD";
|
||||
|
||||
@Operation(summary = "原材料卡片-免密准备原生打印数据(桌面端用)")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint")
|
||||
public Result<Map<String, Object>> rawMaterialCardAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialCard card = rawMaterialCardService.getById(id);
|
||||
if (card == null) return Result.error("未找到原材料卡片");
|
||||
PrintBizTemplateBind bind = printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE);
|
||||
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) return Result.error("绑定的打印模板不存在");
|
||||
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(card);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("cardId", card.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原材料卡片-免密准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════
|
||||
|
||||
@Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)")
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
|
||||
import org.jeecg.modules.print.support.PrintServerPdfJobService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* @Description: 原材料卡片
|
||||
@@ -34,10 +55,24 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
@RequestMapping("/xslmes/mesXslRawMaterialCard")
|
||||
@Slf4j
|
||||
public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMaterialCard, IMesXslRawMaterialCardService> {
|
||||
|
||||
/** 与 PrintBizTypeCatalog / 业务打印绑定一致 */
|
||||
private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD";
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialCardService mesXslRawMaterialCardService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
@Autowired
|
||||
private PrintServerEnvironmentService printServerEnvironmentService;
|
||||
@Autowired
|
||||
private PrintServerPdfJobService printServerPdfJobService;
|
||||
@Autowired
|
||||
private IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
@Autowired
|
||||
private IPrintTemplateService printTemplateService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
@@ -122,6 +157,87 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询可用打印机(与 /print/template/queryPrinters 返回结构一致)
|
||||
*/
|
||||
@Operation(summary = "原材料卡片-查询可用打印机")
|
||||
@GetMapping(value = "/queryPrinters")
|
||||
/** 有 list 可进页拉打印机;有 edit 可打印拉打印机,避免单权限缺另一项时 403 */
|
||||
@RequiresPermissions(
|
||||
value = {"xslmes:mes_xsl_raw_material_card:list", "xslmes:mes_xsl_raw_material_card:edit"},
|
||||
logical = Logical.OR)
|
||||
public Result<Map<String, Object>> queryPrinters() {
|
||||
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf
|
||||
*/
|
||||
@Operation(summary = "原材料卡片-准备原生打印数据")
|
||||
@GetMapping(value = "/prepareNativePrint")
|
||||
/** 与 printPdf 一致:准备模板与打印数据属于「打印」动作,使用 edit 权限 */
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
|
||||
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
|
||||
if (card == null) {
|
||||
return Result.error("未找到原材料卡片");
|
||||
}
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE);
|
||||
if (bind == null) {
|
||||
return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
}
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) {
|
||||
return Result.error("绑定的打印模板不存在");
|
||||
}
|
||||
ArrayNode mapping =
|
||||
PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
|
||||
JsonNode bizRoot = objectMapper.valueToTree(card);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("cardId", card.getId());
|
||||
out.put("templateCode", bind.getTemplateCode());
|
||||
out.put("templateJson", tpl.getTemplateJson());
|
||||
out.put("paperWidthMm", tpl.getPaperWidthMm());
|
||||
out.put("paperHeightMm", tpl.getPaperHeightMm());
|
||||
out.put("paperOrientation", tpl.getPaperOrientation());
|
||||
out.put("printData", objectMapper.convertValue(printData, Map.class));
|
||||
return Result.OK(out);
|
||||
} catch (Exception e) {
|
||||
log.error("原材料卡片准备打印数据失败 id={}", id, e);
|
||||
return Result.error("准备打印数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端生成的 PDF Base64 提交到服务器打印机(与 /print/template/directPrintPdf 一致)
|
||||
*/
|
||||
@AutoLog(value = "原材料卡片-PDF后端打印")
|
||||
@Operation(summary = "原材料卡片-PDF后端打印")
|
||||
@PostMapping(value = "/printPdf")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
|
||||
public Result<String> printPdf(@RequestBody Map<String, Object> body) {
|
||||
String id = String.valueOf(body.getOrDefault("id", "")).trim();
|
||||
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
|
||||
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
|
||||
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return Result.error("id 不能为空");
|
||||
}
|
||||
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
|
||||
if (card == null) {
|
||||
return Result.error("未找到原材料卡片");
|
||||
}
|
||||
String prefix =
|
||||
StringUtils.isNotBlank(card.getBarcode()) ? card.getBarcode() : card.getId();
|
||||
String fn =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("原材料卡片-" + prefix + ".pdf");
|
||||
return printServerPdfJobService.submitPdfBase64(
|
||||
printerName, pdfBase64, fn, "RAW_CARD_" + prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.jeecg.modules.print.catalog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
|
||||
/**
|
||||
* 业务类型及可映射字段目录(与实体 JSON 字段名一致,camelCase)。
|
||||
* 新增业务:在此注册 bizCode 与字段列表。
|
||||
*/
|
||||
public final class PrintBizTypeCatalog {
|
||||
|
||||
private static final Map<String, PrintBizTypeVO> REGISTRY = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
registerRawMaterialCard();
|
||||
}
|
||||
|
||||
private PrintBizTypeCatalog() {}
|
||||
|
||||
private static void registerRawMaterialCard() {
|
||||
PrintBizTypeVO vo = new PrintBizTypeVO();
|
||||
vo.setBizCode("MES_RAW_MATERIAL_CARD");
|
||||
vo.setBizName("原材料卡片");
|
||||
vo.setDescription("mes_xsl_raw_material_card / MesXslRawMaterialCard");
|
||||
List<PrintBizFieldItemVO> fields = new ArrayList<>();
|
||||
fields.add(f("id", "主键"));
|
||||
fields.add(f("barcode", "条码"));
|
||||
fields.add(f("splitDetailId", "拆码明细行ID"));
|
||||
fields.add(f("batchNo", "批次号"));
|
||||
fields.add(f("entryDate", "入场日期"));
|
||||
fields.add(f("materialId", "物料ID"));
|
||||
fields.add(f("materialName", "物料名称"));
|
||||
fields.add(f("materialDesc", "物料描述"));
|
||||
fields.add(f("supplierId", "供应商ID"));
|
||||
fields.add(f("supplierName", "供应商名称"));
|
||||
fields.add(f("manufacturerMaterialName", "厂家物料名称"));
|
||||
fields.add(f("shelfLife", "保质期"));
|
||||
fields.add(f("totalWeight", "总重"));
|
||||
fields.add(f("remainingWeight", "剩余重量"));
|
||||
fields.add(f("remainingQuantity", "剩余数量"));
|
||||
fields.add(f("status", "状态(字典)"));
|
||||
fields.add(f("testResult", "检测结果(字典)"));
|
||||
fields.add(f("warehouseArea", "库区/库位"));
|
||||
fields.add(f("unloadOperator", "卸货操作人"));
|
||||
fields.add(f("priorityPickup", "优先出库"));
|
||||
fields.add(f("createBy", "创建人"));
|
||||
fields.add(f("createTime", "创建时间"));
|
||||
fields.add(f("updateBy", "更新人"));
|
||||
fields.add(f("updateTime", "更新时间"));
|
||||
vo.setFields(Collections.unmodifiableList(fields));
|
||||
REGISTRY.put(vo.getBizCode(), vo);
|
||||
}
|
||||
|
||||
private static PrintBizFieldItemVO f(String key, String label) {
|
||||
return new PrintBizFieldItemVO(key, label, "");
|
||||
}
|
||||
|
||||
public static List<PrintBizTypeVO> listAll() {
|
||||
return new ArrayList<>(REGISTRY.values());
|
||||
}
|
||||
|
||||
public static PrintBizTypeVO getByCode(String bizCode) {
|
||||
if (bizCode == null) {
|
||||
return null;
|
||||
}
|
||||
return REGISTRY.get(bizCode.trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
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.PrintBizTypeCatalog;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.entity.PrintTemplate;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.jeecg.modules.print.service.IPrintTemplateService;
|
||||
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
|
||||
import org.jeecg.modules.print.util.PrintNativeTemplateFieldExtractor;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 业务与打印模板绑定:可视化配置字段映射
|
||||
*/
|
||||
@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;
|
||||
|
||||
@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);
|
||||
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);
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "业务打印绑定-删除")
|
||||
@Operation(summary = "业务打印绑定-删除")
|
||||
@DeleteMapping("/delete")
|
||||
@RequiresPermissions("print:bizBind:delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
service.removeById(id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "已注册的业务类型及字段目录")
|
||||
@GetMapping("/bizTypes")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizTypeVO>> bizTypes() {
|
||||
return Result.OK(PrintBizTypeCatalog.listAll());
|
||||
}
|
||||
|
||||
@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 = "按业务编码查询绑定(供打印调用)")
|
||||
@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);
|
||||
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());
|
||||
if (PrintBizTypeCatalog.getByCode(entity.getBizCode()) != null
|
||||
&& StringUtils.isBlank(entity.getBizName())) {
|
||||
entity.setBizName(PrintBizTypeCatalog.getByCode(entity.getBizCode()).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;
|
||||
}
|
||||
}
|
||||
@@ -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 的输出,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.print.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
|
||||
/**
|
||||
* 业务与打印模板绑定(字段映射)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "业务打印模板绑定")
|
||||
@TableName("print_biz_template_bind")
|
||||
public class PrintBizTemplateBind extends JeecgEntity implements Serializable {
|
||||
|
||||
@Schema(description = "业务编码")
|
||||
@TableField("biz_code")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称")
|
||||
@TableField("biz_name")
|
||||
private String bizName;
|
||||
|
||||
@Schema(description = "打印模板主键")
|
||||
@TableField("template_id")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "打印模板编码")
|
||||
@TableField("template_code")
|
||||
private String templateCode;
|
||||
|
||||
@Schema(description = "字段映射 JSON:[{templateField,bizField}]")
|
||||
@TableField("field_mapping_json")
|
||||
private String fieldMappingJson;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@TableField("remark")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.print.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
|
||||
/** 业务打印模板绑定 Mapper */
|
||||
public interface PrintBizTemplateBindMapper extends BaseMapper<PrintBizTemplateBind> {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jeecg.modules.print.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
|
||||
/** 业务打印模板绑定 */
|
||||
public interface IPrintBizTemplateBindService extends IService<PrintBizTemplateBind> {
|
||||
|
||||
PrintBizTemplateBind getByBizCode(String bizCode);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.print.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
|
||||
import org.jeecg.modules.print.mapper.PrintBizTemplateBindMapper;
|
||||
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PrintBizTemplateBindServiceImpl extends ServiceImpl<PrintBizTemplateBindMapper, PrintBizTemplateBind>
|
||||
implements IPrintBizTemplateBindService {
|
||||
|
||||
@Override
|
||||
public PrintBizTemplateBind getByBizCode(String bizCode) {
|
||||
if (bizCode == null || bizCode.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<PrintBizTemplateBind> q = new LambdaQueryWrapper<>();
|
||||
q.eq(PrintBizTemplateBind::getBizCode, bizCode.trim());
|
||||
q.last("LIMIT 1");
|
||||
return getOne(q);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.jeecg.modules.print.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 服务端打印机枚举能力(与 {@link org.jeecg.modules.print.controller.PrintTemplateController#queryPrinters} 一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PrintServerEnvironmentService {
|
||||
|
||||
@Value("${print.network-printers:}")
|
||||
private String networkPrinters;
|
||||
|
||||
/** 与打印模板页 /print/template/queryPrinters 返回结构完全一致 */
|
||||
public Map<String, Object> buildPrinterQueryResult() {
|
||||
Map<String, Object> res = new HashMap<>(8);
|
||||
List<String> serverPrinters = new ArrayList<>();
|
||||
String serverDefaultPrinter = "";
|
||||
try {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService service : services) {
|
||||
if (service != null && StringUtils.isNotBlank(service.getName())) {
|
||||
serverPrinters.add(service.getName().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
|
||||
if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) {
|
||||
serverDefaultPrinter = defaultService.getName().trim();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询服务器打印机失败: {}", e.getMessage());
|
||||
}
|
||||
List<String> networkPrinterList =
|
||||
StringUtils.isBlank(networkPrinters)
|
||||
? new ArrayList<>()
|
||||
: java.util.Arrays.stream(networkPrinters.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> capability = new LinkedHashMap<>(4);
|
||||
capability.put("localSupported", false);
|
||||
capability.put(
|
||||
"localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。");
|
||||
capability.put("serverSupported", true);
|
||||
capability.put("networkSupported", true);
|
||||
|
||||
res.put("capability", capability);
|
||||
res.put("serverPrinters", serverPrinters);
|
||||
res.put("serverDefaultPrinter", serverDefaultPrinter);
|
||||
res.put("networkPrinters", networkPrinterList);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.jeecg.modules.print.support;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterAbortException;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import javax.print.Doc;
|
||||
import javax.print.DocFlavor;
|
||||
import javax.print.DocPrintJob;
|
||||
import javax.print.PrintException;
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import javax.print.SimpleDoc;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 服务端将 PDF Base64 提交至打印队列(与打印模板 {@code /print/template/directPrintPdf} 逻辑一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PrintServerPdfJobService {
|
||||
|
||||
/**
|
||||
* @param templateCodeOrPrefix 用于默认作业名:{@code QH-MES-{code}.pdf}
|
||||
*/
|
||||
public Result<String> submitPdfBase64(
|
||||
String printerName, String pdfBase64, String fileName, String templateCodeOrPrefix) {
|
||||
if (StringUtils.isBlank(pdfBase64)) {
|
||||
return Result.error("pdfBase64 不能为空");
|
||||
}
|
||||
String lastResolvedPrinterLabel = null;
|
||||
try {
|
||||
PrintService target = resolvePrintService(printerName);
|
||||
if (target == null) {
|
||||
return Result.error("未找到可用打印机,请检查服务器打印机配置");
|
||||
}
|
||||
final String resolvedPrinterLabel = target.getName();
|
||||
lastResolvedPrinterLabel = resolvedPrinterLabel;
|
||||
String base64Body = pdfBase64;
|
||||
int commaIdx = pdfBase64.indexOf(",");
|
||||
if (pdfBase64.startsWith("data:") && commaIdx > 0) {
|
||||
base64Body = pdfBase64.substring(commaIdx + 1);
|
||||
}
|
||||
byte[] pdfBytes = Base64.getDecoder().decode(base64Body);
|
||||
String prefix = StringUtils.isNotBlank(templateCodeOrPrefix) ? templateCodeOrPrefix.trim() : "MES";
|
||||
String printJobName =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + prefix + ".pdf");
|
||||
if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) {
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
}
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
|
||||
PDFRenderer renderer = new PDFRenderer(document);
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(target);
|
||||
job.setJobName(printJobName);
|
||||
job.setPrintable(
|
||||
(graphics, pageFormat, pageIndex) -> {
|
||||
if (pageIndex >= document.getNumberOfPages()) {
|
||||
return Printable.NO_SUCH_PAGE;
|
||||
}
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = renderer.renderImageWithDPI(pageIndex, 150);
|
||||
} catch (Exception ex) {
|
||||
throw new PrinterException("PDF页面渲染失败: " + ex.getMessage());
|
||||
}
|
||||
Graphics2D g2 = (Graphics2D) graphics;
|
||||
double imageableX = pageFormat.getImageableX();
|
||||
double imageableY = pageFormat.getImageableY();
|
||||
double imageableWidth = pageFormat.getImageableWidth();
|
||||
double imageableHeight = pageFormat.getImageableHeight();
|
||||
double scale =
|
||||
Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight());
|
||||
int drawWidth = (int) Math.round(image.getWidth() * scale);
|
||||
int drawHeight = (int) Math.round(image.getHeight() * scale);
|
||||
int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2);
|
||||
int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2);
|
||||
g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null);
|
||||
return Printable.PAGE_EXISTS;
|
||||
});
|
||||
HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet();
|
||||
patts.add(new JobName(printJobName, Locale.getDefault()));
|
||||
job.print(patts);
|
||||
}
|
||||
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
|
||||
} catch (PrinterAbortException e) {
|
||||
log.error("PDF后端打印失败(PrinterAbortException)", e);
|
||||
return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel));
|
||||
} catch (Exception e) {
|
||||
log.error("PDF后端打印失败", e);
|
||||
return Result.error("PDF后端打印失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public PrintService resolvePrintService(String printerName) {
|
||||
PrintService target = null;
|
||||
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
|
||||
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
if (services != null) {
|
||||
for (PrintService serviceItem : services) {
|
||||
if (serviceItem != null
|
||||
&& printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
|
||||
target = serviceItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = PrintServiceLookup.lookupDefaultPrintService();
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private boolean tryPrintPdfBytesWithDocFlavor(
|
||||
PrintService printService, byte[] pdfBytes, String jobName) {
|
||||
DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf");
|
||||
if (!printService.isDocFlavorSupported(flavor)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
DocPrintJob docJob = printService.createPrintJob();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes);
|
||||
Doc doc = new SimpleDoc(in, flavor, null);
|
||||
HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
|
||||
if (StringUtils.isNotBlank(jobName)) {
|
||||
attrs.add(new JobName(jobName, Locale.getDefault()));
|
||||
}
|
||||
docJob.print(doc, attrs);
|
||||
return true;
|
||||
} catch (PrintException e) {
|
||||
log.warn(
|
||||
"PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}",
|
||||
printService.getName(),
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildPdfPrinterAbortHint(
|
||||
String requestedPrinterName, String resolvedPrintQueueName) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("打印任务被系统取消(PrinterAbortException)。常见原因:");
|
||||
sb.append(
|
||||
"1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机,在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName;");
|
||||
sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;");
|
||||
sb.append("3) 运行服务的 Windows 账户无权访问打印队列。");
|
||||
if (StringUtils.isNotBlank(resolvedPrintQueueName)) {
|
||||
sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("。");
|
||||
}
|
||||
if (StringUtils.isNotBlank(requestedPrinterName)
|
||||
&& !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) {
|
||||
sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("。");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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 org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/** 按映射规则把业务 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");
|
||||
if (StringUtils.isAnyBlank(templateField, bizField)) {
|
||||
continue;
|
||||
}
|
||||
JsonNode val = resolvePath(bizRoot, bizField);
|
||||
setPath(printData, templateField, val);
|
||||
}
|
||||
return printData;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
cur = cur.get(p);
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
private static void setPath(ObjectNode target, String path, JsonNode value) {
|
||||
if (target == null || StringUtils.isBlank(path)) {
|
||||
return;
|
||||
}
|
||||
String[] parts = path.split("\\.");
|
||||
if (parts.length == 1) {
|
||||
putLeaf(target, parts[0], value);
|
||||
return;
|
||||
}
|
||||
ObjectNode cur = target;
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
String p = parts[i];
|
||||
JsonNode next = cur.get(p);
|
||||
if (next == null || !next.isObject()) {
|
||||
ObjectNode created = MAPPER.createObjectNode();
|
||||
cur.set(p, created);
|
||||
cur = created;
|
||||
} else {
|
||||
cur = (ObjectNode) next;
|
||||
}
|
||||
}
|
||||
String leaf = parts[parts.length - 1];
|
||||
putLeaf(cur, leaf, value);
|
||||
}
|
||||
|
||||
private static void putLeaf(ObjectNode node, String key, JsonNode value) {
|
||||
if (value == null || value.isNull()) {
|
||||
node.putNull(key);
|
||||
} else if (value.isObject() || value.isArray()) {
|
||||
node.set(key, value);
|
||||
} else if (value.isTextual()) {
|
||||
node.put(key, value.asText());
|
||||
} else if (value.isBoolean()) {
|
||||
node.put(key, value.booleanValue());
|
||||
} else if (value.isNumber()) {
|
||||
node.put(key, value.doubleValue());
|
||||
} else {
|
||||
node.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode n, String key) {
|
||||
if (n == null || !n.isObject()) {
|
||||
return "";
|
||||
}
|
||||
JsonNode v = n.get(key);
|
||||
return v == null || v.isNull() ? "" : v.asText("");
|
||||
}
|
||||
|
||||
/** 将任意 JsonNode 转为可序列化的 JSON 树(复制) */
|
||||
public static JsonNode parseBizJson(Object bizDataJson) throws Exception {
|
||||
if (bizDataJson == null) {
|
||||
return MAPPER.createObjectNode();
|
||||
}
|
||||
if (bizDataJson instanceof String s) {
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return MAPPER.createObjectNode();
|
||||
}
|
||||
return MAPPER.readTree(s);
|
||||
}
|
||||
return MAPPER.valueToTree(bizDataJson);
|
||||
}
|
||||
|
||||
/** 规范化映射列表:解析字符串或数组 */
|
||||
public static ArrayNode parseMappingArray(String fieldMappingJson) throws Exception {
|
||||
if (StringUtils.isBlank(fieldMappingJson)) {
|
||||
return MAPPER.createArrayNode();
|
||||
}
|
||||
JsonNode n = MAPPER.readTree(fieldMappingJson);
|
||||
if (n.isArray()) {
|
||||
return (ArrayNode) n;
|
||||
}
|
||||
return MAPPER.createArrayNode();
|
||||
}
|
||||
|
||||
public static String mappingArrayToJson(ArrayNode arr) throws Exception {
|
||||
return MAPPER.writeValueAsString(arr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
|
||||
|
||||
/**
|
||||
* 从原生打印模板 JSON 中收集所有 bindField / 表格列 field,供业务字段映射使用。
|
||||
*
|
||||
* <p>原生设计器在 {@code dataBinding.params} 中维护「参数」列表(如 Parameter1~Parameter10),画布元素通过
|
||||
* bindField 引用这些 key;仅扫描 elements 会漏掉未拖放到画布上的参数,故必须先合并 dataBinding。
|
||||
*/
|
||||
public final class PrintNativeTemplateFieldExtractor {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private PrintNativeTemplateFieldExtractor() {}
|
||||
|
||||
public static List<PrintTemplateFieldItemVO> extract(String templateJson) {
|
||||
List<PrintTemplateFieldItemVO> list = new ArrayList<>();
|
||||
if (StringUtils.isBlank(templateJson)) {
|
||||
return list;
|
||||
}
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(templateJson);
|
||||
// 优先收录设计器「参数/明细字段」目录,保证显示名与组件库一致,且不被后续元素扫描覆盖
|
||||
collectDataBinding(root, seen, list);
|
||||
JsonNode elements = root.get("elements");
|
||||
if (elements != null && elements.isArray()) {
|
||||
for (JsonNode el : elements) {
|
||||
collectFromElement(el, seen, list);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return list;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 解析 schema.dataBinding:params(参数键)、detailTables(明细字段) */
|
||||
private static void collectDataBinding(JsonNode root, Set<String> seen, List<PrintTemplateFieldItemVO> list) {
|
||||
JsonNode db = root.get("dataBinding");
|
||||
if (db == null || !db.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode params = db.get("params");
|
||||
if (params != null && params.isArray()) {
|
||||
for (JsonNode p : params) {
|
||||
if (p == null || !p.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String key = text(p, "key").trim();
|
||||
if (StringUtils.isBlank(key) || !seen.add(key)) {
|
||||
continue;
|
||||
}
|
||||
list.add(new PrintTemplateFieldItemVO(key, "param", text(p, "label")));
|
||||
}
|
||||
}
|
||||
JsonNode detailTables = db.get("detailTables");
|
||||
if (detailTables != null && detailTables.isArray()) {
|
||||
for (JsonNode t : detailTables) {
|
||||
if (t == null || !t.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String tableKey = text(t, "tableKey").trim();
|
||||
JsonNode fields = t.get("fields");
|
||||
if (fields == null || !fields.isArray()) {
|
||||
continue;
|
||||
}
|
||||
for (JsonNode f : fields) {
|
||||
if (f == null || !f.isObject()) {
|
||||
continue;
|
||||
}
|
||||
String fk = text(f, "key").trim();
|
||||
if (StringUtils.isBlank(fk)) {
|
||||
continue;
|
||||
}
|
||||
// 与画布列 bindField 一致时多为短 key;多表明细同字段再加 tableKey 前缀消歧
|
||||
String bindKey = fk;
|
||||
if (seen.contains(bindKey) && StringUtils.isNotBlank(tableKey)) {
|
||||
bindKey = tableKey + "." + fk;
|
||||
}
|
||||
if (seen.contains(bindKey) || !seen.add(bindKey)) {
|
||||
continue;
|
||||
}
|
||||
list.add(new PrintTemplateFieldItemVO(bindKey, "detailField", text(f, "label")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectFromElement(JsonNode el, Set<String> seen, List<PrintTemplateFieldItemVO> list) {
|
||||
if (el == null || !el.isObject()) {
|
||||
return;
|
||||
}
|
||||
String bindField = text(el, "bindField");
|
||||
if (StringUtils.isNotBlank(bindField) && seen.add(bindField.trim())) {
|
||||
list.add(
|
||||
new PrintTemplateFieldItemVO(
|
||||
bindField.trim(),
|
||||
text(el, "type"),
|
||||
firstNonBlank(text(el, "title"), text(el, "text"))));
|
||||
}
|
||||
JsonNode cols = el.get("columns");
|
||||
if (cols != null && cols.isArray()) {
|
||||
for (JsonNode c : cols) {
|
||||
String field = firstNonBlank(text(c, "bindField"), text(c, "field"));
|
||||
if (StringUtils.isNotBlank(field) && seen.add(field.trim())) {
|
||||
list.add(new PrintTemplateFieldItemVO(field.trim(), "column", text(c, "title")));
|
||||
}
|
||||
}
|
||||
}
|
||||
JsonNode nested = el.get("elements");
|
||||
if (nested != null && nested.isArray()) {
|
||||
for (JsonNode child : nested) {
|
||||
collectFromElement(child, seen, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode n, String key) {
|
||||
if (n == null || !n.isObject()) {
|
||||
return "";
|
||||
}
|
||||
JsonNode v = n.get(key);
|
||||
return v == null || v.isNull() ? "" : v.asText("");
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String a, String b) {
|
||||
if (StringUtils.isNotBlank(a)) {
|
||||
return a;
|
||||
}
|
||||
return StringUtils.isNotBlank(b) ? b : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 PrintBizFieldItemVO implements Serializable {
|
||||
@Schema(description = "字段路径(支持 a.b,与 JSON 一致)")
|
||||
private String fieldKey;
|
||||
|
||||
@Schema(description = "展示名称")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "说明")
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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 = "业务编码")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称")
|
||||
private String bizName;
|
||||
|
||||
@Schema(description = "说明")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "业务侧可用字段目录")
|
||||
private List<PrintBizFieldItemVO> fields;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.jeecg.modules.print.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "模板中的数据占位字段")
|
||||
public class PrintTemplateFieldItemVO implements Serializable {
|
||||
@Schema(description = "模板 bindField 路径")
|
||||
private String bindField;
|
||||
|
||||
@Schema(description = "元素类型")
|
||||
private String elementType;
|
||||
|
||||
@Schema(description = "元素标题/提示")
|
||||
private String titleHint;
|
||||
}
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user