From f8374519c3fab8cb8db1e951fd5911281c7a6d3d Mon Sep 17 00:00:00 2001
From: Jerry Yan <792602257@qq.com>
Date: Thu, 1 Jan 2026 21:26:34 +0800
Subject: [PATCH] =?UTF-8?q?feat(puzzle):=20=E6=B7=BB=E5=8A=A0=E6=8B=BC?=
=?UTF-8?q?=E5=9B=BE=E7=94=9F=E6=88=90=E5=BC=82=E6=AD=A5=E5=A4=84=E7=90=86?=
=?UTF-8?q?=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除 @RequiredArgsConstructor 注解,改用手动构造函数注入
- 添加 ThreadPoolExecutor 实现拼图生成异步处理
- 新增 generateAsync 方法支持异步生成拼图
- 新增 generateSync 方法支持同步生成拼图
- 重构核心生成逻辑为 doGenerateInternal 方法供同步异步共用
- 在 FaceMatchingOrchestrator 中优化拼图模板生成逻辑
- 支持根据场景选择同步或异步生成模式
- 添加线程池队列大小监控和日志记录
---
.../service/IPuzzleGenerateService.java | 21 ++-
.../impl/PuzzleGenerateServiceImpl.java | 170 +++++++++++++++---
.../FaceMatchingOrchestrator.java | 152 +++++++---------
3 files changed, 225 insertions(+), 118 deletions(-)
diff --git a/src/main/java/com/ycwl/basic/puzzle/service/IPuzzleGenerateService.java b/src/main/java/com/ycwl/basic/puzzle/service/IPuzzleGenerateService.java
index 959fac44..cdfbe609 100644
--- a/src/main/java/com/ycwl/basic/puzzle/service/IPuzzleGenerateService.java
+++ b/src/main/java/com/ycwl/basic/puzzle/service/IPuzzleGenerateService.java
@@ -12,10 +12,29 @@ import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
public interface IPuzzleGenerateService {
/**
- * 生成拼图图片
+ * 生成拼图图片(默认同步模式)
*
* @param request 生成请求
* @return 生成结果(包含图片URL等信息)
*/
PuzzleGenerateResponse generate(PuzzleGenerateRequest request);
+
+ /**
+ * 同步生成拼图图片
+ *
立即执行并阻塞等待结果返回
+ *
+ * @param request 生成请求
+ * @return 生成结果(包含图片URL等信息)
+ */
+ PuzzleGenerateResponse generateSync(PuzzleGenerateRequest request);
+
+ /**
+ * 异步生成拼图图片
+ * 提交到队列,由固定大小的线程池异步处理,不等待结果
+ * 队列满时会降级为同步执行(CallerRunsPolicy)
+ *
+ * @param request 生成请求
+ * @return 生成记录ID(可用于后续追踪状态)
+ */
+ Long generateAsync(PuzzleGenerateRequest request);
}
diff --git a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java
index c840a2b9..e43ed6df 100644
--- a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java
+++ b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java
@@ -20,7 +20,6 @@ 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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -38,6 +37,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
* 拼图图片生成服务实现
@@ -47,33 +49,104 @@ import java.util.UUID;
*/
@Slf4j
@Service
-@RequiredArgsConstructor
public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
private final PuzzleTemplateMapper templateMapper;
private final PuzzleElementMapper elementMapper;
private final PuzzleGenerationRecordMapper recordMapper;
- @Lazy
private final PuzzleImageRenderer imageRenderer;
- @Lazy
private final PuzzleElementFillEngine fillEngine;
- @Lazy
private final ScenicRepository scenicRepository;
- @Lazy
private final PuzzleDuplicationDetector duplicationDetector;
- @Lazy
private final PrinterService printerService;
+ private final ThreadPoolExecutor puzzleGenerateExecutor;
+
+ public PuzzleGenerateServiceImpl(
+ PuzzleTemplateMapper templateMapper,
+ PuzzleElementMapper elementMapper,
+ PuzzleGenerationRecordMapper recordMapper,
+ @Lazy PuzzleImageRenderer imageRenderer,
+ @Lazy PuzzleElementFillEngine fillEngine,
+ @Lazy ScenicRepository scenicRepository,
+ @Lazy PuzzleDuplicationDetector duplicationDetector,
+ @Lazy PrinterService printerService) {
+ this.templateMapper = templateMapper;
+ this.elementMapper = elementMapper;
+ this.recordMapper = recordMapper;
+ this.imageRenderer = imageRenderer;
+ this.fillEngine = fillEngine;
+ this.scenicRepository = scenicRepository;
+ this.duplicationDetector = duplicationDetector;
+ this.printerService = printerService;
+ this.puzzleGenerateExecutor = new ThreadPoolExecutor(
+ 4,
+ 256,
+ 30,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(256),
+ new ThreadPoolExecutor.CallerRunsPolicy()
+ );;
+ }
@Override
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
+ // 默认使用同步模式
+ return generateSync(request);
+ }
+
+ @Override
+ public PuzzleGenerateResponse generateSync(PuzzleGenerateRequest request) {
+ return doGenerate(request);
+ }
+
+ @Override
+ public Long generateAsync(PuzzleGenerateRequest request) {
+ // 1. 参数校验
+ validateRequest(request);
+
+ // 2. 查询模板
+ PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
+ if (template == null) {
+ throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
+ }
+ if (template.getStatus() != 1) {
+ throw new IllegalArgumentException("模板已禁用: " + request.getTemplateCode());
+ }
+ Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
+
+ // 3. 创建 PENDING 状态的记录
+ PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
+ record.setStatus(0); // 生成中
+ recordMapper.insert(record);
+ Long recordId = record.getId();
+
+ log.info("异步拼图生成任务已提交: recordId={}, templateCode={}, 当前队列大小={}",
+ recordId, request.getTemplateCode(), puzzleGenerateExecutor.getQueue().size());
+
+ // 4. 提交到线程池异步执行
+ puzzleGenerateExecutor.execute(() -> {
+ try {
+ doGenerateInternal(request, template, resolvedScenicId, record);
+ } catch (Exception e) {
+ log.error("异步拼图生成失败: recordId={}, templateCode={}",
+ recordId, request.getTemplateCode(), e);
+ recordMapper.updateFail(recordId, e.getMessage());
+ }
+ });
+
+ return recordId;
+ }
+
+ /**
+ * 核心生成逻辑(同步执行)
+ */
+ private PuzzleGenerateResponse doGenerate(PuzzleGenerateRequest request) {
long startTime = System.currentTimeMillis();
log.info("开始生成拼图: templateCode={}, userId={}, faceId={}",
request.getTemplateCode(), request.getUserId(), request.getFaceId());
- // 业务层校验:faceId 必填
- if (request.getFaceId() == null) {
- throw new IllegalArgumentException("人脸ID不能为空");
- }
+ // 参数校验
+ validateRequest(request);
// 1. 查询模板和元素
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
@@ -135,16 +208,66 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
record.setContentHash(contentHash);
recordMapper.insert(record);
+ // 9. 执行核心生成逻辑
+ return doGenerateInternal(request, template, resolvedScenicId, record, startTime);
+ }
+
+ /**
+ * 校验请求参数
+ */
+ private void validateRequest(PuzzleGenerateRequest request) {
+ if (request.getFaceId() == null) {
+ throw new IllegalArgumentException("人脸ID不能为空");
+ }
+ }
+
+ /**
+ * 核心生成逻辑(内部方法,同步/异步共用)
+ * 注意:此方法会在调用线程中执行渲染和上传操作
+ *
+ * @param request 生成请求
+ * @param template 模板
+ * @param resolvedScenicId 景区ID
+ * @param record 生成记录(已插入数据库)
+ * @return 生成结果(异步模式下不关心返回值)
+ */
+ private PuzzleGenerateResponse doGenerateInternal(PuzzleGenerateRequest request,
+ PuzzleTemplateEntity template,
+ Long resolvedScenicId,
+ PuzzleGenerationRecordEntity record) {
+ return doGenerateInternal(request, template, resolvedScenicId, record, System.currentTimeMillis());
+ }
+
+ /**
+ * 核心生成逻辑(内部方法,同步/异步共用)
+ */
+ private PuzzleGenerateResponse doGenerateInternal(PuzzleGenerateRequest request,
+ PuzzleTemplateEntity template,
+ Long resolvedScenicId,
+ PuzzleGenerationRecordEntity record,
+ long startTime) {
+ List elements = elementMapper.getByTemplateId(template.getId());
+ if (elements.isEmpty()) {
+ throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
+ }
+
+ // 按z-index排序元素
+ elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
+ Comparator.nullsFirst(Comparator.naturalOrder())));
+
+ // 准备dynamicData
+ Map finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
+
try {
- // 9. 渲染图片
+ // 渲染图片
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
- // 10. 上传原图到OSS(未裁切)
+ // 上传原图到OSS(未裁切)
String originalImageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
log.info("原图上传成功: url={}", originalImageUrl);
- // 11. 处理用户区域裁切
- String finalImageUrl = originalImageUrl; // 默认使用原图
+ // 处理用户区域裁切
+ String finalImageUrl = originalImageUrl;
BufferedImage finalImage = resultImage;
if (StrUtil.isNotBlank(template.getUserArea())) {
@@ -155,12 +278,11 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
log.info("裁切后图片上传成功: userArea={}, url={}", template.getUserArea(), finalImageUrl);
} catch (Exception e) {
log.error("图片裁切失败,使用原图: userArea={}", template.getUserArea(), e);
- // 裁切失败时使用原图
}
}
- // 12. 更新记录为成功
- long duration = (int) (System.currentTimeMillis() - startTime);
+ // 更新记录为成功
+ long duration = System.currentTimeMillis() - startTime;
long fileSize = estimateFileSize(finalImage, request.getOutputFormat());
recordMapper.updateSuccess(
record.getId(),
@@ -172,23 +294,22 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
(int) duration
);
- log.info("拼图生成成功(新生成): recordId={}, originalUrl={}, finalUrl={}, duration={}ms",
+ 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添加到打印队列
+ originalImageUrl,
record.getId()
);
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
} catch (Exception e) {
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
- // 添加失败不影响拼图生成流程
}
}
@@ -199,13 +320,12 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
finalImage.getHeight(),
(int) duration,
record.getId(),
- false, // isDuplicate=false
- null // originalRecordId=null
+ false,
+ null
);
} catch (Exception e) {
log.error("拼图生成失败: templateCode={}", request.getTemplateCode(), e);
- // 更新记录为失败
recordMapper.updateFail(record.getId(), e.getMessage());
throw new RuntimeException("图片生成失败: " + e.getMessage(), e);
}
diff --git a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java
index 22ef51cc..290b93b2 100644
--- a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java
+++ b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java
@@ -151,12 +151,7 @@ public class FaceMatchingOrchestrator {
processSourceRelations(context, searchResult, faceId, isNew);
// 步骤7: 异步生成拼图模板
- Thread thread = asyncGeneratePuzzleTemplate(context.face.getScenicId(), faceId, context.face.getMemberId());
- if (Strings.CI.equals(scene, "printer")) {
- if (thread != null) {
- thread.join();
- }
- }
+ asyncGeneratePuzzleTemplate(context.face.getScenicId(), faceId, context.face.getMemberId(), scene);
return searchResult;
@@ -367,101 +362,74 @@ public class FaceMatchingOrchestrator {
*
* @return
*/
- private Thread asyncGeneratePuzzleTemplate(Long scenicId, Long faceId, Long memberId) {
+ private void asyncGeneratePuzzleTemplate(Long scenicId, Long faceId, Long memberId, String scene) {
if (redisTemplate.hasKey("puzzle_generated:face:" + faceId)) {
- return null;
+ return;
}
redisTemplate.opsForValue().set(
"puzzle_generated:face:" + faceId,
"1",
60 * 10, TimeUnit.SECONDS);
- Thread thread = new Thread(() -> {
- try {
- log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
+ try {
+ log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
- // 查询该景区所有启用状态的拼图模板
- List templateList = puzzleTemplateService.listTemplates(
- scenicId, null, 1); // 查询启用状态的模板
+ // 查询该景区所有启用状态的拼图模板
+ List templateList = puzzleTemplateService.listTemplates(
+ scenicId, null, 1); // 查询启用状态的模板
- if (templateList == null || templateList.isEmpty()) {
- log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
- return;
- }
-
- log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
-
- // 获取人脸信息用于动态数据
- FaceEntity face = faceRepository.getFace(faceId);
- if (face == null) {
- log.warn("人脸信息不存在,无法生成拼图: faceId={}", faceId);
- return;
- }
- ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(face.getScenicId());
-
- // 准备公共动态数据
- Map baseDynamicData = new HashMap<>();
- if (face.getFaceUrl() != null) {
- baseDynamicData.put("faceImage", face.getFaceUrl());
- baseDynamicData.put("userAvatar", face.getFaceUrl());
- }
- baseDynamicData.put("faceId", String.valueOf(faceId));
- baseDynamicData.put("scenicName", scenicBasic.getName());
- baseDynamicData.put("scenicText", scenicBasic.getName());
- baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
-
- // 使用虚拟线程池并行生成所有模板
- AtomicInteger successCount = new AtomicInteger(0);
- AtomicInteger failCount = new AtomicInteger(0);
-
- try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
- // 为每个模板创建一个异步任务
- List> futures = templateList.stream()
- .map(template -> CompletableFuture.runAsync(() -> {
- try {
- log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
- scenicId, template.getCode(), template.getName());
-
- // 构建生成请求
- PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
- generateRequest.setScenicId(scenicId);
- generateRequest.setUserId(memberId);
- generateRequest.setFaceId(faceId);
- generateRequest.setBusinessType("face_matching");
- generateRequest.setTemplateCode(template.getCode());
- generateRequest.setOutputFormat("PNG");
- generateRequest.setQuality(90);
- generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
- generateRequest.setRequireRuleMatch(true);
-
- // 调用拼图生成服务
- PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
-
- log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
- scenicId, template.getCode(), response.getImageUrl());
- successCount.incrementAndGet();
-
- } catch (Exception e) {
- log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
- scenicId, template.getCode(), template.getName(), e);
- failCount.incrementAndGet();
- }
- }, executor))
- .toList();
-
- // 等待所有任务完成
- CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
- }
-
- log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
- scenicId, templateList.size(), successCount.get(), failCount.get());
-
- } catch (Exception e) {
- // 异步任务失败不影响主流程,仅记录日志
- log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
+ if (templateList == null || templateList.isEmpty()) {
+ log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
+ return;
}
- }, "PuzzleTemplateGenerator-" + scenicId);
- thread.start();
- return thread;
+
+ log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
+
+ // 获取人脸信息用于动态数据
+ FaceEntity face = faceRepository.getFace(faceId);
+ if (face == null) {
+ log.warn("人脸信息不存在,无法生成拼图: faceId={}", faceId);
+ return;
+ }
+ ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(face.getScenicId());
+
+ // 准备公共动态数据
+ Map baseDynamicData = new HashMap<>();
+ if (face.getFaceUrl() != null) {
+ baseDynamicData.put("faceImage", face.getFaceUrl());
+ baseDynamicData.put("userAvatar", face.getFaceUrl());
+ }
+ baseDynamicData.put("faceId", String.valueOf(faceId));
+ baseDynamicData.put("scenicName", scenicBasic.getName());
+ baseDynamicData.put("scenicText", scenicBasic.getName());
+ baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
+
+ templateList
+ .forEach(template -> {
+ log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
+ scenicId, template.getCode(), template.getName());
+
+ // 构建生成请求
+ PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
+ generateRequest.setScenicId(scenicId);
+ generateRequest.setUserId(memberId);
+ generateRequest.setFaceId(faceId);
+ generateRequest.setBusinessType("face_matching");
+ generateRequest.setTemplateCode(template.getCode());
+ generateRequest.setOutputFormat("PNG");
+ generateRequest.setQuality(90);
+ generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
+ generateRequest.setRequireRuleMatch(true);
+ if (template.getAutoAddPrint() > 0 && Strings.CI.equals(scene, "printer")) {
+ puzzleGenerateService.generateSync(generateRequest);
+ } else {
+ puzzleGenerateService.generateAsync(generateRequest);
+ }
+ });
+ } catch (Exception e) {
+ // 异步任务失败不影响主流程,仅记录日志
+ log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
+ }
+ return;
}
/**