From afc1272ff5fe9ec1bc6bcae59288d83e0ce916ae Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 19 Sep 2025 11:15:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=BE=AE=E4=BF=A1=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E4=BC=98=E5=8C=96=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E5=88=87=E7=89=87=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/chain/loader/ExcelFileLoader.java | 41 +++++ .../chain/loader/ResourceLoaderFactory.java | 2 + .../ruoyi/chain/split/ExcelTextSplitter.java | 17 ++ .../org/ruoyi/service/DashscopeService.java | 23 --- .../ruoyi/service/PdfImageExtractService.java | 34 ---- .../service/impl/DashscopeServiceImpl.java | 150 ------------------ .../impl/PdfImageExtractServiceImpl.java | 147 ----------------- ruoyi-modules/pom.xml | 1 + .../knowledge/KnowledgeController.java | 15 +- ruoyi-modules/ruoyi-wechat/pom.xml | 32 ++++ .../org/ruoyi/builder/AbstractBuilder.java | 12 ++ .../java/org/ruoyi/builder/ImageBuilder.java | 24 +++ .../java/org/ruoyi/builder/TextBuilder.java | 22 +++ .../org/ruoyi/config/WxCpConfiguration.java | 130 +++++++++++++++ .../java/org/ruoyi/config/WxCpProperties.java | 48 ++++++ .../controller/WeixinServerController.java | 51 ++++++ .../controller/WeixinUserController.java | 55 +++++++ .../ruoyi/controller/WxPortalController.java | 89 +++++++++++ .../java/org/ruoyi/domin/ReceiveMessage.java | 58 +++++++ .../java/org/ruoyi/domin/WeixinQrCode.java | 15 ++ .../org/ruoyi/handler/AbstractHandler.java | 9 ++ .../ruoyi/handler/ContactChangeHandler.java | 34 ++++ .../org/ruoyi/handler/EnterAgentHandler.java | 29 ++++ .../org/ruoyi/handler/LocationHandler.java | 44 +++++ .../java/org/ruoyi/handler/LogHandler.java | 26 +++ .../java/org/ruoyi/handler/MenuHandler.java | 34 ++++ .../java/org/ruoyi/handler/MsgHandler.java | 43 +++++ .../java/org/ruoyi/handler/NullHandler.java | 23 +++ .../java/org/ruoyi/handler/ScanHandler.java | 8 + .../org/ruoyi/handler/SubscribeHandler.java | 63 ++++++++ .../org/ruoyi/handler/UnsubscribeHandler.java | 28 ++++ .../org/ruoyi/service/VxLoginService.java | 108 +++++++++++++ .../org/ruoyi/service/WeixinUserService.java | 10 ++ .../service/impl/WeixinUserServiceImpl.java | 65 ++++++++ .../main/java/org/ruoyi/util/KeyUtils.java | 24 +++ .../java/org/ruoyi/util/WeixinApiUtil.java | 81 ++++++++++ .../java/org/ruoyi/util/WeixinMsgUtil.java | 66 ++++++++ .../org/ruoyi/util/WeixinQrCodeCacheUtil.java | 34 ++++ .../src/main/java/org/ruoyi/util/XmlUtil.java | 28 ++++ 39 files changed, 1356 insertions(+), 367 deletions(-) create mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ExcelFileLoader.java create mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/split/ExcelTextSplitter.java delete mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/DashscopeService.java delete mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/PdfImageExtractService.java delete mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/DashscopeServiceImpl.java delete mode 100644 ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/PdfImageExtractServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-wechat/pom.xml create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/AbstractBuilder.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/ImageBuilder.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/TextBuilder.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpConfiguration.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/AbstractHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ContactChangeHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/EnterAgentHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LocationHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LogHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MenuHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MsgHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/NullHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ScanHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/SubscribeHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/UnsubscribeHandler.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java create mode 100644 ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ExcelFileLoader.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ExcelFileLoader.java new file mode 100644 index 00000000..b47ce11f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ExcelFileLoader.java @@ -0,0 +1,41 @@ +package org.ruoyi.chain.loader; + +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.chain.split.TextSplitter; +import org.ruoyi.common.core.exception.UtilException; +import org.springframework.stereotype.Component; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +@Component +@AllArgsConstructor +@Slf4j +public class ExcelFileLoader implements ResourceLoader { + private final TextSplitter textSplitter; + private static final int DEFAULT_BUFFER_SIZE = 8192; + @Override + public String getContent(InputStream inputStream) { + // 使用带缓冲的输入流包装(保持原流不自动关闭) + try (InputStream bufferedStream = new BufferedInputStream(inputStream, DEFAULT_BUFFER_SIZE)) { + ApacheTikaDocumentParser apacheTikaDocumentParser = new ApacheTikaDocumentParser(); + Document document = apacheTikaDocumentParser.parse(bufferedStream); + return document.text(); + } catch (IOException e) { + String errorMsg = "Excel文件流读取失败"; + throw new UtilException(errorMsg, e); + } catch (RuntimeException e) { + String errorMsg = "Excel内容解析异常"; + throw new UtilException(errorMsg, e); + } + } + + @Override + public List getChunkList(String content, String kid) { + return textSplitter.split(content, kid); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java index 94f95b09..c17a39fd 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java @@ -36,6 +36,8 @@ public class ResourceLoaderFactory { return new MarkDownFileLoader(markdownTextSplitter); }else if (FileType.isCodeFile(fileType)) { return new CodeFileLoader(codeTextSplitter); + } else if (FileType.isExcel(fileType)) { + return new ExcelFileLoader(excelTextSplitter); }else { return new TextFileLoader(characterTextSplitter); } diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/split/ExcelTextSplitter.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/split/ExcelTextSplitter.java new file mode 100644 index 00000000..cc2b5f04 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/split/ExcelTextSplitter.java @@ -0,0 +1,17 @@ +package org.ruoyi.chain.split; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@AllArgsConstructor +@Slf4j +public class ExcelTextSplitter implements TextSplitter{ + @Override + public List split(String content, String kid) { + return null; + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/DashscopeService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/DashscopeService.java deleted file mode 100644 index 3c8f498e..00000000 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/DashscopeService.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.ruoyi.service; - -import java.io.IOException; - -/** - * @Description: 阿里百炼api - * @Date: 2025/6/4 下午2:24 - */ -public interface DashscopeService { - - /** - * 视觉推理(QVQ) - * @param imageUrl 图片可访问的地址 - * @return - */ - String qvq(String imageUrl) throws IOException; - /** - * 视觉推理(QVQ) 使用本地文件(输入Base64编码或本地路径) - * @param localPath 图片文件的绝对路径 - * @return - */ - String qvq4LocalPath(String localPath) throws IOException; -} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/PdfImageExtractService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/PdfImageExtractService.java deleted file mode 100644 index c9929786..00000000 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/PdfImageExtractService.java +++ /dev/null @@ -1,34 +0,0 @@ - - /** - * 处理文件内容 - * - * @param unzip Base64编码的图片数组 - * @return 文件内容结果列表 - * @throws IOException 如果API调用过程中发生错误 - */ - List dealFileContent(String[] unzip) throws IOException; - /** - *利用百炼接口处理文件内容 - * - * @param imageUrl 传入图片地址 - * @return 文件内容结果列表 - * @throws IOException 如果API调用过程中发生错误 - */ - List dealFileContent4Dashscope(String imageUrl) throws IOException; - - /** - * 利用百炼接口处理文件内容 - * - * 视觉推理(QVQ) 使用本地文件(输入Base64编码或本地路径) - * @param localPath 图片文件的绝对路径 - * @return - */ - List dealFileContent4DashscopeBase64(String localPath)throws IOException; - /** - * 提取PDF中的图片并调用gpt-4o-mini,识别图片内容并返回 - * @param file - * @return - * @throws IOException - */ - List extractImages(MultipartFile file) throws IOException; -} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/DashscopeServiceImpl.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/DashscopeServiceImpl.java deleted file mode 100644 index 0f6b0014..00000000 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/DashscopeServiceImpl.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.ruoyi.service.impl; - -import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; -import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; -import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; -import com.alibaba.dashscope.common.MultiModalMessage; -import com.alibaba.dashscope.common.Role; -import io.reactivex.Flowable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.domain.PdfFileContentResult; -import org.ruoyi.service.DashscopeService; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -/** - * @Description: 阿里百炼API - * @Date: 2025/6/4 下午2:28 - */ -@Service -@Slf4j -public class DashscopeServiceImpl implements DashscopeService { - - private boolean isFirstPrint; - - @Value("${dashscope.model}") - private String serviceModel; - @Value("${dashscope.key}") - private String serviceKey; - - /** - * 视觉推理(QVQ) - * @param imageUrl 图片可访问地址 - * @return - */ - @Override - public String qvq(String imageUrl) throws IOException { - try { - // 构建多模态消息 - MultiModalMessage userMessage = MultiModalMessage.builder() - .role(Role.USER.getValue()) - .content(Arrays.asList( - Collections.singletonMap("text", "这张图片有什么"), - Collections.singletonMap("image", imageUrl) - )) - .build(); - - // 构建请求参数 - MultiModalConversationParam param = MultiModalConversationParam.builder() - .apiKey(serviceKey) // 使用配置文件中的API Key - .model(serviceModel) - .message(userMessage) - .build(); - - MultiModalConversation conv = new MultiModalConversation(); - - // 调用API - Flowable result = conv.streamCall( - param); - - StringBuilder reasoningContent = new StringBuilder(); - StringBuilder finalContent = new StringBuilder(); - isFirstPrint = true; - - result.blockingForEach(message -> handleGenerationResult(message, reasoningContent, finalContent)); - - return finalContent.toString().replaceAll("[\n\r\s]", ""); - } catch (Exception e) { - log.error("调用百炼API失败: {}", e.getMessage(), e); - throw new IOException("百炼API调用失败: " + e.getMessage(), e); - } - } - /** - * 视觉推理(QVQ) 使用本地文件(输入Base64编码或本地路径) - * @param localPath 图片文件的绝对路径 - * @return - */ - @Override - public String qvq4LocalPath(String localPath) throws IOException { - try { - // 构建多模态消息 - String filePath = "file://"+ localPath; - log.info("filePath: {}", filePath); - MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue()) - .content(Arrays.asList(new HashMap(){{put("image", filePath);}}, - new HashMap(){{put("text", "这张图片有什么");}})).build(); - - // 构建请求参数 - MultiModalConversationParam param = MultiModalConversationParam.builder() - .apiKey(serviceKey) // 使用配置文件中的API Key - .model(serviceModel) - .message(userMessage) - .build(); - MultiModalConversation conv = new MultiModalConversation(); - - // 调用API - Flowable result = conv.streamCall( - param); - - StringBuilder reasoningContent = new StringBuilder(); - StringBuilder finalContent = new StringBuilder(); - isFirstPrint = true; - - result.blockingForEach(message -> handleGenerationResult(message, reasoningContent, finalContent)); - - return finalContent.toString().replaceAll("[\n\r\s]", ""); - } catch (Exception e) { - log.error("调用百炼API失败: {}", e.getMessage(), e); - throw new IOException("百炼API调用失败: " + e.getMessage(), e); - } - } - - - private void handleGenerationResult(MultiModalConversationResult message, StringBuilder reasoningContent, StringBuilder finalContent) { - String re = message.getOutput().getChoices().get(0).getMessage().getReasoningContent(); - String reasoning = Objects.isNull(re) ? "" : re; - - List> content = message.getOutput().getChoices().get(0).getMessage() - .getContent(); - if (!reasoning.isEmpty()) { - reasoningContent.append(reasoning); - } - - if (Objects.nonNull(content) && !content.isEmpty()) { - Object text = content.get(0).get("text"); - finalContent.append(text); - } - - // 检查是否是最后一个响应 - if (message.getOutput().getChoices().get(0).getFinishReason() != null) { - // 输出思考过程 - if (reasoningContent.length() > 0) { - System.out.println("====================思考过程===================="); - System.out.println(reasoningContent.toString()); - } - // 输出完整回复 - if (finalContent.length() > 0) { - System.out.println("====================完整回复===================="); - System.out.println(finalContent.toString()); - } - } - } -} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/PdfImageExtractServiceImpl.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/PdfImageExtractServiceImpl.java deleted file mode 100644 index 32708439..00000000 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/PdfImageExtractServiceImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.ruoyi.service.impl; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClient.Builder; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import org.ruoyi.domain.PdfFileContentResult; -import org.ruoyi.service.DashscopeService; -import org.ruoyi.service.PdfImageExtractService; -import org.ruoyi.utils.ZipUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -/** - * PDF图片提取服务实现类 - */ -@Service -@Slf4j -@Data -public class PdfImageExtractServiceImpl implements PdfImageExtractService { - - @Value("${pdf.extract.service.url}") - private String serviceUrl; - @Value("${pdf.extract.ai-api.url}") - private String aiApiUrl; - @Value("${pdf.extract.ai-api.key}") - private String aiApiKey; - - private final OkHttpClient client = new Builder() - .connectTimeout(100, TimeUnit.SECONDS) - .readTimeout(150, TimeUnit.SECONDS) - .writeTimeout(150, TimeUnit.SECONDS) - .callTimeout(300, TimeUnit.SECONDS) - .build(); - - private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - -// @Override - public byte[] extractImages(MultipartFile pdfFile, String imageFormat, boolean allowDuplicates) - throws IOException { - // 构建multipart请求 - RequestBody requestBody = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("fileInput", pdfFile.getOriginalFilename(), - RequestBody.create(MediaType.parse("application/pdf"), pdfFile.getBytes())) - .addFormDataPart("format", imageFormat) - .addFormDataPart("allowDuplicates", String.valueOf(allowDuplicates)) - .build(); - - // 创建请求 - Request request = new Request.Builder() - .url(serviceUrl + "/api/v1/misc/extract-images") - .post(requestBody) - .build(); - - // 执行请求 - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) { - throw new IOException("请求失败: " + response.code()); - } - return response.body().bytes(); - } - } - - /** - * 处理文件内容 - * - * @param unzip Base64编码的图片数组 - * @return 文件内容结果列表 - * @throws IOException 如果API调用过程中发生错误 - */ -// @Override - public List dealFileContent(String[] unzip) throws IOException { - List results = new ArrayList<>(); - int i = 0; - for (String base64Image : unzip) { - // 构建请求JSON - String requestJson = String.format("{" - + "\"model\": \"gpt-4o\"," - + "\"stream\": false," - + "\"messages\": [{" - + "\"role\": \"user\"," - + "\"content\": [{" - + "\"type\": \"text\"," - + "\"text\": \"这张图片有什么\"" - + "}, {" - + "\"type\": \"image_url\"," - + "\"image_url\": {" - + "\"url\": \"%s\"" - + "}}" - + "]}]," - + "\"max_tokens\": 400" - + "}", base64Image); - - // 创建请求 - Request request = new Request.Builder() - .url(aiApiUrl) - .addHeader("Authorization", "Bearer " + aiApiKey) - .post(RequestBody.create(JSON, requestJson)) - .build(); - - // 执行请求 - try { - log.info("=============call=" + ++i); - - Response response = client.newCall(request).execute(); - log.info("=============response=" + response); - if (!response.isSuccessful()) { - throw new IOException("API请求失败: " + response.code() + response.toString()); - } - - String responseBody = response.body().string(); - log.info("=============responseBody=" + responseBody); - // 使用文件名(这里使用base64的前10个字符作为标识)和API返回内容创建结果对象 - String filename = base64Image.substring(0, Math.min(base64Image.length(), 10)); - results.add(new PdfFileContentResult(filename, responseBody)); - } catch (Exception e) { - log.error(e.getMessage()); - throw new RuntimeException(e); - } - } - return results; - } - -// @Override - public List extractImages(MultipartFile file) throws IOException { - String format = "png"; - boolean allowDuplicates = true; - // 获取ZIP数据 - byte[] zipData = this.extractImages(file, format, allowDuplicates); - // 解压文件并识别图片内容并返回 - String[] unzip = ZipUtils.unzipForBase64(zipData); - //解析图片内容 - return this.dealFileContent(unzip); - } -} diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 1b3ff0c4..80af4ecd 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -21,6 +21,7 @@ ruoyi-chat ruoyi-system ruoyi-generator + ruoyi-wechat diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java index 38dbd50c..5551d1c4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java @@ -1,6 +1,7 @@ package org.ruoyi.chat.controller.knowledge; import cn.dev33.satoken.stp.StpUtil; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -29,6 +30,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import java.util.Objects; @@ -185,17 +187,4 @@ public class KnowledgeController extends BaseController { return attachService.translationByFile(file, targetLanguage); } - /** - * 提取PDF中的图片并调用gpt-4o-mini,识别图片内容并返回 - * - * @param file PDF文件 - * @return 文件名称和图片内容 - */ - @PostMapping("/extract-images") - @Operation(summary = "提取PDF中的图片并调用大模型,识别图片内容并返回", description = "提取PDF中的图片并调用gpt-4o-mini,识别图片内容并返回") - public R> extractImages( - ) throws IOException { - return R.ok(pdfImageExtractService - .dealFileContent4Dashscope("https://hnzuoran02-1327573163.cos.ap-nanjing.myqcloud.com/crmebimage/public/content/2025/06/04/e115264eb22f423ea0b211709361c29f071avy39ez.jpg")); - } } diff --git a/ruoyi-modules/ruoyi-wechat/pom.xml b/ruoyi-modules/ruoyi-wechat/pom.xml new file mode 100644 index 00000000..7e3cdfe3 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + org.ruoyi + ruoyi-modules + ${revision} + ../pom.xml + + + ruoyi-wechat + + + 17 + 17 + UTF-8 + + + + + org.ruoyi + ruoyi-chat-api + + + org.ruoyi + ruoyi-system-api + + + + diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/AbstractBuilder.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/AbstractBuilder.java new file mode 100644 index 00000000..6e97f5d1 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/AbstractBuilder.java @@ -0,0 +1,12 @@ +package org.ruoyi.builder; + +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; + +/** + * @author Binary Wang + */ +public abstract class AbstractBuilder { + public abstract WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, WxCpService service); +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/ImageBuilder.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/ImageBuilder.java new file mode 100644 index 00000000..f6786b01 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/ImageBuilder.java @@ -0,0 +1,24 @@ +package org.ruoyi.builder; + +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutImageMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; + +/** + * @author Binary Wang + */ +public class ImageBuilder extends AbstractBuilder { + + @Override + public WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, + WxCpService service) { + + WxCpXmlOutImageMessage m = WxCpXmlOutMessage.IMAGE().mediaId(content) + .fromUser(wxMessage.getToUserName()).toUser(wxMessage.getFromUserName()) + .build(); + + return m; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/TextBuilder.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/TextBuilder.java new file mode 100644 index 00000000..707471e8 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/builder/TextBuilder.java @@ -0,0 +1,22 @@ +package org.ruoyi.builder; + +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutTextMessage; + +/** + * @author Binary Wang + */ +public class TextBuilder extends AbstractBuilder { + + @Override + public WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, + WxCpService service) { + WxCpXmlOutTextMessage m = WxCpXmlOutMessage.TEXT().content(content) + .fromUser(wxMessage.getToUserName()).toUser(wxMessage.getFromUserName()) + .build(); + return m; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpConfiguration.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpConfiguration.java new file mode 100644 index 00000000..8a5f0fba --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpConfiguration.java @@ -0,0 +1,130 @@ +package org.ruoyi.config; + + +import com.google.common.collect.Maps; +import jakarta.annotation.PostConstruct; +import lombok.val; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.constant.WxCpConsts; +import me.chanjar.weixin.cp.message.WxCpMessageHandler; +import me.chanjar.weixin.cp.message.WxCpMessageRouter; +import org.ruoyi.handler.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 单实例配置 + * + * @author Binary Wang + */ +@Configuration +@EnableConfigurationProperties(WxCpProperties.class) +public class WxCpConfiguration { + private final LogHandler logHandler; + private NullHandler nullHandler; + private LocationHandler locationHandler; + private MenuHandler menuHandler; + private MsgHandler msgHandler; + private final UnsubscribeHandler unsubscribeHandler; + private SubscribeHandler subscribeHandler; + private WxCpProperties properties; + + private static Map routers = Maps.newHashMap(); + private static Map cpServices = Maps.newHashMap(); + + @Autowired + public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler, + MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler, + SubscribeHandler subscribeHandler, WxCpProperties properties) { + this.logHandler = logHandler; + this.nullHandler = nullHandler; + this.locationHandler = locationHandler; + this.menuHandler = menuHandler; + this.msgHandler = msgHandler; + this.unsubscribeHandler = unsubscribeHandler; + this.subscribeHandler = subscribeHandler; + this.properties = properties; + } + + + public static Map getRouters() { + return routers; + } + + public static WxCpService getCpService(Integer agentId) { + return cpServices.get(agentId); + } + + @PostConstruct + public void initServices() { + cpServices = this.properties.getAppConfigs().stream().map(a -> { + val configStorage = new WxCpDefaultConfigImpl(); + configStorage.setCorpId(this.properties.getCorpId()); + configStorage.setAgentId(a.getAgentId()); + configStorage.setCorpSecret(a.getSecret()); + configStorage.setToken(a.getToken()); + configStorage.setAesKey(a.getAesKey()); + val service = new WxCpServiceImpl(); + service.setWxCpConfigStorage(configStorage); + routers.put(a.getAgentId(), this.newRouter(service)); + return service; + }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a)); + } + + private WxCpMessageRouter newRouter(WxCpService wxCpService) { + final val newRouter = new WxCpMessageRouter(wxCpService); + + // 记录所有事件的日志 (异步执行) + newRouter.rule().handler(this.logHandler).next(); + + // 自定义菜单事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end(); + + // 点击菜单链接事件(这里使用了一个空的处理器,可以根据自己需要进行扩展) + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end(); + + // 关注事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler) + .end(); + + // 取消关注事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.UNSUBSCRIBE) + .handler((WxCpMessageHandler) this.unsubscribeHandler).end(); + + // 上报地理位置事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.LOCATION).handler(this.locationHandler) + .end(); + + // 接收地理位置消息 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION) + .handler(this.locationHandler).end(); + + // 扫码事件(这里使用了一个空的处理器,可以根据自己需要进行扩展) + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SCAN).handler((WxCpMessageHandler) this.nullHandler).end(); + + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end(); + + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end(); + + // 默认 + newRouter.rule().async(false).handler(this.msgHandler).end(); + + return newRouter; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java new file mode 100644 index 00000000..05884475 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/config/WxCpProperties.java @@ -0,0 +1,48 @@ +package org.ruoyi.config; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * @author Binary Wang + */ +@Data +@ConfigurationProperties(prefix = "wechat.cp") +public class WxCpProperties { + /** + * 设置企业微信的corpId + */ + private String corpId; + + private List appConfigs; + + @Getter + @Setter + public static class AppConfig { + /** + * 设置企业微信应用的AgentId + */ + private Integer agentId; + + /** + * 设置企业微信应用的Secret + */ + private String secret; + + /** + * 设置企业微信应用的token + */ + private String token; + + /** + * 设置企业微信应用的EncodingAESKey + */ + private String aesKey; + + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java new file mode 100644 index 00000000..370e5fe5 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinServerController.java @@ -0,0 +1,51 @@ +package org.ruoyi.controller; + + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.service.WeixinUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 企业微信应用 + * + * @author ageerle + * @date 2025-05-03 + */ +@Slf4j +@RestController +public class WeixinServerController { + + @Autowired + private WeixinUserService weixinUserService; + + @GetMapping(value = "/weixin/check") + public String weixinCheck(HttpServletRequest request) { + String signature = request.getParameter("signature"); + String timestamp = request.getParameter("timestamp"); + String nonce = request.getParameter("nonce"); + String echostr = request.getParameter("echostr"); + + if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce)) { + return ""; + } + weixinUserService.checkSignature(signature, timestamp, nonce); + return echostr; + } + + @PostMapping(value = "/weixin/check") + public String weixinMsg(@RequestBody String requestBody, @RequestParam("signature") String signature, + @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce) { + + log.debug("requestBody:{}", requestBody); + log.debug("signature:{}", signature); + log.debug("timestamp:{}", timestamp); + log.debug("nonce:{}", nonce); + + weixinUserService.checkSignature(signature, timestamp, nonce); + return weixinUserService.handleWeixinMsg(requestBody); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java new file mode 100644 index 00000000..4537488b --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WeixinUserController.java @@ -0,0 +1,55 @@ +package org.ruoyi.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.domin.WeixinQrCode; +import org.ruoyi.service.VxLoginService; +import org.ruoyi.system.domain.vo.LoginVo; +import org.ruoyi.util.WeixinApiUtil; +import org.ruoyi.util.WeixinQrCodeCacheUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 微信公众号登录 + * + * @author ageerle + * @date 2025-05-03 + */ +@Slf4j +@RestController +public class WeixinUserController { + + @Autowired + private WeixinApiUtil weixinApiUtil; + + @Autowired + private VxLoginService loginService; + + @GetMapping(value = "/user/qrcode") + public R getQrCode() { + WeixinQrCode qrCode = weixinApiUtil.getQrCode(); + qrCode.setUrl(null); + qrCode.setExpireSeconds(null); + return R.ok(qrCode); + } + + /** + * 校验是否扫描完成 + * 完成,返回 JWT + * 未完成,返回 check failed + */ + @GetMapping(value = "/user/login/qrcode") + public R userLogin(String ticket) { + String openId = WeixinQrCodeCacheUtil.get(ticket); + if (StringUtils.isNotEmpty(openId)) { + log.info("login success,open id:{}", openId); + LoginVo loginVo = loginService.mpLogin(openId); + return R.ok(loginVo); + } + log.info("login error,ticket:{}", ticket); + return R.fail("check failed"); + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java new file mode 100644 index 00000000..676333f3 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/controller/WxPortalController.java @@ -0,0 +1,89 @@ +package org.ruoyi.controller; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.core.utils.JsonUtils; + +import org.ruoyi.config.WxCpConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +/** + * 微信公众号登录校验 + * + * @author ageerle + * @date 2025-05-03 + */ +@RestController +@RequestMapping("/wx/cp") +@Slf4j +public class WxPortalController { + + @Value("${wechat.cp.appConfigs[0].agentId}") + private Integer agentId; + + + @GetMapping(produces = "text/plain;charset=utf-8") + public String authGet( + @RequestParam(name = "msg_signature", required = false) String signature, + @RequestParam(name = "timestamp", required = false) String timestamp, + @RequestParam(name = "nonce", required = false) String nonce, + @RequestParam(name = "echostr", required = false) String echostr) { + log.info("\n接收到来自微信服务器的认证消息:signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]", + signature, timestamp, nonce, echostr); + + if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) { + throw new IllegalArgumentException("请求参数非法,请核实!"); + } + + final WxCpService wxCpService = WxCpConfiguration.getCpService(agentId); + if (wxCpService == null) { + throw new IllegalArgumentException(String.format("未找到对应agentId=[%d]的配置,请核实!", agentId)); + } + + if (wxCpService.checkSignature(signature, timestamp, nonce, echostr)) { + return new WxCpCryptUtil(wxCpService.getWxCpConfigStorage()).decrypt(echostr); + } + + return "非法请求"; + } + + @PostMapping(produces = "application/xml; charset=UTF-8") + public String post( + @RequestBody String requestBody, + @RequestParam("msg_signature") String signature, + @RequestParam("timestamp") String timestamp, + @RequestParam("nonce") String nonce) { + log.info("\n接收微信请求:[signature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ", + signature, timestamp, nonce, requestBody); + + final WxCpService wxCpService = WxCpConfiguration.getCpService(1000002); + WxCpXmlMessage inMessage = WxCpXmlMessage.fromEncryptedXml(requestBody, wxCpService.getWxCpConfigStorage(), + timestamp, nonce, signature); + log.debug("\n消息解密后内容为:\n{} ", JsonUtils.toJson(inMessage)); + WxCpXmlOutMessage outMessage = this.route(1000002, inMessage); + if (outMessage == null) { + return ""; + } + + String out = outMessage.toEncryptedXml(wxCpService.getWxCpConfigStorage()); + log.debug("\n组装回复信息:{}", out); + return out; + } + + private WxCpXmlOutMessage route(Integer agentId, WxCpXmlMessage message) { + try { + return WxCpConfiguration.getRouters().get(agentId).route(message); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; + } + + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java new file mode 100644 index 00000000..9bc6dcb9 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/ReceiveMessage.java @@ -0,0 +1,58 @@ +package org.ruoyi.domin; + +import lombok.Data; + +@Data +public class ReceiveMessage { + /** + * 开发者微信号 + */ + private String toUserName; + /** + * 发送方账号(一个openid) + */ + private String fromUserName; + /** + * 消息创建时间(整形) + */ + private String createTime; + /** + * 消息类型 + */ + private String msgType; + /** + * 文本消息内容 + */ + private String content; + /** + * 消息ID 64位 + */ + String msgId; + /** + * 消息的数据ID 消息来自文章才有 + */ + private String msgDataId; + /** + * 多图文时第几篇文章,从1开始 消息如果来自文章才有 + */ + private String idx; + /** + * 订阅事件 subscribe 订阅 unsbscribe 取消订阅 + */ + private String event; + /** + * 扫码 - ticket + */ + private String ticket; + + public String getReplyTextMsg(String msg) { + String xml = "\n" + + " \n" + + " \n" + + " " + System.currentTimeMillis() + "\n" + + " \n" + + " \n" + + " "; + return xml; + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java new file mode 100644 index 00000000..0363b3bf --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/domin/WeixinQrCode.java @@ -0,0 +1,15 @@ +package org.ruoyi.domin; + +import lombok.Data; + +/** + * @author https://www.wdbyte.com + */ +@Data +public class WeixinQrCode { + + private String ticket; + private Long expireSeconds; + private String url; + private String qrCodeUrl; +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/AbstractHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/AbstractHandler.java new file mode 100644 index 00000000..7ec2b0ce --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/AbstractHandler.java @@ -0,0 +1,9 @@ +package org.ruoyi.handler; + +import me.chanjar.weixin.cp.message.WxCpMessageHandler; + +/** + * @author Binary Wang + */ +public abstract class AbstractHandler implements WxCpMessageHandler { +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ContactChangeHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ContactChangeHandler.java new file mode 100644 index 00000000..178229db --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ContactChangeHandler.java @@ -0,0 +1,34 @@ +package org.ruoyi.handler; + + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.ruoyi.builder.TextBuilder; +import org.ruoyi.common.core.utils.JsonUtils; + +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 通讯录变更事件处理器. + * + * @author Binary Wang + */ +@Slf4j +@Component +public class ContactChangeHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + String content = "收到通讯录变更事件,内容:" + JsonUtils.toJson(wxMessage); + log.info(content); + + return new TextBuilder().build(content, wxMessage, cpService); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/EnterAgentHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/EnterAgentHandler.java new file mode 100644 index 00000000..c98fe68a --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/EnterAgentHandler.java @@ -0,0 +1,29 @@ +package org.ruoyi.handler; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; + +import java.util.Map; + +/** + *
+ *
+ * Created by Binary Wang on 2018/8/27.
+ * 
+ * + * @author Binary Wang + */ +@Slf4j +public class EnterAgentHandler extends AbstractHandler { + private static final int TEST_AGENT = 1000002; + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService wxCpService, WxSessionManager sessionManager) throws WxErrorException { + // do something + return null; + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LocationHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LocationHandler.java new file mode 100644 index 00000000..56d1fb34 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LocationHandler.java @@ -0,0 +1,44 @@ +package org.ruoyi.handler; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.ruoyi.builder.TextBuilder; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Slf4j +@Component +public class LocationHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.LOCATION)) { + //TODO 接收处理用户发送的地理位置消息 + try { + String content = "感谢反馈,您的的地理位置已收到!"; + return new TextBuilder().build(content, wxMessage, null); + } catch (Exception e) { + log.error("位置消息接收处理失败", e); + return null; + } + } + + //上报地理位置事件 + log.info("\n上报地理位置,纬度 : {}\n经度 : {}\n精度 : {}", + wxMessage.getLatitude(), wxMessage.getLongitude(), String.valueOf(wxMessage.getPrecision())); + + //TODO 可以将用户地理位置信息保存到本地数据库,以便以后使用 + + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LogHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LogHandler.java new file mode 100644 index 00000000..ea12c40d --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/LogHandler.java @@ -0,0 +1,26 @@ +package org.ruoyi.handler; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.ruoyi.common.core.utils.JsonUtils; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Slf4j +@Component +public class LogHandler extends AbstractHandler { + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + log.info("\n接收到请求消息,内容:{}", JsonUtils.toJson(wxMessage)); + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MenuHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MenuHandler.java new file mode 100644 index 00000000..803db7fa --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MenuHandler.java @@ -0,0 +1,34 @@ +package org.ruoyi.handler; + +import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Component +public class MenuHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + + String msg = String.format("type:%s, event:%s, key:%s", + wxMessage.getMsgType(), wxMessage.getEvent(), + wxMessage.getEventKey()); + if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) { + return null; + } + + return WxCpXmlOutMessage.TEXT().content(msg) + .fromUser(wxMessage.getToUserName()).toUser(wxMessage.getFromUserName()) + .build(); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MsgHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MsgHandler.java new file mode 100644 index 00000000..6527371e --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/MsgHandler.java @@ -0,0 +1,43 @@ +package org.ruoyi.handler; + +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; + +import org.ruoyi.builder.TextBuilder; +import org.ruoyi.service.IChatVxService; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Component +@RequiredArgsConstructor +public class MsgHandler extends AbstractHandler { + + private final IChatVxService chatVxService; + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + final String msgType = wxMessage.getMsgType(); + if (msgType == null) { + // 如果msgType没有,就自己根据具体报文内容做处理 + } + + if (!msgType.equals(WxConsts.XmlMsgType.EVENT)) { + //TODO 可以选择将消息保存到本地 + } + //TODO 组装回复消息 + String content = chatVxService.chat(wxMessage.getContent()); + + return new TextBuilder().build(content, wxMessage, cpService); + + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/NullHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/NullHandler.java new file mode 100644 index 00000000..2677d73d --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/NullHandler.java @@ -0,0 +1,23 @@ +package org.ruoyi.handler; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Component +public class NullHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ScanHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ScanHandler.java new file mode 100644 index 00000000..4d9a5025 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/ScanHandler.java @@ -0,0 +1,8 @@ +package org.ruoyi.handler; + +/** + * @author Binary Wang + */ +public abstract class ScanHandler extends AbstractHandler { + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/SubscribeHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/SubscribeHandler.java new file mode 100644 index 00000000..c5e42fb4 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/SubscribeHandler.java @@ -0,0 +1,63 @@ +package org.ruoyi.handler; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpUser; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.ruoyi.builder.TextBuilder; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Slf4j +@Component +public class SubscribeHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) throws WxErrorException { + + log.info("新关注用户 OPENID: " + wxMessage.getFromUserName()); + + // 获取微信用户基本信息 + WxCpUser userWxInfo = cpService.getUserService().getById(wxMessage.getFromUserName()); + + if (userWxInfo != null) { + // TODO 可以添加关注用户到本地 + } + + WxCpXmlOutMessage responseResult = null; + try { + responseResult = handleSpecial(wxMessage); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + if (responseResult != null) { + return responseResult; + } + + try { + return new TextBuilder().build("感谢关注", wxMessage, cpService); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; + } + + /** + * 处理特殊请求,比如如果是扫码进来的,可以做相应处理 + */ + private WxCpXmlOutMessage handleSpecial(WxCpXmlMessage wxMessage) { + //TODO + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/UnsubscribeHandler.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/UnsubscribeHandler.java new file mode 100644 index 00000000..77933460 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/handler/UnsubscribeHandler.java @@ -0,0 +1,28 @@ +package org.ruoyi.handler; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author Binary Wang + */ +@Slf4j +@Component +public class UnsubscribeHandler extends AbstractHandler { + + @Override + public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService cpService, + WxSessionManager sessionManager) { + String openId = wxMessage.getFromUserName(); + log.info("取消关注用户 OPENID: " + openId); + // TODO 可以更新本地数据库为取消关注状态 + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java new file mode 100644 index 00000000..874a8bf9 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/VxLoginService.java @@ -0,0 +1,108 @@ +package org.ruoyi.service; + +import cn.dev33.satoken.secure.BCrypt; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.ruoyi.common.core.constant.Constants; +import org.ruoyi.common.core.domain.model.VisitorLoginUser; +import org.ruoyi.common.core.enums.DeviceType; +import org.ruoyi.common.core.enums.UserType; +import org.ruoyi.common.core.service.ConfigService; +import org.ruoyi.common.core.utils.MessageUtils; +import org.ruoyi.common.core.utils.ServletUtils; +import org.ruoyi.common.core.utils.SpringUtils; +import org.ruoyi.common.log.event.LogininforEvent; +import org.ruoyi.common.satoken.utils.LoginHelper; +import org.ruoyi.system.domain.SysUser; +import org.ruoyi.system.domain.bo.SysUserBo; +import org.ruoyi.system.domain.vo.LoginVo; +import org.ruoyi.system.domain.vo.SysUserVo; +import org.ruoyi.system.service.ISysUserService; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** + * 微信公众号登录 + * + * @author ageerle@163.com + * date 2025/4/30 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VxLoginService { + + private final ISysUserService userService; + + private final ConfigService configService; + + public LoginVo mpLogin(String openid) { + // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户 + SysUserVo user = userService.selectUserByOpenId(openid); + VisitorLoginUser loginUser = new VisitorLoginUser(); + if (ObjectUtil.isNull(user)) { + SysUserBo sysUser = new SysUserBo(); + String name = "用户" + UUID.randomUUID().toString().replace("-", ""); + // 设置默认用户名 + sysUser.setUserName(name); + // 设置默认昵称 + sysUser.setNickName(name); + // 设置默认密码 + sysUser.setPassword(BCrypt.hashpw("123456")); + // 设置微信openId + sysUser.setOpenId(openid); + String configValue = configService.getConfigValue("mail", "amount"); + // 设置默认余额 + sysUser.setUserBalance(NumberUtils.toDouble(configValue, 1)); + // 注册用户,设置默认租户为0 + SysUser registerUser = userService.registerUser(sysUser, "0"); + + // 构建登录用户信息 + loginUser.setTenantId("0"); + loginUser.setUserId(registerUser.getUserId()); + loginUser.setUsername(registerUser.getUserName()); + loginUser.setUserType(UserType.APP_USER.getUserType()); + loginUser.setOpenid(openid); + loginUser.setNickName(registerUser.getNickName()); + } else { + // 此处可根据登录用户的数据不同 自行创建 loginUser + loginUser.setTenantId(user.getTenantId()); + loginUser.setUserId(user.getUserId()); + loginUser.setUsername(user.getUserName()); + loginUser.setUserType(user.getUserType()); + loginUser.setNickName(user.getNickName()); + loginUser.setAvatar(user.getWxAvatar()); + loginUser.setOpenid(openid); + } + // 生成token + LoginHelper.loginByDevice(loginUser, DeviceType.XCX); + recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")); + LoginVo loginVo = new LoginVo(); + // 生成令牌 + loginVo.setToken(StpUtil.getTokenValue()); + loginVo.setUserInfo(loginUser); + return loginVo; + } + + /** + * 记录登录信息 + * + * @param tenantId 租户ID + * @param username 用户名 + * @param status 状态 + * @param message 消息内容 + */ + private void recordLogininfor(String tenantId, String username, String status, String message) { + LogininforEvent logininforEvent = new LogininforEvent(); + logininforEvent.setTenantId(tenantId); + logininforEvent.setUsername(username); + logininforEvent.setStatus(status); + logininforEvent.setMessage(message); + logininforEvent.setRequest(ServletUtils.getRequest()); + SpringUtils.context().publishEvent(logininforEvent); + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java new file mode 100644 index 00000000..c9996791 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/WeixinUserService.java @@ -0,0 +1,10 @@ +package org.ruoyi.service; + + +public interface WeixinUserService { + + void checkSignature(String signature, String timestamp, String nonce); + + String handleWeixinMsg(String body); + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java new file mode 100644 index 00000000..57fcdfb7 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/service/impl/WeixinUserServiceImpl.java @@ -0,0 +1,65 @@ +package org.ruoyi.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.ruoyi.domin.ReceiveMessage; +import org.ruoyi.service.WeixinUserService; +import org.ruoyi.util.WeixinMsgUtil; +import org.ruoyi.util.WeixinQrCodeCacheUtil; +import org.springframework.stereotype.Service; + +import java.util.Arrays; + + +@Slf4j +@Service +public class WeixinUserServiceImpl implements WeixinUserService { + + private String token = "panda"; + + @Override + public void checkSignature(String signature, String timestamp, String nonce) { + String[] arr = new String[] {token, timestamp, nonce}; + Arrays.sort(arr); + StringBuilder content = new StringBuilder(); + for (String str : arr) { + content.append(str); + } + String tmpStr = DigestUtils.sha1Hex(content.toString()); + if (tmpStr.equals(signature)) { + log.info("check success"); + return; + } + log.error("check fail"); + throw new RuntimeException("check fail"); + } + + @Override + public String handleWeixinMsg(String requestBody) { + ReceiveMessage receiveMessage = WeixinMsgUtil.msgToReceiveMessage(requestBody); + // 扫码登录 + if (WeixinMsgUtil.isScanQrCode(receiveMessage)) { + return handleScanLogin(receiveMessage); + } + // 关注 + if (WeixinMsgUtil.isEventAndSubscribe(receiveMessage)) { + return receiveMessage.getReplyTextMsg("感谢您的关注!"); + } + return receiveMessage.getReplyTextMsg("收到(自动回复)"); + } + + /** + * 处理扫码登录 + * + * @param receiveMessage + * @return + */ + private String handleScanLogin(ReceiveMessage receiveMessage) { + String qrCodeTicket = WeixinMsgUtil.getQrCodeTicket(receiveMessage); + if (WeixinQrCodeCacheUtil.get(qrCodeTicket) == null) { + String openId = receiveMessage.getFromUserName(); + WeixinQrCodeCacheUtil.put(qrCodeTicket, openId); + } + return receiveMessage.getReplyTextMsg("你已成功登录!"); + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java new file mode 100644 index 00000000..6bbf917f --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/KeyUtils.java @@ -0,0 +1,24 @@ +package org.ruoyi.util; + +import org.apache.commons.lang3.RandomStringUtils; + +import java.util.UUID; + +/** + * @author https://www.wdbyte.com + */ +public class KeyUtils { + + public synchronized static String key6() { + return RandomStringUtils.randomAlphanumeric(6); + } + + public synchronized static String key16() { + return RandomStringUtils.randomAlphanumeric(16); + } + + public static String uuid32() { + return UUID.randomUUID().toString().replace("-", ""); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java new file mode 100644 index 00000000..1aa9c395 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java @@ -0,0 +1,81 @@ +package org.ruoyi.util; + +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.service.ConfigService; +import org.ruoyi.domin.WeixinQrCode; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.time.LocalDateTime; + +/** + * @author https://www.wdbyte.com + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class WeixinApiUtil { + + private final ConfigService configService; + + private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="; + + private static String ACCESS_TOKEN = null; + private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null; + /** + * 二维码 Ticket 过期时间 + */ + private static int QR_CODE_TICKET_TIMEOUT = 10 * 60; + + /** + * 获取 access token + * + * @return + */ + public synchronized String getAccessToken() { + if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) { + return ACCESS_TOKEN; + } + String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + getKey("appid") + "&secret=" + + getKey("secret"); + String result = HttpUtil.get(api); + JSONObject jsonObject = JSON.parseObject(result); + ACCESS_TOKEN = jsonObject.getString("access_token"); + ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10); + return ACCESS_TOKEN; + } + + /** + * 获取二维码 Ticket + * + * https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html + * + * @return + */ + public WeixinQrCode getQrCode() { + String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken(); + String jsonBody = String.format("{\n" + + " \"expire_seconds\": %d,\n" + + " \"action_name\": \"QR_STR_SCENE\",\n" + + " \"action_info\": {\n" + + " \"scene\": {\n" + + " \"scene_str\": \"%s\"\n" + + " }\n" + + " }\n" + + "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32()); + String result = HttpUtil.post(api, jsonBody); + log.info("get qr code params:{}", jsonBody); + log.info("get qr code result:{}", result); + WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class); + weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString()); + return weixinQrCode; + } + + public String getKey(String key) { + return configService.getConfigValue("weixin", key); + } +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java new file mode 100644 index 00000000..e1fd520c --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinMsgUtil.java @@ -0,0 +1,66 @@ +package org.ruoyi.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.domin.ReceiveMessage; + +/** + * @author https://www.wdbyte.com + */ +public class WeixinMsgUtil { + + // 事件-关注 + private static String EVENT_SUBSCRIBE = "subscribe"; + + /** + * 微信消息转对象 + * + * @param xml + * @return + */ + public static ReceiveMessage msgToReceiveMessage(String xml) { + JSONObject jsonObject = JSON.parseObject(XmlUtil.xml2json(xml)); + ReceiveMessage receiveMessage = new ReceiveMessage(); + receiveMessage.setToUserName(jsonObject.getString("ToUserName")); + receiveMessage.setFromUserName(jsonObject.getString("FromUserName")); + receiveMessage.setCreateTime(jsonObject.getString("CreateTime")); + receiveMessage.setMsgType(jsonObject.getString("MsgType")); + receiveMessage.setContent(jsonObject.getString("Content")); + receiveMessage.setMsgId(jsonObject.getString("MsgId")); + receiveMessage.setEvent(jsonObject.getString("Event")); + receiveMessage.setTicket(jsonObject.getString("Ticket")); + return receiveMessage; + } + + /** + * 是否是订阅事件 + * + * @param receiveMessage + * @return + */ + public static boolean isEventAndSubscribe(ReceiveMessage receiveMessage) { + return StringUtils.equals(receiveMessage.getEvent(), EVENT_SUBSCRIBE); + } + + /** + * 是否是二维码扫描事件 + * + * @param receiveMessage + * @return + */ + public static boolean isScanQrCode(ReceiveMessage receiveMessage) { + return StringUtils.isNotEmpty(receiveMessage.getTicket()); + } + + /** + * 获取扫描的二维码 Ticket + * + * @param receiveMessage + * @return + */ + public static String getQrCodeTicket(ReceiveMessage receiveMessage) { + return receiveMessage.getTicket(); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java new file mode 100644 index 00000000..aaebd1e9 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinQrCodeCacheUtil.java @@ -0,0 +1,34 @@ +package org.ruoyi.util; + +import java.util.LinkedHashMap; + +/** + * 微信二维码缓存工具类 + * + * @author https://www.wdbyte.com + */ +public class WeixinQrCodeCacheUtil { + private static long MAX_CACHE_SIZE = 10000; + private static LinkedHashMap QR_CODE_TICKET_MAP = new LinkedHashMap<>(); + + /** + * 增加一个 Ticket + * 首次 put:value 为 "" + * 再次 put: value 有 openId,若openId已经存在,则已被扫码 + * + * @param key + * @param value + */ + public synchronized static void put(String key, String value) { + QR_CODE_TICKET_MAP.put(key, value); + if (QR_CODE_TICKET_MAP.size() > MAX_CACHE_SIZE) { + String first = QR_CODE_TICKET_MAP.keySet().stream().findFirst().get(); + QR_CODE_TICKET_MAP.remove(first); + } + } + + public synchronized static String get(String key) { + return QR_CODE_TICKET_MAP.remove(key); + } + +} diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java new file mode 100644 index 00000000..d6e58f69 --- /dev/null +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/XmlUtil.java @@ -0,0 +1,28 @@ +package org.ruoyi.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * @author https://www.wdbyte.com + */ +@Slf4j +public class XmlUtil { + + public static String xml2json(String requestBody) { + requestBody = StringUtils.trim(requestBody); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode node = null; + try { + node = xmlMapper.readTree(requestBody.getBytes()); + ObjectMapper jsonMapper = new ObjectMapper(); + return jsonMapper.writeValueAsString(node); + } catch (Exception e) { + log.error("xml 2 json error,msg:" + e.getMessage(), e); + } + return null; + } +}