15 Commits

Author SHA1 Message Date
ageerle
a33159f9a3 移除不必要配置 2025-09-19 14:53:35 +08:00
ageerle
6462752fd6 恢复微信模块,优化知识库切片功能 2025-09-19 14:50:02 +08:00
ageerle
afc1272ff5 恢复微信模块,优化知识库切片功能 2025-09-19 11:15:37 +08:00
ageerle
fa5dc80a93 Merge remote-tracking branch 'origin/mineru/dev'
# Conflicts:
#	ruoyi-admin/src/main/resources/application-dev.yml
#	ruoyi-admin/src/main/resources/application-prod.yml
#	ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java
2025-09-19 10:59:52 +08:00
ageerle
acc2d5d1a8 Merge remote-tracking branch 'origin/pdf-image'
# Conflicts:
#	ruoyi-admin/src/main/resources/application-dev.yml
#	ruoyi-admin/src/main/resources/application-prod.yml
#	ruoyi-modules-api/ruoyi-knowledge-api/pom.xml
#	ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/PdfImageExtractService.java
#	ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/PdfImageExtractServiceImpl.java
#	ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/DealFileService.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java
#	script/sql/update/202505141010.sql
2025-09-19 10:56:26 +08:00
zhouweiyi
6a73e09ac7 refactor(DashscopeServiceImpl): 移除静态变量并改进日志输出逻辑
将静态变量改为实例变量以避免并发问题
重构日志输出逻辑,仅在最后响应时输出完整内容
添加异常堆栈打印以方便调试
2025-06-09 18:01:51 +08:00
zhouweiyi
cbe882af66 chore: 更新百炼模型配置的API密钥
将开发环境和本地环境的百炼模型配置中的API密钥统一更新为占位符'sk-xxxx'
2025-06-04 18:02:26 +08:00
zhouweiyi
1d51a103d0 feat: 集成阿里百炼API实现图片内容识别功能
添加DashscopeService接口及实现,用于调用阿里百炼API进行图片内容识别
修改PdfImageExtractService增加基于百炼API的图片处理逻辑
新增OSS服务方法支持临时文件处理和删除
更新配置文件添加百炼模型相关配置
2025-06-04 17:55:47 +08:00
张鹏翔
0f82711199 perf: 1.优化文件输出路径,避免其他盘符权限问题。
2.采用线程池异步调用多模板OCR图片识别。
2025-05-22 14:05:21 +08:00
ageerle
ed85fef0de Merge pull request #97 from zhangpengxiang/mineru/dev
feat:结合mineru新增pdf转换结构化数据功能
2025-05-21 14:42:47 +08:00
张鹏翔
22d9d9ba85 新增pdf转md后是否进行图片OCR判断 2025-05-21 14:37:51 +08:00
张鹏翔
86825eeb2e 结合mineru新增pdf转换结构化数据功能 2025-05-21 14:25:44 +08:00
ageerle
53e3180658 Merge pull request #93 from janzhou123/pdf-image
feat:增加knowledge_attach_pic表结构sql
2025-05-20 09:29:40 +08:00
zhouweiyi
e43e14454d feat:增加knowledge_attach_pic表结构sql 2025-05-20 09:22:41 +08:00
ageerle
a4e995d46c feat: 新增pdf图片解析分支 2025-05-19 15:33:29 +08:00
46 changed files with 2156 additions and 4 deletions

View File

@@ -40,6 +40,8 @@
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.26</lombok.version>
<bouncycastle.version>1.72</bouncycastle.version>
<!-- Apache Commons Compress 版本 -->
<commons-compress.version>1.26.2</commons-compress.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 -->
@@ -283,6 +285,13 @@
<version>${bouncycastle.version}</version>
</dependency>
<!-- Apache Commons Compress -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>

View File

@@ -85,4 +85,22 @@ sms:
# 腾讯专用
sdkAppId:
pdf:
extract:
service:
url: http://localhost:8080
ai-api:
url: https://api.pandarobot.chat/v1/chat/completions
key: sk-xxxx
transition:
# 是否开启mineru
enable-minerU: true
# mineru conda环境路径
conda-env-path: "F:\\ProgramData\\Computer\\Anaconda\\envs\\mineru"
# 是否开启图片OCR
enable-ocr: true
#百炼模型配置
dashscope:
key: sk-xxxx
model: qvq-max

View File

@@ -0,0 +1,111 @@
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: root
password: root
hikari:
# 最大连接池数量
maxPoolSize: 20
# 最小空闲线程数量
minIdle: 10
# 配置获取连接等待超时的时间
connectionTimeout: 30000
# 校验超时时间
validationTimeout: 5000
# 空闲连接存活最大时间默认10分钟
idleTimeout: 600000
# 此属性控制池中连接的最长生命周期值0表示无限生命周期默认30分钟
maxLifetime: 1800000
# 连接测试query配置检测连接是否有效
connectionTestQuery: SELECT 1
# 多久检查一次连接的活性
keepaliveTime: 30000
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
redis:
# 地址
host: 127.0.0.1
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码(如没有密码请注释掉)
password: root
# 连接超时时间
timeout: 10S
# 是否开启ssl
ssl: false
redisson:
# redis key前缀
keyPrefix:
# 线程池数量
threads: 4
# Netty线程池数量
nettyThreads: 8
# 单节点配置
singleServerConfig:
# 客户端名称
clientName: ${ruoyi.name}
# 最小空闲连接数
connectionMinimumIdleSize: 8
# 连接池大小
connectionPoolSize: 32
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
--- # sms 短信
sms:
enabled: false
# 阿里云 dysmsapi.aliyuncs.com
# 腾讯云 sms.tencentcloudapi.com
endpoint: "dysmsapi.aliyuncs.com"
accessKeyId: xxxxxxx
accessKeySecret: xxxxxx
signName: 测试
# 腾讯专用
sdkAppId:
pdf:
extract:
service:
url: http://localhost:8080
ai-api:
url: https://api.pandarobot.chat/v1/chat/completions
key: sk-xxxx
#百炼模型配置
dashscope:
key: sk-xxxx
model: qvq-max

View File

@@ -15,6 +15,7 @@
<modules>
<module>ruoyi-mcp-server</module>
<module>ruoyi-ai-copilot</module>
</modules>
</project>

View File

@@ -87,8 +87,26 @@
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-tika</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 显式指定兼容的 commons-compress 版本 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<String> getChunkList(String content, String kid) {
return textSplitter.split(content, kid);
}
}

View File

@@ -0,0 +1,449 @@
package org.ruoyi.chain.loader;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentParser;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiChatModel;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.ruoyi.chain.split.TextSplitter;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.core.utils.file.FileUtils;
import org.ruoyi.common.oss.core.OssClient;
import org.ruoyi.common.oss.entity.UploadResult;
import org.ruoyi.common.oss.factory.OssFactory;
import org.ruoyi.config.properties.PdfProperties;
import org.ruoyi.system.domain.SysOss;
import org.ruoyi.system.mapper.SysOssMapper;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Pdf mineru文件加载器
*
* @author zpx
*/
@Slf4j
@Component
@AllArgsConstructor
public class PdfMinerUFileLoader implements ResourceLoader {
private final TextSplitter characterTextSplitter;
private final PdfProperties properties;
private final SysOssMapper sysOssMapper;
// 预编译正则表达式
private static final Pattern MD_IMAGE_PATTERN = Pattern.compile("!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)");
// OCR图片识别线程池
private final ThreadPoolExecutor ocrExecutor = new ThreadPoolExecutor(
// 核心线程数
5,
// 最大线程数
10,
// 空闲线程存活时间
60L, TimeUnit.SECONDS,
// 任务队列容量
new LinkedBlockingQueue<>(100),
// 拒绝策略
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Override
public String getContent(InputStream inputStream) {
String content = "";
File tempPdf = null;
Path outputPath = null;
try {
// 创建临时文件
tempPdf = createTempFile(inputStream);
//构建输出路径
outputPath = buildOutputPath();
// 执行转换命令
Process process = buildProcess(properties.getTransition().getCondaEnvPath(), tempPdf, outputPath);
//打印执行日志
logProcessOutput(process);
int exitCode = process.waitFor();
//验证转换结果
String verifyResult = verifyResult(tempPdf, outputPath, exitCode);
// 获取生成的.md文件路径
Path mdFilePath = Paths.get(verifyResult);
if (Files.exists(mdFilePath)) {
log.info("找到Markdown文件: " + mdFilePath);
DocumentParser documentParser = new ApacheTikaDocumentParser();
Document document = FileSystemDocumentLoader.loadDocument(mdFilePath.toString(), documentParser);
if (null != document) {
content = document.text();
// 判断是否md文档
String fileType = FilenameUtils.getExtension(mdFilePath.getFileName().toString());
//判断是否需要进行图片OCR识别
if ("md".contains(fileType) && properties.getTransition().isEnableOcr()) {
// 如果是md文件查找所有图片语法如果是本地图片替换成网络图片
StringBuffer sb = replaceImageUrl(content, mdFilePath);
content = sb.toString();
}
} else {
log.warn("无法解析文档内容");
}
} else {
log.warn("未找到预期的 .md 文件");
}
return content;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (tempPdf != null) {
try {
// 清理临时文件
Files.deleteIfExists(tempPdf.toPath());
} catch (IOException e) {
log.warn("删除临时文件失败: {}", tempPdf.getAbsolutePath(), e);
}
}
//清理输出目录
if (outputPath != null) {
cleanOutputDirectory(outputPath);
}
}
}
@Override
public List<String> getChunkList(String content, String kid) {
return characterTextSplitter.split(content, kid);
}
/**
* 创建临时PDF文件
*
* @param is 输入流
* @return
* @throws IOException
*/
private static File createTempFile(InputStream is) throws IOException {
File tempFile = File.createTempFile("upload_", ".pdf");
Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
return tempFile;
}
/**
* 构建跨平台文件输出路径
*
* @return
* @throws IOException
*/
private static Path buildOutputPath() throws IOException {
Path basePath = isWindows() ?
// Windows C盘用户路径下 minerUOutPut避免其他盘符权限问题
Paths.get(System.getProperty("user.home")).resolve("minerUOutPut") :
Paths.get("/var/minerUOutPut");
if (!Files.exists(basePath)) {
Files.createDirectories(basePath);
}
return basePath;
}
/**
* 判断当前操作系统是否为Windows
*
* @return
*/
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("win");
}
/**
* 执行命令
*
* @param condaEnv conda环境路径
* @param inputFile 输入文件
* @param outputPath 输出路径
* @return
* @throws IOException
*/
private static Process buildProcess(String condaEnv, File inputFile, Path outputPath) throws IOException {
ProcessBuilder pb = new ProcessBuilder();
String[] command;
if (isWindows()) {
command = new String[]{
"cmd", "/c",
"call", "conda", "activate",
condaEnv.replace("\"", ""),
"&&", "magic-pdf",
"-p", inputFile.getAbsolutePath(),
"-o", outputPath.toString()
};
} else {
command = new String[]{
"bash", "-c",
String.format("source '%s/bin/activate' && magic-pdf -p '%s' -o '%s'",
condaEnv,
inputFile.getAbsolutePath(),
outputPath.toString())
};
}
return pb.command(command)
.redirectErrorStream(true)
.start();
}
/**
* 实时日志输出
*
* @param process 进程
*/
private static void logProcessOutput(Process process) {
Executors.newSingleThreadExecutor().submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("[PROCESS LOG] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
/**
* 验证转换结果
*
* @param inputFile 输入文件
* @param outputPath 输出路径
* @param exitCode 退出码
* @return
*/
private static String verifyResult(File inputFile, Path outputPath, int exitCode) {
String baseName = FilenameUtils.removeExtension(inputFile.getName());
Path expectedMd = outputPath
.resolve(baseName)
.resolve("auto")
.resolve(baseName + ".md");
if (exitCode == 0 && Files.exists(expectedMd)) {
log.info("转换成功:{}", expectedMd.toString());
return expectedMd.toString();
}
return String.format("转换失败(退出码%d| 预期文件:%s", exitCode, expectedMd);
}
/**
* 正则匹配图片语法,多线程进行处理
*
* @param content 文本内容
* @param basePath 图片路径
* @return
*/
private StringBuffer replaceImageUrl(String content, Path basePath) throws Exception {
List<ImageMatch> matches = new ArrayList<>();
Matcher matcher = MD_IMAGE_PATTERN.matcher(content);
// 收集所有匹配的图片项
while (matcher.find()) {
ImageMatch imgMatch = new ImageMatch();
imgMatch.altText = matcher.group(1);
imgMatch.imageUrl = matcher.group(2);
imgMatch.start = matcher.start();
imgMatch.end = matcher.end();
matches.add(imgMatch);
}
if (matches.isEmpty()) {
return new StringBuffer(content);
}
// 提交任务到线程池
List<Future<String>> futures = new ArrayList<>();
for (ImageMatch imgMatch : matches) {
// 为每个图片项创建独立任务
Future<String> future = ocrExecutor.submit(() -> processImage(imgMatch, basePath));
futures.add(future);
}
// 按原始顺序拼接结果
StringBuffer sb = new StringBuffer();
int previousEnd = 0;
for (int i = 0; i < matches.size(); i++) {
ImageMatch imgMatch = matches.get(i);
// 阻塞等待结果
String replacement = futures.get(i).get();
// 插入未匹配的原始文本和处理后的结果
sb.append(content.substring(previousEnd, imgMatch.start));
sb.append(replacement);
previousEnd = imgMatch.end;
}
// 添加剩余文本
sb.append(content.substring(previousEnd));
return sb;
}
/**
* 图片处理任务
*
* @param imgMatch 图片匹配结果
* @param basePath 本地图片路径
* @return
*/
private String processImage(ImageMatch imgMatch, Path basePath) {
try {
if (!imgMatch.imageUrl.startsWith("http")) {
// 处理本地图片
Path imagePath = basePath.getParent().resolve(imgMatch.imageUrl).normalize();
if (!Files.exists(imagePath)) {
log.error("图片路径不存在: {}", imagePath);
return String.format("![%s](%s)", imgMatch.altText, imgMatch.imageUrl);
}
// 文件后缀安全提取
String originalFileName = imagePath.getFileName().toString();
String suffix = "";
int lastDotIndex = originalFileName.lastIndexOf(".");
if (lastDotIndex != -1) {
suffix = originalFileName.substring(lastDotIndex);
}
// 上传OSS
try (InputStream inputStream = Files.newInputStream(imagePath)) {
OssClient storage = OssFactory.instance();
UploadResult uploadResult = storage.uploadSuffix(inputStream, suffix, FileUtils.getMimeType(suffix));
// 保存数据库记录
SysOss sysOss = new SysOss();
sysOss.setUrl(uploadResult.getUrl());
sysOss.setFileSuffix(suffix);
sysOss.setFileName(uploadResult.getFilename());
sysOss.setOriginalName(originalFileName);
sysOss.setService(storage.getConfigKey());
sysOssMapper.insert(sysOss);
// OCR处理
String networkUrl = uploadResult.getUrl();
//⚠️ 注意:确保 URL 是公网可访问的,否则模型无法加载图片。
//另一种解决方案使用base64 但是需要申请apikey , 使用demo会出现token超出长度问题。
String ocrResult = safeImageUrlOCR(networkUrl);
return String.format("![%s%s](%s)", imgMatch.altText, ocrResult, networkUrl);
}
} else {
// 处理远程图片
String ocrResult = safeImageUrlOCR(imgMatch.imageUrl);
return String.format("![%s%s](%s)", imgMatch.altText, ocrResult, imgMatch.imageUrl);
}
} catch (Exception e) {
log.error("图片处理失败: {}", imgMatch.imageUrl, e);
return String.format("![%s](%s)", imgMatch.altText, imgMatch.imageUrl);
}
}
/**
* OCR调用
*
* @param imageUrl 图片URL
* @return
*/
private String safeImageUrlOCR(String imageUrl) {
try {
return imageUrlOCR(imageUrl);
} catch (Exception e) {
log.warn("OCR处理失败: {}", imageUrl, e);
// OCR失败时返回空字符串
return "";
}
}
/**
* 多模态OCR识别图片内容
*
* @param imageUrl 图片URL
* @return
*/
private static String imageUrlOCR(String imageUrl) {
OpenAiChatModel model = OpenAiChatModel.builder()
.apiKey("demo")
.modelName("gpt-4o-mini")
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.build();
UserMessage userMessage = UserMessage.from(
TextContent.from(
"请按以下逻辑处理图片:\n" +
"1. 文字检测:识别图中所有可见文字(包括水印/标签若无文字则跳至步骤3\n" +
"2. 文字处理:\n" +
" a. 对识别到的文字进行❗核心信息提炼\n" +
" b. ❗禁止直接输出原文内容\n" +
" c. 描述文字位置(如'顶部居中')、字体特征(颜色/大小)\n" +
"3. 视觉描述:\n" +
" a. 若无文字则用❗50字内简洁描述主体对象、场景、色彩搭配与画面氛围\n" +
" b. 若有文字则补充说明文字与画面的关系\n" +
"4. 输出规则:\n" +
" - 最终输出为纯文本,格式:'[文字总结] 视觉描述 关键词xx,xx'\n" +
" - 关键词从内容中提取3个最具代表性的名词\n" +
" - 无文字时格式:'[空] 简洁描述 关键词xx,xx'"
),
ImageContent.from(imageUrl)
);
ChatResponse chat = model.chat(userMessage);
AiMessage answer = chat.aiMessage();
return answer.text();
}
/**
* 静态内部类保存图片匹配信息
*/
private static class ImageMatch {
String altText; // 替换文本
String imageUrl; // 图片地址
int start; // 匹配起始位置
int end; // 匹配结束位置
}
/**
* 清理输出目录
*
* @param outputPath 输出目录
*/
private static void cleanOutputDirectory(Path outputPath) {
if (Files.exists(outputPath)) {
try {
Files.walk(outputPath)
// 按逆序删除(子目录先删)
.sorted((p1, p2) -> -p1.compareTo(p2))
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
log.warn("清理输出目录失败: {}", path, e);
}
});
} catch (IOException e) {
log.error("遍历输出目录失败", e);
}
}
}
}

View File

@@ -1,10 +1,14 @@
package org.ruoyi.chain.loader;
import lombok.AllArgsConstructor;
import org.ruoyi.chain.split.*;
import org.ruoyi.config.properties.PdfProperties;
import org.ruoyi.chain.split.CharacterTextSplitter;
import org.ruoyi.chain.split.CodeTextSplitter;
import org.ruoyi.chain.split.MarkdownTextSplitter;
import org.ruoyi.constant.FileType;
import org.ruoyi.system.mapper.SysOssMapper;
import org.springframework.stereotype.Component;
@AllArgsConstructor
@@ -13,6 +17,10 @@ public class ResourceLoaderFactory {
private final CharacterTextSplitter characterTextSplitter;
private final CodeTextSplitter codeTextSplitter;
private final MarkdownTextSplitter markdownTextSplitter;
private final TokenTextSplitter tokenTextSplitter;
private final ExcelTextSplitter excelTextSplitter;
private final PdfProperties pdfProperties;
private final SysOssMapper sysOssMapper;
public ResourceLoader getLoaderByFileType(String fileType){
@@ -20,12 +28,16 @@ public class ResourceLoaderFactory {
return new TextFileLoader(characterTextSplitter);
} else if (FileType.isWord(fileType)) {
return new WordLoader(characterTextSplitter);
} else if (FileType.isPdf(fileType) && pdfProperties.getTransition().isEnableMinerU()) {
return new PdfMinerUFileLoader(characterTextSplitter,pdfProperties,sysOssMapper);
} else if (FileType.isPdf(fileType)) {
return new PdfFileLoader(characterTextSplitter);
} else if (FileType.isMdFile(fileType)) {
}else if (FileType.isMdFile(fileType)) {
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);
}

View File

@@ -0,0 +1,59 @@
package org.ruoyi.chain.split;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.domain.vo.KnowledgeInfoVo;
import org.ruoyi.service.IKnowledgeInfoService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Component
@AllArgsConstructor
@Slf4j
public class ExcelTextSplitter implements TextSplitter{
@Override
public List<String> split(String content, String kid) {
// 使用默认配置
String knowledgeSeparator = "#";
int textBlockSize = 10000;
int overlapChar = 500;
List<String> chunkList = new ArrayList<>();
if (content.contains(knowledgeSeparator) && StringUtils.isNotBlank(knowledgeSeparator)) {
// 按自定义分隔符切分
String[] chunks = content.split(knowledgeSeparator);
chunkList.addAll(Arrays.asList(chunks));
} else {
int indexMin = 0;
int len = content.length();
int i = 0;
int right = 0;
while (true) {
if (len > right) {
int begin = i * textBlockSize - overlapChar;
if (begin < indexMin) {
begin = indexMin;
}
int end = textBlockSize * (i + 1) + overlapChar;
if (end > len) {
end = len;
}
String chunk = content.substring(begin, end);
chunkList.add(chunk);
i++;
right = right + textBlockSize;
} else {
break;
}
}
}
return chunkList;
}
}

View File

@@ -0,0 +1,83 @@
package org.ruoyi.config.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* PDF 配置属性
*
* @author zpx
*/
@Data
@Component
@ConfigurationProperties(prefix = "pdf")
public class PdfProperties {
/**
* Extract 配置
*/
private ExtractConfig extract;
/**
* Transition 配置
*/
private TransitionConfig transition;
@Data
@NoArgsConstructor
public static class ExtractConfig {
/**
* Service 配置
*/
private ServiceConfig service;
/**
* AI API 配置
*/
private AiApiConfig aiApi;
@Data
@NoArgsConstructor
public static class ServiceConfig {
/**
* 服务地址 URL
*/
private String url;
}
@Data
@NoArgsConstructor
public static class AiApiConfig {
/**
* AI API 地址 URL
*/
private String url;
/**
* API 密钥
*/
private String key;
}
}
@Data
@NoArgsConstructor
public static class TransitionConfig {
/**
* 是否启用 MinerU
*/
private boolean enableMinerU;
/**
* MinerU Conda 环境路径
*/
private String condaEnvPath;
/**
* 是否启用图片 OCR
*/
private boolean enableOcr;
}
}

View File

@@ -7,6 +7,8 @@ import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.weaviate.WeaviateEmbeddingStore;
import io.weaviate.client.Config;
@@ -29,6 +31,7 @@ import org.ruoyi.domain.bo.StoreEmbeddingBo;
import org.ruoyi.service.VectorStoreService;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 向量库管理

View File

@@ -32,6 +32,15 @@ public interface ISysOssService {
String downloadByByte(Long ossId) throws IOException;
String downloadToTempPath(Long ossId) throws IOException;
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据文件路径删除文件
*
* @param filePath 文件路径
* @return 是否删除成功
*/
boolean deleteFile(String filePath);
}

View File

@@ -209,4 +209,48 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
}
return oss;
}
@Override
public String downloadToTempPath(Long ossId) throws IOException {
SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
if (ObjectUtil.isNull(sysOss)) {
throw new ServiceException("文件数据不存在!");
}
OssClient storage = OssFactory.instance();
try (InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
// 创建临时文件
String suffix = StringUtils.isNotEmpty(sysOss.getFileSuffix()) ? sysOss.getFileSuffix() : "";
java.io.File tempFile = java.io.File.createTempFile("download_", suffix);
// 确保临时文件在JVM退出时删除
tempFile.deleteOnExit();
// 将输入流内容写入临时文件
cn.hutool.core.io.FileUtil.writeFromStream(inputStream, tempFile);
// 返回临时文件的绝对路径
return tempFile.getAbsolutePath();
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 根据文件路径删除文件
*
* @param filePath 文件路径
* @return 是否删除成功
*/
@Override
public boolean deleteFile(String filePath) {
if (StringUtils.isEmpty(filePath)) {
return false;
}
try {
java.io.File file = new java.io.File(filePath);
if (file.exists() && file.isFile()) {
return file.delete();
}
return false;
} catch (Exception e) {
throw new ServiceException("删除文件失败: " + e.getMessage());
}
}
}

View File

@@ -21,6 +21,7 @@
<module>ruoyi-chat</module>
<module>ruoyi-system</module>
<module>ruoyi-generator</module>
<module>ruoyi-wechat</module>
</modules>
<properties>

View File

@@ -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;

View File

@@ -36,7 +36,6 @@ import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
/**
* 知识库Service业务层处理
*
@@ -191,7 +190,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
* 保存前的数据校验
*/
private void validEntityBeforeSave(KnowledgeInfo entity) {
//TODO 做一些数据校验,如唯一约束
// TODO 做一些数据校验,如唯一约束
}
/**
@@ -200,7 +199,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>ruoyi-wechat</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-chat-api</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
public abstract class AbstractBuilder {
public abstract WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, WxCpService service);
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
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;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
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;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@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<Integer, WxCpMessageRouter> routers = Maps.newHashMap();
private static Map<Integer, WxCpService> 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<Integer, WxCpMessageRouter> 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;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Data
@ConfigurationProperties(prefix = "wechat.cp")
public class WxCpProperties {
/**
* 设置企业微信的corpId
*/
private String corpId;
private List<AppConfig> appConfigs;
@Getter
@Setter
public static class AppConfig {
/**
* 设置企业微信应用的AgentId
*/
private Integer agentId;
/**
* 设置企业微信应用的Secret
*/
private String secret;
/**
* 设置企业微信应用的token
*/
private String token;
/**
* 设置企业微信应用的EncodingAESKey
*/
private String aesKey;
}
}

View File

@@ -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);
}
}

View File

@@ -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<WeixinQrCode> 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<LoginVo> 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");
}
}

View File

@@ -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;
}
}

View File

@@ -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 = "<xml>\n"
+ " <ToUserName><![CDATA[" + getFromUserName() + "]]></ToUserName>\n"
+ " <FromUserName><![CDATA[" + getToUserName() + "]]></FromUserName>\n"
+ " <CreateTime>" + System.currentTimeMillis() + "</CreateTime>\n"
+ " <MsgType><![CDATA[text]]></MsgType>\n"
+ " <Content><![CDATA[" + msg + "]]></Content>\n"
+ " </xml>";
return xml;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
package org.ruoyi.handler;
import me.chanjar.weixin.cp.message.WxCpMessageHandler;
/**
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public abstract class AbstractHandler implements WxCpMessageHandler {
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
@Component
public class ContactChangeHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
WxSessionManager sessionManager) {
String content = "收到通讯录变更事件,内容:" + JsonUtils.toJson(wxMessage);
log.info(content);
return new TextBuilder().build(content, wxMessage, cpService);
}
}

View File

@@ -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;
/**
* <pre>
*
* Created by Binary Wang on 2018/8/27.
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
public class EnterAgentHandler extends AbstractHandler {
private static final int TEST_AGENT = 1000002;
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, WxSessionManager sessionManager) throws WxErrorException {
// do something
return null;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
@Component
public class LocationHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> 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;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
@Component
public class LogHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
WxSessionManager sessionManager) {
log.info("\n接收到请求消息内容{}", JsonUtils.toJson(wxMessage));
return null;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Component
public class MenuHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> 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();
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Component
@RequiredArgsConstructor
public class MsgHandler extends AbstractHandler {
private final IChatVxService chatVxService;
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> 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);
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Component
public class NullHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
WxSessionManager sessionManager) {
return null;
}
}

View File

@@ -0,0 +1,8 @@
package org.ruoyi.handler;
/**
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public abstract class ScanHandler extends AbstractHandler {
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
@Component
public class SubscribeHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> 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;
}
}

View File

@@ -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 <a href="https://github.com/binarywang">Binary Wang</a>
*/
@Slf4j
@Component
public class UnsubscribeHandler extends AbstractHandler {
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
WxSessionManager sessionManager) {
String openId = wxMessage.getFromUserName();
log.info("取消关注用户 OPENID: " + openId);
// TODO 可以更新本地数据库为取消关注状态
return null;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,10 @@
package org.ruoyi.service;
public interface WeixinUserService {
void checkSignature(String signature, String timestamp, String nonce);
String handleWeixinMsg(String body);
}

View File

@@ -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("你已成功登录!");
}
}

View File

@@ -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("-", "");
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, String> QR_CODE_TICKET_MAP = new LinkedHashMap<>();
/**
* 增加一个 Ticket
* 首次 putvalue 为 ""
* 再次 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);
}
}

View File

@@ -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;
}
}