You've already forked FrameTour-BE
feat(puzzle): 新增微信小程序二维码生成功能
- 在DataSourceContext中新增scenicId字段用于景区关联 - 实现WechatQrcodeDataSourceStrategy策略类,支持生成并上传微信小程序码 - 扩展DataSourceType枚举,增加WECHAT_QRCODE类型 - 修改PuzzleElementFillEngine执行方法,支持传入scenicId参数 - 在PuzzleGenerateServiceImpl中集成二维码自动生成逻辑 - 新增generateWechatQrcode方法用于生成并上传小程序码到OSS - 完善日志记录和异常处理机制 - 添加必要的工具类和存储服务依赖注入
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -15,6 +15,11 @@ public class DataSourceContext {
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 可扩展的其他上下文数据
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验模板与请求景区的合法性
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user