feat(puzzle): 优化拼图生成逻辑并新增完整测试套件

- 在PuzzleGenerateServiceImpl中改进图片上传逻辑,支持contentType指定
- 在PuzzleImageRenderer中优化背景图片缩放算法,使用原生Java方法提升性能
- 修改scaleImage方法实现,完善多种图片适配模式(COVER、CONTAIN、FILL等)
- 新增PuzzleRealScenarioIntegrationTest集成测试类,覆盖真实业务场景
- 添加PuzzleTemplateServiceImplTest单元测试,使用Mockito模拟数据库交互
- 创建MockImageUtil工具类,支持测试过程中生成各类模拟图片
- 构建PuzzleTestDataBuilder测试数据构造器,简化测试模板和元素创建
- 增加RealScenarioTestHelper辅助类,提升测试代码复用性
-
This commit is contained in:
2025-11-17 16:50:53 +08:00
parent 443f92ff92
commit e2b450682b
7 changed files with 1198 additions and 18 deletions

View File

@@ -19,6 +19,7 @@ import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Comparator;
@@ -148,15 +149,16 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
byte[] imageBytes = baos.toByteArray();
// 生成文件名
String fileName = String.format("puzzle/%s/%s.%s",
templateCode,
String fileName = String.format("%s.%s",
UUID.randomUUID().toString().replace("-", ""),
outputFormat.toLowerCase()
);
// 使用项目现有的存储工厂上传
// 使用项目现有的存储工厂上传(转换为InputStream)
try {
return StorageFactory.use().uploadFile(imageBytes, "puzzle", fileName);
ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
String contentType = "PNG".equals(outputFormat) ? "image/png" : "image/jpeg";
return StorageFactory.use().uploadFile(contentType, inputStream, "puzzle", templateCode, fileName);
} catch (Exception e) {
log.error("上传图片失败: fileName={}", fileName, e);
throw new IOException("图片上传失败", e);

View File

@@ -109,7 +109,7 @@ public class PuzzleImageRenderer {
// 图片背景
try {
BufferedImage bgImage = downloadImage(template.getBackgroundImage());
BufferedImage scaledBg = ImgUtil.scale(bgImage, template.getCanvasWidth(), template.getCanvasHeight());
Image scaledBg = bgImage.getScaledInstance(template.getCanvasWidth(), template.getCanvasHeight(), Image.SCALE_SMOOTH);
g2d.drawImage(scaledBg, 0, 0, null);
} catch (Exception e) {
log.error("绘制背景图片失败: {}", template.getBackgroundImage(), e);
@@ -151,8 +151,8 @@ public class PuzzleImageRenderer {
drawRoundedImage(g2d, scaledImage, element.getXPosition(), element.getYPosition(),
element.getWidth(), element.getHeight(), borderRadius);
} else {
g2d.drawImage(scaledImage, element.getXPosition(), element.getYPosition(),
element.getWidth(), element.getHeight(), null);
// 直接绘制缩放后的图片,不再进行二次缩放
g2d.drawImage(scaledImage, element.getXPosition(), element.getYPosition(), null);
}
// 恢复透明度
@@ -167,28 +167,82 @@ public class PuzzleImageRenderer {
* 缩放图片
*/
private BufferedImage scaleImage(BufferedImage source, PuzzleElementEntity element) {
String fitMode = StrUtil.isNotBlank(element.getImageFitMode()) ? element.getImageFitMode() : "CONTAIN";
String fitMode = StrUtil.isNotBlank(element.getImageFitMode()) ? element.getImageFitMode() : "FILL";
int targetWidth = element.getWidth();
int targetHeight = element.getHeight();
switch (fitMode) {
case "COVER":
// 等比缩放填充(可能裁剪)
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
case "FILL":
// 拉伸填充
return ImgUtil.scale(source, element.getWidth(), element.getHeight());
// 等比缩放填充(可能裁剪)- 使用原生Java缩放
return scaleImageKeepRatio(source, targetWidth, targetHeight, true);
case "CONTAIN":
// 等比缩放适应
return scaleImageKeepRatio(source, targetWidth, targetHeight, false);
case "SCALE_DOWN":
// 缩小适应(不放大)
if (source.getWidth() <= element.getWidth() && source.getHeight() <= element.getHeight()) {
if (source.getWidth() <= targetWidth && source.getHeight() <= targetHeight) {
return source;
}
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
case "CONTAIN":
return scaleImageKeepRatio(source, targetWidth, targetHeight, false);
case "FILL":
default:
// 等比缩放适应
return ImgUtil.scale(source, element.getWidth(), element.getHeight(), Color.TRANSPARENT);
// 拉伸填充到目标尺寸
BufferedImage scaled = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.drawImage(source, 0, 0, targetWidth, targetHeight, null);
g.dispose();
return scaled;
}
}
/**
* 等比缩放图片
*/
private BufferedImage scaleImageKeepRatio(BufferedImage source, int targetWidth, int targetHeight, boolean cover) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
double widthRatio = (double) targetWidth / sourceWidth;
double heightRatio = (double) targetHeight / sourceHeight;
// cover模式使用较大的比例(填充),contain模式使用较小的比例(适应)
double ratio = cover ? Math.max(widthRatio, heightRatio) : Math.min(widthRatio, heightRatio);
int scaledWidth = (int) (sourceWidth * ratio);
int scaledHeight = (int) (sourceHeight * ratio);
// 创建目标尺寸的画布
BufferedImage result = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = result.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// 居中绘制缩放后的图片
int x = (targetWidth - scaledWidth) / 2;
int y = (targetHeight - scaledHeight) / 2;
g.drawImage(source, x, y, scaledWidth, scaledHeight, null);
g.dispose();
return result;
}
/**
* 将Image转换为BufferedImage(已废弃,改用直接绘制)
*/
@Deprecated
private BufferedImage toBufferedImage(Image image, int width, int height) {
BufferedImage buffered = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = buffered.createGraphics();
// 居中绘制
int x = (width - image.getWidth(null)) / 2;
int y = (height - image.getHeight(null)) / 2;
g.drawImage(image, x, y, null);
g.dispose();
return buffered;
}
/**
* 绘制圆角图片
*/