From 4cbd0dc2551748ac4f48c31f06d3a1ee0bdbc457 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 20 Nov 2025 11:00:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(puzzle):=20=E6=96=B0=E5=A2=9E=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BA=8C=E7=BB=B4=E7=A0=81?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在DataSourceContext中新增scenicId字段用于景区关联 - 实现WechatQrcodeDataSourceStrategy策略类,支持生成并上传微信小程序码 - 扩展DataSourceType枚举,增加WECHAT_QRCODE类型 - 修改PuzzleElementFillEngine执行方法,支持传入scenicId参数 - 在PuzzleGenerateServiceImpl中集成二维码自动生成逻辑 - 新增generateWechatQrcode方法用于生成并上传小程序码到OSS - 完善日志记录和异常处理机制 - 添加必要的工具类和存储服务依赖注入 --- .../puzzle/fill/PuzzleElementFillEngine.java | 4 +- .../fill/datasource/DataSourceContext.java | 5 + .../WechatQrcodeDataSourceStrategy.java | 109 ++++++++++++++++++ .../puzzle/fill/enums/DataSourceType.java | 7 +- .../impl/PuzzleGenerateServiceImpl.java | 105 ++++++++++++++++- 5 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/puzzle/fill/datasource/WechatQrcodeDataSourceStrategy.java diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java b/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java index 0bf72fa1..e990f432 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java @@ -49,9 +49,10 @@ public class PuzzleElementFillEngine { * * @param templateId 模板ID * @param faceId 人脸ID + * @param scenicId 景区ID * @return 填充后的dynamicData */ - public Map execute(Long templateId, Long faceId) { + public Map execute(Long templateId, Long faceId, Long scenicId) { Map 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; diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java index 3a6f03ac..aeffe286 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java @@ -15,6 +15,11 @@ public class DataSourceContext { */ private Long faceId; + /** + * 景区ID + */ + private Long scenicId; + /** * 可扩展的其他上下文数据 */ diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/WechatQrcodeDataSourceStrategy.java b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/WechatQrcodeDataSourceStrategy.java new file mode 100644 index 00000000..d91f9036 --- /dev/null +++ b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/WechatQrcodeDataSourceStrategy.java @@ -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(); + } +} diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/enums/DataSourceType.java b/src/main/java/com/ycwl/basic/puzzle/fill/enums/DataSourceType.java index 330fa7da..6214efe9 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/enums/DataSourceType.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/enums/DataSourceType.java @@ -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; 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 3bbdba6e..f6dd31e4 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 @@ -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 finalDynamicData = buildDynamicData(template, request, resolvedScenicId); + Map 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 buildDynamicData(PuzzleTemplateEntity template, PuzzleGenerateRequest request, - Long scenicId) { + Long scenicId, + List elements) { Map 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 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 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); + } + } + } + } + /** * 校验模板与请求景区的合法性 *