2 Commits

Author SHA1 Message Date
ageer
a4314dbbde feat: mcp测试版 2025-04-14 23:10:17 +08:00
ageerle
188dc1e55e feat: mcp 测试版本 2025-04-14 17:23:33 +08:00
66 changed files with 624 additions and 2976 deletions

View File

@@ -322,6 +322,9 @@ wechat:
spring:
ai:
openai:
api-key: sk-xX
base-url: https://api.pandarobot.chat/
ollama:
base-url: http://localhost:11434
mcp:

View File

@@ -49,7 +49,7 @@ public class ChatRequest {
/**
* 用户id
*/
private String userId;
private Long userId;
/**
* 应用ID

View File

@@ -0,0 +1,83 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-ai</artifactId>
<version>1.0.0</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>call-mcp-server</artifactId>
<name>Archetype - call-mcp-server</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>17</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package org.ruoyi.rocket.callmcpserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CallMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(CallMcpServerApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package org.ruoyi.rocket.callmcpserver.cofing;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* @author ageer
*/
@Configuration
public class McpClientCfg implements McpSyncClientCustomizer {
@Override
public void customize(String name, McpClient.SyncSpec spec) {
// do nothing
spec.requestTimeout(Duration.ofSeconds(30));
}
}

View File

@@ -0,0 +1,74 @@
package org.ruoyi.rocket.callmcpserver.view;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
/**
* @author jianzhang
* 2025/03/18/下午8:00
*/
@RestController
@RequestMapping("/dashscope/chat-client")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
public ChatController(ChatClient.Builder chatClientBuilder,ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.defaultOptions(
OpenAiChatOptions.builder().model("gpt-4o-mini").build())
.build();
}
@RequestMapping(value = "/generate_stream", method = RequestMethod.GET)
public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
response.setCharacterEncoding("UTF-8");
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
Flux<ChatResponse> chatResponseFlux = this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor)
.stream()
.chatResponse();
Flux<String> content = this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor)
.stream()
.content();
content.subscribe(
content1 -> System.out.println("chatResponse"+content1)
);
return chatResponseFlux;
}
@GetMapping("/advisor/chat/{id}/{prompt}")
public Flux<String> advisorChat(
HttpServletResponse response,
@PathVariable String id,
@PathVariable String prompt) {
response.setCharacterEncoding("UTF-8");
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
return this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor).stream().content();
}
}

View File

@@ -0,0 +1,21 @@
package org.ruoyi.rocket.callmcpserver.view;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author jianzhang
* 2025/03/18/下午8:00
*/
@Controller
public class IndexController {
@GetMapping("/")
public String chat(Model model) {
//model.addAttribute("name", "User");
// 返回视图名称,对应 templates/index.html
return "index";
}
}

View File

@@ -0,0 +1,16 @@
server:
port: 9999
spring:
ai:
openai:
api-key: sk-xXe1WMPjhlVb1aiI1b4c6c8934D8463f9e4b67Ed8718B772
base-url: https://api.pandarobot.chat/
mcp:
client:
enabled: true
name: call-mcp-server
sse:
connections:
server1:
url: http://127.0.0.1:6040

View File

@@ -0,0 +1,40 @@
{
"mcpServers": {
"fileSystem": {
"command": "D:\\software\\nodeJs\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:\\software\\sqlite"
]
},
"sqlLite": {
"command": "D:\\Program Files\\python3.12.3\\Scripts\\uvx.exe",
"args": [
"mcp-server-sqlite",
"--db-path",
"D:\\work-space-study\\spring-ai-mcp-demo\\mcp-client\\src\\main\\resources\\test.db"
]
},
"fetch": {
"command": "D:\\Program Files\\python3.12.3\\Scripts\\uvx.exe",
"args": [
"mcp-server-fetch"
]
},
"baidu-map": {
"command": "D:\\Program Files\\python3.12.3\\Scripts\\uvx.exe",
"args": [
"run",
"--with",
"mcp[cli]",
"mcp",
"run",
"D:\\work-space-python\\python-baidu-map\\baidu_map_mcp_server\\map.py"
],
"env": {
"BAIDU_MAPS_API_KEY": "{百度地图API-KEY}"
}
}
}
}

View File

@@ -0,0 +1,20 @@
{
"mcpServers": {
"fileSystem": {
"command": "D:\\software\\nodeJs\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:\\software\\sqlite"
]
},
"sqlLite": {
"command": "D:\\Program Files\\python3.12.3\\Scripts\\uvx.exe",
"args": [
"mcp-server-sqlite",
"--db-path",
"D:\\work-space-study\\spring-ai-mcp-demo\\mcp-client\\src\\main\\resources\\test.db"
]
}
}
}

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 对话助手</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto p-4 max-w-3xl">
<!-- 标题 -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800">AI 对话助手</h1>
<p class="text-gray-600 mt-2">基于 Spring AI 的流式对话系统 By AhuCodingBeast</p>
</div>
<!-- 聊天容器 -->
<div id="chat-container" class="bg-white rounded-xl shadow-lg p-4 mb-4 h-[500px] overflow-y-auto space-y-4">
<!-- 初始欢迎消息 -->
<div class="ai-message flex items-start gap-3">
<div class="bg-green-100 p-3 rounded-lg max-w-[85%]">
<span class="text-gray-800">您好我是AI助手有什么可以帮您</span>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="flex gap-2">
<input type="text" id="message-input"
class="flex-1 border border-gray-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="输入您的问题...">
<button id="send-button"
class="bg-blue-500 text-white px-6 py-3 rounded-xl hover:bg-blue-600 transition-colors flex items-center">
<span>发送</span>
<svg id="loading-spinner" class="hidden w-4 h-4 ml-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</button>
</div>
</div>
<script>
const chatContainer = document.getElementById('chat-container');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const loadingSpinner = document.getElementById('loading-spinner');
// 发送消息处理
function handleSend() {
const message = messageInput.value.trim();
if (!message) return;
// 添加用户消息
addMessage(message, 'user');
messageInput.value = '';
// 构建API URL
const apiUrl = new URL('http://localhost:9999/dashscope/chat-client/generate_stream');
apiUrl.searchParams.append('id', '01');
apiUrl.searchParams.append('prompt', message);
// 显示加载状态
sendButton.disabled = true;
loadingSpinner.classList.remove('hidden');
// 创建EventSource连接
const eventSource = new EventSource(apiUrl);
let aiMessageElement = null;
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log(data);
const content = data.result?.output?.text || '';
const finishReason = data.result?.metadata?.finishReason;
// 创建消息容器(如果不存在)
if (!aiMessageElement) {
aiMessageElement = addMessage('', 'ai');
}
// 追加内容
if (content) {
aiMessageElement.querySelector('.message-content').textContent += content;
autoScroll();
}
// 处理结束
if (finishReason === 'STOP') {
eventSource.close();
sendButton.disabled = false;
loadingSpinner.classList.add('hidden');
}
} catch (error) {
console.error('解析错误:', error);
}
};
eventSource.onerror = (error) => {
console.error('连接错误:', error);
eventSource.close();
sendButton.disabled = false;
loadingSpinner.classList.add('hidden');
addMessage('对话连接异常,请重试', 'ai', true);
};
}
// 添加消息到容器
function addMessage(content, type, isError = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `${type}-message flex items-start gap-3`;
const bubble = document.createElement('div');
bubble.className = `p-3 rounded-lg max-w-[85%] ${
type === 'user'
? 'bg-blue-500 text-white ml-auto'
: `bg-green-100 ${isError ? 'text-red-500' : 'text-gray-800'}`
}`;
const contentSpan = document.createElement('span');
contentSpan.className = 'message-content';
contentSpan.textContent = content;
bubble.appendChild(contentSpan);
messageDiv.appendChild(bubble);
chatContainer.appendChild(messageDiv);
autoScroll();
return bubble;
}
// 自动滚动到底部
function autoScroll() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 事件监听
sendButton.addEventListener('click', handleSend);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
</script>
</body>
</html>

View File

@@ -49,16 +49,16 @@
<artifactId>spring-ai-mcp</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system-api</artifactId>
<exclusions>
<exclusion>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-translation</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.ruoyi</groupId>-->
<!-- <artifactId>ruoyi-system-api</artifactId>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>org.ruoyi</groupId>-->
<!-- <artifactId>ruoyi-common-translation</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@@ -1,8 +1,5 @@
package org.ruoyi.mcp.service;
import lombok.RequiredArgsConstructor;
import org.ruoyi.system.domain.vo.SysUserVo;
import org.ruoyi.system.mapper.SysUserMapper;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
@@ -10,18 +7,14 @@ import org.springframework.stereotype.Service;
* @author ageer
*/
@Service
@RequiredArgsConstructor
public class McpCustomService {
private final SysUserMapper userMapper;
public record User(String userName, Double userBalance) {
public record User(String userName, String userBalance) {
}
@Tool(description = "根据用户名称查询用户信息")
public User getUserBalance(String username) {
SysUserVo sysUserVo = userMapper.selectUserByUserName(username);
return new User(sysUserVo.getUserName(),sysUserVo.getUserBalance());
return new User("admin","99.99");
}
}

View File

@@ -1,97 +0,0 @@
--- # 监控中心配置
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://43.139.70.230:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: ry-vue
password: xx
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: 123456
# 连接超时时间
timeout: 10S
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:

View File

@@ -1,10 +0,0 @@
spring:
application:
name: mcp-server
ai:
mcp:
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages

View File

@@ -1,332 +1,12 @@
# 项目相关配置
ruoyi:
# 名称
name: "ruoyi"
# 版本
version: ${revision}
# 版权年份
copyrightYear: 2025
# 实例演示开关
demoEnabled: true
# 获取ip地址开关
addressEnabled: false
captcha:
enable: false
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
# 数字验证码位数
numberLength: 1
# 字符验证码长度
charLength: 4
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 6040
servlet:
# 应用的访问路径
context-path: /
# undertow 配置
undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
application:
name: ${ruoyi.name}
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: @profiles.active@
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 50MB
# 设置总上传的文件大小
max-request-size: 200MB
mvc:
format:
date-time: yyyy-MM-dd HH:mm:ss
jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization:
# 格式化输出
indent_output: false
# 忽略无法转换的对象
fail_on_empty_beans: false
deserialization:
# 允许对象忽略json中不存在的属性
fail_on_unknown_properties: false
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token有效期 设为7天 (必定过期) 单位: 秒
timeout: 604800
# token临时有效期 (指定时间无操作就过期) 单位: 秒
activity-timeout: 604800
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# 是否尝试从header里读取token
is-read-header: true
# 是否尝试从cookie里读取token
is-read-cookie: false
# token前缀
token-prefix: "Bearer"
# jwt秘钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
# security配置
security:
# 排除路径
excludes:
# 支付回调
- /pay/returnUrl
- /pay/notifyUrl
# 上传文件
- /resource/oss/upload
# 重置密码
- /auth/reset/password
# 聊天接口
- /chat
# 静态资源
- /*.html
- /**/*.html
- /**/*.css
- /**/*.js
# 公共路径
- /favicon.ico
- /error
# swagger 文档配置
- /*/api-docs
- /*/api-docs/**
# actuator 监控配置
- /actuator
- /actuator/**
# 多租户配置
tenant:
# 是否开启
enable: false
# 排除表
excludes:
- sys_menu
- sys_tenant
- sys_tenant_package
- sys_role_dept
- sys_role_menu
- sys_user_post
- sys_user_role
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
# 不支持多包, 如有需要可在注解配置 或 提升扫包等级
# 例如 com.**.**.mapper
mapperPackage: org.ruoyi.**.mapper
# 对应的 XML 文件位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描多个package用逗号或者分号分隔
typeAliasesPackage: org.ruoyi.**.domain
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
checkConfigLocation: false
configuration:
# 自动驼峰命名规则camel case映射
mapUnderscoreToCamelCase: true
# MyBatis 自动映射策略
# NONE不启用 PARTIAL只对非嵌套 resultMap 自动映射 FULL对所有 resultMap 自动映射
autoMappingBehavior: FULL
# MyBatis 自动映射时未知列或未知属性处理策
# NONE不做处理 WARNING打印相关警告 FAILING抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
banner: true
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID
# 逻辑已删除值
logicDeleteValue: 2
# 逻辑未删除值
logicNotDeleteValue: 0
# 字段验证策略之 insert,在 insert 的时候的字段验证策略
# IGNORED 忽略 NOT_NULL 非NULL NOT_EMPTY 非空 DEFAULT 默认 NEVER 不加入 SQL
insertStrategy: NOT_NULL
# 字段验证策略之 update,在 update 的时候的字段验证策略
updateStrategy: NOT_NULL
# 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件
where-strategy: NOT_NULL
# 数据加密
mybatis-encryptor:
# 是否开启加密
enable: false
# 默认加密算法
algorithm: BASE64
# 编码方式 BASE64/HEX。默认BASE64
encode: BASE64
# 安全秘钥 对称算法的秘钥 如AESSM4
password:
# 公私钥 非对称算法的公私钥 如SM2RSA
publicKey:
privateKey:
# Swagger配置
swagger:
info:
# 标题
title: '标题:${ruoyi.name}多租户管理系统_接口文档'
# 描述
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
# 版本
version: '版本号: ${ruoyi.version}'
# 作者信息
contact:
name: ageerle
email: ageerle@163.com
url: https://gitee.com/ageerle/ruoyi-ai
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
springdoc:
api-docs:
# 是否开启接口文档
enabled: true
swagger-ui:
# 持久化认证数据
persistAuthorization: true
#这里定义了两个分组,可定义多个,也可以不定义
group-configs:
- group: 1.演示模块
packages-to-scan: org.ruoyi.demo
- group: 2.通用模块
packages-to-scan: org.ruoyi.web
- group: 3.系统模块
packages-to-scan: org.ruoyi.system
- group: 4.代码生成模块
packages-to-scan: org.ruoyi.generator
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 全局线程池相关配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
--- # 分布式锁 lock4j 全局配置
lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒
acquire-timeout: 3000
# 分布式锁的超时时间,默认为 30 秒
expire: 30000
--- # Actuator 监控端点的配置项
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/sys-console.log
# websocket
websocket:
enabled: true
# 路径
path: '/resource/websocket'
# 设置访问源地址
allowedOrigins: '*'
# 微信小程序配置信息
wx:
miniapp:
configs:
- appid: # 你的appid
secret: # 你的secret
token: #微信小程序消息服务器配置的token
aesKey: #微信小程序消息服务器配置的EncodingAESKey
msgDataFormat: JSON
# 企业微信应用
wechat:
cp:
corpId:
appConfigs:
- agentId:
secret: ''
token: ''
aesKey: ''
spring:
name: mcp-server
ai:
openai:
api-key: sk-xX
base-url: https://api.pandarobot.chat
ollama:
base-url: http://localhost:11434
init:
pull-model-strategy: always
timeout: 60s
max-retries: 1
mcp:
client:
enabled: true
name: call-mcp-server
sse:
connections:
server1:
url: http://127.0.0.1:8080
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages

View File

@@ -60,17 +60,12 @@
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-mcp</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-openai</artifactId>-->
<!-- <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>

View File

@@ -113,12 +113,6 @@
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
<version>1.0.0-M6</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -18,7 +18,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Objects;
@@ -34,13 +34,13 @@ import java.util.Objects;
public class SSEEventSourceListener extends EventSourceListener {
@Autowired(required = false)
public SSEEventSourceListener(ResponseBodyEmitter emitter) {
public SSEEventSourceListener(SseEmitter emitter) {
this.emitter = emitter;
}
private ResponseBodyEmitter emitter;
private SseEmitter emitter;
private StringBuilder stringBuffer;
private StringBuilder stringBuffer = new StringBuilder();
private String modelName;
@@ -61,7 +61,6 @@ public class SSEEventSourceListener extends EventSourceListener {
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, String data) {
try {
if ("[DONE]".equals(data)) {
//成功响应
emitter.complete();
@@ -72,25 +71,23 @@ public class SSEEventSourceListener extends EventSourceListener {
chatCostService.deductToken(chatRequest);
return;
}
// 解析返回内容
ObjectMapper mapper = new ObjectMapper();
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);
if(completionResponse == null || CollectionUtil.isEmpty(completionResponse.getChoices())){
return;
}
Object content = completionResponse.getChoices().get(0).getDelta().getContent();
if(content == null){
content = completionResponse.getChoices().get(0).getDelta().getReasoningContent();
if(content == null) return;
if(content != null ){
if(StringUtils.isEmpty(modelName)){
modelName = completionResponse.getModel();
}
stringBuffer.append(content);
emitter.send(content);
}
if(StringUtils.isEmpty(modelName)){
modelName = completionResponse.getModel();
}
stringBuffer.append(content);
emitter.send(data);
} catch (Exception e) {
log.error("sse信息推送失败{}内容:{}",e.getMessage(),data);
eventSource.cancel();
emitter.completeWithError(e);
}
}

View File

@@ -40,4 +40,9 @@ public interface IChatCostService {
* 判断用户是否付费
*/
void checkUserGrade();
/**
* 获取登录用户id
*/
Long getUserId();
}

View File

@@ -22,5 +22,5 @@ public interface IChatService {
* 客户端发送消息到服务端
* @param chatRequest 请求对象
*/
SseEmitter mcpChat(ChatRequest chatRequest,SseEmitter emitter);
void mcpChat(ChatRequest chatRequest,SseEmitter emitter);
}

View File

@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.enums.BillingType;
import org.ruoyi.chat.enums.UserGradeType;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.common.chat.config.LocalCache;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.chat.utils.TikTokensUtil;
import org.ruoyi.common.core.domain.model.LoginUser;
@@ -96,6 +97,12 @@ public class ChatCostServiceImpl implements IChatCostService {
chatToken.setUserId(chatMessageBo.getUserId());
chatTokenService.editToken(chatToken);
}
Object userId = LocalCache.CACHE.get("userId");
if(userId!=null){
chatMessageBo.setUserId((Long) userId);
}else {
chatMessageBo.setUserId(getUserId());
}
// 保存消息记录
chatMessageService.insertByBo(chatMessageBo);
}

View File

@@ -7,24 +7,13 @@ import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatRequestModel;
import io.github.ollama4j.models.generate.OllamaStreamHandler;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.util.SSEUtil;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.IChatModelService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@@ -35,27 +24,11 @@ import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class OllamaServiceImpl implements IChatService {
public class OllamaServiceImpl {
@Autowired
private IChatModelService chatModelService;
@Autowired
private IChatModelService chatModelService;
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
public OllamaServiceImpl(ChatClient.Builder chatClientBuilder,ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.defaultOptions(
OllamaOptions.builder()
.model(OllamaModel.QWEN_2_5_7B)
.temperature(0.4)
.build())
.build();
}
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
String host = chatModelVo.getApiHost();
@@ -100,44 +73,4 @@ public class OllamaServiceImpl implements IChatService {
return emitter;
}
@Override
public SseEmitter mcpChat(ChatRequest chatRequest, SseEmitter emitter) {
List<Message> msgList = chatRequest.getMessages();
// 添加记忆
for (int i = 0; i < msgList.size(); i++) {
org.springframework.ai.chat.messages.Message springAiMessage = new UserMessage(msgList.get(i).getContent().toString());
chatMemory.add(String.valueOf(i),springAiMessage);
}
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, chatRequest.getUserId(), 10);
this.chatClient.prompt(chatRequest.getPrompt())
.advisors(messageChatMemoryAdvisor)
.stream()
.chatResponse()
.subscribe(
chatResponse -> {
try {
emitter.send(chatResponse, MediaType.APPLICATION_JSON);
} catch (IOException e) {
e.printStackTrace();
}
},
error -> {
try {
emitter.completeWithError(error);
} catch (Exception e) {
e.printStackTrace();
}
},
() -> {
try {
emitter.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
);
return emitter;
}
}

View File

@@ -0,0 +1,78 @@
package org.ruoyi.chat.service.chat.impl;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.request.ChatRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.util.List;
@Service
@Slf4j
public class OpenAIServiceImpl implements IChatService {
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
public OpenAIServiceImpl(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.defaultOptions(
OpenAiChatOptions.builder()
.model("gpt-4o-mini")
.temperature(0.4)
.build())
.build();
}
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
return emitter;
}
@Override
public void mcpChat(ChatRequest chatRequest, SseEmitter emitter) {
List<Message> msgList = chatRequest.getMessages();
// 添加记忆
for (int i = 0; i < msgList.size(); i++) {
org.springframework.ai.chat.messages.Message springAiMessage = new UserMessage(msgList.get(i).getContent().toString());
chatMemory.add(String.valueOf(i), springAiMessage);
}
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, chatRequest.getUserId().toString(), 10);
Flux<String> content = chatClient
.prompt(chatRequest.getPrompt())
.advisors(messageChatMemoryAdvisor)
.stream().content();
content.publishOn(Schedulers.boundedElastic())
.doOnNext(text -> {
try {
emitter.send(text);
} catch (IOException e) {
emitter.completeWithError(e);
}
})
.doOnError(error -> {
log.error("Error in SSE stream: ", error);
emitter.completeWithError(error);
})
.doOnComplete(emitter::complete)
.subscribe();
}
}

View File

@@ -11,11 +11,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.chat.listener.SSEEventSourceListener;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.service.chat.ISseService;
import org.ruoyi.chat.util.IpUtil;
import org.ruoyi.chat.util.SSEUtil;
import org.ruoyi.common.chat.config.LocalCache;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.chat.entity.Tts.TextToSpeech;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
@@ -33,7 +35,9 @@ import org.ruoyi.common.core.utils.file.MimeTypeUtils;
import org.ruoyi.common.redis.utils.RedisUtils;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.EmbeddingService;
import org.ruoyi.service.IChatModelService;
import org.ruoyi.service.VectorStoreService;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
@@ -74,27 +78,35 @@ public class SseServiceImpl implements ISseService {
private final IChatService chatService;
private final IChatModelService chatModelService;
private static final String requestIdTemplate = "company-%d";
private static final ObjectMapper mapper = new ObjectMapper();
private final ChatConfig chatConfig;
@Override
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
SseEmitter sseEmitter = new SseEmitter(0L);
SseEmitter sseEmitter = new SseEmitter();
try {
// 构建消息列表增加联网、知识库等内容
buildChatMessageList(chatRequest);
if (!StpUtil.isLogin()) {
// 未登录用户限制对话次数
checkUnauthenticatedUserChatLimit(request);
}else {
LocalCache.CACHE.put("userId", chatCostService.getUserId());
chatRequest.setUserId(chatCostService.getUserId());
// 保存消息记录 并扣除费用
// chatCostService.deductToken(chatRequest);
}
// 根据模型名称前缀调用不同的处理逻辑
switchModelAndHandle(chatRequest,sseEmitter);
// 未登录用户限制对话次数
checkUnauthenticatedUserChatLimit(request);
// 保存消息记录 并扣除费用
chatCostService.deductToken(chatRequest);
} catch (Exception e) {
String message = e.getMessage();
SSEUtil.sendErrorEvent(sseEmitter, message);
return sseEmitter;
log.error(e.getMessage(),e);
sseEmitter.completeWithError(e);
}
return sseEmitter;
}
@@ -106,8 +118,7 @@ public class SseServiceImpl implements ISseService {
* @throws ServiceException 如果当日免费次数已用完
*/
public void checkUnauthenticatedUserChatLimit(HttpServletRequest request) throws ServiceException {
// 未登录用户限制对话次数
if (!StpUtil.isLogin()) {
String clientIp = IpUtil.getClientIp(request);
// 访客每天默认只能对话5次
int timeWindowInSeconds = 5;
@@ -125,13 +136,14 @@ public class SseServiceImpl implements ISseService {
count++;
RedisUtils.setCacheObject(redisKey, count);
}
}
}
/**
* 根据模型名称前缀调用不同的处理逻辑
*/
private void switchModelAndHandle(ChatRequest chatRequest,SseEmitter emitter) {
SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(emitter);
String model = chatRequest.getModel();
// 如果模型名称以ollama开头则调用ollama中部署的本地模型
if (model.startsWith("ollama-")) {
@@ -142,8 +154,24 @@ public class SseServiceImpl implements ISseService {
} else {
throw new IllegalArgumentException("Invalid ollama model name: " + chatRequest.getModel());
}
} else if (model.startsWith("gpt-4-gizmo")) {
chatRequest.setModel("gpt-4-gizmo");
} else {
if (model.startsWith("gpt-4-gizmo")) {
chatRequest.setModel("gpt-4-gizmo");
}
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
//openAiStreamClient = chatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
ChatCompletion completion = ChatCompletion
.builder()
.messages(chatRequest.getMessages())
.model(chatRequest.getModel())
.temperature(0.2)
.topP(1.0)
.stream(true)
.build();
openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
}
}
@@ -151,9 +179,10 @@ public class SseServiceImpl implements ISseService {
* 构建消息列表
*/
private void buildChatMessageList(ChatRequest chatRequest){
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
// 获取对话消息列表
List<Message> messages = chatRequest.getMessages();
String sysPrompt = chatRequest.getSysPrompt();
String sysPrompt = chatModelVo.getSystemPrompt();
if(StringUtils.isEmpty(sysPrompt)){
sysPrompt ="你是一个由RuoYI-AI开发的人工智能助手名字叫熊猫助手。你擅长中英文对话能够理解并处理各种问题提供安全、有帮助、准确的回答。" +
"当前时间:"+ DateUtils.getDate();
@@ -162,8 +191,9 @@ public class SseServiceImpl implements ISseService {
Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build();
messages.add(0,sysMessage);
chatRequest.setSysPrompt(sysPrompt);
// 查询向量库相关信息加入到上下文
if(chatRequest.getKid()!=null){
if(StringUtils.isNotEmpty(chatRequest.getKid())){
List<Message> knMessages = new ArrayList<>();
String content = messages.get(messages.size() - 1).getContent().toString();
List<String> nearestList;

View File

@@ -1,6 +1,7 @@
package org.ruoyi.chat.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@@ -19,7 +20,7 @@ public class SSEUtil {
* @param sseEmitter sse事件对象
* @param errorMessage 错误信息
*/
public static void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) {
public static void sendErrorEvent(ResponseBodyEmitter sseEmitter, String errorMessage) {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.name("error")
.data(errorMessage);

View File

@@ -1,99 +0,0 @@
<?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">
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-demo</artifactId>
<description>
demo模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sms</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-translation</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-encrypt</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,52 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.mail.utils.MailUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
/**
* 邮件发送案例
*
* @author Michelle.Chung
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mail")
public class MailController {
/**
* 发送邮件
*
* @param to 接收人
* @param subject 标题
* @param text 内容
*/
@GetMapping("/sendSimpleMessage")
public R<Void> sendSimpleMessage(String to, String subject, String text) {
MailUtils.sendText(to, subject, text);
return R.ok();
}
/**
* 发送邮件(带附件)
*
* @param to 接收人
* @param subject 标题
* @param text 内容
* @param filePath 附件路径
*/
@GetMapping("/sendMessageWithAttachment")
public R<Void> sendMessageWithAttachment(String to, String subject, String text, String filePath) {
MailUtils.sendText(to, subject, text, new File(filePath));
return R.ok();
}
}

View File

@@ -1,95 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.constant.CacheNames;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.redis.utils.RedisUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
/**
* spring-cache 演示案例
*
* @author Lion Li
*/
// 类级别 缓存统一配置
//@CacheConfig(cacheNames = CacheNames.DEMO_CACHE)
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/cache")
public class RedisCacheController {
/**
* 测试 @Cacheable
* <p>
* 表示这个方法有了缓存的功能,方法的返回值会被缓存下来
* 下一次调用该方法前,会去检查是否缓存中已经有值
* 如果有就直接返回,不调用方法
* 如果没有,就调用方法,然后把结果缓存起来
* 这个注解「一般用在查询方法上」
* <p>
* 重点说明: 缓存注解严谨与其他筛选数据功能一起使用
* 例如: 数据权限注解 会造成 缓存击穿 与 数据不一致问题
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@Cacheable(cacheNames = "demo:cache#60s#10m#20", key = "#key", condition = "#key != null")
@GetMapping("/test1")
public R<String> test1(String key, String value) {
return R.ok("操作成功", value);
}
/**
* 测试 @CachePut
* <p>
* 加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用
* 它「通常用在新增或者实时更新方法上」
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@CachePut(cacheNames = CacheNames.DEMO_CACHE, key = "#key", condition = "#key != null")
@GetMapping("/test2")
public R<String> test2(String key, String value) {
return R.ok("操作成功", value);
}
/**
* 测试 @CacheEvict
* <p>
* 使用了CacheEvict注解的方法,会清空指定缓存
* 「一般用在删除的方法上」
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@CacheEvict(cacheNames = CacheNames.DEMO_CACHE, key = "#key", condition = "#key != null")
@GetMapping("/test3")
public R<String> test3(String key, String value) {
return R.ok("操作成功", value);
}
/**
* 测试设置过期时间
* 手动设置过期时间10秒
* 11秒后获取 判断是否相等
*/
@GetMapping("/test6")
public R<Boolean> test6(String key, String value) {
RedisUtils.setCacheObject(key, value);
boolean flag = RedisUtils.expire(key, Duration.ofSeconds(10));
System.out.println("***********" + flag);
try {
Thread.sleep(11 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = RedisUtils.getCacheObject(key);
return R.ok(value.equals(obj));
}
}

View File

@@ -1,71 +0,0 @@
package org.ruoyi.demo.controller;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.lock.executor.RedissonLockExecutor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalTime;
/**
* 测试分布式锁的样例
*
* @author shenxinquan
*/
@Slf4j
@RestController
@RequestMapping("/demo/redisLock")
public class RedisLockController {
@Autowired
private LockTemplate lockTemplate;
/**
* 测试lock4j 注解
*/
@Lock4j(keys = {"#key"})
@GetMapping("/testLock4j")
public R<String> testLock4j(String key, String value) {
System.out.println("start:" + key + ",time:" + LocalTime.now().toString());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end :" + key + ",time:" + LocalTime.now().toString());
return R.ok("操作成功", value);
}
/**
* 测试lock4j 工具
*/
@GetMapping("/testLock4jLockTemplate")
public R<String> testLock4jLockTemplate(String key, String value) {
final LockInfo lockInfo = lockTemplate.lock(key, 30000L, 5000L, RedissonLockExecutor.class);
if (null == lockInfo) {
throw new RuntimeException("业务处理中,请稍后再试");
}
// 获取锁成功,处理业务
try {
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
//
}
System.out.println("执行简单方法1 , 当前线程:" + Thread.currentThread().getName());
} finally {
//释放锁
lockTemplate.releaseLock(lockInfo);
}
//结束
return R.ok("操作成功", value);
}
}

View File

@@ -1,47 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.redis.utils.RedisUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Redis 发布订阅 演示案例
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/redis/pubsub")
public class RedisPubSubController {
/**
* 发布消息
*
* @param key 通道Key
* @param value 发送内容
*/
@GetMapping("/pub")
public R<Void> pub(String key, String value) {
RedisUtils.publish(key, value, consumer -> {
System.out.println("发布通道 => " + key + ", 发送值 => " + value);
});
return R.ok("操作成功");
}
/**
* 订阅消息
*
* @param key 通道Key
*/
@GetMapping("/sub")
public R<Void> sub(String key) {
RedisUtils.subscribe(key, String.class, msg -> {
System.out.println("订阅通道 => " + key + ", 接收值 => " + msg);
});
return R.ok("操作成功");
}
}

View File

@@ -1,64 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.ratelimiter.annotation.RateLimiter;
import org.ruoyi.common.ratelimiter.enums.LimitType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试分布式限流样例
*
* @author Lion Li
*/
@Slf4j
@RestController
@RequestMapping("/demo/rateLimiter")
public class RedisRateLimiterController {
/**
* 测试全局限流
* 全局影响
*/
@RateLimiter(count = 2, time = 10)
@GetMapping("/test")
public R<String> test(String value) {
return R.ok("操作成功", value);
}
/**
* 测试请求IP限流
* 同一IP请求受影响
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.IP)
@GetMapping("/testip")
public R<String> testip(String value) {
return R.ok("操作成功", value);
}
/**
* 测试集群实例限流
* 启动两个后端服务互不影响
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.CLUSTER)
@GetMapping("/testcluster")
public R<String> testcluster(String value) {
return R.ok("操作成功", value);
}
/**
* 测试请求IP限流(key基于参数获取)
* 同一IP请求受影响
*
* 简单变量获取 #变量 复杂表达式 #{#变量 != 1 ? 1 : 0}
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.IP, key = "#value")
@GetMapping("/testObj")
public R<String> testObj(String value) {
return R.ok("操作成功", value);
}
}

View File

@@ -1,76 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.sms.config.properties.SmsProperties;
import org.ruoyi.common.sms.core.SmsTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 短信演示案例
* 请先阅读文档 否则无法使用
*
* @author Lion Li
* @version 4.2.0
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/sms")
public class SmsController {
private final SmsProperties smsProperties;
// private final SmsTemplate smsTemplate; // 可以使用spring注入
// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
/**
* 发送短信Aliyun
*
* @param phones 电话号
* @param templateId 模板ID
*/
@GetMapping("/sendAliyun")
public R<Object> sendAliyun(String phones, String templateId) {
if (!smsProperties.getEnabled()) {
return R.fail("当前系统没有开启短信功能!");
}
if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
return R.fail("阿里云依赖未引入!");
}
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
Map<String, String> map = new HashMap<>(1);
map.put("code", "1234");
Object send = smsTemplate.send(phones, templateId, map);
return R.ok(send);
}
/**
* 发送短信Tencent
*
* @param phones 电话号
* @param templateId 模板ID
*/
@GetMapping("/sendTencent")
public R<Object> sendTencent(String phones, String templateId) {
if (!smsProperties.getEnabled()) {
return R.fail("当前系统没有开启短信功能!");
}
if (!SpringUtils.containsBean("tencentSmsTemplate")) {
return R.fail("腾讯云依赖未引入!");
}
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
Map<String, String> map = new HashMap<>(1);
// map.put("2", "测试测试");
map.put("1", "1234");
Object send = smsTemplate.send(phones, templateId, map);
return R.ok(send);
}
}

View File

@@ -1,31 +0,0 @@
package org.ruoyi.demo.controller;
import org.ruoyi.common.core.domain.R;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* swagger3 用法示例
*
* @author Lion Li
*/
@RestController
@RequestMapping("/swagger/demo")
public class Swagger3DemoController {
/**
* 上传请求
* 必须使用 @RequestPart 注解标注为文件
*
* @param file 文件
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<String> upload(@RequestPart("file") MultipartFile file) {
return R.ok("操作成功", file.getOriginalFilename());
}
}

View File

@@ -1,90 +0,0 @@
package org.ruoyi.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.demo.domain.TestDemo;
import org.ruoyi.demo.mapper.TestDemoMapper;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* 测试批量方法
*
* @author Lion Li
* @date 2021-05-30
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/batch")
public class TestBatchController extends BaseController {
/**
* 为了便于测试 直接引入mapper
*/
private final TestDemoMapper testDemoMapper;
/**
* 新增批量方法 可完美替代 saveBatch 秒级插入上万数据 (对mysql负荷较大)
* <p>
* 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
*/
@PostMapping("/add")
// @DS("slave")
public R<Void> add() {
List<TestDemo> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
TestDemo testDemo = new TestDemo();
testDemo.setOrderNum(-1);
testDemo.setTestKey("批量新增");
testDemo.setValue("测试新增");
list.add(testDemo);
}
return toAjax(testDemoMapper.insertBatch(list));
}
/**
* 新增或更新 可完美替代 saveOrUpdateBatch 高性能
* <p>
* 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
*/
@PostMapping("/addOrUpdate")
// @DS("slave")
public R<Void> addOrUpdate() {
List<TestDemo> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
TestDemo testDemo = new TestDemo();
testDemo.setOrderNum(-1);
testDemo.setTestKey("批量新增");
testDemo.setValue("测试新增");
list.add(testDemo);
}
testDemoMapper.insertBatch(list);
for (int i = 0; i < list.size(); i++) {
TestDemo testDemo = list.get(i);
testDemo.setTestKey("批量新增或修改");
testDemo.setValue("批量新增或修改");
if (i % 2 == 0) {
testDemo.setId(null);
}
}
return toAjax(testDemoMapper.insertOrUpdateBatch(list));
}
/**
* 删除批量方法
*/
@DeleteMapping()
// @DS("slave")
public R<Void> remove() {
return toAjax(testDemoMapper.delete(new LambdaQueryWrapper<TestDemo>()
.eq(TestDemo::getOrderNum, -1L)));
}
}

View File

@@ -1,147 +0,0 @@
package org.ruoyi.demo.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.core.utils.ValidatorUtils;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.core.validate.QueryGroup;
import org.ruoyi.common.excel.core.ExcelResult;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.demo.domain.TestDemo;
import org.ruoyi.demo.domain.bo.TestDemoBo;
import org.ruoyi.demo.domain.bo.TestDemoImportVo;
import org.ruoyi.demo.domain.vo.TestDemoVo;
import org.ruoyi.demo.service.ITestDemoService;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 测试单表Controller
*
* @author Lion Li
* @date 2021-07-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/demo")
public class TestDemoController extends BaseController {
private final ITestDemoService testDemoService;
/**
* 查询测试单表列表
*/
@SaCheckPermission("demo:demo:list")
@GetMapping("/list")
public TableDataInfo<TestDemoVo> list(@Validated(QueryGroup.class) TestDemoBo bo, PageQuery pageQuery) {
return testDemoService.queryPageList(bo, pageQuery);
}
/**
* 自定义分页查询
*/
@SaCheckPermission("demo:demo:list")
@GetMapping("/page")
public TableDataInfo<TestDemoVo> page(@Validated(QueryGroup.class) TestDemoBo bo, PageQuery pageQuery) {
return testDemoService.customPageList(bo, pageQuery);
}
/**
* 导入数据
*
* @param file 导入文件
*/
@Log(title = "测试单表", businessType = BusinessType.IMPORT)
@SaCheckPermission("demo:demo:import")
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Void> importData(@RequestPart("file") MultipartFile file) throws Exception {
ExcelResult<TestDemoImportVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), TestDemoImportVo.class, true);
List<TestDemo> list = MapstructUtils.convert(excelResult.getList(), TestDemo.class);
testDemoService.saveBatch(list);
return R.ok(excelResult.getAnalysis());
}
/**
* 导出测试单表列表
*/
@SaCheckPermission("demo:demo:export")
@Log(title = "测试单表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Validated TestDemoBo bo, HttpServletResponse response) {
List<TestDemoVo> list = testDemoService.queryList(bo);
// 测试雪花id导出
// for (TestDemoVo vo : list) {
// vo.setId(1234567891234567893L);
// }
ExcelUtil.exportExcel(list, "测试单表", TestDemoVo.class, response);
}
/**
* 获取测试单表详细信息
*
* @param id 测试ID
*/
@SaCheckPermission("demo:demo:query")
@GetMapping("/{id}")
public R<TestDemoVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("id") Long id) {
return R.ok(testDemoService.queryById(id));
}
/**
* 新增测试单表
*/
@SaCheckPermission("demo:demo:add")
@Log(title = "测试单表", businessType = BusinessType.INSERT)
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS, message = "{repeat.submit.message}")
@PostMapping()
public R<Void> add(@RequestBody TestDemoBo bo) {
// 使用校验工具对标 @Validated(AddGroup.class) 注解
// 用于在非 Controller 的地方校验对象
ValidatorUtils.validate(bo, AddGroup.class);
return toAjax(testDemoService.insertByBo(bo));
}
/**
* 修改测试单表
*/
@SaCheckPermission("demo:demo:edit")
@Log(title = "测试单表", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody TestDemoBo bo) {
return toAjax(testDemoService.updateByBo(bo));
}
/**
* 删除测试单表
*
* @param ids 测试ID串
*/
@SaCheckPermission("demo:demo:remove")
@Log(title = "测试单表", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(testDemoService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}

View File

@@ -1,55 +0,0 @@
package org.ruoyi.demo.controller;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.demo.domain.TestDemoEncrypt;
import org.ruoyi.demo.mapper.TestDemoEncryptMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 测试数据库加解密功能
*
* @author Lion Li
*/
@Validated
@RestController
@RequestMapping("/demo/encrypt")
public class TestEncryptController {
@Autowired
private TestDemoEncryptMapper mapper;
@Value("${mybatis-encryptor.enable}")
private Boolean encryptEnable;
/**
* 测试数据库加解密
*
* @param key 测试key
* @param value 测试value
*/
@GetMapping()
public R<Map<String, TestDemoEncrypt>> test(String key, String value) {
if (!encryptEnable) {
throw new RuntimeException("加密功能未开启!");
}
Map<String, TestDemoEncrypt> map = new HashMap<>(2);
TestDemoEncrypt demo = new TestDemoEncrypt();
demo.setTestKey(key);
demo.setValue(value);
mapper.insert(demo);
map.put("加密", demo);
TestDemoEncrypt testDemo = mapper.selectById(demo.getId());
map.put("解密", testDemo);
return R.ok(map);
}
}

View File

@@ -1,97 +0,0 @@
package org.ruoyi.demo.controller;
import cn.hutool.core.collection.CollUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 测试Excel功能
*
* @author Lion Li
*/
@RestController
@RequestMapping("/demo/excel")
public class TestExcelController {
/**
* 单列表多数据
*/
@GetMapping("/exportTemplateOne")
public void exportTemplateOne(HttpServletResponse response) {
Map<String, String> map = new HashMap<>();
map.put("title", "单列表多数据");
map.put("test1", "数据测试1");
map.put("test2", "数据测试2");
map.put("test3", "数据测试3");
map.put("test4", "数据测试4");
map.put("testTest", "666");
List<TestObj> list = new ArrayList<>();
list.add(new TestObj("单列表测试1", "列表测试1", "列表测试2", "列表测试3", "列表测试4"));
list.add(new TestObj("单列表测试2", "列表测试5", "列表测试6", "列表测试7", "列表测试8"));
list.add(new TestObj("单列表测试3", "列表测试9", "列表测试10", "列表测试11", "列表测试12"));
ExcelUtil.exportTemplate(CollUtil.newArrayList(map, list), "单列表.xlsx", "excel/单列表.xlsx", response);
}
/**
* 多列表多数据
*/
@GetMapping("/exportTemplateMuliti")
public void exportTemplateMuliti(HttpServletResponse response) {
Map<String, String> map = new HashMap<>();
map.put("title1", "标题1");
map.put("title2", "标题2");
map.put("title3", "标题3");
map.put("title4", "标题4");
map.put("author", "Lion Li");
List<TestObj1> list1 = new ArrayList<>();
list1.add(new TestObj1("list1测试1", "list1测试2", "list1测试3"));
list1.add(new TestObj1("list1测试4", "list1测试5", "list1测试6"));
list1.add(new TestObj1("list1测试7", "list1测试8", "list1测试9"));
List<TestObj1> list2 = new ArrayList<>();
list2.add(new TestObj1("list2测试1", "list2测试2", "list2测试3"));
list2.add(new TestObj1("list2测试4", "list2测试5", "list2测试6"));
List<TestObj1> list3 = new ArrayList<>();
list3.add(new TestObj1("list3测试1", "list3测试2", "list3测试3"));
List<TestObj1> list4 = new ArrayList<>();
list4.add(new TestObj1("list4测试1", "list4测试2", "list4测试3"));
list4.add(new TestObj1("list4测试4", "list4测试5", "list4测试6"));
list4.add(new TestObj1("list4测试7", "list4测试8", "list4测试9"));
list4.add(new TestObj1("list4测试10", "list4测试11", "list4测试12"));
Map<String, Object> multiListMap = new HashMap<>();
multiListMap.put("map", map);
multiListMap.put("data1", list1);
multiListMap.put("data2", list2);
multiListMap.put("data3", list3);
multiListMap.put("data4", list4);
ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
}
@Data
@AllArgsConstructor
static class TestObj1 {
private String test1;
private String test2;
private String test3;
}
@Data
@AllArgsConstructor
static class TestObj {
private String name;
private String list1;
private String list2;
private String list3;
private String list4;
}
}

View File

@@ -1,70 +0,0 @@
package org.ruoyi.demo.controller;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.utils.MessageUtils;
import org.hibernate.validator.constraints.Range;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试国际化
*
* @author Lion Li
*/
@Validated
@RestController
@RequestMapping("/demo/i18n")
public class TestI18nController {
/**
* 通过code获取国际化内容
* code为 messages.properties 中的 key
* <p>
* 测试使用 user.register.success
*
* @param code 国际化code
*/
@GetMapping()
public R<Void> get(String code) {
return R.ok(MessageUtils.message(code));
}
/**
* Validator 校验国际化
* 不传值 分别查看异常返回
* <p>
* 测试使用 not.null
*/
@GetMapping("/test1")
public R<Void> test1(@NotBlank(message = "{not.null}") String str) {
return R.ok(str);
}
/**
* Bean 校验国际化
* 不传值 分别查看异常返回
* <p>
* 测试使用 not.null
*/
@GetMapping("/test2")
public R<TestI18nBo> test2(@Validated TestI18nBo bo) {
return R.ok(bo);
}
@Data
public static class TestI18nBo {
@NotBlank(message = "{not.null}")
private String name;
@NotNull(message = "{not.null}")
@Range(min = 0, max = 100, message = "{length.not.valid}")
private Integer age;
}
}

View File

@@ -1,76 +0,0 @@
package org.ruoyi.demo.controller;
import lombok.Data;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.sensitive.annotation.Sensitive;
import org.ruoyi.common.sensitive.core.SensitiveService;
import org.ruoyi.common.sensitive.core.SensitiveStrategy;
import org.ruoyi.common.web.core.BaseController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试数据脱敏控制器
* <p>
* 默认管理员不过滤
* 需自行根据业务重写实现
*
* @author Lion Li
* @version 3.6.0
* @see SensitiveService
*/
@RestController
@RequestMapping("/demo/sensitive")
public class TestSensitiveController extends BaseController {
/**
* 测试数据脱敏
*/
@GetMapping("/test")
public R<TestSensitive> test() {
TestSensitive testSensitive = new TestSensitive();
testSensitive.setIdCard("210397198608215431");
testSensitive.setPhone("17640125371");
testSensitive.setAddress("北京市朝阳区某某四合院1203室");
testSensitive.setEmail("17640125371@163.com");
testSensitive.setBankCard("6226456952351452853");
return R.ok(testSensitive);
}
@Data
static class TestSensitive {
/**
* 身份证
*/
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
/**
* 电话
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phone;
/**
* 地址
*/
@Sensitive(strategy = SensitiveStrategy.ADDRESS)
private String address;
/**
* 邮箱
*/
@Sensitive(strategy = SensitiveStrategy.EMAIL)
private String email;
/**
* 银行卡
*/
@Sensitive(strategy = SensitiveStrategy.BANK_CARD)
private String bankCard;
}
}

View File

@@ -1,107 +0,0 @@
package org.ruoyi.demo.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.core.validate.QueryGroup;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.demo.domain.bo.TestTreeBo;
import org.ruoyi.demo.domain.vo.TestTreeVo;
import org.ruoyi.demo.service.ITestTreeService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 测试树表Controller
*
* @author Lion Li
* @date 2021-07-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/tree")
public class TestTreeController extends BaseController {
private final ITestTreeService testTreeService;
/**
* 查询测试树表列表
*/
@SaCheckPermission("demo:tree:list")
@GetMapping("/list")
public R<List<TestTreeVo>> list(@Validated(QueryGroup.class) TestTreeBo bo) {
List<TestTreeVo> list = testTreeService.queryList(bo);
return R.ok(list);
}
/**
* 导出测试树表列表
*/
@SaCheckPermission("demo:tree:export")
@Log(title = "测试树表", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public void export(@Validated TestTreeBo bo, HttpServletResponse response) {
List<TestTreeVo> list = testTreeService.queryList(bo);
ExcelUtil.exportExcel(list, "测试树表", TestTreeVo.class, response);
}
/**
* 获取测试树表详细信息
*
* @param id 测试树ID
*/
@SaCheckPermission("demo:tree:query")
@GetMapping("/{id}")
public R<TestTreeVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("id") Long id) {
return R.ok(testTreeService.queryById(id));
}
/**
* 新增测试树表
*/
@SaCheckPermission("demo:tree:add")
@Log(title = "测试树表", businessType = BusinessType.INSERT)
@RepeatSubmit
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody TestTreeBo bo) {
return toAjax(testTreeService.insertByBo(bo));
}
/**
* 修改测试树表
*/
@SaCheckPermission("demo:tree:edit")
@Log(title = "测试树表", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody TestTreeBo bo) {
return toAjax(testTreeService.updateByBo(bo));
}
/**
* 删除测试树表
*
* @param ids 测试树ID串
*/
@SaCheckPermission("demo:tree:remove")
@Log(title = "测试树表", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(testTreeService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}

View File

@@ -1,90 +0,0 @@
package org.ruoyi.demo.controller.queue;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.redis.utils.QueueUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 有界队列 演示案例
* <p>
* 轻量级队列 重量级数据量 请使用 MQ
* <p>
* 集群测试通过 同一个数据只会被消费一次 做好事务补偿
* 集群测试流程 在其中一台发送数据 两端分别调用获取接口 一次获取一条
*
* @author Lion Li
* @version 3.6.0
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/queue/bounded")
public class BoundedQueueController {
/**
* 添加队列数据
*
* @param queueName 队列名
* @param capacity 容量
*/
@GetMapping("/add")
public R<Void> add(String queueName, int capacity) {
// 用完了一定要销毁 否则会一直存在
boolean b = QueueUtils.destroyQueue(queueName);
log.info("通道: {} , 删除: {}", queueName, b);
// 初始化设置一次即可
if (QueueUtils.trySetBoundedQueueCapacity(queueName, capacity)) {
log.info("通道: {} , 设置容量: {}", queueName, capacity);
} else {
log.info("通道: {} , 设置容量失败", queueName);
return R.fail("操作失败");
}
for (int i = 0; i < 11; i++) {
String data = "data-" + i;
boolean flag = QueueUtils.addBoundedQueueObject(queueName, data);
if (flag == false) {
log.info("通道: {} , 发送数据: {} 失败, 通道已满", queueName, data);
} else {
log.info("通道: {} , 发送数据: {}", queueName, data);
}
}
return R.ok("操作成功");
}
/**
* 删除队列数据
*
* @param queueName 队列名
*/
@GetMapping("/remove")
public R<Void> remove(String queueName) {
String data = "data-" + 5;
if (QueueUtils.removeQueueObject(queueName, data)) {
log.info("通道: {} , 删除数据: {}", queueName, data);
} else {
return R.fail("操作失败");
}
return R.ok("操作成功");
}
/**
* 获取队列数据
*
* @param queueName 队列名
*/
@GetMapping("/get")
public R<Void> get(String queueName) {
String data;
do {
data = QueueUtils.getQueueObject(queueName);
log.info("通道: {} , 获取数据: {}", queueName, data);
} while (data != null);
return R.ok("操作成功");
}
}

View File

@@ -1,90 +0,0 @@
package org.ruoyi.demo.controller.queue;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.redis.utils.QueueUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 延迟队列 演示案例
* <p>
* 轻量级队列 重量级数据量 请使用 MQ
* 例如: 创建订单30分钟后过期处理
* <p>
* 集群测试通过 同一个数据只会被消费一次 做好事务补偿
* 集群测试流程 两台集群分别开启订阅 在其中一台发送数据 观察接收消息的规律
*
* @author Lion Li
* @version 3.6.0
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/queue/delayed")
public class DelayedQueueController {
/**
* 订阅队列
*
* @param queueName 队列名
*/
@GetMapping("/subscribe")
public R<Void> subscribe(String queueName) {
log.info("通道: {} 监听中......", queueName);
// 项目初始化设置一次即可
QueueUtils.subscribeBlockingQueue(queueName, (String orderNum) -> {
// 观察接收时间
log.info("通道: {}, 收到数据: {}", queueName, orderNum);
});
return R.ok("操作成功");
}
/**
* 添加队列数据
*
* @param queueName 队列名
* @param orderNum 订单号
* @param time 延迟时间(秒)
*/
@GetMapping("/add")
public R<Void> add(String queueName, String orderNum, Long time) {
QueueUtils.addDelayedQueueObject(queueName, orderNum, time, TimeUnit.SECONDS);
// 观察发送时间
log.info("通道: {} , 发送数据: {}", queueName, orderNum);
return R.ok("操作成功");
}
/**
* 删除队列数据
*
* @param queueName 队列名
* @param orderNum 订单号
*/
@GetMapping("/remove")
public R<Void> remove(String queueName, String orderNum) {
if (QueueUtils.removeDelayedQueueObject(queueName, orderNum)) {
log.info("通道: {} , 删除数据: {}", queueName, orderNum);
} else {
return R.fail("操作失败");
}
return R.ok("操作成功");
}
/**
* 销毁队列
*
* @param queueName 队列名
*/
@GetMapping("/destroy")
public R<Void> destroy(String queueName) {
// 用完了一定要销毁 否则会一直存在
QueueUtils.destroyDelayedQueue(queueName);
return R.ok("操作成功");
}
}

View File

@@ -1,22 +0,0 @@
package org.ruoyi.demo.controller.queue;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 实体类 注意不允许使用内部类 否则会找不到类
*
* @author Lion Li
* @version 3.6.0
*/
@Data
@NoArgsConstructor
public class PriorityDemo implements Comparable<PriorityDemo> {
private String name;
private Integer orderNum;
@Override
public int compareTo(PriorityDemo other) {
return Integer.compare(getOrderNum(), other.getOrderNum());
}
}

View File

@@ -1,89 +0,0 @@
package org.ruoyi.demo.controller.queue;
import cn.hutool.core.util.RandomUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.redis.utils.QueueUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 优先队列 演示案例
* <p>
* 轻量级队列 重量级数据量 请使用 MQ
* <p>
* 集群测试通过 同一个消息只会被消费一次 做好事务补偿
* 集群测试流程 在其中一台发送数据 两端分别调用获取接口 一次获取一条
*
* @author Lion Li
* @version 3.6.0
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/queue/priority")
public class PriorityQueueController {
/**
* 添加队列数据
*
* @param queueName 队列名
*/
@GetMapping("/add")
public R<Void> add(String queueName) {
// 用完了一定要销毁 否则会一直存在
boolean b = QueueUtils.destroyQueue(queueName);
log.info("通道: {} , 删除: {}", queueName, b);
for (int i = 0; i < 10; i++) {
int randomNum = RandomUtil.randomInt(10);
PriorityDemo data = new PriorityDemo();
data.setName("data-" + i);
data.setOrderNum(randomNum);
if (QueueUtils.addPriorityQueueObject(queueName, data)) {
log.info("通道: {} , 发送数据: {}", queueName, data);
} else {
log.info("通道: {} , 发送数据: {}, 发送失败", queueName, data);
}
}
return R.ok("操作成功");
}
/**
* 删除队列数据
*
* @param queueName 队列名
* @param name 对象名
* @param orderNum 排序号
*/
@GetMapping("/remove")
public R<Void> remove(String queueName, String name, Integer orderNum) {
PriorityDemo data = new PriorityDemo();
data.setName(name);
data.setOrderNum(orderNum);
if (QueueUtils.removeQueueObject(queueName, data)) {
log.info("通道: {} , 删除数据: {}", queueName, data);
} else {
return R.fail("操作失败");
}
return R.ok("操作成功");
}
/**
* 获取队列数据
*
* @param queueName 队列名
*/
@GetMapping("/get")
public R<Void> get(String queueName) {
PriorityDemo data;
do {
data = QueueUtils.getQueueObject(queueName);
log.info("通道: {} , 获取数据: {}", queueName, data);
} while (data != null);
return R.ok("操作成功");
}
}

View File

@@ -1,68 +0,0 @@
package org.ruoyi.demo.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.tenant.core.TenantEntity;
import java.io.Serial;
/**
* 测试单表对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_demo")
public class TestDemo extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 部门id
*/
private Long deptId;
/**
* 用户id
*/
private Long userId;
/**
* 排序号
*/
@OrderBy(asc = false, sort = 1)
private Integer orderNum;
/**
* key键
*/
private String testKey;
/**
* 值
*/
private String value;
/**
* 版本
*/
@Version
private Long version;
/**
* 删除标志
*/
@TableLogic
private Long delFlag;
}

View File

@@ -1,29 +0,0 @@
package org.ruoyi.demo.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.encrypt.annotation.EncryptField;
import org.ruoyi.common.encrypt.enumd.AlgorithmType;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_demo")
public class TestDemoEncrypt extends TestDemo {
/**
* key键
*/
// @EncryptField(algorithm=AlgorithmType.SM2, privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgZSlOvw8FBiH+aFJWLYZP/VRjg9wjfRarTkGBZd/T3N+gCgYIKoEcz1UBgi2hRANCAAR5DGuQwJqkxnbCsP+iPSDoHWIF4RwcR5EsSvT8QPxO1wRkR2IhCkzvRb32x2CUgJFdvoqVqfApFDPZzShqzBwX", publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEeQxrkMCapMZ2wrD/oj0g6B1iBeEcHEeRLEr0/ED8TtcEZEdiIQpM70W99sdglICRXb6KlanwKRQz2c0oaswcFw==")
@EncryptField(algorithm = AlgorithmType.RSA, privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANBBEeueWlXlkkj2+WY5l+IWe42d8b5K28g+G/CFKC/yYAEHtqGlCsBOrb+YBkG9mPzmuYA/n9k0NFIc8E8yY5vZQaroyFBrTTWEzG9RY2f7Y3svVyybs6jpXSUs4xff8abo7wL1Y/wUaeatTViamxYnyTvdTmLm3d+JjRij68rxAgMBAAECgYAB0TnhXraSopwIVRfmboea1b0upl+BUdTJcmci412UjrKr5aE695ZLPkXbFXijVu7HJlyyv94NVUdaMACV7Ku/S2RuNB70M7YJm8rAjHFC3/i2ZeIM60h1Ziy4QKv0XM3pRATlDCDNhC1WUrtQCQSgU8kcp6eUUppruOqDzcY04QJBAPm9+sBP9CwDRgy3e5+V8aZtJkwDstb0lVVV/KY890cydVxiCwvX3fqVnxKMlb+x0YtH0sb9v+71xvK2lGobaRECQQDVePU6r/cCEfpc+nkWF6osAH1f8Mux3rYv2DoBGvaPzV2BGfsLed4neRfCwWNCKvGPCdW+L0xMJg8+RwaoBUPhAkAT5kViqXxFPYWJYd1h2+rDXhMdH3ZSlm6HvDBDdrwlWinr0Iwcx3iSjPV93uHXwm118aUj4fg3LDJMCKxOwBxhAkByrQXfvwOMYygBprRBf/j0plazoWFrbd6lGR0f1uI5IfNnFRPdeFw1DEINZ2Hw+6zEUF44SqRMC+4IYJNc02dBAkBCgy7RvfyV/A7N6kKXxTHauY0v6XwSSvpeKtRJkbIcRWOdIYvaHO9L7cklj3vIEdwjSUp9K4VTBYYlmAz1xh03", publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQQRHrnlpV5ZJI9vlmOZfiFnuNnfG+StvIPhvwhSgv8mABB7ahpQrATq2/mAZBvZj85rmAP5/ZNDRSHPBPMmOb2UGq6MhQa001hMxvUWNn+2N7L1csm7Oo6V0lLOMX3/Gm6O8C9WP8FGnmrU1YmpsWJ8k73U5i5t3fiY0Yo+vK8QIDAQAB")
private String testKey;
/**
* 值
*/
// @EncryptField // 什么也不写走默认yml配置
// @EncryptField(algorithm = AlgorithmType.SM4, password = "10rfylhtccpuyke5")
@EncryptField(algorithm = AlgorithmType.AES, password = "10rfylhtccpuyke5")
private String value;
}

View File

@@ -1,65 +0,0 @@
package org.ruoyi.demo.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.tenant.core.TenantEntity;
import java.io.Serial;
/**
* 测试树表对象 test_tree
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_tree")
public class TestTree extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 父ID
*/
private Long parentId;
/**
* 部门id
*/
private Long deptId;
/**
* 用户id
*/
private Long userId;
/**
* 树节点名
*/
private String treeName;
/**
* 版本
*/
@Version
private Long version;
/**
* 删除标志
*/
@TableLogic
private Long delFlag;
}

View File

@@ -1,61 +0,0 @@
package org.ruoyi.demo.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.core.domain.BaseEntity;
import org.ruoyi.demo.domain.TestDemo;
/**
* 测试单表业务对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = TestDemo.class, reverseConvertGenerate = false)
public class TestDemoBo extends BaseEntity {
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long id;
/**
* 部门id
*/
@NotNull(message = "部门id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long deptId;
/**
* 用户id
*/
@NotNull(message = "用户id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long userId;
/**
* 排序号
*/
@NotNull(message = "排序号不能为空", groups = {AddGroup.class, EditGroup.class})
private Integer orderNum;
/**
* key键
*/
@NotBlank(message = "key键不能为空", groups = {AddGroup.class, EditGroup.class})
private String testKey;
/**
* 值
*/
@NotBlank(message = "值不能为空", groups = {AddGroup.class, EditGroup.class})
private String value;
}

View File

@@ -1,52 +0,0 @@
package org.ruoyi.demo.domain.bo;
import com.alibaba.excel.annotation.ExcelProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 测试单表业务对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
public class TestDemoImportVo {
/**
* 部门id
*/
@NotNull(message = "部门id不能为空")
@ExcelProperty(value = "部门id")
private Long deptId;
/**
* 用户id
*/
@NotNull(message = "用户id不能为空")
@ExcelProperty(value = "用户id")
private Long userId;
/**
* 排序号
*/
@NotNull(message = "排序号不能为空")
@ExcelProperty(value = "排序号")
private Long orderNum;
/**
* key键
*/
@NotBlank(message = "key键不能为空")
@ExcelProperty(value = "key键")
private String testKey;
/**
* 值
*/
@NotBlank(message = "值不能为空")
@ExcelProperty(value = "")
private String value;
}

Some files were not shown because too many files have changed in this diff Show More