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