feat(puzzle): 新增微信小程序二维码生成功能

- 在DataSourceContext中新增scenicId字段用于景区关联
- 实现WechatQrcodeDataSourceStrategy策略类,支持生成并上传微信小程序码
- 扩展DataSourceType枚举,增加WECHAT_QRCODE类型
- 修改PuzzleElementFillEngine执行方法,支持传入scenicId参数
- 在PuzzleGenerateServiceImpl中集成二维码自动生成逻辑
- 新增generateWechatQrcode方法用于生成并上传小程序码到OSS
- 完善日志记录和异常处理机制
- 添加必要的工具类和存储服务依赖注入
This commit is contained in:
2025-11-20 11:00:53 +08:00
parent 90cf0d44c9
commit 4cbd0dc255
5 changed files with 222 additions and 8 deletions

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.puzzle.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
@@ -13,7 +14,9 @@ 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.PuzzleImageRenderer;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.utils.WxMpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -22,7 +25,10 @@ import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -45,12 +51,18 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
private final PuzzleGenerationRecordMapper recordMapper;
private final PuzzleImageRenderer imageRenderer;
private final PuzzleElementFillEngine fillEngine;
private final ScenicRepository scenicRepository;
@Override
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
long startTime = System.currentTimeMillis();
log.info("开始生成拼图: templateCode={}, userId={}, orderId={}",
request.getTemplateCode(), request.getUserId(), request.getOrderId());
log.info("开始生成拼图: templateCode={}, userId={}, faceId={}",
request.getTemplateCode(), request.getUserId(), request.getFaceId());
// 业务层校验:faceId 必填
if (request.getFaceId() == null) {
throw new IllegalArgumentException("人脸ID不能为空");
}
// 1. 查询模板和元素
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
@@ -75,7 +87,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
Comparator.nullsFirst(Comparator.naturalOrder())));
// 4. 准备dynamicData(合并自动填充和手动数据)
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId);
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
// 5. 创建生成记录
PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
@@ -131,7 +143,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
record.setTemplateId(template.getId());
record.setTemplateCode(template.getCode());
record.setUserId(request.getUserId());
record.setOrderId(request.getOrderId());
record.setFaceId(request.getFaceId());
record.setBusinessType(request.getBusinessType());
record.setScenicId(scenicId);
record.setStatus(0); // 生成中
@@ -198,15 +210,31 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
*/
private Map<String, String> buildDynamicData(PuzzleTemplateEntity template,
PuzzleGenerateRequest request,
Long scenicId) {
Long scenicId,
List<PuzzleElementEntity> elements) {
Map<String, String> dynamicData = new HashMap<>();
// 0. 检查是否需要自动生成 travelResultWxaCode 二维码
if (request.getFaceId() != null && scenicId != null) {
boolean hasTravelResultWxaCode = elements.stream()
.anyMatch(e -> "travelResultWxaCode".equals(e.getElementKey()));
if (hasTravelResultWxaCode && !dynamicDataContainsKey(request.getDynamicData(), "travelResultWxaCode")) {
String qrcodeUrl = generateWechatQrcode(request.getFaceId(), scenicId);
if (qrcodeUrl != null) {
dynamicData.put("travelResultWxaCode", qrcodeUrl);
log.info("自动生成微信小程序二维码成功: faceId={}, url={}", request.getFaceId(), qrcodeUrl);
}
}
}
// 1. 自动填充(基于faceId和规则)
if (request.getFaceId() != null) {
try {
Map<String, String> autoFilled = fillEngine.execute(
template.getId(),
request.getFaceId()
request.getFaceId(),
scenicId
);
if (autoFilled != null && !autoFilled.isEmpty()) {
dynamicData.putAll(autoFilled);
@@ -228,6 +256,71 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
return dynamicData;
}
/**
* 检查dynamicData中是否包含指定key
*/
private boolean dynamicDataContainsKey(Map<String, String> dynamicData, String key) {
return dynamicData != null && dynamicData.containsKey(key);
}
/**
* 生成微信小程序二维码
*
* @param faceId 人脸ID
* @param scenicId 景区ID
* @return 二维码URL,失败返回null
*/
private String generateWechatQrcode(Long faceId, Long scenicId) {
File qrcode = null;
try {
// 获取景区的小程序配置
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(scenicId);
if (scenicMpConfig == null) {
log.error("生成微信小程序二维码失败: 未找到景区[{}]的小程序配置", scenicId);
return null;
}
// 生成临时文件
qrcode = new File("qrcode_" + faceId + "_" + UUID.randomUUID().toString().substring(0, 8) + ".jpg");
// 调用微信API生成小程序码
WxMpUtil.generateUnlimitedWXAQRCode(
scenicMpConfig.getAppId(),
scenicMpConfig.getAppSecret(),
"pages/videoSynthesis/from_face",
faceId.toString(),
qrcode
);
// 上传到OSS
try (FileInputStream fis = new FileInputStream(qrcode)) {
String fileName = String.format("qrcode_%d_%s.jpg",
faceId,
UUID.randomUUID().toString().replace("-", "").substring(0, 16));
return StorageFactory.use().uploadFile(
"image/jpeg",
fis,
"puzzle",
"wechat_qrcode",
fileName
);
}
} catch (Exception e) {
log.error("生成微信小程序二维码失败: faceId={}, scenicId={}", faceId, scenicId, e);
return null;
} finally {
// 清理临时文件
if (qrcode != null && qrcode.exists()) {
try {
Files.delete(qrcode.toPath());
} catch (Exception e) {
log.warn("删除临时二维码文件失败: {}", qrcode.getAbsolutePath(), e);
}
}
}
}
/**
* 校验模板与请求景区的合法性
*