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

@@ -49,9 +49,10 @@ public class PuzzleElementFillEngine {
*
* @param templateId 模板ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @return 填充后的dynamicData
*/
public Map<String, String> execute(Long templateId, Long faceId) {
public Map<String, String> execute(Long templateId, Long faceId, Long scenicId) {
Map<String, String> dynamicData = new HashMap<>();
if (faceId == null) {
@@ -103,6 +104,7 @@ public class PuzzleElementFillEngine {
// 5. 批量填充dynamicData
DataSourceContext dataSourceContext = DataSourceContext.builder()
.faceId(faceId)
.scenicId(scenicId)
.build();
int successCount = 0;

View File

@@ -15,6 +15,11 @@ public class DataSourceContext {
*/
private Long faceId;
/**
* 景区ID
*/
private Long scenicId;
/**
* 可扩展的其他上下文数据
*/

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.puzzle.fill.datasource;
import com.fasterxml.jackson.databind.JsonNode;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.utils.WxMpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.util.UUID;
/**
* 微信小程序二维码数据源策略
* 用于生成微信小程序二维码并上传至OSS
*/
@Slf4j
@Component
public class WechatQrcodeDataSourceStrategy implements DataSourceStrategy {
@Autowired
private ScenicRepository scenicRepository;
@Override
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
if (context.getFaceId() == null) {
log.warn("生成微信小程序二维码失败: faceId为空");
return null;
}
if (context.getScenicId() == null) {
log.warn("生成微信小程序二维码失败: scenicId为空");
return null;
}
try {
// 获取景区的小程序配置
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(context.getScenicId());
if (scenicMpConfig == null) {
log.error("生成微信小程序二维码失败: 未找到景区[{}]的小程序配置", context.getScenicId());
return null;
}
// 从sourceFilter中获取page路径,默认使用 pages/videoSynthesis/from_face
String page = "pages/videoSynthesis/from_face";
if (sourceFilter != null && sourceFilter.has("page")) {
page = sourceFilter.get("page").asText();
}
// 生成临时文件
File qrcode = new File("qrcode_" + context.getFaceId() + "_" + UUID.randomUUID().toString().substring(0, 8) + ".jpg");
try {
// 调用微信API生成小程序码
WxMpUtil.generateUnlimitedWXAQRCode(
scenicMpConfig.getAppId(),
scenicMpConfig.getAppSecret(),
page,
context.getFaceId().toString(),
qrcode
);
// 上传到OSS
try (FileInputStream fis = new FileInputStream(qrcode)) {
String fileName = String.format("qrcode_%d_%s.jpg",
context.getFaceId(),
UUID.randomUUID().toString().replace("-", "").substring(0, 16));
String qrcodeUrl = StorageFactory.use().uploadFile(
"image/jpeg",
fis,
"puzzle",
"wechat_qrcode",
fileName
);
log.info("生成微信小程序二维码成功: faceId={}, page={}, url={}",
context.getFaceId(), page, qrcodeUrl);
return qrcodeUrl;
}
} finally {
// 清理临时文件
if (qrcode.exists()) {
try {
Files.delete(qrcode.toPath());
} catch (Exception e) {
log.warn("删除临时二维码文件失败: {}", qrcode.getAbsolutePath(), e);
}
}
}
} catch (Exception e) {
log.error("生成微信小程序二维码异常: faceId={}, scenicId={}",
context.getFaceId(), context.getScenicId(), e);
return null;
}
}
@Override
public String getSupportedType() {
return DataSourceType.WECHAT_QRCODE.getCode();
}
}

View File

@@ -31,7 +31,12 @@ public enum DataSourceType {
/**
* 静态值(直接使用fallbackValue)
*/
STATIC_VALUE("STATIC_VALUE", "静态值");
STATIC_VALUE("STATIC_VALUE", "静态值"),
/**
* 微信小程序二维码(生成小程序二维码)
*/
WECHAT_QRCODE("WECHAT_QRCODE", "微信小程序二维码");
private final String code;
private final String description;

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);
}
}
}
}
/**
* 校验模板与请求景区的合法性
*