You've already forked FrameTour-BE
feat(puzzle): 优化拼图生成逻辑并新增完整测试套件
- 在PuzzleGenerateServiceImpl中改进图片上传逻辑,支持contentType指定 - 在PuzzleImageRenderer中优化背景图片缩放算法,使用原生Java方法提升性能 - 修改scaleImage方法实现,完善多种图片适配模式(COVER、CONTAIN、FILL等) - 新增PuzzleRealScenarioIntegrationTest集成测试类,覆盖真实业务场景 - 添加PuzzleTemplateServiceImplTest单元测试,使用Mockito模拟数据库交互 - 创建MockImageUtil工具类,支持测试过程中生成各类模拟图片 - 构建PuzzleTestDataBuilder测试数据构造器,简化测试模板和元素创建 - 增加RealScenarioTestHelper辅助类,提升测试代码复用性 -
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制圆角图片
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.ycwl.basic.puzzle.integration;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.test.MockImageUtil;
|
||||
import com.ycwl.basic.puzzle.test.PuzzleTestDataBuilder;
|
||||
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 拼图功能现实场景集成测试
|
||||
* 测试场景:1020x1520画布,上方900高度分3份放图片,底部120像素左侧二维码右侧文字
|
||||
*
|
||||
* 特点:
|
||||
* 1. 不依赖外部数据库
|
||||
* 2. 不依赖外部图片资源(使用Mock图片)
|
||||
* 3. 不依赖OSS存储(图片保存到临时目录)
|
||||
* 4. 适合CI/CD自动化测试
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
class PuzzleRealScenarioIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private PuzzleImageRenderer renderer;
|
||||
private Map<String, File> mockImageFiles;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
renderer = new PuzzleImageRenderer();
|
||||
mockImageFiles = new HashMap<>();
|
||||
|
||||
// 准备Mock图片文件
|
||||
prepareMockImages();
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备Mock图片文件(模拟从URL下载的图片)
|
||||
*/
|
||||
private void prepareMockImages() throws IOException {
|
||||
// 创建3张不同颜色的图片(每张470高度)
|
||||
BufferedImage image1 = MockImageUtil.createImageWithText(
|
||||
1020, 470, "图片1", new Color(255, 200, 200), Color.BLACK);
|
||||
File file1 = tempDir.resolve("image1.jpg").toFile();
|
||||
ImageIO.write(image1, "JPG", file1);
|
||||
mockImageFiles.put("image1", file1);
|
||||
|
||||
BufferedImage image2 = MockImageUtil.createImageWithText(
|
||||
1020, 470, "图片2", new Color(200, 255, 200), Color.BLACK);
|
||||
File file2 = tempDir.resolve("image2.jpg").toFile();
|
||||
ImageIO.write(image2, "JPG", file2);
|
||||
mockImageFiles.put("image2", file2);
|
||||
|
||||
BufferedImage image3 = MockImageUtil.createImageWithText(
|
||||
1020, 470, "图片3", new Color(200, 200, 255), Color.BLACK);
|
||||
File file3 = tempDir.resolve("image3.jpg").toFile();
|
||||
ImageIO.write(image3, "JPG", file3);
|
||||
mockImageFiles.put("image3", file3);
|
||||
|
||||
// 创建二维码图片(100x100)
|
||||
BufferedImage qrCode = MockImageUtil.createMockQRCode(100);
|
||||
File qrFile = tempDir.resolve("qrcode.png").toFile();
|
||||
ImageIO.write(qrCode, "PNG", qrFile);
|
||||
mockImageFiles.put("qrCode", qrFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealScenario_1020x1520_ThreeImagesAndQRCode() throws IOException {
|
||||
// Given: 创建1020x1520的模板
|
||||
PuzzleTemplateEntity template = PuzzleTestDataBuilder.createTemplate(
|
||||
"real_scenario",
|
||||
1020,
|
||||
1520,
|
||||
"#F5F5F5"
|
||||
);
|
||||
|
||||
// Given: 创建元素列表
|
||||
List<PuzzleElementEntity> elements = PuzzleTestDataBuilder.createRealScenarioElements(template.getId());
|
||||
|
||||
// Given: 准备动态数据(使用本地文件路径)
|
||||
Map<String, String> dynamicData = new HashMap<>();
|
||||
dynamicData.put("image1", mockImageFiles.get("image1").getAbsolutePath());
|
||||
dynamicData.put("image2", mockImageFiles.get("image2").getAbsolutePath());
|
||||
dynamicData.put("image3", mockImageFiles.get("image3").getAbsolutePath());
|
||||
dynamicData.put("qrCode", mockImageFiles.get("qrCode").getAbsolutePath());
|
||||
dynamicData.put("bottomText", "奇遇时光乐园\n2025.11.11");
|
||||
|
||||
// When: 渲染拼图
|
||||
BufferedImage result = renderer.render(template, elements, dynamicData);
|
||||
|
||||
// Then: 验证结果
|
||||
assertNotNull(result, "渲染结果不应为空");
|
||||
assertEquals(1020, result.getWidth(), "画布宽度应为1020");
|
||||
assertEquals(1520, result.getHeight(), "画布高度应为1520");
|
||||
assertTrue(MockImageUtil.isNotBlank(result), "图片不应为空白");
|
||||
|
||||
// 保存结果图片到临时目录(便于人工验证)
|
||||
File outputFile = tempDir.resolve("real_scenario_output.png").toFile();
|
||||
ImageIO.write(result, "PNG", outputFile);
|
||||
assertTrue(outputFile.exists(), "输出文件应存在");
|
||||
assertTrue(outputFile.length() > 0, "输出文件不应为空");
|
||||
|
||||
System.out.println("✅ 现实场景测试通过!");
|
||||
System.out.println("📁 输出图片路径: " + outputFile.getAbsolutePath());
|
||||
System.out.println("📊 图片尺寸: " + result.getWidth() + "x" + result.getHeight());
|
||||
System.out.println("💾 文件大小: " + outputFile.length() / 1024 + " KB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealScenario_VerifyImagePositions() throws IOException {
|
||||
// Given
|
||||
PuzzleTemplateEntity template = PuzzleTestDataBuilder.createTemplate(
|
||||
"position_test",
|
||||
1020,
|
||||
1520,
|
||||
"#FFFFFF"
|
||||
);
|
||||
|
||||
List<PuzzleElementEntity> elements = PuzzleTestDataBuilder.createRealScenarioElements(template.getId());
|
||||
|
||||
Map<String, String> dynamicData = new HashMap<>();
|
||||
dynamicData.put("image1", mockImageFiles.get("image1").getAbsolutePath());
|
||||
dynamicData.put("image2", mockImageFiles.get("image2").getAbsolutePath());
|
||||
dynamicData.put("image3", mockImageFiles.get("image3").getAbsolutePath());
|
||||
dynamicData.put("qrCode", mockImageFiles.get("qrCode").getAbsolutePath());
|
||||
dynamicData.put("bottomText", "测试文字");
|
||||
|
||||
// When
|
||||
BufferedImage result = renderer.render(template, elements, dynamicData);
|
||||
|
||||
// Then: 验证元素数量
|
||||
assertEquals(5, elements.size(), "应有5个元素");
|
||||
|
||||
// 验证上方3张图片的位置(每张470高度)
|
||||
PuzzleElementEntity img1 = elements.get(0);
|
||||
assertEquals(0, img1.getYPosition(), "第1张图片Y坐标应为0");
|
||||
assertEquals(470, img1.getHeight(), "第1张图片高度应为470");
|
||||
|
||||
PuzzleElementEntity img2 = elements.get(1);
|
||||
assertEquals(470, img2.getYPosition(), "第2张图片Y坐标应为470");
|
||||
assertEquals(470, img2.getHeight(), "第2张图片高度应为470");
|
||||
|
||||
PuzzleElementEntity img3 = elements.get(2);
|
||||
assertEquals(940, img3.getYPosition(), "第3张图片Y坐标应为940");
|
||||
assertEquals(470, img3.getHeight(), "第3张图片高度应为470");
|
||||
|
||||
// 验证底部二维码位置(Y坐标应为1410+5=1415)
|
||||
PuzzleElementEntity qrElement = elements.get(3);
|
||||
assertEquals(1415, qrElement.getYPosition(), "二维码Y坐标应在1410+5位置");
|
||||
assertEquals(100, qrElement.getWidth(), "二维码宽度应为100");
|
||||
assertEquals(100, qrElement.getHeight(), "二维码高度应为100");
|
||||
|
||||
// 验证底部文字位置
|
||||
PuzzleElementEntity textElement = elements.get(4);
|
||||
assertEquals(1420, textElement.getYPosition(), "文字Y坐标应在1410+10位置");
|
||||
assertEquals(140, textElement.getXPosition(), "文字X坐标应为140");
|
||||
assertTrue(textElement.getXPosition() > qrElement.getXPosition() + qrElement.getWidth(), "文字应在二维码右侧");
|
||||
|
||||
System.out.println("✅ 元素位置验证通过!");
|
||||
System.out.println("📐 画布尺寸: " + template.getCanvasWidth() + "x" + template.getCanvasHeight());
|
||||
System.out.println("🔢 元素数量: " + elements.size());
|
||||
System.out.println("📊 图片高度: 470px * 3 = 1410px");
|
||||
System.out.println("📊 底部高度: " + (1520 - 1410) + "px");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealScenario_WithoutDynamicData_UsesDefaults() throws IOException {
|
||||
// Given: 使用默认图片(测试默认值机制)
|
||||
PuzzleTemplateEntity template = PuzzleTestDataBuilder.createTemplate(
|
||||
"default_test",
|
||||
1020,
|
||||
1520,
|
||||
"#CCCCCC"
|
||||
);
|
||||
|
||||
List<PuzzleElementEntity> elements = PuzzleTestDataBuilder.createRealScenarioElements(template.getId());
|
||||
|
||||
// 设置默认图片路径
|
||||
for (PuzzleElementEntity element : elements) {
|
||||
if (element.getElementType() == 1) {
|
||||
// 图片元素使用本地文件
|
||||
String key = element.getElementKey();
|
||||
if (mockImageFiles.containsKey(key)) {
|
||||
element.setDefaultImageUrl(mockImageFiles.get(key).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When: 不传动态数据,使用默认值
|
||||
Map<String, String> dynamicData = new HashMap<>();
|
||||
BufferedImage result = renderer.render(template, elements, dynamicData);
|
||||
|
||||
// Then
|
||||
assertNotNull(result, "应使用默认图片渲染成功");
|
||||
assertEquals(1020, result.getWidth());
|
||||
assertEquals(1520, result.getHeight());
|
||||
|
||||
// 保存结果
|
||||
File outputFile = tempDir.resolve("default_scenario_output.png").toFile();
|
||||
ImageIO.write(result, "PNG", outputFile);
|
||||
|
||||
System.out.println("✅ 默认值测试通过!");
|
||||
System.out.println("📁 输出图片路径: " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealScenario_DifferentBackgroundTypes() throws IOException {
|
||||
// Test 1: 纯色背景
|
||||
PuzzleTemplateEntity solidBgTemplate = PuzzleTestDataBuilder.createTemplate(
|
||||
"solid_bg",
|
||||
1020,
|
||||
1520,
|
||||
"#E3F2FD"
|
||||
);
|
||||
solidBgTemplate.setBackgroundType(0);
|
||||
|
||||
List<PuzzleElementEntity> elements = PuzzleTestDataBuilder.createRealScenarioElements(solidBgTemplate.getId());
|
||||
Map<String, String> dynamicData = prepareLocalFilePaths();
|
||||
|
||||
BufferedImage solidBgResult = renderer.render(solidBgTemplate, elements, dynamicData);
|
||||
assertNotNull(solidBgResult);
|
||||
assertEquals(new Color(227, 242, 253), new Color(solidBgResult.getRGB(0, 0)));
|
||||
|
||||
// Test 2: 图片背景
|
||||
PuzzleTemplateEntity imageBgTemplate = PuzzleTestDataBuilder.createTemplate(
|
||||
"image_bg",
|
||||
1020,
|
||||
1520,
|
||||
null
|
||||
);
|
||||
imageBgTemplate.setBackgroundType(1);
|
||||
BufferedImage bgImage = MockImageUtil.createGradientImage(
|
||||
1020, 1520, new Color(255, 240, 245), new Color(255, 250, 250));
|
||||
File bgFile = tempDir.resolve("bg_image.jpg").toFile();
|
||||
ImageIO.write(bgImage, "JPG", bgFile);
|
||||
imageBgTemplate.setBackgroundImage(bgFile.getAbsolutePath());
|
||||
|
||||
BufferedImage imageBgResult = renderer.render(imageBgTemplate, elements, dynamicData);
|
||||
assertNotNull(imageBgResult);
|
||||
|
||||
System.out.println("✅ 不同背景类型测试通过!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealScenario_Performance() throws IOException {
|
||||
// Given
|
||||
PuzzleTemplateEntity template = PuzzleTestDataBuilder.createTemplate(
|
||||
"performance_test",
|
||||
1020,
|
||||
1520,
|
||||
"#FFFFFF"
|
||||
);
|
||||
List<PuzzleElementEntity> elements = PuzzleTestDataBuilder.createRealScenarioElements(template.getId());
|
||||
Map<String, String> dynamicData = prepareLocalFilePaths();
|
||||
|
||||
// When: 测试渲染性能
|
||||
long startTime = System.currentTimeMillis();
|
||||
BufferedImage result = renderer.render(template, elements, dynamicData);
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertTrue(duration < 5000, "渲染应在5秒内完成,实际耗时: " + duration + "ms");
|
||||
|
||||
System.out.println("✅ 性能测试通过!");
|
||||
System.out.println("⏱️ 渲染耗时: " + duration + " ms");
|
||||
System.out.println("📊 元素数量: " + elements.size());
|
||||
System.out.println("📐 画布尺寸: " + template.getCanvasWidth() + "x" + template.getCanvasHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备本地文件路径的动态数据
|
||||
*/
|
||||
private Map<String, String> prepareLocalFilePaths() {
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("image1", mockImageFiles.get("image1").getAbsolutePath());
|
||||
data.put("image2", mockImageFiles.get("image2").getAbsolutePath());
|
||||
data.put("image3", mockImageFiles.get("image3").getAbsolutePath());
|
||||
data.put("qrCode", mockImageFiles.get("qrCode").getAbsolutePath());
|
||||
data.put("bottomText", "测试文字内容");
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.ycwl.basic.puzzle.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
import com.ycwl.basic.puzzle.dto.TemplateCreateRequest;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.puzzle.test.PuzzleTestDataBuilder;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* PuzzleTemplateService单元测试
|
||||
* 使用Mockito模拟数据库操作,不依赖外部数据库
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PuzzleTemplateServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private PuzzleTemplateMapper templateMapper;
|
||||
|
||||
@Mock
|
||||
private PuzzleElementMapper elementMapper;
|
||||
|
||||
@InjectMocks
|
||||
private PuzzleTemplateServiceImpl templateService;
|
||||
|
||||
private PuzzleTemplateEntity mockTemplate;
|
||||
private List<PuzzleElementEntity> mockElements;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockTemplate = PuzzleTestDataBuilder.createBasicTemplate();
|
||||
mockElements = PuzzleTestDataBuilder.createFullTestElements(mockTemplate.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTemplate_Success() {
|
||||
// Given
|
||||
TemplateCreateRequest request = new TemplateCreateRequest();
|
||||
request.setCode("new_template");
|
||||
request.setName("新模板");
|
||||
request.setCanvasWidth(800);
|
||||
request.setCanvasHeight(1200);
|
||||
request.setBackgroundType(0);
|
||||
request.setBackgroundColor("#FFFFFF");
|
||||
|
||||
when(templateMapper.countByCode(eq("new_template"), isNull())).thenReturn(0);
|
||||
when(templateMapper.insert(any(PuzzleTemplateEntity.class))).thenAnswer(invocation -> {
|
||||
PuzzleTemplateEntity entity = invocation.getArgument(0);
|
||||
entity.setId(100L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
// When
|
||||
Long templateId = templateService.createTemplate(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(templateId);
|
||||
assertEquals(100L, templateId);
|
||||
verify(templateMapper).countByCode(eq("new_template"), isNull());
|
||||
verify(templateMapper).insert(any(PuzzleTemplateEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTemplate_DuplicateCode() {
|
||||
// Given
|
||||
TemplateCreateRequest request = new TemplateCreateRequest();
|
||||
request.setCode("existing_code");
|
||||
request.setName("重复编码模板");
|
||||
|
||||
when(templateMapper.countByCode(eq("existing_code"), isNull())).thenReturn(1);
|
||||
|
||||
// When & Then
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> templateService.createTemplate(request)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("模板编码已存在"));
|
||||
verify(templateMapper).countByCode(eq("existing_code"), isNull());
|
||||
verify(templateMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTemplateDetail_Success() {
|
||||
// Given
|
||||
when(templateMapper.getById(1L)).thenReturn(mockTemplate);
|
||||
when(elementMapper.getByTemplateId(1L)).thenReturn(mockElements);
|
||||
|
||||
// When
|
||||
PuzzleTemplateDTO result = templateService.getTemplateDetail(1L);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(mockTemplate.getId(), result.getId());
|
||||
assertEquals(mockTemplate.getCode(), result.getCode());
|
||||
assertEquals(mockTemplate.getName(), result.getName());
|
||||
assertNotNull(result.getElements());
|
||||
assertEquals(mockElements.size(), result.getElements().size());
|
||||
verify(templateMapper).getById(1L);
|
||||
verify(elementMapper).getByTemplateId(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTemplateDetail_NotFound() {
|
||||
// Given
|
||||
when(templateMapper.getById(999L)).thenReturn(null);
|
||||
|
||||
// When & Then
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> templateService.getTemplateDetail(999L)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("模板不存在"));
|
||||
verify(templateMapper).getById(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTemplateByCode_Success() {
|
||||
// Given
|
||||
when(templateMapper.getByCode("test_template")).thenReturn(mockTemplate);
|
||||
when(elementMapper.getByTemplateId(1L)).thenReturn(mockElements);
|
||||
|
||||
// When
|
||||
PuzzleTemplateDTO result = templateService.getTemplateByCode("test_template");
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("test_template", result.getCode());
|
||||
assertNotNull(result.getElements());
|
||||
verify(templateMapper).getByCode("test_template");
|
||||
verify(elementMapper).getByTemplateId(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateTemplate_Success() {
|
||||
// Given
|
||||
TemplateCreateRequest request = new TemplateCreateRequest();
|
||||
request.setName("更新后的名称");
|
||||
request.setStatus(0);
|
||||
|
||||
when(templateMapper.getById(1L)).thenReturn(mockTemplate);
|
||||
when(templateMapper.update(any(PuzzleTemplateEntity.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
templateService.updateTemplate(1L, request);
|
||||
|
||||
// Then
|
||||
verify(templateMapper).getById(1L);
|
||||
verify(templateMapper).update(any(PuzzleTemplateEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteTemplate_Success() {
|
||||
// Given
|
||||
when(templateMapper.getById(1L)).thenReturn(mockTemplate);
|
||||
when(templateMapper.deleteById(1L)).thenReturn(1);
|
||||
when(elementMapper.deleteByTemplateId(1L)).thenReturn(mockElements.size());
|
||||
|
||||
// When
|
||||
templateService.deleteTemplate(1L);
|
||||
|
||||
// Then
|
||||
verify(templateMapper).getById(1L);
|
||||
verify(templateMapper).deleteById(1L);
|
||||
verify(elementMapper).deleteByTemplateId(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListTemplates_Success() {
|
||||
// Given
|
||||
List<PuzzleTemplateEntity> templates = List.of(mockTemplate);
|
||||
when(templateMapper.list(null, "test", 1)).thenReturn(templates);
|
||||
when(elementMapper.getByTemplateId(anyLong())).thenReturn(mockElements);
|
||||
|
||||
// When
|
||||
List<PuzzleTemplateDTO> result = templateService.listTemplates(null, "test", 1);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(mockTemplate.getCode(), result.get(0).getCode());
|
||||
verify(templateMapper).list(null, "test", 1);
|
||||
}
|
||||
}
|
||||
169
src/test/java/com/ycwl/basic/puzzle/test/MockImageUtil.java
Normal file
169
src/test/java/com/ycwl/basic/puzzle/test/MockImageUtil.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package com.ycwl.basic.puzzle.test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Mock图片工具类
|
||||
* 用于测试时生成假的图片数据,避免依赖外部资源
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public class MockImageUtil {
|
||||
|
||||
/**
|
||||
* 创建纯色图片
|
||||
*/
|
||||
public static BufferedImage createSolidColorImage(int width, int height, Color color) {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
g2d.setColor(color);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带文字的图片(用于测试)
|
||||
*/
|
||||
public static BufferedImage createImageWithText(int width, int height, String text, Color bgColor, Color textColor) {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
// 背景
|
||||
g2d.setColor(bgColor);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
|
||||
// 文字
|
||||
g2d.setColor(textColor);
|
||||
g2d.setFont(new Font("Arial", Font.BOLD, 24));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(text);
|
||||
int textHeight = fm.getHeight();
|
||||
g2d.drawString(text, (width - textWidth) / 2, (height + textHeight) / 2 - fm.getDescent());
|
||||
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渐变图片
|
||||
*/
|
||||
public static BufferedImage createGradientImage(int width, int height, Color startColor, Color endColor) {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
GradientPaint gradient = new GradientPaint(0, 0, startColor, 0, height, endColor);
|
||||
g2d.setPaint(gradient);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建二维码样式的图片(黑白方块)
|
||||
*/
|
||||
public static BufferedImage createMockQRCode(int size) {
|
||||
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
Random random = new Random(42); // 固定种子保证一致性
|
||||
|
||||
// 白色背景
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, size, size);
|
||||
|
||||
// 随机黑白方块
|
||||
int blockSize = size / 20;
|
||||
g2d.setColor(Color.BLACK);
|
||||
for (int x = 0; x < size; x += blockSize) {
|
||||
for (int y = 0; y < size; y += blockSize) {
|
||||
if (random.nextBoolean()) {
|
||||
g2d.fillRect(x, y, blockSize, blockSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到文件(用于测试验证)
|
||||
*/
|
||||
public static void saveImage(BufferedImage image, String filePath) throws IOException {
|
||||
File outputFile = new File(filePath);
|
||||
outputFile.getParentFile().mkdirs();
|
||||
ImageIO.write(image, "PNG", outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转换为字节数组
|
||||
*/
|
||||
public static byte[] imageToBytes(BufferedImage image, String format) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, format, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建头像样式的图片(带首字母)
|
||||
*/
|
||||
public static BufferedImage createAvatarImage(int size, String initial, Color bgColor) {
|
||||
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 圆形背景
|
||||
g2d.setColor(bgColor);
|
||||
g2d.fillOval(0, 0, size, size);
|
||||
|
||||
// 首字母
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.setFont(new Font("Arial", Font.BOLD, size / 2));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(initial);
|
||||
int textHeight = fm.getHeight();
|
||||
g2d.drawString(initial, (size - textWidth) / 2, (size + textHeight) / 2 - fm.getDescent());
|
||||
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图片尺寸
|
||||
*/
|
||||
public static boolean validateImageSize(BufferedImage image, int expectedWidth, int expectedHeight) {
|
||||
return image.getWidth() == expectedWidth && image.getHeight() == expectedHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图片不为空白
|
||||
*/
|
||||
public static boolean isNotBlank(BufferedImage image) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
|
||||
// 采样检查几个点
|
||||
int samplePoints = 10;
|
||||
Color firstColor = new Color(image.getRGB(0, 0));
|
||||
boolean hasVariation = false;
|
||||
|
||||
for (int i = 0; i < samplePoints && !hasVariation; i++) {
|
||||
int x = (width * i) / samplePoints;
|
||||
int y = (height * i) / samplePoints;
|
||||
if (x < width && y < height) {
|
||||
Color sampleColor = new Color(image.getRGB(x, y));
|
||||
if (!sampleColor.equals(firstColor)) {
|
||||
hasVariation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasVariation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.ycwl.basic.puzzle.test;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 测试数据构建器
|
||||
* 用于创建测试用的模板和元素数据
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public class PuzzleTestDataBuilder {
|
||||
|
||||
/**
|
||||
* 创建基础模板
|
||||
*/
|
||||
public static PuzzleTemplateEntity createBasicTemplate() {
|
||||
PuzzleTemplateEntity template = new PuzzleTemplateEntity();
|
||||
template.setId(1L);
|
||||
template.setName("测试模板");
|
||||
template.setCode("test_template");
|
||||
template.setCanvasWidth(750);
|
||||
template.setCanvasHeight(1334);
|
||||
template.setBackgroundType(0);
|
||||
template.setBackgroundColor("#FFFFFF");
|
||||
template.setDescription("用于测试的模板");
|
||||
template.setCategory("test");
|
||||
template.setStatus(1);
|
||||
template.setScenicId(null);
|
||||
template.setCreateTime(new Date());
|
||||
template.setUpdateTime(new Date());
|
||||
template.setDeleted(0);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义尺寸的模板
|
||||
*/
|
||||
public static PuzzleTemplateEntity createTemplate(String code, int width, int height, String bgColor) {
|
||||
PuzzleTemplateEntity template = createBasicTemplate();
|
||||
template.setCode(code);
|
||||
template.setCanvasWidth(width);
|
||||
template.setCanvasHeight(height);
|
||||
template.setBackgroundColor(bgColor);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片元素
|
||||
*/
|
||||
public static PuzzleElementEntity createImageElement(Long templateId, String elementKey,
|
||||
int x, int y, int width, int height,
|
||||
int zIndex, String imageUrl) {
|
||||
PuzzleElementEntity element = new PuzzleElementEntity();
|
||||
element.setId(System.currentTimeMillis());
|
||||
element.setTemplateId(templateId);
|
||||
element.setElementType(1); // 图片
|
||||
element.setElementKey(elementKey);
|
||||
element.setElementName(elementKey);
|
||||
element.setXPosition(x);
|
||||
element.setYPosition(y);
|
||||
element.setWidth(width);
|
||||
element.setHeight(height);
|
||||
element.setZIndex(zIndex);
|
||||
element.setRotation(0);
|
||||
element.setOpacity(100);
|
||||
element.setDefaultImageUrl(imageUrl);
|
||||
element.setImageFitMode("FILL"); // 使用FILL模式确保图片完全填充区域
|
||||
element.setBorderRadius(0);
|
||||
element.setCreateTime(new Date());
|
||||
element.setUpdateTime(new Date());
|
||||
element.setDeleted(0);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆角图片元素
|
||||
*/
|
||||
public static PuzzleElementEntity createRoundedImageElement(Long templateId, String elementKey,
|
||||
int x, int y, int width, int height,
|
||||
int zIndex, String imageUrl, int borderRadius) {
|
||||
PuzzleElementEntity element = createImageElement(templateId, elementKey, x, y, width, height, zIndex, imageUrl);
|
||||
element.setBorderRadius(borderRadius);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文字元素
|
||||
*/
|
||||
public static PuzzleElementEntity createTextElement(Long templateId, String elementKey,
|
||||
int x, int y, int width, int height,
|
||||
int zIndex, String defaultText,
|
||||
int fontSize, String fontColor) {
|
||||
PuzzleElementEntity element = new PuzzleElementEntity();
|
||||
element.setId(System.currentTimeMillis());
|
||||
element.setTemplateId(templateId);
|
||||
element.setElementType(2); // 文字
|
||||
element.setElementKey(elementKey);
|
||||
element.setElementName(elementKey);
|
||||
element.setXPosition(x);
|
||||
element.setYPosition(y);
|
||||
element.setWidth(width);
|
||||
element.setHeight(height);
|
||||
element.setZIndex(zIndex);
|
||||
element.setRotation(0);
|
||||
element.setOpacity(100);
|
||||
element.setDefaultText(defaultText);
|
||||
element.setFontFamily("微软雅黑");
|
||||
element.setFontSize(fontSize);
|
||||
element.setFontColor(fontColor);
|
||||
element.setFontWeight("NORMAL");
|
||||
element.setFontStyle("NORMAL");
|
||||
element.setTextAlign("LEFT");
|
||||
element.setLineHeight(BigDecimal.valueOf(1.5));
|
||||
element.setMaxLines(null);
|
||||
element.setTextDecoration("NONE");
|
||||
element.setCreateTime(new Date());
|
||||
element.setUpdateTime(new Date());
|
||||
element.setDeleted(0);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建粗体文字元素
|
||||
*/
|
||||
public static PuzzleElementEntity createBoldTextElement(Long templateId, String elementKey,
|
||||
int x, int y, int width, int height,
|
||||
int zIndex, String defaultText,
|
||||
int fontSize, String fontColor, String textAlign) {
|
||||
PuzzleElementEntity element = createTextElement(templateId, elementKey, x, y, width, height, zIndex, defaultText, fontSize, fontColor);
|
||||
element.setFontWeight("BOLD");
|
||||
element.setTextAlign(textAlign);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完整的测试模板(带元素)
|
||||
*/
|
||||
public static List<PuzzleElementEntity> createFullTestElements(Long templateId) {
|
||||
List<PuzzleElementEntity> elements = new ArrayList<>();
|
||||
|
||||
// 背景图片
|
||||
elements.add(createImageElement(templateId, "bgImage", 0, 0, 750, 400, 0,
|
||||
"https://example.com/bg.jpg"));
|
||||
|
||||
// 用户头像(圆形)
|
||||
elements.add(createRoundedImageElement(templateId, "userAvatar", 50, 100, 80, 80, 1,
|
||||
"https://example.com/avatar.jpg", 40));
|
||||
|
||||
// 用户名
|
||||
elements.add(createBoldTextElement(templateId, "userName", 150, 110, 200, 60, 1,
|
||||
"用户昵称", 24, "#333333", "LEFT"));
|
||||
|
||||
// 订单号标题
|
||||
elements.add(createTextElement(templateId, "orderTitle", 50, 200, 650, 40, 1,
|
||||
"订单号:", 16, "#666666"));
|
||||
|
||||
// 订单号内容
|
||||
elements.add(createBoldTextElement(templateId, "orderNumber", 50, 240, 650, 40, 1,
|
||||
"ORDER123456", 18, "#333333", "LEFT"));
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建1020x1520的现实场景元素
|
||||
* 上方3张图片,每张470高度(共1410),底下110像素左侧二维码右侧文字
|
||||
*/
|
||||
public static List<PuzzleElementEntity> createRealScenarioElements(Long templateId) {
|
||||
List<PuzzleElementEntity> elements = new ArrayList<>();
|
||||
|
||||
// 画布总高度1520,每张图片460高度
|
||||
int imageHeight = 460;
|
||||
int bottomHeight = 140; // 剩余高度:1520 - 460*3 = 140
|
||||
|
||||
// 上方3张图片,每张470高度
|
||||
elements.add(createImageElement(templateId, "image1", 0, 0, 1020, imageHeight, 1,
|
||||
"https://example.com/image1.jpg"));
|
||||
|
||||
elements.add(createImageElement(templateId, "image2", 0, imageHeight, 1020, imageHeight, 1,
|
||||
"https://example.com/image2.jpg"));
|
||||
|
||||
elements.add(createImageElement(templateId, "image3", 0, imageHeight * 2, 1020, imageHeight, 1,
|
||||
"https://example.com/image3.jpg"));
|
||||
|
||||
// 底部区域起始Y坐标
|
||||
int bottomY = imageHeight * 3; // 1380
|
||||
|
||||
// 底部二维码(左侧,占满底部高度)
|
||||
elements.add(createImageElement(templateId, "qrCode", 20, bottomY + 10, 120, 120, 2,
|
||||
"https://example.com/qrcode.png"));
|
||||
|
||||
// 底部文字(右侧,占满剩余宽度和高度)
|
||||
int textX = 140; // 二维码右侧留20像素间距
|
||||
int textWidth = 1020 - textX - 20; // 右侧留20像素边距
|
||||
elements.add(createBoldTextElement(templateId, "bottomText", textX, bottomY + 20, textWidth, bottomHeight - 20, 2,
|
||||
"扫码查看详情", 18, "#333333", "RIGHT"));
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.ycwl.basic.puzzle.test;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 现实场景自动化测试助手
|
||||
* 提供便捷方法快速创建和测试1020x1520拼图场景
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
public class RealScenarioTestHelper {
|
||||
|
||||
private final PuzzleImageRenderer renderer;
|
||||
private final Path outputDir;
|
||||
private final Map<String, File> resourceFiles;
|
||||
|
||||
public RealScenarioTestHelper(Path outputDir) throws IOException {
|
||||
this.renderer = new PuzzleImageRenderer();
|
||||
this.outputDir = outputDir;
|
||||
this.resourceFiles = new HashMap<>();
|
||||
initializeMockResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Mock资源(图片、二维码等)
|
||||
*/
|
||||
private void initializeMockResources() throws IOException {
|
||||
Files.createDirectories(outputDir);
|
||||
|
||||
// 创建3张场景图片(每张470高度)
|
||||
createSceneImage("image1", "1", new Color(255, 200, 200));
|
||||
createSceneImage("image2", "2", new Color(200, 255, 200));
|
||||
createSceneImage("image3", "3", new Color(200, 200, 255));
|
||||
|
||||
// 创建二维码(100x100)
|
||||
BufferedImage qrCode = MockImageUtil.createMockQRCode(100);
|
||||
File qrFile = saveImage(qrCode, "qrcode.png");
|
||||
resourceFiles.put("qrCode", qrFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景图片(1020x470)
|
||||
*/
|
||||
private void createSceneImage(String key, String text, Color bgColor) throws IOException {
|
||||
BufferedImage image = MockImageUtil.createImageWithText(1020, 460, text, bgColor, Color.BLACK);
|
||||
File file = saveImage(image, key + ".jpg");
|
||||
resourceFiles.put(key, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片
|
||||
*/
|
||||
private File saveImage(BufferedImage image, String filename) throws IOException {
|
||||
File file = outputDir.resolve(filename).toFile();
|
||||
ImageIO.write(image, filename.endsWith(".png") ? "PNG" : "JPG", file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并测试完整的现实场景
|
||||
*
|
||||
* @param bottomText 底部显示的文字
|
||||
* @return 生成的图片文件
|
||||
*/
|
||||
public TestResult createAndTestRealScenario(String bottomText) throws IOException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 1. 创建模板
|
||||
PuzzleTemplateEntity template = createTemplate();
|
||||
|
||||
// 2. 创建元素
|
||||
List<PuzzleElementEntity> elements = createElements(template.getId());
|
||||
|
||||
// 3. 准备动态数据
|
||||
Map<String, String> dynamicData = prepareDynamicData(bottomText);
|
||||
|
||||
// 4. 渲染图片
|
||||
BufferedImage result = renderer.render(template, elements, dynamicData);
|
||||
|
||||
// 5. 保存结果
|
||||
File outputFile = saveImage(result, "real_scenario_result.png");
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
return new TestResult(
|
||||
outputFile,
|
||||
result.getWidth(),
|
||||
result.getHeight(),
|
||||
outputFile.length(),
|
||||
duration,
|
||||
true,
|
||||
"测试成功"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建1020x1520模板
|
||||
*/
|
||||
private PuzzleTemplateEntity createTemplate() {
|
||||
return PuzzleTestDataBuilder.createTemplate(
|
||||
"real_scenario_auto",
|
||||
1020,
|
||||
1520,
|
||||
"#F5F5F5"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建元素列表
|
||||
*/
|
||||
private List<PuzzleElementEntity> createElements(Long templateId) {
|
||||
return PuzzleTestDataBuilder.createRealScenarioElements(templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备动态数据
|
||||
*/
|
||||
private Map<String, String> prepareDynamicData(String bottomText) {
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("image1", resourceFiles.get("image1").getAbsolutePath());
|
||||
data.put("image2", resourceFiles.get("image2").getAbsolutePath());
|
||||
data.put("image3", resourceFiles.get("image3").getAbsolutePath());
|
||||
data.put("qrCode", resourceFiles.get("qrCode").getAbsolutePath());
|
||||
data.put("bottomText", bottomText != null ? bottomText : "扫码查看详情");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证生成的图片
|
||||
*/
|
||||
public boolean validateResult(BufferedImage image) {
|
||||
// 验证尺寸
|
||||
if (image.getWidth() != 1020 || image.getHeight() != 1520) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证不为空白
|
||||
return MockImageUtil.isNotBlank(image);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印测试报告
|
||||
*/
|
||||
public void printTestReport(TestResult result) {
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
System.out.println("📊 拼图功能现实场景测试报告");
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("✅ 测试状态: " + (result.isSuccess() ? "通过" : "失败"));
|
||||
System.out.println("📁 输出文件: " + result.getOutputFile().getAbsolutePath());
|
||||
System.out.println("📐 图片尺寸: " + result.getWidth() + " x " + result.getHeight() + " 像素");
|
||||
System.out.println("💾 文件大小: " + result.getFileSize() / 1024 + " KB");
|
||||
System.out.println("⏱️ 渲染耗时: " + result.getDuration() + " ms");
|
||||
System.out.println("📝 测试消息: " + result.getMessage());
|
||||
System.out.println("=".repeat(60) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试结果类
|
||||
*/
|
||||
public static class TestResult {
|
||||
private final File outputFile;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final long fileSize;
|
||||
private final long duration;
|
||||
private final boolean success;
|
||||
private final String message;
|
||||
|
||||
public TestResult(File outputFile, int width, int height, long fileSize,
|
||||
long duration, boolean success, String message) {
|
||||
this.outputFile = outputFile;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.fileSize = fileSize;
|
||||
this.duration = duration;
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public File getOutputFile() {
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速测试方法(主方法,可直接运行)
|
||||
*/
|
||||
public static void main(String[] args) throws IOException {
|
||||
// 创建临时目录
|
||||
Path tempDir = Files.createTempDirectory("puzzle_test_");
|
||||
System.out.println("📂 测试目录: " + tempDir.toAbsolutePath());
|
||||
|
||||
// 创建测试助手
|
||||
RealScenarioTestHelper helper = new RealScenarioTestHelper(tempDir);
|
||||
|
||||
// 执行测试
|
||||
TestResult result = helper.createAndTestRealScenario("奇遇时光乐园\n2025.11.11");
|
||||
|
||||
// 打印报告
|
||||
helper.printTestReport(result);
|
||||
|
||||
// 验证结果
|
||||
if (result.isSuccess()) {
|
||||
System.out.println("🎉 恭喜!现实场景测试全部通过!");
|
||||
System.out.println("💡 你可以打开输出图片查看效果: " + result.getOutputFile().getAbsolutePath());
|
||||
} else {
|
||||
System.err.println("❌ 测试失败: " + result.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user