From 0ebf6d4258cbdbf6b71724d8fdadef60e32b9f9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=9F=B3=E5=A4=B4=E4=BA=BA?= <3076767823@qq.com>
Date: Fri, 7 Feb 2025 11:03:05 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=85=A8?=
=?UTF-8?q?=E5=B1=80=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../common/exception/ControllerAdvice.java | 139 ++++++++
.../com/dpkj/common/exception/ErrorEnum.java | 70 ++++
.../dpkj/common/exception/ErrorInterface.java | 23 ++
.../dpkj/common/exception/RRException.java | 59 ++++
.../com/dpkj/common/utils/TemplateUtils.java | 333 ++++++++++++++++++
src/main/java/com/dpkj/common/vo/Result.java | 9 +
6 files changed, 633 insertions(+)
create mode 100644 src/main/java/com/dpkj/common/exception/ControllerAdvice.java
create mode 100644 src/main/java/com/dpkj/common/exception/ErrorEnum.java
create mode 100644 src/main/java/com/dpkj/common/exception/ErrorInterface.java
create mode 100644 src/main/java/com/dpkj/common/exception/RRException.java
create mode 100644 src/main/java/com/dpkj/common/utils/TemplateUtils.java
diff --git a/src/main/java/com/dpkj/common/exception/ControllerAdvice.java b/src/main/java/com/dpkj/common/exception/ControllerAdvice.java
new file mode 100644
index 0000000..596c4c8
--- /dev/null
+++ b/src/main/java/com/dpkj/common/exception/ControllerAdvice.java
@@ -0,0 +1,139 @@
+package com.dpkj.common.exception;
+
+import com.dpkj.common.vo.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Controller;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * controller控制器异常处理接口实现类
+ *
其它module可以集成此类进行controller层的异常处理
+ * @author 石头人
+ */
+@Slf4j
+@Component
+@RestControllerAdvice(annotations = {RestController.class, Controller.class})
+public class ControllerAdvice {
+
+ /**
+ * controller方法中的参数校验失败,但是前提是要使用
+ * #@Validated注解开启参数校验
+ *
+ * @param e 异常
+ * @return Result
+ */
+ @ExceptionHandler(value = MethodArgumentNotValidException.class)
+ public Result bindingException(MethodArgumentNotValidException e) {
+ // 获得所有校验出错的返回集
+ BindingResult bindingResult = e.getBindingResult();
+ List fieldErrors = bindingResult.getFieldErrors();
+ // 循环获得所有校验异常的字段
+ Map fieldMap = new HashMap<>();
+ for (FieldError fieldError : fieldErrors) {
+ fieldMap.put(fieldError.getField(), fieldError.getDefaultMessage());
+ }
+
+ String errMsg = fieldMap.values().toString().replaceAll("]", "").replaceAll("\\[", "");
+ log.warn(errMsg);
+ // 返回给前端
+ return Result.error(errMsg);
+ }
+
+ /**
+ * 处理空指针异常
+ * @param nullPointerException 空指针异常
+ * @return Result
+ */
+ @ExceptionHandler(value = NullPointerException.class)
+ public Result nullPointException(NullPointerException nullPointerException) {
+ log.error("空指针异常类型: {},信息: {}", nullPointerException.getClass(),nullPointerException.getMessage());
+ return Result.error(ErrorEnum.NULL_POINTER_EXCEPTION);
+ }
+
+ /**
+ * 所有的运行时异常,抛出异常
+ * @param throwable 异常
+ * @return Result
+ */
+ @ExceptionHandler(value = Throwable.class)
+ public Result handleException(Throwable throwable) {
+ log.error("异常类型: {}, {}, 信息为: {}", throwable.getCause(), throwable.getClass(), throwable.getMessage());
+ if (throwable instanceof RRException){
+ RRException rrException = (RRException) throwable;
+ return Result.error(rrException.getCode(), rrException.getMsg());
+ }
+ return Result.error(ErrorEnum.RUNTIME_EXCEPTION);
+ }
+
+ /**
+ * http信息无可读
+ * @param e 异常
+ * @return Result
+ */
+ @ExceptionHandler(value = HttpMessageNotReadableException.class)
+ public Result httpMessageNotReadAbleException(HttpMessageNotReadableException e){
+ log.warn("异常类型: {} 无可读信息: {}", e.getClass(), e.getMessage());
+ return Result.error(ErrorEnum.HTTP_MESSAGE_NOT_READABLE_EXCEPTION);
+ }
+
+ /**
+ * 运行时异常
+ * @param e 运行异常对象
+ * @return Result
+ */
+ @ExceptionHandler(value = RuntimeException.class)
+ public Result runtimeException(RuntimeException e){
+ log.error("运行时异常:{}", e.getMessage());
+ if (e instanceof RRException){
+ RRException rrException = (RRException) e;
+ return Result.error(rrException.getCode(), rrException.getMsg());
+ }
+ return Result.error(ErrorEnum.RUNTIME_EXCEPTION);
+ }
+
+ /**
+ * 请求不支持
+ * @return Result
+ */
+ @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
+ public Result httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){
+ log.warn("暂不支持该请求: {}", e.getMessage());
+ return Result.error("暂不支持此请求方式");
+ }
+
+ /**
+ * 参数类型错误
+ * @return Result
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public Result methodArgument(MethodArgumentTypeMismatchException e){
+ log.warn("参数发生错误: {}", e.getMessage());
+ return Result.error("参数发生错误");
+ }
+
+ /**
+ * 缺少请求参数
+ * @param e 缺少请求参数异常
+ * @return Result
+ */
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ public Result exception(MissingServletRequestParameterException e){
+ log.warn("缺少请求参数: {}", e.getMessage());
+ return Result.error("缺少请求参数");
+ }
+
+}
diff --git a/src/main/java/com/dpkj/common/exception/ErrorEnum.java b/src/main/java/com/dpkj/common/exception/ErrorEnum.java
new file mode 100644
index 0000000..f2ea1ad
--- /dev/null
+++ b/src/main/java/com/dpkj/common/exception/ErrorEnum.java
@@ -0,0 +1,70 @@
+package com.dpkj.common.exception;
+
+import lombok.Getter;
+
+
+/**
+ * 错误返回枚举类
+ */
+@Getter
+public enum ErrorEnum implements ErrorInterface{
+
+// ==========================================================================
+
+ /**
+ * 成功范围
+ * @code 200
+ * @apiNote 访问成功
+ */
+ SUCCESS(200, "访问成功"),
+
+ /**
+ * 系统异常
+ * @code 500
+ * @apiNote 系统异常
+ */
+ FAIL(500, "系统异常"),
+
+ /**
+ * 调用对象位空(null)
+ * @code 10002
+ * @apiNote 调用对象位空(null)
+ */
+ NULL_POINTER_EXCEPTION(501, "调用对象位空(null)"),
+
+ /**
+ * 运行时异常,
+ * @code 10003
+ * @apiNote 系统发生错误,请联系管理员
+ */
+ RUNTIME_EXCEPTION(502, "系统发生错误,请联系管理员"),
+
+ /**
+ * Http传入的参数没有可以读的数据
+ * @code 10004
+ * @apiNote 传入的数据不可读
+ */
+ HTTP_MESSAGE_NOT_READABLE_EXCEPTION(503, "传入的数据不可读"),
+
+
+
+// ==========================================================================
+
+ ;
+ private final Integer code;
+ private final String message;
+ ErrorEnum(Integer code, String message){
+ this.message = message;
+ this.code = code;
+ }
+
+ @Override
+ public int getCode(){
+ return this.code;
+ }
+
+ @Override
+ public String getMessage(){
+ return this.message;
+ }
+}
diff --git a/src/main/java/com/dpkj/common/exception/ErrorInterface.java b/src/main/java/com/dpkj/common/exception/ErrorInterface.java
new file mode 100644
index 0000000..c610dfe
--- /dev/null
+++ b/src/main/java/com/dpkj/common/exception/ErrorInterface.java
@@ -0,0 +1,23 @@
+package com.dpkj.common.exception;
+
+/**
+ * 错误枚举接口,所有的错误枚举都需要实现该接口,如果需要自定义
+ * 错误信息,可通过实现该接口与RespBean进行使用
+ *
+ * @author 石头人
+ * @see com.dpkj.common.vo.Result
+ * @since 2023-07-27 13:05:00
+ */
+public interface ErrorInterface {
+
+ /**
+ * 获取响应码
+ */
+ int getCode();
+
+ /**
+ * 获取响应信息
+ */
+ String getMessage();
+
+}
diff --git a/src/main/java/com/dpkj/common/exception/RRException.java b/src/main/java/com/dpkj/common/exception/RRException.java
new file mode 100644
index 0000000..ebfcafc
--- /dev/null
+++ b/src/main/java/com/dpkj/common/exception/RRException.java
@@ -0,0 +1,59 @@
+package com.dpkj.common.exception;
+
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 总的异常,所有的自定义异常都需要继承类
+ *
+ * @author 石头人
+ * @since 2023-07-24 09:42:00
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class RRException extends RuntimeException{
+
+ private static final long serialVersionUID = 1L;
+
+ private String msg;
+ private int code = 500;
+
+ public RRException(){
+ super("系统错误");
+ this.msg = "系统错误";
+ }
+
+ public RRException(Throwable throwable){
+ super(throwable);
+ }
+
+ public RRException(String msg) {
+ super(msg);
+ this.msg = msg;
+ }
+
+ public RRException(String msg, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ }
+
+ public RRException(int code, String msg) {
+ super(msg);
+ this.msg = msg;
+ this.code = code;
+ }
+ public RRException(ErrorInterface error){
+ super(error.getMessage());
+ this.code = error.getCode();
+ this.msg = error.getMessage();
+ }
+
+ public RRException(int code,String msg, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ this.code = code;
+ }
+
+}
+
diff --git a/src/main/java/com/dpkj/common/utils/TemplateUtils.java b/src/main/java/com/dpkj/common/utils/TemplateUtils.java
new file mode 100644
index 0000000..a70d93e
--- /dev/null
+++ b/src/main/java/com/dpkj/common/utils/TemplateUtils.java
@@ -0,0 +1,333 @@
+package com.dpkj.common.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import com.dpkj.common.exception.RRException;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+import lombok.extern.slf4j.Slf4j;
+import org.jsoup.Jsoup;
+import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
+import org.springframework.stereotype.Service;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.StringTemplateResolver;
+import org.w3c.dom.Document;
+import org.xhtmlrenderer.layout.SharedContext;
+import org.xhtmlrenderer.simple.Graphics2DRenderer;
+
+import javax.imageio.ImageIO;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 模板服务类
+ *
+ * @author 石头人
+ * @version 1.0
+ * @since 2025-01-18 15:33:51
+ */
+@Service
+@Slf4j
+public class TemplateUtils {
+
+
+ /**
+ * 生成小票图片
+ *
+ * @param data json数据,用来填充模板
+ * @param template 模板(html字符串或者模板名称)
+ * @param width 图片宽度
+ * @param height 图片高度
+ * @param saveDir 图片的保存路径,如果为空,那么不进行图片的保存
+ * @return 图片字节数组
+ */
+ public byte[] generateReceiptImage(JSONObject data, String template, int width, int height, String saveDir) {
+ try {
+ // 获取模板上下文
+ Context context = this.getContext(data);
+
+ TemplateEngine templateEngine = new TemplateEngine();
+ if (checkIsHtml(template)) {
+ StringTemplateResolver resolver = new StringTemplateResolver();
+ resolver.setTemplateMode("HTML");
+ // 设置模板引擎使用这个自定义的模板解析器
+ templateEngine.setTemplateResolver(resolver);
+ }else {
+ SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
+ resolver.setPrefix("classpath:/templates/");
+ resolver.setSuffix(".html");
+ resolver.setTemplateMode(TemplateMode.HTML);
+ resolver.setCacheable(true);
+ resolver.setApplicationContext(new AnnotationConfigReactiveWebApplicationContext());
+ templateEngine.setTemplateResolver(resolver);
+ }
+
+ // 渲染模板
+ String html = templateEngine.process(template, context);
+
+ BufferedImage image = this.generate(html, width, height);
+
+ // 保存图片
+ if (saveDir != null && !"".equals(saveDir)) {
+ String outputPath = saveDir + "\\genera_image_" + System.currentTimeMillis() + ".png";
+ ImageIO.write(image, "PNG", new File(outputPath));
+ }
+
+ ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+ ImageIO.write(image, "PNG", byteOutputStream);
+ return byteOutputStream.toByteArray();
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RRException("图片打印失败");
+ }
+ }
+
+
+ /**
+ * 生成图片
+ * @param html html内容
+ */
+ private BufferedImage generate(String html, int width, int height){
+ try {
+ // 转换为xhtml
+ String xhtml = this.htmlToXhtml(html);
+
+ // 转换为document
+ Document document = this.xhtmlToDocument(xhtml);
+
+ // 生成图片
+ return createImageToDocument(document, width, height);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RRException("图片打印失败");
+ }
+ }
+
+
+ /**
+ * 通过JsonObject进行context内容填充
+ * @param data jsonObject
+ * @return context
+ */
+ private Context getContext(JSONObject data) {
+ // 创建Thymeleaf上下文
+ Context context = new Context();
+ Set keys = data.keySet();
+ for (String key : keys) {
+ // 判单是否有图片生成,统一后面采用的是_2base64Type
+ String[] split = key.split("_");
+ if (split.length > 1 && split[1].equals("2base64Type")) {
+ int width = split.length > 2 ? Integer.parseInt(split[2]) : 100;
+ int height = split.length > 3 ? Integer.parseInt(split[3]) : 100;
+ // 如果是图片类型,需要进行base64转换
+ String base64 = this.generateQRCode(String.valueOf(data.get(key)), width, height);
+ context.setVariable(split[0], "data:image/jpeg;base64," + base64);
+ } else {
+ // 普通字段直接设置
+ context.setVariable(key, data.get(key));
+ }
+ }
+
+ return context;
+ }
+
+
+ /**
+ * 根据内容生成二维码
+ * @param content 转换内容
+ */
+ private String generateQRCode(String content, int width, int height) {
+ try {
+ Map hints = new HashMap<>();
+ hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码为 UTF-8
+ hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); // 设置纠错级别
+
+ QRCodeWriter qrCodeWriter = new QRCodeWriter();
+ BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
+
+ BufferedImage qrImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ qrImage.createGraphics();
+
+ Graphics2D graphics = (Graphics2D) qrImage.getGraphics();
+ graphics.setColor(Color.WHITE);
+ graphics.fillRect(0, 0, width, height);
+ graphics.setColor(Color.BLACK);
+
+ for (int i = 0; i < width; i++) {
+ for (int j = 0; j < height; j++) {
+ if (bitMatrix.get(i, j)) {
+ graphics.fillRect(i, j, 1, 1);
+ }
+ }
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageIO.write(qrImage, "png", baos);
+ return Base64.getEncoder().encodeToString(baos.toByteArray());
+ }catch (Exception e){
+ log.error("二维码生成失败");
+ throw new RRException("二维码生成失败");
+ }
+ }
+
+
+ /**
+ * xhtml 转换为 Document
+ * @param xhtml xhtml
+ * @return document
+ * @throws Exception e
+ */
+ private Document xhtmlToDocument(String xhtml) throws Exception {
+ // 创建DocumentBuilder
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ // 解析XHTML字符串为Document
+ return builder.parse(new ByteArrayInputStream(xhtml.getBytes(StandardCharsets.UTF_8)));
+ }
+
+
+ /**
+ * 转换,将html转换为xhtml
+ * @param html html内容
+ * @return xhtml
+ */
+ private String htmlToXhtml(String html) {
+ org.jsoup.nodes.Document doc = Jsoup.parse(html);
+ doc.outputSettings()
+ .syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml)
+ .charset(StandardCharsets.UTF_8);
+
+ // 不使用外部DTD
+ return "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ doc.body().html() +
+ "\n\n" +
+ "";
+ }
+
+
+ /**
+ * 通过document转换为图片
+ * @param document doc
+ * @param width 图片的宽度
+ * @param height 图片的高度
+ * @return bufferedImage
+ */
+ private BufferedImage createImageToDocument(Document document, int width, int height) {
+ try {
+ // 创建Dimension对象
+ Dimension dimension = new Dimension(width, height);
+
+ // 创建图片
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D graphics = (Graphics2D) image.getGraphics();
+
+ // 设置白色背景
+ graphics.setColor(Color.WHITE);
+ graphics.fillRect(0, 0, width, height);
+
+ // 设置渲染提示
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+ // 创建渲染器
+ Graphics2DRenderer renderer = new Graphics2DRenderer();
+ renderer.setDocument(document, null);
+
+ // 设置渲染参数
+ SharedContext sharedContext = renderer.getSharedContext();
+ sharedContext.setInteractive(false);
+ sharedContext.setDPI(256f);
+
+ // 使用系统默认字体,假设系统默认字体支持UTF - 8
+ Font font = new Font(Font.DIALOG, Font.PLAIN, 20);
+ graphics.setFont(font);
+
+ // 使用Dimension对象进行布局
+ renderer.layout(graphics, dimension);
+ renderer.render(graphics);
+
+ graphics.dispose();
+ return image;
+ } catch (Exception e) {
+ throw new RRException("渲染图片失败", e);
+ }
+ }
+
+
+ /**
+ * 校验这个模板内容是不是html字符串,而非模板名称
+ * @param template template
+ * @return 是否是html字符串
+ */
+ private boolean checkIsHtml(String template){
+ try {
+ String pattern = "<(\"[^\"]*\"|'[^']*'|[^'\">])*>";
+ Pattern r = Pattern.compile(pattern);
+ return r.matcher(template).find();
+ }catch (Exception e) {
+ return false;
+ }
+ }
+
+
+ /**
+ * 将document对象转换为字符串
+ * @param doc document
+ * @return document转换为的字符串
+ */
+ private String documentToString(Document doc) {
+ try {
+ TransformerFactory tf = TransformerFactory.newInstance();
+ Transformer transformer = tf.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+
+ StringWriter writer = new StringWriter();
+ transformer.transform(new DOMSource(doc), new StreamResult(writer));
+ return writer.getBuffer().toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
+
diff --git a/src/main/java/com/dpkj/common/vo/Result.java b/src/main/java/com/dpkj/common/vo/Result.java
index 47c5e90..ed17053 100644
--- a/src/main/java/com/dpkj/common/vo/Result.java
+++ b/src/main/java/com/dpkj/common/vo/Result.java
@@ -1,6 +1,7 @@
package com.dpkj.common.vo;
import com.dpkj.common.constant.CommonConst;
+import com.dpkj.common.exception.ErrorEnum;
import lombok.Data;
import java.io.Serializable;
@@ -69,6 +70,14 @@ public class Result implements Serializable {
return error(CommonConst.SC_500, msg, null);
}
+ public static Result error(ErrorEnum errorEnum) {
+ return error(errorEnum.getCode(), errorEnum.getMessage(), null);
+ }
+
+ public static Result error(int code, String message) {
+ return error(code, message, null);
+ }
+
public static Result error(int code, String msg, T data) {
Result r = new Result();
r.setCode(code);