7 changed files with 1459 additions and 0 deletions
@ -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; |
|||
|
|||
/** |
|||
* 二维码生成工具类 |
|||
* <p> |
|||
* 提供二维码的生成、解析、本地存储以及 OSS 上传功能。 |
|||
* 生成后自动保存附件记录(zdxt_file 表)和二维码生成历史(qr_code_generate_history 表)。 |
|||
* 路径处理兼容 Windows 和 Linux。 |
|||
* </p> |
|||
* |
|||
* @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<DecodeHintType, Object> 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); |
|||
} |
|||
|
|||
/** |
|||
* 获取跨平台默认存储路径 |
|||
* <p> |
|||
* 使用系统临时目录,保证在 Windows 和 Linux 下都能正确运行: |
|||
* <ul> |
|||
* <li>Windows: C:\Users\xxx\AppData\Local\Temp\zdxt\qrcode</li> |
|||
* <li>Linux: /tmp/zdxt/qrcode</li> |
|||
* </ul> |
|||
* </p> |
|||
* |
|||
* @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<EncodeHintType, Object> 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); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
/** |
|||
* 附件工具类 |
|||
* <p> |
|||
* 封装文件本地存储 + 附件记录(zdxt_file 表)的统一操作, |
|||
* 独立于 RuoYi OSS,文件直接保存到本地磁盘。 |
|||
* 供业务模块直接调用,简化文件处理流程。 |
|||
* </p> |
|||
* <p> |
|||
* 配置项(application.yml): |
|||
* <pre> |
|||
* zdxt: |
|||
* file: |
|||
* upload-path: D:/zdxt/upload # 文件存储根路径(Windows 示例) |
|||
* url-prefix: /upload # 文件访问 URL 前缀 |
|||
* </pre> |
|||
* </p> |
|||
* |
|||
* @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<ZdxtFileVo> uploadFiles(MultipartFile[] files, String businessId) { |
|||
return uploadFiles(files, businessId, null); |
|||
} |
|||
|
|||
/** |
|||
* 批量上传文件,带分类代码 |
|||
* |
|||
* @param files 文件数组 |
|||
* @param businessId 业务ID |
|||
* @param sortCode 分类代码 |
|||
* @return 附件视图对象列表 |
|||
*/ |
|||
public static List<ZdxtFileVo> 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<ZdxtFileVo> 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<ZdxtFileVo> 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<String> 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<ZdxtFileVo> files = queryByBusinessId(businessId); |
|||
if (files.isEmpty()) { |
|||
return true; |
|||
} |
|||
// 删除本地文件
|
|||
files.forEach(f -> deleteLocalFileByPath(f.getUploadPath())); |
|||
List<String> 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()); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 11 KiB |
@ -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 二维码工具类测试用例 |
|||
* <p> |
|||
* 分为两部分: |
|||
* 1. 纯图片生成测试(不依赖 OSS/DB,可独立运行) |
|||
* 2. 生成+保存集成测试(需要 Spring 上下文、OSS 和数据库) |
|||
* </p> |
|||
* |
|||
* @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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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<ZdxtFileVo> 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<ZdxtFileVo> 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<ZdxtFileVo> list = ZdxtFileUtil.queryByBusinessId(""); |
|||
Assertions.assertNotNull(list); |
|||
Assertions.assertTrue(list.isEmpty(), "空业务ID应返回空列表"); |
|||
|
|||
List<ZdxtFileVo> 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<ZdxtFileVo> before = ZdxtFileUtil.queryByBusinessId(TEST_BUSINESS_ID); |
|||
System.out.println("删除前附件数: " + before.size()); |
|||
|
|||
Boolean result = ZdxtFileUtil.deleteByBusinessId(TEST_BUSINESS_ID); |
|||
System.out.println("按业务ID删除结果: " + result); |
|||
|
|||
List<ZdxtFileVo> 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("批量测试数据已清理"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue