feat(puzzle): 添加拼图生成异步处理能力

- 移除 @RequiredArgsConstructor 注解,改用手动构造函数注入
- 添加 ThreadPoolExecutor 实现拼图生成异步处理
- 新增 generateAsync 方法支持异步生成拼图
- 新增 generateSync 方法支持同步生成拼图
- 重构核心生成逻辑为 doGenerateInternal 方法供同步异步共用
- 在 FaceMatchingOrchestrator 中优化拼图模板生成逻辑
- 支持根据场景选择同步或异步生成模式
- 添加线程池队列大小监控和日志记录
This commit is contained in:
2026-01-01 21:26:34 +08:00
parent 44f5008fd1
commit f8374519c3
3 changed files with 225 additions and 118 deletions

View File

@@ -12,10 +12,29 @@ import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
public interface IPuzzleGenerateService {
/**
* 生成拼图图片
* 生成拼图图片(默认同步模式)
*
* @param request 生成请求
* @return 生成结果(包含图片URL等信息)
*/
PuzzleGenerateResponse generate(PuzzleGenerateRequest request);
/**
* 同步生成拼图图片
* <p>立即执行并阻塞等待结果返回</p>
*
* @param request 生成请求
* @return 生成结果(包含图片URL等信息)
*/
PuzzleGenerateResponse generateSync(PuzzleGenerateRequest request);
/**
* 异步生成拼图图片
* <p>提交到队列,由固定大小的线程池异步处理,不等待结果</p>
* <p>队列满时会降级为同步执行(CallerRunsPolicy)</p>
*
* @param request 生成请求
* @return 生成记录ID(可用于后续追踪状态)
*/
Long generateAsync(PuzzleGenerateRequest request);
}

View File

@@ -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<PuzzleElementEntity> 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<String, String> 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);
}

View File

@@ -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,15 +362,14 @@ 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);
@@ -409,15 +403,8 @@ public class FaceMatchingOrchestrator {
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<CompletableFuture<Void>> futures = templateList.stream()
.map(template -> CompletableFuture.runAsync(() -> {
try {
templateList
.forEach(template -> {
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName());
@@ -432,36 +419,17 @@ public class FaceMatchingOrchestrator {
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();
if (template.getAutoAddPrint() > 0 && Strings.CI.equals(scene, "printer")) {
puzzleGenerateService.generateSync(generateRequest);
} else {
puzzleGenerateService.generateAsync(generateRequest);
}
}, 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);
}
}, "PuzzleTemplateGenerator-" + scenicId);
thread.start();
return thread;
return;
}
/**