diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/QrCodeUtil.java b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/QrCodeUtil.java new file mode 100644 index 0000000..3e6c6f1 --- /dev/null +++ b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/QrCodeUtil.java @@ -0,0 +1,482 @@ +package com.zdxt.enforcementcode.util; + +import cn.hutool.core.util.IdUtil; +import com.google.zxing.*; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.zdxt.enforcementcode.domain.bo.QrCodeGenerateHistoryBo; +import com.zdxt.enforcementcode.domain.bo.ZdxtFileBo; +import com.zdxt.enforcementcode.domain.vo.QrCodeGenerateHistoryVo; +import com.zdxt.enforcementcode.service.IQrCodeGenerateHistoryService; +import com.zdxt.enforcementcode.service.IZdxtFileService; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.oss.core.OssClient; +import org.dromara.common.oss.entity.UploadResult; +import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Date; +import java.util.Hashtable; + +/** + * 二维码生成工具类 + *
+ * 提供二维码的生成、解析、本地存储以及 OSS 上传功能。 + * 生成后自动保存附件记录(zdxt_file 表)和二维码生成历史(qr_code_generate_history 表)。 + * 路径处理兼容 Windows 和 Linux。 + *
+ * + * @author zdxt + */ +@Slf4j +@Component +public class QrCodeUtil { + + private static IQrCodeGenerateHistoryService historyService; + private static IZdxtFileService fileService; + + @Autowired + public void setHistoryService(IQrCodeGenerateHistoryService historyService) { + QrCodeUtil.historyService = historyService; + } + + @Autowired + public void setFileService(IZdxtFileService fileService) { + QrCodeUtil.fileService = fileService; + } + + // ==================== 常量 ==================== + + private static final String CHARSET = "utf-8"; + private static final String IMAGE_FORMAT = "jpg"; + private static final String IMAGE_SUFFIX = ".jpg"; + private static final String IMAGE_CONTENT_TYPE = "image/jpeg"; + /** 默认二维码尺寸(像素) */ + private static final int DEFAULT_QR_SIZE = 300; + /** LOGO 最大宽度 */ + private static final int LOGO_WIDTH = 60; + /** LOGO 最大高度 */ + private static final int LOGO_HEIGHT = 60; + /** 蓝色二维码前景色 */ + private static final int BLUE_FOREGROUND_COLOR = 0xFF003399; + /** classpath 中的默认 LOGO 图片路径 */ + private static final String LOGO_CLASSPATH = "images/enterprise_pub_qrcode_logo.jpg"; + + // ==================== 生成 + 保存(核心业务方法) ==================== + + /** + * 生成标准黑白二维码,上传到 OSS,保存 zdxt_file 和 qr_code_generate_history 记录 + * + * @param content 二维码内容(通常为 URL) + * @param enterpriseNumber 企业纳税识别号 + * @param businessId 关联的业务ID + * @return 二维码生成历史记录 + */ + public static QrCodeGenerateHistoryVo generateAndSave(String content, String enterpriseNumber, + String businessId) { + return generateAndSave(content, enterpriseNumber, businessId, null, DEFAULT_QR_SIZE); + } + + /** + * 生成标准黑白二维码(可指定分类代码和尺寸),上传到 OSS 并保存记录 + * + * @param content 二维码内容 + * @param enterpriseNumber 企业纳税识别号 + * @param businessId 关联的业务ID + * @param sortCode 分类代码(可为 null) + * @param size 二维码尺寸(像素) + * @return 二维码生成历史记录 + */ + public static QrCodeGenerateHistoryVo generateAndSave(String content, String enterpriseNumber, + String businessId, String sortCode, int size) { + BufferedImage image = createQrCodeImage(content, size); + return uploadAndSaveRecords(image, enterpriseNumber, businessId, sortCode); + } + + /** + * 生成蓝色二维码(含默认 LOGO),上传到 OSS 并保存记录 + * + * @param content 二维码内容 + * @param enterpriseNumber 企业纳税识别号 + * @param businessId 关联的业务ID + * @param size 二维码尺寸(像素) + * @return 二维码生成历史记录 + */ + public static QrCodeGenerateHistoryVo generateBlueWithLogoAndSave(String content, String enterpriseNumber, + String businessId, int size) { + return generateBlueWithLogoAndSave(content, enterpriseNumber, businessId, null, size); + } + + /** + * 生成蓝色二维码(含默认 LOGO、可指定分类代码),上传到 OSS 并保存记录 + * + * @param content 二维码内容 + * @param enterpriseNumber 企业纳税识别号 + * @param businessId 关联的业务ID + * @param sortCode 分类代码(可为 null) + * @param size 二维码尺寸(像素) + * @return 二维码生成历史记录 + */ + public static QrCodeGenerateHistoryVo generateBlueWithLogoAndSave(String content, String enterpriseNumber, + String businessId, String sortCode, int size) { + BufferedImage image = createBlueQrCodeWithLogo(content, size); + return uploadAndSaveRecords(image, enterpriseNumber, businessId, sortCode); + } + + // ==================== 纯图片生成方法 ==================== + + /** + * 创建标准黑白二维码图片 + * + * @param content 二维码内容 + * @param size 二维码尺寸(像素) + * @return BufferedImage + */ + public static BufferedImage createQrCodeImage(String content, int size) { + return createQrCodeImage(content, null, size, false, 0xFF000000); + } + + /** + * 创建带 LOGO 的二维码图片(LOGO 来自本地文件路径) + * + * @param content 二维码内容 + * @param logoPath LOGO 图片的本地文件路径 + * @param size 二维码尺寸(像素) + * @param compressLogo 是否压缩 LOGO + * @return BufferedImage + */ + public static BufferedImage createQrCodeImageWithLogo(String content, String logoPath, + int size, boolean compressLogo) { + return createQrCodeImage(content, logoPath, size, compressLogo, 0xFF000000); + } + + /** + * 创建蓝色二维码图片(含 classpath 中的默认 LOGO) + * + * @param content 二维码内容 + * @param size 二维码尺寸(像素) + * @return BufferedImage + */ + public static BufferedImage createBlueQrCodeWithLogo(String content, int size) { + BufferedImage image = createQrCodeImage(content, null, size, false, BLUE_FOREGROUND_COLOR); + try (InputStream logoStream = QrCodeUtil.class.getClassLoader().getResourceAsStream(LOGO_CLASSPATH)) { + if (logoStream != null) { + insertLogoFromStream(image, logoStream, size); + } else { + log.warn("LOGO 图片未找到: {}", LOGO_CLASSPATH); + } + } catch (Exception e) { + log.warn("插入 LOGO 失败: {}", e.getMessage()); + } + return image; + } + + /** + * 解析二维码图片内容 + * + * @param file 二维码图片文件 + * @return 解析出的文本内容,解析失败返回 null + */ + public static String decodeQrCode(File file) { + try { + BufferedImage image = ImageIO.read(file); + if (image == null) { + return null; + } + BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + Hashtable+ * 使用系统临时目录,保证在 Windows 和 Linux 下都能正确运行: + *
+ * 封装文件本地存储 + 附件记录(zdxt_file 表)的统一操作, + * 独立于 RuoYi OSS,文件直接保存到本地磁盘。 + * 供业务模块直接调用,简化文件处理流程。 + *
+ *+ * 配置项(application.yml): + *
+ * zdxt: + * file: + * upload-path: D:/zdxt/upload # 文件存储根路径(Windows 示例) + * url-prefix: /upload # 文件访问 URL 前缀 + *+ * + * + * @author zdxt + */ +@Slf4j +@Component +public class ZdxtFileUtil { + + private static IZdxtFileService fileService; + + /** 文件存储根路径 */ + private static String uploadBasePath; + + /** 文件访问 URL 前缀 */ + private static String uploadUrlPrefix; + + private static final DateTimeFormatter DATE_DIR_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + + @Autowired + public void setFileService(IZdxtFileService fileService) { + ZdxtFileUtil.fileService = fileService; + } + + @Value("${zdxt.file.upload-path:#{T(java.lang.System).getProperty('user.dir') + '/upload'}}") + public void setUploadBasePath(String path) { + ZdxtFileUtil.uploadBasePath = path; + log.info("ZdxtFileUtil 文件存储根路径: {}", path); + } + + @Value("${zdxt.file.url-prefix:/upload}") + public void setUploadUrlPrefix(String prefix) { + ZdxtFileUtil.uploadUrlPrefix = prefix; + } + + // ==================== 上传方法 ==================== + + /** + * 上传文件并保存附件记录(自动获取当前登录用户信息) + * + * @param file 上传的文件 + * @param businessId 业务ID(关联的业务主键) + * @return 附件视图对象 + */ + public static ZdxtFileVo uploadFile(MultipartFile file, String businessId) { + return uploadFile(file, businessId, null); + } + + /** + * 上传文件并保存附件记录,带分类代码(自动获取当前登录用户信息) + * + * @param file 上传的文件 + * @param businessId 业务ID + * @param sortCode 分类代码 + * @return 附件视图对象 + */ + public static ZdxtFileVo uploadFile(MultipartFile file, String businessId, String sortCode) { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (ObjectUtil.isNull(loginUser)) { + throw new ServiceException("用户未登录,无法上传文件"); + } + String userId = LoginHelper.getUserIdStr(); + String userName = loginUser.getNickname(); + return uploadFile(file, businessId, sortCode, userId, userName); + } + + /** + * 上传文件并保存附件记录(手动指定上传人信息) + * + * @param file 上传的文件 + * @param businessId 业务ID + * @param sortCode 分类代码(可为 null) + * @param userId 上传人员ID + * @param userName 上传人员姓名 + * @return 附件视图对象 + */ + public static ZdxtFileVo uploadFile(MultipartFile file, String businessId, String sortCode, + String userId, String userName) { + // 1. 校验文件 + validateFile(file); + + // 2. 解析文件信息 + String originalFileName = file.getOriginalFilename(); + String suffix = StringUtils.substring(originalFileName, + originalFileName.lastIndexOf("."), originalFileName.length()); + String fileType = suffix.replace(".", ""); + long fileSize = file.getSize(); + + // 3. 保存文件到本地磁盘 + String datePath = LocalDate.now().format(DATE_DIR_FORMAT); + String saveName = IdUtil.fastSimpleUUID() + suffix; + String relativePath = datePath + "/" + saveName; + Path targetDir = Paths.get(uploadBasePath, datePath); + Path targetFile = targetDir.resolve(saveName); + + try { + Files.createDirectories(targetDir); + try (InputStream is = file.getInputStream()) { + Files.copy(is, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + log.error("文件保存到本地失败: {}", e.getMessage(), e); + throw new ServiceException("文件上传失败: " + e.getMessage()); + } + + // 访问路径:urlPrefix + /日期路径/文件名 + String accessUrl = uploadUrlPrefix + "/" + relativePath; + + // 4. 构建附件记录并保存到数据库 + ZdxtFileBo bo = new ZdxtFileBo(); + bo.setGuid(IdUtil.fastSimpleUUID()); + bo.setBusinessId(businessId); + bo.setSortCode(sortCode); + bo.setFileName(originalFileName); + bo.setUploadPersonId(userId); + bo.setUploadPersonName(userName); + bo.setUploadPath(accessUrl); + bo.setUploadTime(new Date()); + bo.setFileType(fileType); + bo.setFileSuffix(suffix); + bo.setFileSaveName(saveName); + bo.setFileSize(String.valueOf(fileSize)); + bo.setIsDelete2(0L); + + // 填充机构信息(如果当前用户已登录) + try { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser != null) { + bo.setOrgCode(LoginHelper.getDeptId()); + bo.setOrgName(LoginHelper.getDeptName()); + bo.setSassId(LoginHelper.getTenantId()); + } + } catch (Exception ignored) { + // 未登录场景(手动传入用户信息时)忽略 + } + + Boolean success = fileService.insertByBo(bo); + if (!success) { + throw new ServiceException("保存附件记录失败"); + } + + log.info("文件上传成功: fileName={}, businessId={}, guid={}, path={}", + originalFileName, businessId, bo.getGuid(), targetFile.toAbsolutePath()); + return fileService.queryById(bo.getGuid()); + } + + /** + * 上传字节数组并保存附件记录(供内部或程序化调用,如二维码图片保存) + * + * @param data 文件字节内容 + * @param fileName 文件名(含后缀,如 qrcode.jpg) + * @param businessId 业务ID + * @param sortCode 分类代码(可为 null) + * @return 附件视图对象 + */ + public static ZdxtFileVo uploadBytes(byte[] data, String fileName, String businessId, String sortCode) { + if (data == null || data.length == 0) { + throw new ServiceException("上传内容不能为空"); + } + if (StringUtils.isBlank(fileName) || !fileName.contains(".")) { + throw new ServiceException("文件名不合法"); + } + + String suffix = fileName.substring(fileName.lastIndexOf(".")); + String fileType = suffix.replace(".", ""); + + // 保存到本地 + String datePath = LocalDate.now().format(DATE_DIR_FORMAT); + String saveName = IdUtil.fastSimpleUUID() + suffix; + String relativePath = datePath + "/" + saveName; + Path targetDir = Paths.get(uploadBasePath, datePath); + Path targetFile = targetDir.resolve(saveName); + + try { + Files.createDirectories(targetDir); + Files.write(targetFile, data); + } catch (IOException e) { + log.error("字节数据保存到本地失败: {}", e.getMessage(), e); + throw new ServiceException("文件保存失败: " + e.getMessage()); + } + + String accessUrl = uploadUrlPrefix + "/" + relativePath; + + ZdxtFileBo bo = new ZdxtFileBo(); + bo.setGuid(IdUtil.fastSimpleUUID()); + bo.setBusinessId(businessId); + bo.setSortCode(sortCode); + bo.setFileName(fileName); + bo.setUploadPath(accessUrl); + bo.setUploadTime(new Date()); + bo.setFileType(fileType); + bo.setFileSuffix(suffix); + bo.setFileSaveName(saveName); + bo.setFileSize(String.valueOf(data.length)); + bo.setIsDelete2(0L); + + // 填充登录用户信息(如可用) + try { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser != null) { + bo.setUploadPersonId(LoginHelper.getUserIdStr()); + bo.setUploadPersonName(loginUser.getNickname()); + bo.setOrgCode(LoginHelper.getDeptId()); + bo.setOrgName(LoginHelper.getDeptName()); + bo.setSassId(LoginHelper.getTenantId()); + } + } catch (Exception ignored) { + } + + Boolean success = fileService.insertByBo(bo); + if (!success) { + throw new ServiceException("保存附件记录失败"); + } + + log.info("字节数据上传成功: fileName={}, businessId={}, guid={}", fileName, businessId, bo.getGuid()); + return fileService.queryById(bo.getGuid()); + } + + /** + * 批量上传文件 + * + * @param files 文件数组 + * @param businessId 业务ID + * @return 附件视图对象列表 + */ + public static List
+ * 分为两部分: + * 1. 纯图片生成测试(不依赖 OSS/DB,可独立运行) + * 2. 生成+保存集成测试(需要 Spring 上下文、OSS 和数据库) + *
+ * + * @author zdxt + */ +@SpringBootTest +@DisplayName("QrCodeUtil 二维码工具类测试") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class QrCodeUtilTest { + + /** 测试用二维码内容 */ + private static final String TEST_CONTENT = "https://www.example.com/qr?id=test123"; + /** 测试用企业纳税识别号 */ + private static final String TEST_ENTERPRISE_NUMBER = "91110000TEST001"; + /** 测试用业务ID */ + private static final String TEST_BUSINESS_ID = "TEST_BIZ_QR_001"; + + // ==================== 纯图片生成测试 ==================== + + @DisplayName("生成标准黑白二维码图片") + @Test + @Order(1) + public void testCreateQrCodeImage() { + BufferedImage image = QrCodeUtil.createQrCodeImage(TEST_CONTENT, 300); + + Assertions.assertNotNull(image, "生成的二维码图片不应为 null"); + Assertions.assertEquals(300, image.getWidth(), "宽度应为 300"); + Assertions.assertEquals(300, image.getHeight(), "高度应为 300"); + System.out.println("标准二维码生成成功: " + image.getWidth() + "x" + image.getHeight()); + } + + @DisplayName("生成自定义尺寸二维码图片") + @Test + @Order(1) + public void testCreateQrCodeImageCustomSize() { + BufferedImage image = QrCodeUtil.createQrCodeImage(TEST_CONTENT, 500); + + Assertions.assertNotNull(image, "生成的二维码图片不应为 null"); + Assertions.assertTrue(image.getWidth() > 0, "宽度应大于0"); + System.out.println("自定义尺寸二维码生成成功: " + image.getWidth() + "x" + image.getHeight()); + } + + @DisplayName("生成带本地 LOGO 的二维码图片") + @Test + @Order(1) + public void testCreateQrCodeImageWithLogo() { + // 先生成一个标准二维码保存为临时文件作为 LOGO + BufferedImage logoImage = QrCodeUtil.createQrCodeImage("LOGO", 60); + Path tempLogo = QrCodeUtil.saveToLocal(logoImage, "temp_logo.jpg"); + + try { + BufferedImage image = QrCodeUtil.createQrCodeImageWithLogo( + TEST_CONTENT, tempLogo.toAbsolutePath().toString(), 300, true); + + Assertions.assertNotNull(image, "带 LOGO 二维码不应为 null"); + System.out.println("带 LOGO 二维码生成成功"); + } finally { + // 清理临时文件 + tempLogo.toFile().delete(); + } + } + + @DisplayName("生成蓝色二维码(含默认 LOGO)") + @Test + @Order(1) + public void testCreateBlueQrCodeWithLogo() { + BufferedImage image = QrCodeUtil.createBlueQrCodeWithLogo(TEST_CONTENT, 300); + + Assertions.assertNotNull(image, "蓝色二维码图片不应为 null"); + Assertions.assertEquals(300, image.getWidth(), "宽度应为 300"); + + // 验证左上角像素为白色(margin 区域) + int topLeftPixel = image.getRGB(0, 0); + Assertions.assertEquals(0xFFFFFFFF, topLeftPixel, "左上角应为白色(边距区域)"); + System.out.println("蓝色二维码(含 LOGO)生成成功"); + } + + // ==================== Base64 编码测试 ==================== + + @DisplayName("生成标准二维码的 Base64 字符串") + @Test + @Order(2) + public void testToBase64() { + String base64 = QrCodeUtil.toBase64(TEST_CONTENT, 300); + + Assertions.assertNotNull(base64, "Base64 字符串不应为 null"); + Assertions.assertTrue(base64.startsWith("data:image/jpeg;base64,"), "应包含 data:image 前缀"); + Assertions.assertTrue(base64.length() > 100, "Base64 长度应足够大"); + System.out.println("标准二维码 Base64 长度: " + base64.length()); + } + + @DisplayName("生成蓝色二维码的 Base64 字符串") + @Test + @Order(2) + public void testToBlueBase64WithLogo() { + String base64 = QrCodeUtil.toBlueBase64WithLogo(TEST_CONTENT, 300); + + Assertions.assertNotNull(base64, "Base64 字符串不应为 null"); + Assertions.assertTrue(base64.startsWith("data:image/jpeg;base64,"), "应包含 data:image 前缀"); + System.out.println("蓝色二维码 Base64 长度: " + base64.length()); + } + + // ==================== 本地文件存储测试(跨平台) ==================== + + @DisplayName("保存二维码到指定本地目录") + @Test + @Order(3) + public void testSaveToLocalWithDir() { + BufferedImage image = QrCodeUtil.createQrCodeImage(TEST_CONTENT, 300); + Path defaultDir = QrCodeUtil.getDefaultStoragePath(); + String fileName = "test_qr_dir.jpg"; + + Path savedPath = QrCodeUtil.saveToLocal(image, defaultDir.toString(), fileName); + + Assertions.assertTrue(Files.exists(savedPath), "文件应存在于磁盘"); + Assertions.assertTrue(savedPath.toFile().length() > 0, "文件大小应大于 0"); + System.out.println("保存到指定目录: " + savedPath.toAbsolutePath()); + + // 清理 + savedPath.toFile().delete(); + } + + @DisplayName("保存二维码到默认存储路径") + @Test + @Order(3) + public void testSaveToLocalDefault() { + BufferedImage image = QrCodeUtil.createQrCodeImage(TEST_CONTENT, 300); + String fileName = "test_qr_default.jpg"; + + Path savedPath = QrCodeUtil.saveToLocal(image, fileName); + + Assertions.assertTrue(Files.exists(savedPath), "文件应存在于默认路径"); + System.out.println("保存到默认路径: " + savedPath.toAbsolutePath()); + + // 清理 + savedPath.toFile().delete(); + } + + @DisplayName("获取跨平台默认存储路径") + @Test + @Order(3) + public void testGetDefaultStoragePath() { + Path path = QrCodeUtil.getDefaultStoragePath(); + + Assertions.assertNotNull(path, "默认路径不应为 null"); + String pathStr = path.toString(); + Assertions.assertTrue(pathStr.contains("zdxt"), "路径应包含 zdxt"); + Assertions.assertTrue(pathStr.contains("qrcode"), "路径应包含 qrcode"); + + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + System.out.println("Windows 默认路径: " + pathStr); + } else { + System.out.println("Linux 默认路径: " + pathStr); + } + } + + // ==================== 二维码解析测试 ==================== + + @DisplayName("生成二维码后再解析验证内容一致性") + @Test + @Order(4) + public void testDecodeQrCode() { + // 1. 生成二维码并保存到临时文件 + BufferedImage image = QrCodeUtil.createQrCodeImage(TEST_CONTENT, 300); + Path tempPath = QrCodeUtil.saveToLocal(image, "test_decode.jpg"); + + try { + // 2. 解析二维码 + String decoded = QrCodeUtil.decodeQrCode(tempPath.toFile()); + + Assertions.assertNotNull(decoded, "解析结果不应为 null"); + Assertions.assertEquals(TEST_CONTENT, decoded, "解析内容应与原始内容一致"); + System.out.println("二维码解析成功: " + decoded); + } finally { + // 清理 + tempPath.toFile().delete(); + } + } + + @DisplayName("解析蓝色二维码内容") + @Test + @Order(4) + public void testDecodeBlueQrCode() { + BufferedImage image = QrCodeUtil.createBlueQrCodeWithLogo(TEST_CONTENT, 400); + Path tempPath = QrCodeUtil.saveToLocal(image, "test_decode_blue.jpg"); + + try { + String decoded = QrCodeUtil.decodeQrCode(tempPath.toFile()); + Assertions.assertNotNull(decoded, "蓝色二维码解析结果不应为 null"); + Assertions.assertEquals(TEST_CONTENT, decoded, "蓝色二维码解析内容应一致"); + System.out.println("蓝色二维码解析成功: " + decoded); + } finally { + tempPath.toFile().delete(); + } + } + + // ==================== 生成 + 保存集成测试(需 OSS + DB) ==================== + + @DisplayName("生成标准二维码并保存到 OSS 和数据库") + @Test + @Order(5) + public void testGenerateAndSave() { + QrCodeGenerateHistoryVo vo = QrCodeUtil.generateAndSave( + TEST_CONTENT, TEST_ENTERPRISE_NUMBER, TEST_BUSINESS_ID + ); + + Assertions.assertNotNull(vo, "返回的历史记录 VO 不应为 null"); + Assertions.assertNotNull(vo.getId(), "历史记录 ID 不应为 null"); + Assertions.assertNotNull(vo.getQrCodeUrl(), "二维码 URL 不应为 null"); + Assertions.assertEquals(TEST_ENTERPRISE_NUMBER, vo.getEnterpriseNumber(), "企业编号应一致"); + Assertions.assertEquals(0L, vo.getStatus(), "状态应为有效(0)"); + System.out.println("生成并保存成功: id=" + vo.getId() + ", url=" + vo.getQrCodeUrl()); + } + + @DisplayName("生成标准二维码(带分类代码和自定义尺寸)并保存") + @Test + @Order(5) + public void testGenerateAndSaveWithSortCodeAndSize() { + QrCodeGenerateHistoryVo vo = QrCodeUtil.generateAndSave( + TEST_CONTENT, TEST_ENTERPRISE_NUMBER, TEST_BUSINESS_ID, "QR_SORT_001", 400 + ); + + Assertions.assertNotNull(vo, "返回的历史记录 VO 不应为 null"); + Assertions.assertNotNull(vo.getQrCodeUrl(), "二维码 URL 不应为 null"); + System.out.println("带分类代码生成成功: id=" + vo.getId()); + } + + @DisplayName("生成蓝色二维码(含 LOGO)并保存到 OSS 和数据库") + @Test + @Order(5) + public void testGenerateBlueWithLogoAndSave() { + QrCodeGenerateHistoryVo vo = QrCodeUtil.generateBlueWithLogoAndSave( + TEST_CONTENT, TEST_ENTERPRISE_NUMBER, TEST_BUSINESS_ID, 300 + ); + + Assertions.assertNotNull(vo, "返回的历史记录 VO 不应为 null"); + Assertions.assertNotNull(vo.getQrCodeUrl(), "蓝色二维码 URL 不应为 null"); + Assertions.assertEquals(TEST_ENTERPRISE_NUMBER, vo.getEnterpriseNumber(), "企业编号应一致"); + System.out.println("蓝色二维码生成并保存成功: id=" + vo.getId() + ", url=" + vo.getQrCodeUrl()); + } + + @DisplayName("生成蓝色二维码(含分类代码)并保存") + @Test + @Order(5) + public void testGenerateBlueWithLogoAndSaveWithSortCode() { + QrCodeGenerateHistoryVo vo = QrCodeUtil.generateBlueWithLogoAndSave( + TEST_CONTENT, TEST_ENTERPRISE_NUMBER, TEST_BUSINESS_ID, "QR_BLUE_SORT", 500 + ); + + Assertions.assertNotNull(vo); + System.out.println("蓝色二维码(带分类)生成成功: id=" + vo.getId()); + } + + // ==================== 异常场景测试 ==================== + + @DisplayName("空内容生成二维码应抛出异常") + @Test + public void testCreateQrCodeWithEmptyContent() { + Assertions.assertThrows(Exception.class, () -> + QrCodeUtil.createQrCodeImage("", 300) + ); + System.out.println("空内容生成正确抛出异常"); + } + + @DisplayName("解析非二维码文件应抛出异常") + @Test + public void testDecodeNonQrCodeFile() throws Exception { + // 创建一个纯白色图片(非二维码) + BufferedImage whiteImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < 100; x++) { + for (int y = 0; y < 100; y++) { + whiteImage.setRGB(x, y, 0xFFFFFFFF); + } + } + Path tempPath = QrCodeUtil.saveToLocal(whiteImage, "test_not_qr.jpg"); + + try { + Assertions.assertThrows(Exception.class, () -> + QrCodeUtil.decodeQrCode(tempPath.toFile()) + ); + System.out.println("非二维码图片解析正确抛出异常"); + } finally { + tempPath.toFile().delete(); + } + } +} diff --git a/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/ZdxtFileUtilTest.java b/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/ZdxtFileUtilTest.java new file mode 100644 index 0000000..b476873 --- /dev/null +++ b/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/ZdxtFileUtilTest.java @@ -0,0 +1,194 @@ +package org.dromara.test; + +import com.zdxt.enforcementcode.domain.vo.ZdxtFileVo; +import com.zdxt.enforcementcode.util.ZdxtFileUtil; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * ZdxtFileUtil 附件工具类测试用例 + * + * @author zdxt + */ +@SpringBootTest +@DisplayName("ZdxtFileUtil 附件工具类测试") +public class ZdxtFileUtilTest { + + /** 测试用业务ID */ + private static final String TEST_BUSINESS_ID = "TEST_BIZ_FILE_001"; + /** 记录上传成功后的 guid,供后续查询/删除使用 */ + private static String uploadedGuid; + + // ==================== 上传测试 ==================== + + @DisplayName("上传文件 - 手动指定用户信息") + @Test + @Order(1) + public void testUploadFileWithManualUser() { + MockMultipartFile mockFile = new MockMultipartFile( + "file", + "test-document.txt", + "text/plain", + "Hello, this is a test file content.".getBytes(StandardCharsets.UTF_8) + ); + + ZdxtFileVo vo = ZdxtFileUtil.uploadFile( + mockFile, TEST_BUSINESS_ID, "TEST_SORT", + "test_user_001", "测试用户" + ); + + Assertions.assertNotNull(vo, "上传后返回的 VO 不应为 null"); + Assertions.assertNotNull(vo.getGuid(), "guid 不应为 null"); + Assertions.assertEquals(TEST_BUSINESS_ID, vo.getBusinessId(), "businessId 应一致"); + Assertions.assertEquals("test-document.txt", vo.getFileName(), "文件名应一致"); + Assertions.assertEquals(".txt", vo.getFileSuffix(), "文件后缀应为 .txt"); + Assertions.assertEquals("txt", vo.getFileType(), "文件类型应为 txt"); + Assertions.assertNotNull(vo.getUploadPath(), "uploadPath 不应为 null"); + System.out.println("上传成功: guid=" + vo.getGuid() + ", url=" + vo.getUploadPath()); + + uploadedGuid = vo.getGuid(); + } + + @DisplayName("上传文件 - 空文件应抛出异常") + @Test + public void testUploadEmptyFile() { + MockMultipartFile emptyFile = new MockMultipartFile( + "file", "empty.txt", "text/plain", new byte[0] + ); + + Assertions.assertThrows(Exception.class, () -> + ZdxtFileUtil.uploadFile(emptyFile, "BIZ_EMPTY", null, "user1", "用户1") + ); + System.out.println("空文件上传正确抛出异常"); + } + + @DisplayName("上传文件 - 无后缀文件名应抛出异常") + @Test + public void testUploadFileWithoutSuffix() { + MockMultipartFile noSuffixFile = new MockMultipartFile( + "file", "noextension", "application/octet-stream", "data".getBytes() + ); + + Assertions.assertThrows(Exception.class, () -> + ZdxtFileUtil.uploadFile(noSuffixFile, "BIZ_NO_EXT", null, "user1", "用户1") + ); + System.out.println("无后缀文件上传正确抛出异常"); + } + + // ==================== 查询测试 ==================== + + @DisplayName("根据业务ID查询附件列表") + @Test + @Order(2) + public void testQueryByBusinessId() { + List