feat(puzzle): 支持拼图原图保存与自动打印功能

- 在PuzzleGenerationRecordEntity中新增originalImageUrl字段用于存储未裁切的原图URL
- 在PuzzleTemplateEntity中新增autoAddPrint、canPrint和userArea字段支持打印配置
- 更新PuzzleGenerationRecordMapper.xml以支持新字段的读写操作
- 在PuzzleGenerateServiceImpl中实现原图上传、用户区域裁切以及自动添加到打印队列逻辑
- 新增cropImage方法处理图片按指定区域裁切
- 集成PrinterService实现拼图完成后自动添加到打印队列功能
- 优化生成流程日志记录,区分原图和最终图片的URL信息
This commit is contained in:
2025-12-17 22:56:50 +08:00
parent 3e938ad171
commit 10b39ec4c1
6 changed files with 132 additions and 19 deletions

View File

@@ -76,6 +76,12 @@ public class PuzzleGenerationRecordEntity {
@TableField("result_image_url")
private String resultImageUrl;
/**
* 原始图片URL(未裁切的图片,用于打印)
*/
@TableField("original_image_url")
private String originalImageUrl;
/**
* 文件大小(字节)
*/

View File

@@ -97,6 +97,24 @@ public class PuzzleTemplateEntity {
@TableField("scenic_id")
private Long scenicId;
/**
* 自动添加到打印队列:1-开启 0-关闭
*/
@TableField("auto_add_print")
private Integer autoAddPrint;
/**
* 是否可以打印:1-可以 0-不可以
*/
@TableField("can_print")
private Integer canPrint;
/**
* 用户查看区域(裁切区域),格式:x,y,w,h
*/
@TableField("user_area")
private String userArea;
/**
* 创建时间
*/

View File

@@ -52,6 +52,7 @@ public interface PuzzleGenerationRecordMapper {
*/
int updateSuccess(@Param("id") Long id,
@Param("resultImageUrl") String resultImageUrl,
@Param("originalImageUrl") String originalImageUrl,
@Param("resultFileSize") Long resultFileSize,
@Param("resultWidth") Integer resultWidth,
@Param("resultHeight") Integer resultHeight,

View File

@@ -17,6 +17,7 @@ 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.service.printer.PrinterService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.utils.WxMpUtil;
import lombok.RequiredArgsConstructor;
@@ -60,6 +61,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
private final ScenicRepository scenicRepository;
@Lazy
private final PuzzleDuplicationDetector duplicationDetector;
@Lazy
private final PrinterService printerService;
@Override
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
@@ -136,30 +139,64 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
// 9. 渲染图片
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
// 10. 上传到OSS
String imageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
log.info("上传成功: url={}", imageUrl);
// 10. 上传原图到OSS(未裁切)
String originalImageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
log.info("图上传成功: url={}", originalImageUrl);
// 11. 更新记录为成功
// 11. 处理用户区域裁切
String finalImageUrl = originalImageUrl; // 默认使用原图
BufferedImage finalImage = resultImage;
if (StrUtil.isNotBlank(template.getUserArea())) {
try {
BufferedImage croppedImage = cropImage(resultImage, template.getUserArea());
finalImageUrl = uploadImage(croppedImage, template.getCode() + "_cropped", request.getOutputFormat(), request.getQuality());
finalImage = croppedImage;
log.info("裁切后图片上传成功: userArea={}, url={}", template.getUserArea(), finalImageUrl);
} catch (Exception e) {
log.error("图片裁切失败,使用原图: userArea={}", template.getUserArea(), e);
// 裁切失败时使用原图
}
}
// 12. 更新记录为成功
long duration = (int) (System.currentTimeMillis() - startTime);
long fileSize = estimateFileSize(resultImage, request.getOutputFormat());
long fileSize = estimateFileSize(finalImage, request.getOutputFormat());
recordMapper.updateSuccess(
record.getId(),
imageUrl,
finalImageUrl,
originalImageUrl,
fileSize,
resultImage.getWidth(),
resultImage.getHeight(),
finalImage.getWidth(),
finalImage.getHeight(),
(int) duration
);
log.info("拼图生成成功(新生成): recordId={}, imageUrl={}, duration={}ms",
record.getId(), imageUrl, duration);
log.info("拼图生成成功(新生成): recordId={}, originalUrl={}, finalUrl={}, duration={}ms",
record.getId(), originalImageUrl, finalImageUrl, duration);
// 13. 检查是否自动添加到打印队列
if (template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
try {
Integer printRecordId = printerService.addUserPhotoFromPuzzle(
request.getUserId(),
resolvedScenicId,
request.getFaceId(),
originalImageUrl, // 使用原图URL添加到打印队列
record.getId()
);
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
} catch (Exception e) {
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
// 添加失败不影响拼图生成流程
}
}
return PuzzleGenerateResponse.success(
imageUrl,
finalImageUrl,
fileSize,
resultImage.getWidth(),
resultImage.getHeight(),
finalImage.getWidth(),
finalImage.getHeight(),
(int) duration,
record.getId(),
false, // isDuplicate=false
@@ -405,4 +442,43 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
return templateScenicId;
}
/**
* 裁切图片
* @param image 原图
* @param userArea 裁切区域,格式:x,y,w,h
* @return 裁切后的图片
*/
private BufferedImage cropImage(BufferedImage image, String userArea) {
if (StrUtil.isBlank(userArea)) {
return image;
}
try {
String[] parts = userArea.split(",");
if (parts.length != 4) {
throw new IllegalArgumentException("userArea格式错误,应为:x,y,w,h");
}
int x = Integer.parseInt(parts[0].trim());
int y = Integer.parseInt(parts[1].trim());
int w = Integer.parseInt(parts[2].trim());
int h = Integer.parseInt(parts[3].trim());
// 边界检查
if (x < 0 || y < 0 || w <= 0 || h <= 0) {
throw new IllegalArgumentException("裁切区域参数必须为正数");
}
if (x + w > image.getWidth() || y + h > image.getHeight()) {
throw new IllegalArgumentException("裁切区域超出图片边界");
}
// 执行裁切
return image.getSubimage(x, y, w, h);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("userArea格式错误,参数必须为数字", e);
}
}
}