feat(puzzle): 实现拼图生成去重机制

- 新增内容哈希计算逻辑,基于元素内容生成SHA256哈希用于去重判断
- 添加重复图片检测功能,当所有IMAGE元素使用相同URL时抛出异常
- 实现历史记录查询接口,根据模板ID、内容哈希和景区ID查找重复记录
- 扩展生成响应对象,增加isDuplicate和originalRecordId字段标识复用情况
- 更新数据库实体和Mapper,新增content_hash、is_duplicate等字段支持去重
- 添加完整的单元测试和集成测试,覆盖去重检测、哈希计算等核心逻辑
- 引入DuplicateImageException和PuzzleBizException异常类完善错误处理
This commit is contained in:
2025-11-21 11:02:43 +08:00
parent 6ef710201c
commit 0db713b4a8
10 changed files with 890 additions and 11 deletions

View File

@@ -14,6 +14,7 @@ import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
import com.ycwl.basic.puzzle.util.PuzzleDuplicationDetector;
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory;
@@ -53,6 +54,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
private final PuzzleImageRenderer imageRenderer;
private final PuzzleElementFillEngine fillEngine;
private final ScenicRepository scenicRepository;
private final PuzzleDuplicationDetector duplicationDetector;
@Override
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
@@ -90,19 +92,51 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
// 4. 准备dynamicData(合并自动填充和手动数据)
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
// 5. 创建生成记录
// 5. 执行重复图片检测
// 如果所有IMAGE元素使用相同URL,抛出DuplicateImageException
duplicationDetector.detectDuplicateImages(finalDynamicData, elements);
// 6. 计算内容哈希
String contentHash = duplicationDetector.calculateContentHash(finalDynamicData);
// 7. 查询历史记录(去重核心逻辑)
PuzzleGenerationRecordEntity duplicateRecord = duplicationDetector.findDuplicateRecord(
template.getId(), contentHash, resolvedScenicId);
if (duplicateRecord != null) {
// 发现重复内容,直接返回历史记录
long duration = System.currentTimeMillis() - startTime;
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
// 直接返回历史图片URL(语义化生成成功)
return PuzzleGenerateResponse.success(
duplicateRecord.getResultImageUrl(),
duplicateRecord.getResultFileSize(),
duplicateRecord.getResultWidth(),
duplicateRecord.getResultHeight(),
(int) duration,
duplicateRecord.getId(),
true, // isDuplicate=true
duplicateRecord.getId() // originalRecordId(复用时指向自己)
);
}
// 8. 没有历史记录,创建新的生成记录
PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
record.setContentHash(contentHash);
record.setIsDuplicate(false);
recordMapper.insert(record);
try {
// 5. 渲染图片
// 9. 渲染图片
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
// 5. 上传到OSS
// 10. 上传到OSS
String imageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
log.info("图片上传成功: url={}", imageUrl);
// 6. 更新记录为成功
// 11. 更新记录为成功
long duration = (int) (System.currentTimeMillis() - startTime);
long fileSize = estimateFileSize(resultImage, request.getOutputFormat());
recordMapper.updateSuccess(
@@ -114,7 +148,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
(int) duration
);
log.info("拼图生成成功: recordId={}, imageUrl={}, duration={}ms",
log.info("拼图生成成功(新生成): recordId={}, imageUrl={}, duration={}ms",
record.getId(), imageUrl, duration);
return PuzzleGenerateResponse.success(
@@ -123,7 +157,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
resultImage.getWidth(),
resultImage.getHeight(),
(int) duration,
record.getId()
record.getId(),
false, // isDuplicate=false
null // originalRecordId=null
);
} catch (Exception e) {