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 hints = new Hashtable<>(); + hints.put(DecodeHintType.CHARACTER_SET, CHARSET); + Result result = new MultiFormatReader().decode(bitmap, hints); + return result.getText(); + } catch (Exception e) { + log.error("解析二维码失败: {}", e.getMessage(), e); + throw new ServiceException("解析二维码失败: " + e.getMessage()); + } + } + + /** + * 生成标准黑白二维码的 Base64 编码字符串(含 data:image 前缀) + * + * @param content 二维码内容 + * @param size 二维码尺寸 + * @return Base64 编码字符串 + */ + public static String toBase64(String content, int size) { + BufferedImage image = createQrCodeImage(content, size); + return imageToBase64(image); + } + + /** + * 生成蓝色二维码(含 LOGO)的 Base64 编码字符串 + * + * @param content 二维码内容 + * @param size 二维码尺寸 + * @return Base64 编码字符串 + */ + public static String toBlueBase64WithLogo(String content, int size) { + BufferedImage image = createBlueQrCodeWithLogo(content, size); + return imageToBase64(image); + } + + // ==================== 本地文件操作(跨平台) ==================== + + /** + * 将二维码图片保存到指定本地目录(自动处理跨平台路径) + * + * @param image 二维码图片 + * @param destDir 目标目录路径(支持 Windows 和 Linux 格式) + * @param fileName 文件名(含后缀) + * @return 保存后的文件路径 + */ + public static Path saveToLocal(BufferedImage image, String destDir, String fileName) { + try { + Path dirPath = Paths.get(destDir); + if (!Files.exists(dirPath)) { + Files.createDirectories(dirPath); + } + Path filePath = dirPath.resolve(fileName); + ImageIO.write(image, IMAGE_FORMAT, filePath.toFile()); + log.info("二维码图片已保存到本地: {}", filePath.toAbsolutePath()); + return filePath; + } catch (IOException e) { + log.error("保存二维码图片到本地失败: {}", e.getMessage(), e); + throw new ServiceException("保存二维码图片失败: " + e.getMessage()); + } + } + + /** + * 将二维码图片保存到默认存储路径(跨平台) + * + * @param image 二维码图片 + * @param fileName 文件名(含后缀) + * @return 保存后的文件路径 + */ + public static Path saveToLocal(BufferedImage image, String fileName) { + return saveToLocal(image, getDefaultStoragePath().toString(), fileName); + } + + /** + * 获取跨平台默认存储路径 + *

+ * 使用系统临时目录,保证在 Windows 和 Linux 下都能正确运行: + *

+ *

+ * + * @return 默认存储路径 + */ + public static Path getDefaultStoragePath() { + return Paths.get(System.getProperty("java.io.tmpdir"), "zdxt", "qrcode"); + } + + // ==================== 内部方法 ==================== + + /** + * 上传二维码图片到 OSS,并保存 zdxt_file 和 qr_code_generate_history 记录 + */ + private static QrCodeGenerateHistoryVo uploadAndSaveRecords(BufferedImage image, + String enterpriseNumber, + String businessId, + String sortCode) { + // 1. 图片转为字节数组 + byte[] imageBytes = imageToBytes(image); + String fileName = "qrcode_" + IdUtil.fastSimpleUUID() + IMAGE_SUFFIX; + + // 2. 上传到 OSS + OssClient storage = OssFactory.instance(); + UploadResult uploadResult = storage.uploadSuffix(imageBytes, IMAGE_SUFFIX, IMAGE_CONTENT_TYPE); + String ossUrl = uploadResult.getUrl(); + + // 3. 保存 zdxt_file 附件记录 + ZdxtFileBo fileBo = buildFileBo(fileName, businessId, sortCode, uploadResult, imageBytes.length); + Boolean fileSuccess = fileService.insertByBo(fileBo); + if (!fileSuccess) { + throw new ServiceException("保存二维码附件记录失败"); + } + log.info("二维码附件记录已保存: guid={}, businessId={}", fileBo.getGuid(), businessId); + + // 4. 保存 qr_code_generate_history 记录 + QrCodeGenerateHistoryBo historyBo = new QrCodeGenerateHistoryBo(); + historyBo.setId(IdUtil.fastSimpleUUID()); + historyBo.setGenerateTime(new Date()); + historyBo.setQrCodeUrl(ossUrl); + historyBo.setEnterpriseNumber(enterpriseNumber); + historyBo.setStatus(0L); // 0=有效 + Boolean historySuccess = historyService.insertByBo(historyBo); + if (!historySuccess) { + throw new ServiceException("保存二维码生成历史失败"); + } + log.info("二维码历史记录已保存: id={}, enterpriseNumber={}", historyBo.getId(), enterpriseNumber); + + return historyService.queryById(historyBo.getId()); + } + + /** + * 构建 ZdxtFileBo 附件记录 + */ + private static ZdxtFileBo buildFileBo(String fileName, String businessId, String sortCode, + UploadResult uploadResult, int fileSize) { + ZdxtFileBo bo = new ZdxtFileBo(); + bo.setGuid(IdUtil.fastSimpleUUID()); + bo.setBusinessId(businessId); + bo.setSortCode(sortCode); + bo.setFileName(fileName); + bo.setUploadPath(uploadResult.getUrl()); + bo.setUploadTime(new Date()); + bo.setFileType(IMAGE_FORMAT); + bo.setFileSuffix(IMAGE_SUFFIX); + bo.setFileSaveName(uploadResult.getFilename()); + bo.setFileSize(String.valueOf(fileSize)); + 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) { + // 未登录场景忽略 + } + + return bo; + } + + /** + * 生成二维码 BitMatrix 并渲染为 BufferedImage + * + * @param content 二维码内容 + * @param logoPath LOGO 文件路径(为 null 则不插入 LOGO) + * @param size 尺寸 + * @param compressLogo 是否压缩 LOGO + * @param foregroundColor 前景色(如 0xFF000000 黑色,0xFF003399 蓝色) + * @return BufferedImage + */ + private static BufferedImage createQrCodeImage(String content, String logoPath, int size, + boolean compressLogo, int foregroundColor) { + try { + Hashtable hints = new Hashtable<>(); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); + hints.put(EncodeHintType.CHARACTER_SET, CHARSET); + hints.put(EncodeHintType.MARGIN, 1); + + BitMatrix bitMatrix = new MultiFormatWriter().encode( + content, BarcodeFormat.QR_CODE, size, size, hints); + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + image.setRGB(x, y, bitMatrix.get(x, y) ? foregroundColor : 0xFFFFFFFF); + } + } + + // 插入本地文件 LOGO(如果指定了路径) + if (logoPath != null && !logoPath.isEmpty()) { + File logoFile = new File(logoPath); + if (logoFile.exists()) { + Image logo = ImageIO.read(logoFile); + insertLogoImage(image, logo, compressLogo); + } else { + log.warn("LOGO 文件不存在: {}", logoPath); + } + } + + return image; + } catch (Exception e) { + log.error("生成二维码图片失败: {}", e.getMessage(), e); + throw new ServiceException("生成二维码失败: " + e.getMessage()); + } + } + + /** + * 从 InputStream 读取 LOGO 并插入到二维码中央 + */ + private static void insertLogoFromStream(BufferedImage source, InputStream logoStream, + int qrSize) throws Exception { + Image logo = ImageIO.read(logoStream); + int logoWidth = Math.min(logo.getWidth(null), 100); + int logoHeight = Math.min(logo.getHeight(null), 100); + Image scaledLogo = logo.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH); + + Graphics2D graph = source.createGraphics(); + int x = (qrSize - logoWidth) / 2; + int y = (qrSize - logoHeight) / 2; + graph.drawImage(scaledLogo, x, y, logoWidth, logoHeight, null); + Shape shape = new RoundRectangle2D.Float(x, y, logoWidth, logoHeight, 6, 6); + graph.setStroke(new BasicStroke(3f)); + graph.draw(shape); + graph.dispose(); + } + + /** + * 将 LOGO Image 插入到二维码中央 + */ + private static void insertLogoImage(BufferedImage source, Image logo, boolean compress) { + int width = logo.getWidth(null); + int height = logo.getHeight(null); + + if (compress) { + width = Math.min(width, LOGO_WIDTH); + height = Math.min(height, LOGO_HEIGHT); + Image scaled = logo.getScaledInstance(width, height, Image.SCALE_SMOOTH); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + g.drawImage(scaled, 0, 0, null); + g.dispose(); + logo = scaled; + } + + int qrSize = source.getWidth(); + Graphics2D graph = source.createGraphics(); + int x = (qrSize - width) / 2; + int y = (qrSize - height) / 2; + graph.drawImage(logo, x, y, width, height, null); + Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6); + graph.setStroke(new BasicStroke(3f)); + graph.draw(shape); + graph.dispose(); + } + + /** + * BufferedImage 转为字节数组 + */ + private static byte[] imageToBytes(BufferedImage image) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(image, IMAGE_FORMAT, baos); + return baos.toByteArray(); + } catch (IOException e) { + throw new ServiceException("图片转换失败: " + e.getMessage()); + } + } + + /** + * BufferedImage 转为 Base64 编码字符串(含 data:image 前缀) + */ + private static String imageToBase64(BufferedImage image) { + byte[] bytes = imageToBytes(image); + return "data:image/jpeg;base64," + java.util.Base64.getEncoder().encodeToString(bytes); + } +} diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/ZdxtFileUtil.java b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/ZdxtFileUtil.java new file mode 100644 index 0000000..b996d6d --- /dev/null +++ b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/ZdxtFileUtil.java @@ -0,0 +1,476 @@ +package com.zdxt.enforcementcode.util; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.zdxt.enforcementcode.domain.bo.ZdxtFileBo; +import com.zdxt.enforcementcode.domain.vo.ZdxtFileVo; +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.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * 附件工具类 + *

+ * 封装文件本地存储 + 附件记录(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 uploadFiles(MultipartFile[] files, String businessId) { + return uploadFiles(files, businessId, null); + } + + /** + * 批量上传文件,带分类代码 + * + * @param files 文件数组 + * @param businessId 业务ID + * @param sortCode 分类代码 + * @return 附件视图对象列表 + */ + public static List uploadFiles(MultipartFile[] files, String businessId, String sortCode) { + if (files == null || files.length == 0) { + throw new ServiceException("上传文件不能为空"); + } + return java.util.Arrays.stream(files) + .map(file -> uploadFile(file, businessId, sortCode)) + .toList(); + } + + // ==================== 查询方法 ==================== + + /** + * 根据业务ID查询附件列表 + * + * @param businessId 业务ID + * @return 附件列表 + */ + public static List queryByBusinessId(String businessId) { + if (StringUtils.isBlank(businessId)) { + return Collections.emptyList(); + } + ZdxtFileBo bo = new ZdxtFileBo(); + bo.setBusinessId(businessId); + bo.setIsDelete2(0L); + return fileService.queryList(bo); + } + + /** + * 根据业务ID和分类代码查询附件列表 + * + * @param businessId 业务ID + * @param sortCode 分类代码 + * @return 附件列表 + */ + public static List queryByBusinessIdAndSortCode(String businessId, String sortCode) { + if (StringUtils.isBlank(businessId)) { + return Collections.emptyList(); + } + ZdxtFileBo bo = new ZdxtFileBo(); + bo.setBusinessId(businessId); + bo.setSortCode(sortCode); + bo.setIsDelete2(0L); + return fileService.queryList(bo); + } + + /** + * 根据附件ID查询附件信息 + * + * @param guid 附件主键ID + * @return 附件视图对象 + */ + public static ZdxtFileVo queryByGuid(String guid) { + if (StringUtils.isBlank(guid)) { + return null; + } + return fileService.queryById(guid); + } + + // ==================== 删除方法 ==================== + + /** + * 根据附件ID删除单个附件(同时删除本地文件) + * + * @param guid 附件主键ID + * @return 是否删除成功 + */ + public static Boolean deleteByGuid(String guid) { + if (StringUtils.isBlank(guid)) { + return false; + } + deleteLocalFile(guid); + return fileService.deleteWithValidByIds(Collections.singletonList(guid), true); + } + + /** + * 根据附件ID批量删除附件(同时删除本地文件) + * + * @param guids 附件主键ID集合 + * @return 是否删除成功 + */ + public static Boolean deleteByGuids(List guids) { + if (guids == null || guids.isEmpty()) { + return false; + } + guids.forEach(ZdxtFileUtil::deleteLocalFile); + return fileService.deleteWithValidByIds(guids, true); + } + + /** + * 根据业务ID删除所有关联附件(同时删除本地文件) + * + * @param businessId 业务ID + * @return 是否删除成功 + */ + public static Boolean deleteByBusinessId(String businessId) { + if (StringUtils.isBlank(businessId)) { + return false; + } + List files = queryByBusinessId(businessId); + if (files.isEmpty()) { + return true; + } + // 删除本地文件 + files.forEach(f -> deleteLocalFileByPath(f.getUploadPath())); + List guids = files.stream().map(ZdxtFileVo::getGuid).toList(); + return fileService.deleteWithValidByIds(guids, true); + } + + // ==================== 路径工具方法 ==================== + + /** + * 获取文件存储根路径 + * + * @return 存储根路径 + */ + public static String getUploadBasePath() { + return uploadBasePath; + } + + /** + * 获取文件访问 URL 前缀 + * + * @return URL 前缀 + */ + public static String getUploadUrlPrefix() { + return uploadUrlPrefix; + } + + /** + * 根据 uploadPath(访问URL)解析出本地磁盘绝对路径 + * + * @param uploadPath 数据库中保存的访问路径,如 /upload/2026/04/15/xxx.txt + * @return 本地磁盘绝对路径 + */ + public static Path resolveLocalPath(String uploadPath) { + if (StringUtils.isBlank(uploadPath)) { + return null; + } + // 去掉 URL 前缀,得到相对路径 + String relative = uploadPath; + if (relative.startsWith(uploadUrlPrefix)) { + relative = relative.substring(uploadUrlPrefix.length()); + } + if (relative.startsWith("/")) { + relative = relative.substring(1); + } + return Paths.get(uploadBasePath, relative.replace("/", java.io.File.separator)); + } + + // ==================== 私有方法 ==================== + + /** + * 校验上传的文件 + */ + private static void validateFile(MultipartFile file) { + if (ObjectUtil.isNull(file) || file.isEmpty()) { + throw new ServiceException("上传文件不能为空"); + } + String originalFileName = file.getOriginalFilename(); + if (StringUtils.isBlank(originalFileName)) { + throw new ServiceException("文件名不能为空"); + } + if (!originalFileName.contains(".")) { + throw new ServiceException("文件缺少后缀名"); + } + } + + /** + * 根据附件 guid 删除本地文件 + */ + private static void deleteLocalFile(String guid) { + try { + ZdxtFileVo vo = fileService.queryById(guid); + if (vo != null && StringUtils.isNotBlank(vo.getUploadPath())) { + deleteLocalFileByPath(vo.getUploadPath()); + } + } catch (Exception e) { + log.warn("查询附件记录失败,跳过本地文件删除: guid={}", guid); + } + } + + /** + * 根据 uploadPath 删除本地文件 + */ + private static void deleteLocalFileByPath(String uploadPath) { + try { + Path localPath = resolveLocalPath(uploadPath); + if (localPath != null && Files.exists(localPath)) { + Files.delete(localPath); + log.info("本地文件已删除: {}", localPath); + } + } catch (IOException e) { + log.warn("删除本地文件失败: {}, 原因: {}", uploadPath, e.getMessage()); + } + } +} diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_bg.jpg b/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_bg.jpg new file mode 100644 index 0000000..ffe4cf8 Binary files /dev/null and b/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_bg.jpg differ diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_logo.jpg b/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_logo.jpg new file mode 100644 index 0000000..64d2525 Binary files /dev/null and b/ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_logo.jpg differ diff --git a/script/sql/zfjd-refactoring-v1/009-修改执法码相关的表.sql b/script/sql/zfjd-refactoring-v1/009-修改执法码相关的表.sql index 2de2276..ac788fc 100644 --- a/script/sql/zfjd-refactoring-v1/009-修改执法码相关的表.sql +++ b/script/sql/zfjd-refactoring-v1/009-修改执法码相关的表.sql @@ -12,3 +12,6 @@ ALTER TABLE zdxt_file ADD COLUMN create_dept varchar(100) NULL COMMENT '创建部门' AFTER sass_id, ADD COLUMN create_by varchar(50) NULL COMMENT '创建者' AFTER create_dept, ADD COLUMN update_by varchar(50) NULL COMMENT '更新者' AFTER create_by + +ALTER TABLE zdxt_file + ADD COLUMN create_time datetime NULL COMMENT '创建时间' AFTER create_by diff --git a/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/QrCodeUtilTest.java b/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/QrCodeUtilTest.java new file mode 100644 index 0000000..e020fb0 --- /dev/null +++ b/zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/QrCodeUtilTest.java @@ -0,0 +1,304 @@ +package org.dromara.test; + +import com.zdxt.enforcementcode.domain.vo.QrCodeGenerateHistoryVo; +import com.zdxt.enforcementcode.util.QrCodeUtil; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.context.SpringBootTest; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * QrCodeUtil 二维码工具类测试用例 + *

+ * 分为两部分: + * 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 list = ZdxtFileUtil.queryByBusinessId(TEST_BUSINESS_ID); + Assertions.assertNotNull(list, "查询结果不应为 null"); + System.out.println("按业务ID查询到附件数: " + list.size()); + list.forEach(vo -> System.out.println(" - guid=" + vo.getGuid() + ", fileName=" + vo.getFileName())); + } + + @DisplayName("根据业务ID和分类代码查询附件列表") + @Test + @Order(2) + public void testQueryByBusinessIdAndSortCode() { + List list = ZdxtFileUtil.queryByBusinessIdAndSortCode(TEST_BUSINESS_ID, "TEST_SORT"); + Assertions.assertNotNull(list, "查询结果不应为 null"); + System.out.println("按业务ID+分类代码查询到附件数: " + list.size()); + } + + @DisplayName("根据附件ID查询 - 存在的记录") + @Test + @Order(2) + public void testQueryByGuid() { + if (uploadedGuid == null) { + System.out.println("跳过: 依赖上传测试先执行"); + return; + } + ZdxtFileVo vo = ZdxtFileUtil.queryByGuid(uploadedGuid); + Assertions.assertNotNull(vo, "按 guid 查询结果不应为 null"); + System.out.println("按guid查询成功: " + vo.getFileName()); + } + + @DisplayName("查询空业务ID应返回空列表") + @Test + public void testQueryByBlankBusinessId() { + List list = ZdxtFileUtil.queryByBusinessId(""); + Assertions.assertNotNull(list); + Assertions.assertTrue(list.isEmpty(), "空业务ID应返回空列表"); + + List nullList = ZdxtFileUtil.queryByBusinessId(null); + Assertions.assertNotNull(nullList); + Assertions.assertTrue(nullList.isEmpty(), "null业务ID应返回空列表"); + System.out.println("空业务ID查询验证通过"); + } + + @DisplayName("查询空guid应返回null") + @Test + public void testQueryByBlankGuid() { + Assertions.assertNull(ZdxtFileUtil.queryByGuid("")); + Assertions.assertNull(ZdxtFileUtil.queryByGuid(null)); + System.out.println("空guid查询验证通过"); + } + + // ==================== 删除测试 ==================== + + @DisplayName("删除空guid应返回false") + @Test + public void testDeleteByBlankGuid() { + Assertions.assertFalse(ZdxtFileUtil.deleteByGuid("")); + Assertions.assertFalse(ZdxtFileUtil.deleteByGuid(null)); + System.out.println("空guid删除验证通过"); + } + + @DisplayName("删除空业务ID应返回false") + @Test + public void testDeleteByBlankBusinessId() { + Assertions.assertFalse(ZdxtFileUtil.deleteByBusinessId("")); + Assertions.assertFalse(ZdxtFileUtil.deleteByBusinessId(null)); + System.out.println("空业务ID删除验证通过"); + } + + @DisplayName("按业务ID删除所有测试附件") + @Test + @Order(3) + public void testDeleteByBusinessId() { + // 先确认有数据 + List before = ZdxtFileUtil.queryByBusinessId(TEST_BUSINESS_ID); + System.out.println("删除前附件数: " + before.size()); + + Boolean result = ZdxtFileUtil.deleteByBusinessId(TEST_BUSINESS_ID); + System.out.println("按业务ID删除结果: " + result); + + List after = ZdxtFileUtil.queryByBusinessId(TEST_BUSINESS_ID); + System.out.println("删除后附件数: " + after.size()); + } + + // ==================== 批量上传测试 ==================== + + @DisplayName("批量上传文件") + @Test + public void testUploadFiles() { + MockMultipartFile file1 = new MockMultipartFile( + "files", "batch1.txt", "text/plain", "content1".getBytes() + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", "batch2.txt", "text/plain", "content2".getBytes() + ); + + // 批量上传需要登录用户, 使用手动用户方式逐个验证 + ZdxtFileVo vo1 = ZdxtFileUtil.uploadFile(file1, "TEST_BATCH_BIZ", null, "user_batch", "批量用户"); + ZdxtFileVo vo2 = ZdxtFileUtil.uploadFile(file2, "TEST_BATCH_BIZ", null, "user_batch", "批量用户"); + + Assertions.assertNotNull(vo1); + Assertions.assertNotNull(vo2); + System.out.println("批量上传成功: " + vo1.getGuid() + ", " + vo2.getGuid()); + + // 清理 + ZdxtFileUtil.deleteByBusinessId("TEST_BATCH_BIZ"); + System.out.println("批量测试数据已清理"); + } +}