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; } /**