Browse Source

重写接口第三天

master
庄锐波 5 months ago
parent
commit
de29d38f81
  1. 482
      ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/QrCodeUtil.java
  2. 476
      ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/util/ZdxtFileUtil.java
  3. BIN
      ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_bg.jpg
  4. BIN
      ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_logo.jpg
  5. 3
      script/sql/zfjd-refactoring-v1/009-修改执法码相关的表.sql
  6. 304
      zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/QrCodeUtilTest.java
  7. 194
      zdxt-web-server/zdxt-admin-web-server/src/test/java/org/dromara/test/ZdxtFileUtilTest.java

482
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;
/**
* 二维码生成工具类
* <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);
}
}

476
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;
/**
* 附件工具类
* <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());
}
}
}

BIN
ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

BIN
ruoyi-modules/zdxt-enforcement-code/src/main/resources/images/enterprise_pub_qrcode_logo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

3
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

304
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 二维码工具类测试用例
* <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();
}
}
}

194
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<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…
Cancel
Save